56 Commits

Author SHA1 Message Date
ac99ef3930 Add renovate.json 2024-07-27 12:30:05 +00:00
35e0f3c400 Merge pull request 'Add readmes' (#12) from feature/documentation into main
Reviewed-on: remoll/fast-network-navigation#12
2024-07-27 12:10:12 +00:00
de5f1ec3d3 Merge pull request 'cleanup-backend' (#13) from cleanup-backend into main
Reviewed-on: remoll/fast-network-navigation#13
2024-07-27 12:09:54 +00:00
81c763587d Merge pull request 'style corrections, documentation, duplicate removal, flow improvement' (#11) from feature/backend-refactoring into cleanup-backend
Some checks failed
Build and push docker image / Build (pull_request) Failing after 10s
Reviewed-on: remoll/fast-network-navigation#11
2024-07-27 12:09:10 +00:00
3fa689fd16 a few docker-related fixes 2024-07-26 19:11:26 +02:00
2736a89f70 cleanup in view of docker builds 2024-07-26 13:13:36 +02:00
e50eedf099 add readmes with first pointers
Some checks failed
Build and push docker image / Build (pull_request) Failing after 2m15s
Build and release APK / Build APK (pull_request) Successful in 6m46s
Build web / Build Web (pull_request) Successful in 2m33s
2024-07-25 17:22:29 +02:00
2863c99d7c style corrections, documentation, duplicate removal, flow improvement 2024-07-25 17:15:18 +02:00
80b3d5b012 refactored landmark manager and clean up 2024-07-25 09:37:37 +02:00
d23050b811 fixed parameters folder 2024-07-24 10:34:34 +02:00
127ba8c028 fixed log 2024-07-21 10:16:13 +02:00
94fa735d54 cleaned up backend to use classes and yaml files 2024-07-20 23:16:35 +02:00
14a7f555df more coherent base types 2024-07-17 13:22:43 +02:00
4a291a69c9 Merge branch 'feature/backend/initial-deployment' 2024-07-17 12:47:20 +02:00
7d7a25e2f3 stage changes as reference implementation
All checks were successful
Build and push docker image / Build (pull_request) Successful in 2m50s
Build and release APK / Build APK (pull_request) Successful in 4m11s
Build web / Build Web (pull_request) Successful in 1m28s
2024-07-17 12:35:08 +02:00
f590ebb5ed Merge pull request 'Permafix-optimization-refiner' (#9) from Permafix-optimization-refiner into main
Reviewed-on: remoll/fast-network-navigation#9
2024-07-17 10:32:32 +00:00
b09ec2b083 changed bbox to meters
All checks were successful
Build and push docker image / Build (pull_request) Successful in 3m45s
Build and release APK / Build APK (pull_request) Successful in 6m57s
Build web / Build Web (pull_request) Successful in 2m33s
2024-07-17 12:30:47 +02:00
8d71cab8d5 osmnx does not behave 2024-07-17 12:00:40 +02:00
87df2f70e9 cleanup files 2024-07-17 11:59:42 +02:00
50bc8150c8 permafixed ? 2024-07-16 09:01:58 +02:00
4466f29a3d better map style 2024-07-08 12:21:22 +02:00
25cc0fa300 (theoretically) functional deployment 2024-07-08 11:55:27 +02:00
8f23a4747d further cleanup 2024-07-08 11:55:00 +02:00
4896e95617 cleaned up 2024-07-08 02:13:13 +02:00
30ed2bb9ed permafixed the optimizer ??? 2024-07-08 02:01:42 +02:00
568e7bfbc4 upgraded optimizer 2024-07-08 01:20:17 +02:00
d4e964c5d4 fixed the optimizer_v4 2024-07-07 16:24:15 +02:00
f9c86261cb switch to osmnx 2024-07-07 14:49:10 +02:00
e71c92da40 added some ideas 2024-07-07 10:17:50 +02:00
006b80018a Added 2024-07-05 17:21:47 +02:00
49ce8527a3 cleanup path handling for easier dockerization 2024-06-30 18:42:59 +02:00
bec1827891 Corrected optimizer and landmark attributes in backend 2024-06-26 14:23:51 +02:00
8e33bd1b3f base structs as agreed upon 2024-06-26 12:27:54 +02:00
c26d9222bd Merge branch 'feature/backend/unify-api-communication' 2024-06-26 11:05:57 +02:00
09bcd95cab space
Some checks failed
Build web / Build Web (pull_request) Has been cancelled
Build and release APK / Build APK (pull_request) Has been cancelled
Build and push docker image / Build (pull_request) Has been cancelled
2024-06-26 10:52:51 +02:00
fdcaaf8c16 updated refiner
Some checks failed
Build and push docker image / Build (pull_request) Successful in 3m17s
Build and release APK / Build APK (pull_request) Has been cancelled
Build web / Build Web (pull_request) Has been cancelled
2024-06-26 10:52:24 +02:00
8d068c80a7 Upgraded refiner
All checks were successful
Build and push docker image / Build (pull_request) Successful in 3m8s
Build and release APK / Build APK (pull_request) Successful in 5m0s
Build web / Build Web (pull_request) Successful in 1m41s
2024-06-24 11:06:01 +02:00
b6c9e61be9 Merge pull request 'UI elements using the new structs' (#8) from feature/unify-api-frontend into main
Reviewed-on: remoll/fast-network-navigation#8
2024-06-23 19:21:48 +00:00
eede94add4 working save and load functionality with custom datastructures
Some checks failed
Build and push docker image / Build (pull_request) Failing after 2m8s
Build and release APK / Build APK (pull_request) Successful in 5m15s
Build web / Build Web (pull_request) Successful in 1m13s
2024-06-23 21:19:06 +02:00
813c83a81d removed amentiy=arts_centre
All checks were successful
Build and push docker image / Build (pull_request) Successful in 2m10s
Build and release APK / Build APK (pull_request) Successful in 5m16s
Build web / Build Web (pull_request) Successful in 1m39s
2024-06-23 18:22:14 +02:00
fd378d6289 fixed refiner
All checks were successful
Build and push docker image / Build (pull_request) Successful in 2m36s
Build and release APK / Build APK (pull_request) Successful in 5m31s
Build web / Build Web (pull_request) Successful in 1m15s
2024-06-23 12:08:51 +02:00
34922a2645 Added refiner (for minor landmarks)
All checks were successful
Build and push docker image / Build (pull_request) Successful in 2m17s
Build and release APK / Build APK (pull_request) Successful in 5m13s
Build web / Build Web (pull_request) Successful in 1m14s
2024-06-23 10:50:44 +02:00
db41528702 functional datastructure. Needs to be able to write to storage as well
Some checks failed
Build and push docker image / Build (pull_request) Failing after 3m5s
Build and release APK / Build APK (pull_request) Successful in 7m24s
Build web / Build Web (pull_request) Successful in 3m36s
2024-06-21 19:30:40 +02:00
1f5bd92895 added pep8 example
All checks were successful
Build and push docker image / Build (pull_request) Successful in 2m59s
Build and release APK / Build APK (pull_request) Successful in 4m47s
Build web / Build Web (pull_request) Successful in 1m42s
2024-06-19 14:58:11 +02:00
111e6836f6 reviewed code structure, cleaned comments, now pep8 conform
All checks were successful
Build and push docker image / Build (pull_request) Successful in 2m17s
Build and release APK / Build APK (pull_request) Successful in 6m53s
Build web / Build Web (pull_request) Successful in 1m31s
2024-06-11 20:14:12 +02:00
af4d68f36f fixed optimizer and added a member to Landmark class
All checks were successful
Build and push docker image / Build (pull_request) Successful in 2m16s
Build and release APK / Build APK (pull_request) Successful in 5m28s
Build web / Build Web (pull_request) Successful in 1m23s
2024-06-11 10:46:09 +02:00
53a5a9e873 fixed duplicate landmarks
All checks were successful
Build and push docker image / Build (pull_request) Successful in 3m4s
Build and release APK / Build APK (pull_request) Successful in 5m44s
Build web / Build Web (pull_request) Successful in 2m14s
2024-06-10 22:52:08 +02:00
adbb6466d9 fixed optimizer. works fine now
All checks were successful
Build and push docker image / Build (pull_request) Successful in 2m17s
Build and release APK / Build APK (pull_request) Successful in 5m56s
Build web / Build Web (pull_request) Successful in 1m15s
2024-06-10 14:24:37 +02:00
9a5ae95d97 landmark styling
Some checks failed
Build and push docker image / Build (pull_request) Failing after 2m47s
Build and release APK / Build APK (pull_request) Successful in 4m39s
Build web / Build Web (pull_request) Successful in 1m21s
2024-06-07 15:09:18 +02:00
40943c5c5b finally use correct api key 2024-06-07 15:06:33 +02:00
040e5c9f83 cleaner ci
Some checks failed
Build and push docker image / Build (pull_request) Failing after 2m10s
Build and release APK / Build APK (pull_request) Successful in 6m34s
Build web / Build Web (pull_request) Successful in 1m42s
2024-06-07 10:44:37 +02:00
c58c10b057 fixed input as coordinates
Some checks failed
Build and push docker image / Build (pull_request) Failing after 36s
Build and release APK / Build APK (pull_request) Successful in 5m20s
Build web / Build Web (pull_request) Successful in 1m13s
2024-06-04 00:20:54 +02:00
bcc91c638d Merge branch 'feature/backend/unify-api-communication' of ssh://git.kluster.moll.re:2222/remoll/fast-network-navigation into feature/backend/unify-api-communication
All checks were successful
Build and push docker image / Build (pull_request) Successful in 2m8s
Build and release APK / Build APK (pull_request) Successful in 5m14s
Build web / Build Web (pull_request) Successful in 1m34s
2024-05-30 00:50:56 +02:00
d88f22121e started to implement overpass queries 2024-05-30 00:48:38 +02:00
beee9614c5 Update .gitea/workflows/backed_build-image.yaml
All checks were successful
Build and push docker image / Build (pull_request) Successful in 3m4s
Build and release APK / Build APK (pull_request) Successful in 4m49s
Build web / Build Web (pull_request) Successful in 1m51s
2024-05-29 22:37:22 +00:00
03da8441f2 cleaned up folders and defined proper structs
Some checks failed
Build and push docker image / Build (pull_request) Failing after 2m14s
Build and release APK / Build APK (pull_request) Successful in 5m15s
Build web / Build Web (pull_request) Successful in 2m32s
2024-05-29 22:57:11 +02:00
64 changed files with 3143 additions and 1185 deletions

View File

@@ -2,6 +2,8 @@ on:
pull_request:
branches:
- main
paths:
- backend/**
name: Build and push docker image
@@ -12,13 +14,13 @@ jobs:
steps:
- uses: https://gitea.com/actions/checkout@v4
- name: Login to Docker Registry
uses: docker/login-action@v3
with:
registry: git.kluster.moll.re
username: ${{ gitea.repository_owner }}
password: ${{ secrets.GITEA_TOKEN}}
password: ${{ secrets.PACKAGE_REGISTRY_ACCESS }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@@ -27,5 +29,5 @@ jobs:
uses: docker/build-push-action@v5
with:
context: backend
tags: git.kluster.moll.re/remoll/fast_network_navigation/backend:latest
tags: git.kluster.moll.re/anydev/anyway-backend:latest
push: true

View File

@@ -42,20 +42,24 @@ jobs:
- run: flutter pub get
working-directory: ./frontend
- run: flutter build apk --release --split-per-abi
- name: Add required secrets
run: |
echo ${{ secrets.ANDROID_SECRETS_PROPERTIES }} > ./android/secrets.properties
working-directory: ./frontend
- name: Release APK
uses: https://gitea.com/akkuman/gitea-release-action@v1
- name: Sanity check
run: |
ls
ls -lah android
working-directory: ./frontend
- run: flutter build apk --release --split-per-abi --build-number=${{ gitea.run_number }}
working-directory: ./frontend
- name: Upload APKs to artifacts
uses: https://gitea.com/actions/upload-artifact@v3
with:
files: ./frontend/build/app/outputs/flutter-apk/*.apk
name: Testing release
release_name: testing
tag: testing
tag_name: testing
release_body: "This is a testing release."
prerelease: true
token: ${{ secrets.GITEA_TOKEN }}
env:
NODE_OPTIONS: '--experimental-fetch'
name: app-release
path: frontend/build/app/outputs/flutter-apk/
if-no-files-found: error
retention-days: 15

View File

@@ -1,34 +1,34 @@
on:
pull_request:
branches:
- main
paths:
- frontend/**
# on:
# pull_request:
# branches:
# - main
# paths:
# - frontend/**
name: Build web
# name: Build web
jobs:
build:
name: Build Web
runs-on: ubuntu-latest
steps:
# jobs:
# build:
# name: Build Web
# runs-on: ubuntu-latest
# steps:
- name: Install prerequisites
run: |
sudo apt-get update
sudo apt-get install -y xz-utils
# - name: Install prerequisites
# run: |
# sudo apt-get update
# sudo apt-get install -y xz-utils
- uses: actions/checkout@v4
# - uses: actions/checkout@v4
- uses: https://github.com/subosito/flutter-action@v2
with:
channel: stable
flutter-version: 3.19.6
cache: true
# - uses: https://github.com/subosito/flutter-action@v2
# with:
# channel: stable
# flutter-version: 3.19.6
# cache: true
- run: flutter pub get
working-directory: ./frontend
# - run: flutter pub get
# working-directory: ./frontend
- run: flutter build web
working-directory: ./frontend
# - run: flutter build web
# working-directory: ./frontend

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
cache/

46
.vscode/launch.json vendored
View File

@@ -4,25 +4,49 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
// backend - python using fastapi
{
"name": "frontend",
"cwd": "frontend",
"name": "Backend - debug",
"type": "debugpy",
"request": "launch",
"type": "dart"
"module": "uvicorn",
"env": {
"DEBUG": "true"
},
"args": [
"--app-dir",
"src",
"main:app",
"--reload",
],
"jinja": true,
"cwd": "${workspaceFolder}/backend"
},
{
"name": "frontend (profile mode)",
"cwd": "frontend",
"name": "Backend - tester",
"type": "debugpy",
"request": "launch",
"program": "src/tester.py",
"env": {
"DEBUG": "true"
},
"cwd": "${workspaceFolder}/backend"
},
// frontend - flutter app
{
"name": "Frontend - debug",
"type": "dart",
"flutterMode": "profile"
"request": "launch",
"program": "lib/main.dart",
"cwd": "${workspaceFolder}/frontend"
},
{
"name": "frontend (release mode)",
"cwd": "frontend",
"request": "launch",
"name": "Frontend - profile",
"type": "dart",
"flutterMode": "release"
},
"request": "launch",
"program": "lib/main.dart",
"flutterMode": "profile",
"cwd": "${workspaceFolder}/frontend"
}
]
}

View File

@@ -1,16 +1,40 @@
# fast_network_navigation
# AnyWay - plan city trips your way
AnyWay is a mobile application that helps users plan city trips. The app allows users to specify their preferences and constraints, and then generates a personalized itinerary for them. The planning follows some guiding principles:
- **Personalization**:The user's preferences should be reflected in the choice of destinations.
- **Efficiency**:The itinerary should be optimized for the user's constraints.
- **Flexibility**: We aknowledge that tourism is a dynamic activity, and that users may want to change their plans on the go.
- **Discoverability**: Tourism is an inherently exploratory activity. Once a rough itinerary is generated, detours and spontaneous decisions should be encouraged.
## Architecture
This project is divided into two main components: a frontend and a backend. The frontend is a Flutter application that runs on Android and iOS. The backend is a python server that runs on a cloud provider. The two components communicate via a REST API. Since both components are very interdependent and share many data structures, we opted for a monorepo approach.
### Frontend
See the [frontend README](frontend/README.md) for more information. The application is centered around its map view, which displays the user's itinerary. This is based on the Google Maps API.
### Backend
See the [backend README](backend/README.md) for more information. The backend is responsible for generating the itinerary based on the user's preferences and constraints. Rather than using google maps, we use the OpenStreetMap API, which is much more flexible.
A new Flutter project.
## Getting Started
Refer to the READMEs in the `frontend` and `backend` directories for instructions on how to run the application locally. Notable prerequisites include:
- Flutter SDK
- `google_maps_flutter` plugin
- Python 3
- `fastapi`
- `OSMPythonTools`
- `numpy, scipy`
- Docker
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
## Releases
The project is still in its early stages. We are currently working on a prototype that demonstrates the core functionality of the application.
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
Official releases will be made available on the Google Play Store and the Apple App Store.
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
## Roadmap
See the [project board](https://todos.kluster.moll.re/share/L8vZJGU45Vx9RzzVqTzyCPrpRybXm1OAqaW7YkWb/auth?view=list):
<iframe src="https://todos.kluster.moll.re/share/L8vZJGU45Vx9RzzVqTzyCPrpRybXm1OAqaW7YkWb/auth?view=list" title="Todos" width="100%">

1
backend/.gitignore vendored
View File

@@ -1,5 +1,6 @@
# osm-cache
cache/
apicache/
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]

View File

@@ -1,4 +1,4 @@
FROM python:3
FROM python:3.11-slim
WORKDIR /app
COPY Pipfile Pipfile.lock .
@@ -6,6 +6,12 @@ COPY Pipfile Pipfile.lock .
RUN pip install pipenv
RUN pipenv install --deploy --system
COPY . /src
COPY src src
CMD ["pipenv", "run", "python", "/app/src/main.py"]
EXPOSE 8000
# Set environment variables used by the deployment. These can be overridden by the user using this image.
ENV NUM_WORKERS=1
ENV OSM_CACHE_DIR=/cache
CMD fastapi run src/main.py --port 8000 --workers $NUM_WORKERS

View File

@@ -3,10 +3,14 @@ url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[dev-packages]
[packages]
numpy = "*"
scipy = "*"
fastapi = "*"
pydantic = "*"
geopy = "*"
shapely = "*"
scipy = "*"
osmpythontools = "*"
[dev-packages]
pywikibot = "*"

930
backend/Pipfile.lock generated

File diff suppressed because it is too large Load Diff

16
backend/README.md Normal file
View File

@@ -0,0 +1,16 @@
# Backend
This repository contains the backend code for the application. It utilizes FastAPI that allows to quickly create a RESTful API that exposes the endpoints of the route optimizer.
## Getting Started
- The code of the python application is located in the `src` directory.
- Package management is handled with `pipenv` and the dependencies are listed in the `Pipfile`.
- Since the application is aimed to be deployed in a container, the `Dockerfile` is provided to build the image.
### Deployment
To deploy the backend docker container, we use kubernetes. The deployment configuration is located under [https://git.kluster.moll.re/anydev/deployment-backend/](https://git.kluster.moll.re/anydev/deployment-backend/).
## Development
TBD

View File

@@ -1 +0,0 @@
import app.src

View File

@@ -1,34 +0,0 @@
from .src.optimizer import solve_optimization
from .src.landmarks_manager import Landmark
from fastapi import FastAPI
app = FastAPI()
@app.get("/optimize/{max_steps}/{print_details}")
def main(max_steps: int, print_details: bool):
# CONSTRAINT TO RESPECT MAX NUMBER OF STEPS
#max_steps = 16
# Initialize all landmarks (+ start and goal). Order matters here
landmarks = []
landmarks.append(Landmark("départ", -1, (0, 0)))
landmarks.append(Landmark("tour eiffel", 99, (0,2))) # PUT IN JSON
landmarks.append(Landmark("arc de triomphe", 99, (0,4)))
landmarks.append(Landmark("louvre", 99, (0,6)))
landmarks.append(Landmark("montmartre", 99, (0,10)))
landmarks.append(Landmark("concorde", 99, (0,8)))
landmarks.append(Landmark("arrivée", -1, (0, 0)))
visiting_order = solve_optimization(landmarks, max_steps, print_details)
#return visiting_order
return("max steps :", max_steps, "\n", visiting_order)
"""if __name__ == "__main__":
main()"""

View File

@@ -1,57 +0,0 @@
from OSMPythonTools.api import Api
from OSMPythonTools.overpass import Overpass
from dataclasses import dataclass
# Defines the landmark class (aka some place there is to visit)
@dataclass
class Landmarkkkk :
name : str
attractiveness : int
id : int
@dataclass
class Landmark :
name : str
attractiveness : int
loc : tuple
# Converts a OSM id to a landmark
def add_from_id(id: int, score: int) :
try :
s = 'way/' + str(id) # prepare string for query
obj = api.query(s) # object to add
except :
s = 'relation/' + str(id) # prepare string for query
obj = api.query(s) # object to add
return Landmarkkkk(obj.tag('name:fr'), score, id) # create Landmark out of it
# take a lsit of tuples (id, score) to generate a list of landmarks
def generate_landmarks(ids_and_scores: list) :
L = []
for tup in ids_and_scores :
L.append(add_from_id(tup[0], tup[1]))
return L
api = Api()
l = (7515426, 70)
t = (5013364, 100)
n = (201611261, 99)
a = (226413508, 50)
m = (23762981, 30)
ids_and_scores = [t, l, n, a, m]
landmarks = generate_landmarks(ids_and_scores)
for obj in landmarks :
print(obj)

View File

@@ -1,23 +0,0 @@
import fastapi
from dataclasses import dataclass
@dataclass
class Destination:
name: str
location: tuple
attractiveness: int
d = Destination()
def get_route() -> list[Destination]:
return {"route": "Hello World"}
endpoint = ("/get_route", get_route)
end
if __name__ == "__main__":
fastapi.run()

View File

@@ -1,323 +0,0 @@
from scipy.optimize import linprog
import numpy as np
from scipy.linalg import block_diag
# landmarks = [Landmark_1, Landmark_2, ...]
# Convert the solution of the optimization into the list of edges to follow. Order is taken into account
def untangle(resx: list) :
N = len(resx) # length of res
L = int(np.sqrt(N)) # number of landmarks. CAST INTO INT but should not be a problem because N = L**2 by def.
n_edges = resx.sum() # number of edges
order = []
nonzeroind = np.nonzero(resx)[0] # the return is a little funny so I use the [0]
nonzero_tup = np.unravel_index(nonzeroind, (L,L))
indx = nonzero_tup[0].tolist()
indy = nonzero_tup[1].tolist()
vert = (indx[0], indy[0])
order.append(vert[0])
order.append(vert[1])
while len(order) < n_edges + 1 :
ind = indx.index(vert[1])
vert = (indx[ind], indy[ind])
order.append(vert[1])
return order
# Just to print the result
def print_res(res, landmarks: list, P) :
X = abs(res.x)
order = untangle(X)
things = []
"""N = int(np.sqrt(len(X)))
for i in range(N):
print(X[i*N:i*N+N])
print("Optimal value:", -res.fun) # Minimization, so we negate to get the maximum
print("Optimal point:", res.x)
for i,x in enumerate(X) : X[i] = round(x,0)
print(order)"""
if (X.sum()+1)**2 == len(X) :
print('\nAll landmarks can be visited within max_steps, the following order is suggested : ')
else :
print('Could not visit all the landmarks, the following order is suggested : ')
for idx in order :
print('- ' + landmarks[idx].name)
things.append(landmarks[idx].name)
steps = path_length(P, abs(res.x))
print("\nSteps walked : " + str(steps))
return things
# Checks for cases of circular symmetry in the result
def has_circle(resx: list) :
N = len(resx) # length of res
L = int(np.sqrt(N)) # number of landmarks. CAST INTO INT but should not be a problem because N = L**2 by def.
n_edges = resx.sum() # number of edges
nonzeroind = np.nonzero(resx)[0] # the return is a little funny so I use the [0]
nonzero_tup = np.unravel_index(nonzeroind, (L,L))
indx = nonzero_tup[0].tolist()
indy = nonzero_tup[1].tolist()
verts = []
for i, x in enumerate(indx) :
verts.append((x, indy[i]))
for vert in verts :
visited = []
visited.append(vert)
while len(visited) < n_edges + 1 :
try :
ind = indx.index(vert[1])
vert = (indx[ind], indy[ind])
if vert in visited :
return visited
else :
visited.append(vert)
except :
break
return []
# Constraint to not have d14 and d41 simultaneously. Does not prevent circular symmetry with more elements
def break_sym(landmarks, A_ub, b_ub):
L = len(landmarks)
upper_ind = np.triu_indices(L,0,L)
up_ind_x = upper_ind[0]
up_ind_y = upper_ind[1]
for i, _ in enumerate(up_ind_x) :
l = [0]*L*L
if up_ind_x[i] != up_ind_y[i] :
l[up_ind_x[i]*L + up_ind_y[i]] = 1
l[up_ind_y[i]*L + up_ind_x[i]] = 1
A_ub = np.vstack((A_ub,l))
b_ub.append(1)
"""for i in range(7):
print(l[i*7:i*7+7])
print("\n")"""
return A_ub, b_ub
# Constraint to not have circular paths. Want to go from start -> finish without unconnected loops
def break_circle(landmarks, A_ub, b_ub, circle) :
N = len(landmarks)
l = [0]*N*N
for index in circle :
x = index[0]
y = index[1]
l[x*N+y] = 1
A_ub = np.vstack((A_ub,l))
b_ub.append(len(circle)-1)
"""print("\n\nPREVENT CIRCLE")
for i in range(7):
print(l[i*7:i*7+7])
print("\n")"""
return A_ub, b_ub
# Constraint to respect max number of travels
def respect_number(landmarks, A_ub, b_ub):
h = []
for i in range(len(landmarks)) : h.append([1]*len(landmarks))
T = block_diag(*h)
"""for l in T :
for i in range(7):
print(l[i*7:i*7+7])
print("\n")"""
return np.vstack((A_ub, T)), b_ub + [1]*len(landmarks)
# Constraint to tie the problem together. Necessary but not sufficient to avoid circles
def respect_order(landmarks: list, A_eq, b_eq):
N = len(landmarks)
for i in range(N-1) : # Prevent stacked ones
if i == 0 :
continue
else :
l = [0]*N
l[i] = -1
l = l*N
for j in range(N) :
l[i*N + j] = 1
A_eq = np.vstack((A_eq,l))
b_eq.append(0)
"""for i in range(7):
print(l[i*7:i*7+7])
print("\n")"""
return A_eq, b_eq
# Compute manhattan distance between 2 locations
def manhattan_distance(loc1: tuple, loc2: tuple):
x1, y1 = loc1
x2, y2 = loc2
return abs(x1 - x2) + abs(y1 - y2)
# Constraint to not stay in position
def init_eq_not_stay(landmarks):
L = len(landmarks)
l = [0]*L*L
for i in range(L) :
for j in range(L) :
if j == i :
l[j + i*L] = 1
l[L-1] = 1 # cannot skip from start to finish
#A_eq = np.array([np.array(xi) for xi in A_eq]) # Must convert A_eq into an np array
l = np.array(np.array(l))
"""for i in range(7):
print(l[i*7:i*7+7])"""
return [l], [0]
# Initialize A and c. Compute the distances from all landmarks to each other and store attractiveness
# We want to maximize the sightseeing : max(c) st. A*x < b and A_eq*x = b_eq
def init_ub_dist(landmarks: list, max_steps: int):
# Objective function coefficients. a*x1 + b*x2 + c*x3 + ...
c = []
# Coefficients of inequality constraints (left-hand side)
A = []
for i, spot1 in enumerate(landmarks) :
dist_table = [0]*len(landmarks)
c.append(-spot1.attractiveness)
for j, spot2 in enumerate(landmarks) :
dist_table[j] = manhattan_distance(spot1.loc, spot2.loc)
A.append(dist_table)
c = c*len(landmarks)
A_ub = []
for line in A :
#print(line)
A_ub += line
return c, A_ub, [max_steps]
# Go through the landmarks and force the optimizer to use landmarks where attractiveness is set to -1
def respect_user_mustsee(landmarks: list, A_eq: list, b_eq: list) :
L = len(landmarks)
H = 0 # sort of heuristic to get an idea of the number of steps needed
for i in landmarks :
if i.name == "départ" : elem_prev = i # list of all matches
for i, elem in enumerate(landmarks) :
if elem.attractiveness == -1 :
l = [0]*L*L
if elem.name != "arrivée" :
for j in range(L) :
l[j +i*L] = 1
else : # This ensures we go to goal
for k in range(L-1) :
l[k*L+L-1] = 1
H += manhattan_distance(elem.loc, elem_prev.loc)
elem_prev = elem
"""for i in range(7):
print(l[i*7:i*7+7])
print("\n")"""
A_eq = np.vstack((A_eq,l))
b_eq.append(1)
return A_eq, b_eq, H
# Computes the path length given path matrix (dist_table) and a result
def path_length(P: list, resx: list) :
return np.dot(P, resx)
# Main optimization pipeline
def solve_optimization (landmarks, max_steps, printing_details) :
# SET CONSTRAINTS FOR INEQUALITY
c, A_ub, b_ub = init_ub_dist(landmarks, max_steps) # Add the distances from each landmark to the other
P = A_ub # store the paths for later. Needed to compute path length
A_ub, b_ub = respect_number(landmarks, A_ub, b_ub) # Respect max number of visits.
# TODO : Problems with circular symmetry
A_ub, b_ub = break_sym(landmarks, A_ub, b_ub) # break the symmetry. Only use the upper diagonal values
# SET CONSTRAINTS FOR EQUALITY
A_eq, b_eq = init_eq_not_stay(landmarks) # Force solution not to stay in same place
A_eq, b_eq, H = respect_user_mustsee(landmarks, A_eq, b_eq) # Check if there are user_defined must_see. Also takes care of start/goal
A_eq, b_eq = respect_order(landmarks, A_eq, b_eq) # Respect order of visit (only works when max_steps is limiting factor)
# Bounds for variables (x can only be 0 or 1)
x_bounds = [(0, 1)] * len(c)
# Solve linear programming problem
res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq = b_eq, bounds=x_bounds, method='highs', integrality=3)
# Raise error if no solution is found
if not res.success :
# Override the max_steps using the heuristic
for i, val in enumerate(b_ub) :
if val == max_steps : b_ub[i] = H
# Solve problem again :
res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq = b_eq, bounds=x_bounds, method='highs', integrality=3)
if not res.success :
s = "No solution could be found, even when increasing max_steps using the heuristic"
return s
#raise ValueError("No solution could be found, even when increasing max_steps using the heuristic")
# If there is a solution, we're good to go, just check for
else :
circle = has_circle(res.x)
i = 0
# Break the circular symmetry if needed
while len(circle) != 0 :
A_ub, b_ub = break_circle(landmarks, A_ub, b_ub, circle)
res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq = b_eq, bounds=x_bounds, method='highs', integrality=3)
circle = has_circle(res.x)
i += 1
if printing_details is True :
if i != 0 :
print(f"Neded to recompute paths {i} times because of unconnected loops...")
X = print_res(res, landmarks, P)
return X
else :
return untangle(res.x)

27
backend/src/constants.py Normal file
View File

@@ -0,0 +1,27 @@
import logging.config
from pathlib import Path
import os
LOCATION_PREFIX = Path('src')
PARAMETERS_DIR = LOCATION_PREFIX / 'parameters'
AMENITY_SELECTORS_PATH = PARAMETERS_DIR / 'amenity_selectors.yaml'
LANDMARK_PARAMETERS_PATH = PARAMETERS_DIR / 'landmark_parameters.yaml'
OPTIMIZER_PARAMETERS_PATH = PARAMETERS_DIR / 'optimizer_parameters.yaml'
cache_dir_string = os.getenv('OSM_CACHE_DIR', './cache')
OSM_CACHE_DIR = Path(cache_dir_string)
import logging
import yaml
LOGGING_CONFIG = LOCATION_PREFIX / 'log_config.yaml'
config = yaml.safe_load(LOGGING_CONFIG.read_text())
logging.config.dictConfig(config)
# if we are in a debug session, set the log level to debug
if os.getenv('DEBUG', False):
logging.getLogger().setLevel(logging.DEBUG)

View File

@@ -0,0 +1,34 @@
version: 1
disable_existing_loggers: False
formatters:
simple:
format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
handlers:
console:
class: rich.logging.RichHandler
formatter: simple
# access:
# class: logging.FileHandler
# filename: logs/access.log
# level: INFO
# formatter: simple
loggers:
uvicorn.error:
level: INFO
handlers:
- console
propagate: no
# uvicorn.access:
# level: INFO
# handlers:
# - access
# propagate: no
root:
level: INFO
handlers:
- console
propagate: yes

68
backend/src/main.py Normal file
View File

@@ -0,0 +1,68 @@
import logging
from fastapi import FastAPI, Query, Body
from structs.landmark import Landmark
from structs.preferences import Preferences
from structs.linked_landmarks import LinkedLandmarks
from utils.landmarks_manager import LandmarkManager
from utils.optimizer import Optimizer
from utils.refiner import Refiner
logger = logging.getLogger(__name__)
app = FastAPI()
manager = LandmarkManager()
optimizer = Optimizer()
refiner = Refiner(optimizer=optimizer)
@app.post("/route/new")
def get_route(preferences: Preferences, start: tuple[float, float], end: tuple[float, float] | None = None) -> str:
'''
Main function to call the optimizer.
:param preferences: the preferences specified by the user as the post body
:param start: the coordinates of the starting point as a tuple of floats (as url query parameters)
:param end: the coordinates of the finishing point as a tuple of floats (as url query parameters)
:return: the uuid of the first landmark in the optimized route
'''
if preferences is None:
raise ValueError("Please provide preferences in the form of a 'Preference' BaseModel class.")
if start is None:
raise ValueError("Please provide the starting coordinates as a tuple of floats.")
if end is None:
end = start
logger.info("No end coordinates provided. Using start=end.")
start_landmark = Landmark(name='start', type='start', location=(start[0], start[1]), osm_type='start', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
end_landmark = Landmark(name='end', type='finish', location=(end[0], end[1]), osm_type='end', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
# Generate the landmarks from the start location
landmarks, landmarks_short = manager.generate_landmarks_list(
center_coordinates = start,
preferences = preferences
)
# insert start and finish to the landmarks list
landmarks_short.insert(0, start_landmark)
landmarks_short.append(end_landmark)
# TODO infer these parameters from the preferences
max_walking_time = 4 # hours
detour = 30 # minutes
# First stage optimization
base_tour = optimizer.solve_optimization(max_walking_time*60, landmarks_short)
# Second stage optimization
refined_tour = refiner.refine_optimization(landmarks, base_tour, max_walking_time*60, detour)
linked_tour = LinkedLandmarks(refined_tour)
return linked_tour[0].uuid
@app.get("/landmark/{landmark_uuid}")
def get_landmark(landmark_uuid: str) -> Landmark:
#cherche dans linked_tour et retourne le landmark correspondant
pass

View File

@@ -0,0 +1,32 @@
nature:
leisure: park
geological: ''
natural:
- geyser
- hot_spring
- arch
- volcano
- stone
tourism:
- alpine_hut
- viewpoint
- zoo
waterway: waterfall
shopping:
shop:
- department_store
- mall
sightseeing:
tourism:
- museum
- attraction
- gallery
historic: ''
amenity:
- planetarium
- place_of_worship
- fountain
water:
- reflecting_pool

View File

@@ -0,0 +1,6 @@
city_bbox_side: 5000 #m
radius_close_to: 50
church_coeff: 0.8
park_coeff: 1.2
tag_coeff: 10
N_important: 40

View File

@@ -0,0 +1,4 @@
detour_factor: 1.4
detour_corridor_width: 200
average_walking_speed: 4.8
max_landmarks: 7

View File

@@ -0,0 +1,38 @@
from typing import Optional, Literal
from pydantic import BaseModel, Field
from uuid import uuid4
# Output to frontend
class Landmark(BaseModel) :
# Properties of the landmark
name : str
type: Literal['sightseeing', 'nature', 'shopping', 'start', 'finish']
location : tuple
osm_type : str
osm_id : int
attractiveness : int
n_tags : int
image_url : Optional[str] = None # TODO future
description : Optional[str] = None # TODO future
duration : Optional[int] = 0 # TODO future
# Unique ID of a given landmark
uuid: str = Field(default_factory=uuid4) # TODO implement this ASAP
# Additional properties depending on specific tour
must_do : Optional[bool] = False
must_avoid : Optional[bool] = False
is_secondary : Optional[bool] = False # TODO future
time_to_reach_next : Optional[int] = 0 # TODO fix this in existing code
next_uuid : Optional[str] = None # TODO implement this ASAP
def __hash__(self) -> int:
return self.uuid.int
def __str__(self) -> str:
time_to_next_str = f", time_to_next={self.time_to_reach_next}" if self.time_to_reach_next else ""
return f'Landmark({self.type}): [{self.name} @{self.location}, score={self.attractiveness}{time_to_next_str}]'

View File

@@ -0,0 +1,61 @@
import uuid
from .landmark import Landmark
from utils.get_time_separation import get_time
class LinkedLandmarks:
"""
A list of landmarks that are linked together, e.g. in a route.
Each landmark serves as a node in the linked list, but since we expect these to be consumed through the rest API, a pythonic reference to the next landmark is not well suited. Instead we use the uuid of the next landmark to reference the next landmark in the list. This is not very efficient, but appropriate for the expected use case ("short" trips with onyl few landmarks).
"""
_landmarks = list[Landmark]
total_time = int
uuid = str
def __init__(self, data: list[Landmark] = None) -> None:
"""
Initialize a new LinkedLandmarks object. This expects an ORDERED list of landmarks, where the first landmark is the starting point and the last landmark is the end point.
Args:
data (list[Landmark], optional): The list of landmarks that are linked together. Defaults to None.
"""
self.uuid = uuid.uuid4()
self._landmarks = data if data else []
self._link_landmarks()
def _link_landmarks(self) -> None:
"""
Create the links between the landmarks in the list by setting their .next_uuid and the .time_to_next attributes.
"""
self.total_time = 0
for i, landmark in enumerate(self._landmarks[:-1]):
landmark.next_uuid = self._landmarks[i + 1].uuid
time_to_next = get_time(landmark.location, self._landmarks[i + 1].location)
landmark.time_to_reach_next = time_to_next
self.total_time += time_to_next
self._landmarks[-1].next_uuid = None
self._landmarks[-1].time_to_reach_next = 0
def __getitem__(self, index: int) -> Landmark:
return self._landmarks[index]
def __str__(self) -> str:
return f"LinkedLandmarks, total time: {self.total_time} minutes, {len(self._landmarks)} stops: [{','.join([str(landmark) for landmark in self._landmarks])}]"
def asdict(self) -> dict:
"""
Convert the linked landmarks to a json serializable dictionary.
Returns:
dict: A dictionary representation of the linked landmarks.
"""
return {
'uuid': self.uuid,
'total_time': self.total_time,
'landmarks': [landmark.dict() for landmark in self._landmarks]
}

View File

@@ -0,0 +1,21 @@
from pydantic import BaseModel
from typing import Optional, Literal
class Preference(BaseModel) :
name: str
type: Literal['sightseeing', 'nature', 'shopping', 'start', 'finish']
score: int # score could be from 1 to 5
# Input for optimization
class Preferences(BaseModel) :
# Sightseeing / History & Culture (Musées, bâtiments historiques, opéras, églises)
sightseeing : Preference
# Nature (parcs, jardins, rivières, plages)
nature: Preference
# Shopping (diriger plutôt vers des zones / rues commerçantes)
shopping : Preference
max_time_minute: Optional[int] = 6*60
detour_tolerance_minute: Optional[int] = 0

85
backend/src/tester.py Normal file
View File

@@ -0,0 +1,85 @@
import logging
import yaml
from utils.landmarks_manager import LandmarkManager
from utils.optimizer import Optimizer
from utils.refiner import Refiner
from structs.landmark import Landmark
from structs.linked_landmarks import LinkedLandmarks
from structs.preferences import Preferences, Preference
logger = logging.getLogger(__name__)
def test(start_coords: tuple[float, float], finish_coords: tuple[float, float] = None) -> list[Landmark]:
manager = LandmarkManager()
optimizer = Optimizer()
refiner = Refiner(optimizer=optimizer)
preferences = Preferences(
sightseeing=Preference(
name='sightseeing',
type='sightseeing',
score = 5),
nature=Preference(
name='nature',
type='nature',
score = 5),
shopping=Preference(
name='shopping',
type='shopping',
score = 5),
max_time_minute=180,
detour_tolerance_minute=30
)
# Create start and finish
if finish_coords is None :
finish_coords = start_coords
start = Landmark(name='start', type='start', location=start_coords, osm_type='', osm_id=0, attractiveness=0, n_tags = 0)
finish = Landmark(name='finish', type='finish', location=finish_coords, osm_type='', osm_id=0, attractiveness=0, n_tags = 0)
#finish = Landmark(name='finish', type=LandmarkType(landmark_type='finish'), location=(48.8777055, 2.3640967), osm_type='finish', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
#start = Landmark(name='start', type=LandmarkType(landmark_type='start'), location=(48.847132, 2.312359), osm_type='start', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
#finish = Landmark(name='finish', type=LandmarkType(landmark_type='finish'), location=(48.843185, 2.344533), osm_type='finish', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
#finish = Landmark(name='finish', type=LandmarkType(landmark_type='finish'), location=(48.847132, 2.312359), osm_type='finish', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
# Generate the landmarks from the start location
landmarks, landmarks_short = manager.generate_landmarks_list(
center_coordinates = start_coords,
preferences = preferences
)
# Store data to file for debug purposes
# write_data(landmarks, "landmarks_Strasbourg.txt")
# Insert start and finish to the landmarks list
landmarks_short.insert(0, start)
landmarks_short.append(finish)
# First stage optimization
base_tour = optimizer.solve_optimization(max_time=preferences.max_time_minute, landmarks=landmarks_short)
# Second stage using linear optimization
refined_tour = refiner.refine_optimization(all_landmarks=landmarks, base_tour=base_tour, max_time = preferences.max_time_minute, detour = preferences.detour_tolerance_minute)
linked_tour = LinkedLandmarks(refined_tour)
logger.info(f"Optimized route: {linked_tour}")
# with open('linked_tour.yaml', 'w') as f:
# yaml.dump(linked_tour.asdict(), f)
return linked_tour
#test(tuple((48.8344400, 2.3220540))) # Café Chez César
#test(tuple((48.8375946, 2.2949904))) # Point random
#test(tuple((47.377859, 8.540585))) # Zurich HB
#test(tuple((45.7576485, 4.8330241))) # Lyon Bellecour
test(tuple((48.5848435, 7.7332974))) # Strasbourg Gare
#test(tuple((48.2067858, 16.3692340))) # Vienne

View File

@@ -0,0 +1,39 @@
import yaml
from geopy.distance import geodesic
import constants
with constants.OPTIMIZER_PARAMETERS_PATH.open('r') as f:
parameters = yaml.safe_load(f)
DETOUR_FACTOR = parameters['detour_factor']
AVERAGE_WALKING_SPEED = parameters['average_walking_speed']
def get_time(p1: tuple[float, float], p2: tuple[float, float]) -> int:
"""
Calculate the time in minutes to travel from one location to another.
Args:
p1 (Tuple[float, float]): Coordinates of the starting location.
p2 (Tuple[float, float]): Coordinates of the destination.
detour (float): Detour factor affecting the distance.
speed (float): Walking speed in kilometers per hour.
Returns:
int: Time to travel from p1 to p2 in minutes.
"""
# Compute the straight-line distance in km
if p1 == p2 :
return 0
else:
dist = geodesic(p1, p2).kilometers
# Consider the detour factor for average cityto deterline walking distance (in km)
walk_dist = dist*DETOUR_FACTOR
# Time to walk this distance (in minutes)
walk_time = walk_dist/AVERAGE_WALKING_SPEED*60
return round(walk_time)

View File

@@ -0,0 +1,365 @@
import math as m
import yaml
import logging
from OSMPythonTools.overpass import Overpass, overpassQueryBuilder
from OSMPythonTools.cachingStrategy import CachingStrategy, JSON
from pywikibot import ItemPage, Site
from pywikibot import config
config.put_throttle = 0
config.maxlag = 0
from structs.preferences import Preferences, Preference
from structs.landmark import Landmark
from .take_most_important import take_most_important
import constants
SIGHTSEEING = 'sightseeing'
NATURE = 'nature'
SHOPPING = 'shopping'
class LandmarkManager:
logger = logging.getLogger(__name__)
city_bbox_side: int # bbox side in meters
radius_close_to: int # radius in meters
church_coeff: float # coeff to adjsut score of churches
park_coeff: float # coeff to adjust score of parks
tag_coeff: float # coeff to adjust weight of tags
N_important: int # number of important landmarks to consider
def __init__(self) -> None:
with constants.AMENITY_SELECTORS_PATH.open('r') as f:
self.amenity_selectors = yaml.safe_load(f)
with constants.LANDMARK_PARAMETERS_PATH.open('r') as f:
parameters = yaml.safe_load(f)
self.city_bbox_side = parameters['city_bbox_side']
self.radius_close_to = parameters['radius_close_to']
self.church_coeff = parameters['church_coeff']
self.park_coeff = parameters['park_coeff']
self.tag_coeff = parameters['tag_coeff']
self.N_important = parameters['N_important']
self.overpass = Overpass()
CachingStrategy.use(JSON, cacheDir=constants.OSM_CACHE_DIR)
def generate_landmarks_list(self, center_coordinates: tuple[float, float], preferences: Preferences) -> tuple[list[Landmark], list[Landmark]]:
"""
Generate and prioritize a list of landmarks based on user preferences.
This method fetches landmarks from various categories (sightseeing, nature, shopping) based on the user's preferences
and current location. It scores and corrects these landmarks, removes duplicates, and then selects the most important
landmarks based on a predefined criterion.
Parameters:
center_coordinates (tuple[float, float]): The latitude and longitude of the center location around which to search.
preferences (Preferences): The user's preference settings that influence the landmark selection.
Returns:
tuple[list[Landmark], list[Landmark]]:
- A list of all existing landmarks.
- A list of the most important landmarks based on the user's preferences.
"""
L = []
bbox = self.create_bbox(center_coordinates)
# list for sightseeing
if preferences.sightseeing.score != 0:
score_function = lambda loc, n_tags: int((self.count_elements_close_to(loc) + ((n_tags**1.2)*self.tag_coeff) )*self.church_coeff)
L1 = self.fetch_landmarks(bbox, self.amenity_selectors['sightseeing'], SIGHTSEEING, score_function)
self.correct_score(L1, preferences.sightseeing)
L += L1
# list for nature
if preferences.nature.score != 0:
score_function = lambda loc, n_tags: int((self.count_elements_close_to(loc) + ((n_tags**1.2)*self.tag_coeff) )*self.park_coeff)
L2 = self.fetch_landmarks(bbox, self.amenity_selectors['nature'], NATURE, score_function)
self.correct_score(L2, preferences.nature)
L += L2
# list for shopping
if preferences.shopping.score != 0:
score_function = lambda loc, n_tags: int(self.count_elements_close_to(loc) + ((n_tags**1.2)*self.tag_coeff))
L3 = self.fetch_landmarks(bbox, self.amenity_selectors['shopping'], SHOPPING, score_function)
self.correct_score(L3, preferences.shopping)
L += L3
L = self.remove_duplicates(L)
L_constrained = take_most_important(L, self.N_important)
self.logger.info(f'Generated {len(L)} landmarks around {center_coordinates}, and constrained to {len(L_constrained)} most important ones.')
return L, L_constrained
def remove_duplicates(self, landmarks: list[Landmark]) -> list[Landmark]:
"""
Removes duplicate landmarks based on their names from the given list. Only retains the landmark with highest score
Parameters:
landmarks (list[Landmark]): A list of Landmark objects.
Returns:
list[Landmark]: A list of unique Landmark objects based on their names.
"""
L_clean = []
names = []
for landmark in landmarks:
if landmark.name in names:
continue
else:
names.append(landmark.name)
L_clean.append(landmark)
return L_clean
def correct_score(self, landmarks: list[Landmark], preference: Preference):
"""
Adjust the attractiveness score of each landmark in the list based on user preferences.
This method updates the attractiveness of each landmark by scaling it according to the user's preference score.
The score adjustment is computed using a simple linear transformation based on the preference score.
Args:
landmarks (list[Landmark]): A list of landmarks whose scores need to be corrected.
preference (Preference): The user's preference settings that influence the attractiveness score adjustment.
Raises:
TypeError: If the type of any landmark in the list does not match the expected type in the preference.
"""
if len(landmarks) == 0:
return
if landmarks[0].type != preference.type:
raise TypeError(f"LandmarkType {preference.type} does not match the type of Landmark {landmarks[0].name}")
for elem in landmarks:
elem.attractiveness = int(elem.attractiveness*preference.score/5) # arbitrary computation
def count_elements_close_to(self, coordinates: tuple[float, float]) -> int:
"""
Count the number of OpenStreetMap elements (nodes, ways, relations) within a specified radius of the given location.
This function constructs a bounding box around the specified coordinates based on the radius. It then queries
OpenStreetMap data to count the number of elements within that bounding box.
Args:
coordinates (tuple[float, float]): The latitude and longitude of the location to search around.
Returns:
int: The number of elements (nodes, ways, relations) within the specified radius. Returns 0 if no elements
are found or if an error occurs during the query.
"""
lat = coordinates[0]
lon = coordinates[1]
radius = self.radius_close_to
alpha = (180*radius) / (6371000*m.pi)
bbox = {'latLower':lat-alpha,'lonLower':lon-alpha,'latHigher':lat+alpha,'lonHigher': lon+alpha}
# Build the query to find elements within the radius
radius_query = overpassQueryBuilder(
bbox=[bbox['latLower'],
bbox['lonLower'],
bbox['latHigher'],
bbox['lonHigher']],
elementType=['node', 'way', 'relation']
)
try:
radius_result = self.overpass.query(radius_query)
N_elem = radius_result.countWays() + radius_result.countRelations()
self.logger.debug(f"There are {N_elem} ways/relations within 50m")
if N_elem is None:
return 0
return N_elem
except:
return 0
def create_bbox(self, coordinates: tuple[float, float]) -> tuple[float, float, float, float]:
"""
Create a bounding box around the given coordinates.
Args:
coordinates (tuple[float, float]): The latitude and longitude of the center of the bounding box.
Returns:
tuple[float, float, float, float]: The minimum latitude, minimum longitude, maximum latitude, and maximum longitude
defining the bounding box.
"""
lat = coordinates[0]
lon = coordinates[1]
# Half the side length in km (since it's a square bbox)
half_side_length_km = self.city_bbox_side / 2 / 1000
# Convert distance to degrees
lat_diff = half_side_length_km / 111 # 1 degree latitude is approximately 111 km
lon_diff = half_side_length_km / (111 * m.cos(m.radians(lat))) # Adjust for longitude based on latitude
# Calculate bbox
min_lat = lat - lat_diff
max_lat = lat + lat_diff
min_lon = lon - lon_diff
max_lon = lon + lon_diff
return min_lat, min_lon, max_lat, max_lon
def fetch_landmarks(self, bbox: tuple, amenity_selector: dict, landmarktype: str, score_function: callable) -> list[Landmark]:
"""
Fetches landmarks of a specified type from OpenStreetMap (OSM) within a bounding box centered on given coordinates.
Args:
bbox (tuple[float, float, float, float]): The bounding box coordinates (min_lat, min_lon, max_lat, max_lon).
amenity_selector (dict): The Overpass API query selector for the desired landmark type.
landmarktype (str): The type of the landmark (e.g., 'sightseeing', 'nature', 'shopping').
score_function (callable): The function to compute the score of the landmark based on its attributes.
Returns:
list[Landmark]: A list of Landmark objects that were fetched and filtered based on the provided criteria.
Notes:
- Landmarks are fetched using Overpass API queries.
- Selectors are translated from the dictionary to the Overpass query format. (e.g., 'amenity'='place_of_worship')
- Landmarks are filtered based on various conditions including tags and type.
- Scores are assigned to landmarks based on their attributes and surrounding elements.
"""
return_list = []
# caution, when applying a list of selectors, overpass will search for elements that match ALL selectors simultaneously
# we need to split the selectors into separate queries and merge the results
for sel in dict_to_selector_list(amenity_selector):
self.logger.debug(f"Current selector: {sel}")
query = overpassQueryBuilder(
bbox = bbox,
elementType = ['way', 'relation'],
selector = sel,
# conditions = [],
includeCenter = True,
out = 'body'
)
try:
result = self.overpass.query(query)
except Exception as e:
self.logger.error(f"Error fetching landmarks: {e}")
return
for elem in result.elements():
name = elem.tag('name') # Add name
location = (elem.centerLat(), elem.centerLon()) # Add coordinates (lat, lon)
# TODO: exclude these from the get go
# skip if unprecise location
if name is None or location[0] is None:
continue
# skip if unused
if 'disused:leisure' in elem.tags().keys():
continue
# skip if part of another building
if 'building:part' in elem.tags().keys() and elem.tag('building:part') == 'yes':
continue
osm_type = elem.type() # Add type: 'way' or 'relation'
osm_id = elem.id() # Add OSM id
elem_type = landmarktype # Add the landmark type as 'sightseeing,
n_tags = len(elem.tags().keys()) # Add number of tags
# remove specific tags
skip = False
for tag in elem.tags().keys():
if "pay" in tag:
n_tags -= 1 # discard payment options for tags
if "disused" in tag:
skip = True # skip disused amenities
break
if "wikipedia" in tag:
n_tags += 3 # wikipedia entries count more
if tag == "wikidata":
Q = elem.tag('wikidata')
site = Site("wikidata", "wikidata")
item = ItemPage(site, Q)
item.get()
n_languages = len(item.labels)
n_tags += n_languages/10
if elem_type != "nature":
if "leisure" in tag and elem.tag('leisure') == "park":
elem_type = "nature"
if landmarktype != SHOPPING:
if "shop" in tag:
skip = True
break
if tag == "building" and elem.tag('building') in ['retail', 'supermarket', 'parking']:
skip = True
break
if skip:
continue
score = score_function(location, n_tags)
if score != 0:
# Generate the landmark and append it to the list
landmark = Landmark(
name=name,
type=elem_type,
location=location,
osm_type=osm_type,
osm_id=osm_id,
attractiveness=score,
must_do=False,
n_tags=int(n_tags)
)
return_list.append(landmark)
self.logger.debug(f"Fetched {len(return_list)} landmarks of type {landmarktype} in {bbox}")
return return_list
def dict_to_selector_list(d: dict) -> list:
"""
Convert a dictionary of key-value pairs to a list of Overpass query strings.
Args:
d (dict): A dictionary of key-value pairs representing the selector.
Returns:
list: A list of strings representing the Overpass query selectors.
"""
return_list = []
for key, value in d.items():
if type(value) == list:
val = '|'.join(value)
return_list.append(f'{key}~"{val}"')
elif type(value) == str and len(value) == 0:
return_list.append(f'{key}')
else:
return_list.append(f'{key}={value}')
return return_list

View File

@@ -0,0 +1,519 @@
import yaml, logging
import numpy as np
from scipy.optimize import linprog
from collections import defaultdict, deque
from geopy.distance import geodesic
from structs.landmark import Landmark
from .get_time_separation import get_time
import constants
class Optimizer:
logger = logging.getLogger(__name__)
detour: int = None # accepted max detour time (in minutes)
detour_factor: float # detour factor of straight line vs real distance in cities
average_walking_speed: float # average walking speed of adult
max_landmarks: int # max number of landmarks to visit
def __init__(self) :
# load parameters from file
with constants.OPTIMIZER_PARAMETERS_PATH.open('r') as f:
parameters = yaml.safe_load(f)
self.detour_factor = parameters['detour_factor']
self.average_walking_speed = parameters['average_walking_speed']
self.max_landmarks = parameters['max_landmarks']
# Prevent the use of a particular solution
def prevent_config(self, resx):
"""
Prevent the use of a particular solution by adding constraints to the optimization.
Args:
resx (list[float]): List of edge weights.
Returns:
Tuple[list[int], list[int]]: A tuple containing a new row for constraint matrix and new value for upper bound vector.
"""
for i, elem in enumerate(resx):
resx[i] = round(elem)
N = len(resx) # Number of edges
L = int(np.sqrt(N)) # Number of landmarks
nonzeroind = np.nonzero(resx)[0] # the return is a little funky so I use the [0]
nonzero_tup = np.unravel_index(nonzeroind, (L,L))
ind_a = nonzero_tup[0].tolist()
vertices_visited = ind_a
vertices_visited.remove(0)
ones = [1]*L
h = [0]*N
for i in range(L) :
if i in vertices_visited :
h[i*L:i*L+L] = ones
return h, [len(vertices_visited)-1]
# Prevents the creation of the same circle (both directions)
def prevent_circle(self, circle_vertices: list, L: int) :
"""
Prevent circular paths by by adding constraints to the optimization.
Args:
circle_vertices (list): List of vertices forming a circle.
L (int): Number of landmarks.
Returns:
Tuple[np.ndarray, list[int]]: A tuple containing a new row for constraint matrix and new value for upper bound vector.
"""
l1 = [0]*L*L
l2 = [0]*L*L
for i, node in enumerate(circle_vertices[:-1]) :
next = circle_vertices[i+1]
l1[node*L + next] = 1
l2[next*L + node] = 1
s = circle_vertices[0]
g = circle_vertices[-1]
l1[g*L + s] = 1
l2[s*L + g] = 1
return np.vstack((l1, l2)), [0, 0]
def is_connected(self, resx) :
"""
Determine the order of visits and detect any circular paths in the given configuration.
Args:
resx (list): List of edge weights.
Returns:
Tuple[list[int], Optional[list[list[int]]]]: A tuple containing the visit order and a list of any detected circles.
"""
# first round the results to have only 0-1 values
for i, elem in enumerate(resx):
resx[i] = round(elem)
N = len(resx) # length of res
L = int(np.sqrt(N)) # number of landmarks. CAST INTO INT but should not be a problem because N = L**2 by def.
nonzeroind = np.nonzero(resx)[0] # the return is a little funny so I use the [0]
nonzero_tup = np.unravel_index(nonzeroind, (L,L))
ind_a = nonzero_tup[0].tolist()
ind_b = nonzero_tup[1].tolist()
# Step 1: Create a graph representation
graph = defaultdict(list)
for a, b in zip(ind_a, ind_b):
graph[a].append(b)
# Step 2: Function to perform BFS/DFS to extract journeys
def get_journey(start):
journey_nodes = []
visited = set()
stack = deque([start])
while stack:
node = stack.pop()
if node not in visited:
visited.add(node)
journey_nodes.append(node)
for neighbor in graph[node]:
if neighbor not in visited:
stack.append(neighbor)
return journey_nodes
# Step 3: Extract all journeys
all_journeys_nodes = []
visited_nodes = set()
for node in ind_a:
if node not in visited_nodes:
journey_nodes = get_journey(node)
all_journeys_nodes.append(journey_nodes)
visited_nodes.update(journey_nodes)
for l in all_journeys_nodes :
if 0 in l :
order = l
all_journeys_nodes.remove(l)
break
if len(all_journeys_nodes) == 0 :
return order, None
return order, all_journeys_nodes
def init_ub_dist(self, landmarks: list[Landmark], max_steps: int):
"""
Initialize the objective function coefficients and inequality constraints for the optimization problem.
This function computes the distances between all landmarks and stores their attractiveness to maximize sightseeing.
The goal is to maximize the objective function subject to the constraints A*x < b and A_eq*x = b_eq.
Args:
landmarks (list[Landmark]): List of landmarks.
max_steps (int): Maximum number of steps allowed.
Returns:
Tuple[list[float], list[float], list[int]]: Objective function coefficients, inequality constraint coefficients, and the right-hand side of the inequality constraint.
"""
# Objective function coefficients. a*x1 + b*x2 + c*x3 + ...
c = []
# Coefficients of inequality constraints (left-hand side)
A_ub = []
for spot1 in landmarks :
dist_table = [0]*len(landmarks)
c.append(-spot1.attractiveness)
for j, spot2 in enumerate(landmarks) :
t = get_time(spot1.location, spot2.location)
dist_table[j] = t
closest = sorted(dist_table)[:22]
for i, dist in enumerate(dist_table) :
if dist not in closest :
dist_table[i] = 32700
A_ub += dist_table
c = c*len(landmarks)
return c, A_ub, [max_steps]
def respect_number(self, L: int):
"""
Generate constraints to ensure each landmark is visited only once and cap the total number of visited landmarks.
Args:
L (int): Number of landmarks.
Returns:
Tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
"""
ones = [1]*L
zeros = [0]*L
A = ones + zeros*(L-1)
b = [1]
for i in range(L-1) :
h_new = zeros*i + ones + zeros*(L-1-i)
A = np.vstack((A, h_new))
b.append(1)
A = np.vstack((A, ones*L))
b.append(self.max_landmarks+1)
return A, b
# Constraint to not have d14 and d41 simultaneously. Does not prevent cyclic paths with more elements
def break_sym(self, L):
"""
Generate constraints to prevent simultaneous travel between two landmarks in both directions.
Args:
L (int): Number of landmarks.
Returns:
Tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
"""
upper_ind = np.triu_indices(L,0,L)
up_ind_x = upper_ind[0]
up_ind_y = upper_ind[1]
A = [0]*L*L
b = [1]
for i, _ in enumerate(up_ind_x[1:]) :
l = [0]*L*L
if up_ind_x[i] != up_ind_y[i] :
l[up_ind_x[i]*L + up_ind_y[i]] = 1
l[up_ind_y[i]*L + up_ind_x[i]] = 1
A = np.vstack((A,l))
b.append(1)
return A, b
def init_eq_not_stay(self, L: int):
"""
Generate constraints to prevent staying in the same position (e.g., removing d11, d22, d33, etc.).
Args:
L (int): Number of landmarks.
Returns:
Tuple[list[np.ndarray], list[int]]: Equality constraint coefficients and the right-hand side of the equality constraints.
"""
l = [0]*L*L
for i in range(L) :
for j in range(L) :
if j == i :
l[j + i*L] = 1
l = np.array(np.array(l), dtype=np.int8)
return [l], [0]
def respect_user_must_do(self, landmarks: list[Landmark]) :
"""
Generate constraints to ensure that landmarks marked as 'must_do' are included in the optimization.
Args:
landmarks (list[Landmark]): List of landmarks, where some are marked as 'must_do'.
Returns:
Tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
"""
L = len(landmarks)
A = [0]*L*L
b = [0]
for i, elem in enumerate(landmarks[1:]) :
if elem.must_do is True and elem.name not in ['finish', 'start']:
l = [0]*L*L
l[i*L:i*L+L] = [1]*L # set mandatory departures from landmarks tagged as 'must_do'
A = np.vstack((A,l))
b.append(1)
return A, b
def respect_user_must_avoid(self, landmarks: list[Landmark]) :
"""
Generate constraints to ensure that landmarks marked as 'must_avoid' are skipped in the optimization.
Args:
landmarks (list[Landmark]): List of landmarks, where some are marked as 'must_avoid'.
Returns:
Tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
"""
L = len(landmarks)
A = [0]*L*L
b = [0]
for i, elem in enumerate(landmarks[1:]) :
if elem.must_avoid is True and elem.name not in ['finish', 'start']:
l = [0]*L*L
l[i*L:i*L+L] = [1]*L
A = np.vstack((A,l))
b.append(0) # prevent departures from landmarks tagged as 'must_do'
return A, b
# Constraint to ensure start at start and finish at goal
def respect_start_finish(self, L: int):
"""
Generate constraints to ensure that the optimization starts at the designated start landmark and finishes at the goal landmark.
Args:
L (int): Number of landmarks.
Returns:
Tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
"""
l_start = [1]*L + [0]*L*(L-1) # sets departures only for start (horizontal ones)
l_start[L-1] = 0 # prevents the jump from start to finish
l_goal = [0]*L*L # sets arrivals only for finish (vertical ones)
l_L = [0]*L*(L-1) + [1]*L # prevents arrivals at start and departures from goal
for k in range(L-1) : # sets only vertical ones for goal (go to)
l_L[k*L] = 1
if k != 0 :
l_goal[k*L+L-1] = 1
A = np.vstack((l_start, l_goal))
b = [1, 1]
A = np.vstack((A,l_L))
b.append(0)
return A, b
def respect_order(self, L: int):
"""
Generate constraints to tie the optimization problem together and prevent stacked ones, although this does not fully prevent circles.
Args:
L (int): Number of landmarks.
Returns:
Tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
"""
A = [0]*L*L
b = [0]
for i in range(L-1) : # Prevent stacked ones
if i == 0 or i == L-1: # Don't touch start or finish
continue
else :
l = [0]*L
l[i] = -1
l = l*L
for j in range(L) :
l[i*L + j] = 1
A = np.vstack((A,l))
b.append(0)
return A, b
def link_list(self, order: list[int], landmarks: list[Landmark])->list[Landmark] :
"""
Compute the time to reach from each landmark to the next and create a list of landmarks with updated travel times.
Args:
order (list[int]): List of indices representing the order of landmarks to visit.
landmarks (list[Landmark]): List of all landmarks.
Returns:
list[Landmark]]: The updated linked list of landmarks with travel times
"""
L = []
j = 0
while j < len(order)-1 :
# get landmarks involved
elem = landmarks[order[j]]
next = landmarks[order[j+1]]
# get attributes
elem.time_to_reach_next = get_time(elem.location, next.location)
elem.must_do = True
elem.location = (round(elem.location[0], 5), round(elem.location[1], 5))
elem.next_uuid = next.uuid
L.append(elem)
j += 1
next.location = (round(next.location[0], 5), round(next.location[1], 5))
next.must_do = True
L.append(next)
return L
# Main optimization pipeline
def solve_optimization(
self,
max_time: int,
landmarks: list[Landmark],
) -> list[Landmark]:
"""
Main optimization pipeline to solve the landmark visiting problem.
This method sets up and solves a linear programming problem with constraints to find an optimal tour of landmarks,
considering user-defined must-visit landmarks, start and finish points, and ensuring no cycles are present.
Args:
max_time (int): Maximum time allowed for the tour in minutes.
landmarks (list[Landmark]): List of landmarks to visit.
Returns:
list[Landmark]: The optimized tour of landmarks with updated travel times, or None if no valid solution is found.
"""
L = len(landmarks)
# SET CONSTRAINTS FOR INEQUALITY
c, A_ub, b_ub = self.init_ub_dist(landmarks, max_time) # Add the distances from each landmark to the other
A, b = self.respect_number(L) # Respect max number of visits (no more possible stops than landmarks).
A_ub = np.vstack((A_ub, A), dtype=np.int16)
b_ub += b
A, b = self.break_sym(L) # break the 'zig-zag' symmetry
A_ub = np.vstack((A_ub, A), dtype=np.int16)
b_ub += b
# SET CONSTRAINTS FOR EQUALITY
A_eq, b_eq = self.init_eq_not_stay(L) # Force solution not to stay in same place
A, b = self.respect_user_must_do(landmarks) # Check if there are user_defined must_see. Also takes care of start/goal
A_eq = np.vstack((A_eq, A), dtype=np.int8)
b_eq += b
A, b = self.respect_user_must_avoid(landmarks) # Check if there are user_defined must_see. Also takes care of start/goal
A_eq = np.vstack((A_eq, A), dtype=np.int8)
b_eq += b
A, b = self.respect_start_finish(L) # Force start and finish positions
A_eq = np.vstack((A_eq, A), dtype=np.int8)
b_eq += b
A, b = self.respect_order(L) # Respect order of visit (only works when max_steps is limiting factor)
A_eq = np.vstack((A_eq, A), dtype=np.int8)
b_eq += b
# SET BOUNDS FOR DECISION VARIABLE (x can only be 0 or 1)
x_bounds = [(0, 1)]*L*L
# Solve linear programming problem
res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq = b_eq, bounds=x_bounds, method='highs', integrality=3)
# Raise error if no solution is found
if not res.success :
raise ArithmeticError("No solution could be found, the problem is overconstrained. Please adapt your must_dos")
# If there is a solution, we're good to go, just check for connectiveness
order, circles = self.is_connected(res.x)
#nodes, edges = is_connected(res.x)
i = 0
timeout = 80
while circles is not None and i < timeout:
A, b = self.prevent_config(res.x)
A_ub = np.vstack((A_ub, A))
b_ub += b
#A_ub, b_ub = prevent_circle(order, len(landmarks), A_ub, b_ub)
for circle in circles :
A, b = self.prevent_circle(circle, L)
A_eq = np.vstack((A_eq, A))
b_eq += b
res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq = b_eq, bounds=x_bounds, method='highs', integrality=3)
if not res.success :
raise ArithmeticError("Solving failed because of overconstrained problem")
return None
order, circles = self.is_connected(res.x)
#nodes, edges = is_connected(res.x)
if circles is None :
break
# print(i)
i += 1
if i == timeout :
raise TimeoutError(f"Optimization took too long. No solution found after {timeout} iterations.")
#sort the landmarks in the order of the solution
tour = [landmarks[i] for i in order]
self.logger.debug(f"Re-optimized {i} times, score: {int(-res.fun)}")
return tour

View File

@@ -0,0 +1,340 @@
import yaml, logging
from shapely import buffer, LineString, Point, Polygon, MultiPoint, concave_hull
from math import pi
from structs.landmark import Landmark
from . import take_most_important, get_time_separation
from .optimizer import Optimizer
import constants
class Refiner :
logger = logging.getLogger(__name__)
detour_factor: float # detour factor of straight line vs real distance in cities
detour_corridor_width: float # width of the corridor around the path
average_walking_speed: float # average walking speed of adult
max_landmarks: int # max number of landmarks to visit
optimizer: Optimizer # optimizer object
def __init__(self, optimizer: Optimizer) :
self.optimizer = optimizer
# load parameters from file
with constants.OPTIMIZER_PARAMETERS_PATH.open('r') as f:
parameters = yaml.safe_load(f)
self.detour_factor = parameters['detour_factor']
self.detour_corridor_width = parameters['detour_corridor_width']
self.average_walking_speed = parameters['average_walking_speed']
self.max_landmarks = parameters['max_landmarks'] + 4
def create_corridor(self, landmarks: list[Landmark], width: float) :
"""
Create a corridor around the path connecting the landmarks.
Args:
landmarks (list[Landmark]): the landmark path around which to create the corridor
width (float): Width of the corridor in meters.
Returns:
Geometry: A buffered geometry object representing the corridor around the path.
"""
corrected_width = (180*width)/(6371000*pi)
path = self.create_linestring(landmarks)
obj = buffer(path, corrected_width, join_style="mitre", cap_style="square", mitre_limit=2)
return obj
def create_linestring(self, tour: list[Landmark]) -> LineString :
"""
Create a `LineString` object from a tour.
Args:
tour (list[Landmark]): An ordered sequence of landmarks that represents the visiting order.
Returns:
LineString: A `LineString` object representing the path through the landmarks.
"""
points = []
for landmark in tour :
points.append(Point(landmark.location))
return LineString(points)
# Check if some coordinates are in area. Used for the corridor
def is_in_area(self, area: Polygon, coordinates) -> bool :
"""
Check if a given point is within a specified area.
Args:
area (Polygon): The polygon defining the area.
coordinates (tuple[float, float]): The coordinates of the point to check.
Returns:
bool: True if the point is within the area, otherwise False.
"""
point = Point(coordinates)
return point.within(area)
# Function to determine if two landmarks are close to each other
def is_close_to(self, location1: tuple[float], location2: tuple[float]):
"""
Determine if two locations are close to each other by rounding their coordinates to 3 decimal places.
Args:
location1 (tuple[float, float]): The coordinates of the first location.
location2 (tuple[float, float]): The coordinates of the second location.
Returns:
bool: True if the locations are within 0.001 degrees of each other, otherwise False.
"""
absx = abs(location1[0] - location2[0])
absy = abs(location1[1] - location2[1])
return absx < 0.001 and absy < 0.001
#return (round(location1[0], 3), round(location1[1], 3)) == (round(location2[0], 3), round(location2[1], 3))
def rearrange(self, tour: list[Landmark]) -> list[Landmark]:
"""
Rearrange landmarks to group nearby visits together.
This function reorders landmarks so that nearby landmarks are adjacent to each other in the list,
while keeping 'start' and 'finish' landmarks in their original positions.
Args:
tour (list[Landmark]): Ordered list of landmarks to be rearranged.
Returns:
list[Landmark]: The rearranged list of landmarks with grouped nearby visits.
"""
i = 1
while i < len(tour):
j = i+1
while j < len(tour):
if self.is_close_to(tour[i].location, tour[j].location) and tour[i].name not in ['start', 'finish'] and tour[j].name not in ['start', 'finish']:
# If they are not adjacent, move the j-th element to be adjacent to the i-th element
if j != i + 1:
tour.insert(i + 1, tour.pop(j))
break # Move to the next i-th element after rearrangement
j += 1
i += 1
return tour
def find_shortest_path_through_all_landmarks(self, landmarks: list[Landmark]) -> tuple[list[Landmark], Polygon]:
"""
Find the shortest path through all landmarks using a nearest neighbor heuristic.
This function constructs a path that starts from the 'start' landmark, visits all other landmarks in the order
of their proximity, and ends at the 'finish' landmark. It returns both the ordered list of landmarks and a
polygon representing the path.
Args:
landmarks (list[Landmark]): list of all landmarks including 'start' and 'finish'.
Returns:
tuple[list[Landmark], Polygon]: A tuple where the first element is the list of landmarks in the order they
should be visited, and the second element is a `Polygon` representing
the path connecting all landmarks.
"""
# Step 1: Find 'start' and 'finish' landmarks
start_idx = next(i for i, lm in enumerate(landmarks) if lm.type == 'start')
finish_idx = next(i for i, lm in enumerate(landmarks) if lm.type == 'finish')
start_landmark = landmarks[start_idx]
finish_landmark = landmarks[finish_idx]
# Step 2: Create a list of unvisited landmarks excluding 'start' and 'finish'
unvisited_landmarks = [lm for i, lm in enumerate(landmarks) if i not in [start_idx, finish_idx]]
# Step 3: Initialize the path with the 'start' landmark
path = [start_landmark]
coordinates = [landmarks[start_idx].location]
current_landmark = start_landmark
# Step 4: Use nearest neighbor heuristic to visit all landmarks
while unvisited_landmarks:
nearest_landmark = min(unvisited_landmarks, key=lambda lm: get_time_separation.get_time(current_landmark.location, lm.location))
path.append(nearest_landmark)
coordinates.append(nearest_landmark.location)
current_landmark = nearest_landmark
unvisited_landmarks.remove(nearest_landmark)
# Step 5: Finally add the 'finish' landmark to the path
path.append(finish_landmark)
coordinates.append(landmarks[finish_idx].location)
path_poly = Polygon(coordinates)
return path, path_poly
# Returns a list of minor landmarks around the planned path to enhance experience
def get_minor_landmarks(self, all_landmarks: list[Landmark], visited_landmarks: list[Landmark], width: float) -> list[Landmark] :
"""
Identify landmarks within a specified corridor that have not been visited yet.
This function creates a corridor around the path defined by visited landmarks and then finds landmarks that fall
within this corridor. It returns a list of these landmarks, excluding those already visited, sorted by their importance.
Args:
all_landmarks (list[Landmark]): list of all available landmarks.
visited_landmarks (list[Landmark]): list of landmarks that have already been visited.
width (float): Width of the corridor around the visited landmarks.
Returns:
list[Landmark]: list of important landmarks within the corridor that have not been visited yet.
"""
second_order_landmarks = []
visited_names = []
area = self.create_corridor(visited_landmarks, width)
for visited in visited_landmarks :
visited_names.append(visited.name)
for landmark in all_landmarks :
if self.is_in_area(area, landmark.location) and landmark.name not in visited_names:
second_order_landmarks.append(landmark)
return take_most_important.take_most_important(second_order_landmarks, len(visited_landmarks))
# Try fix the shortest path using shapely
def fix_using_polygon(self, tour: list[Landmark])-> list[Landmark] :
"""
Improve the tour path using geometric methods to ensure it follows a more optimal shape.
This function creates a polygon from the given tour and attempts to refine it using a concave hull. It reorders
the landmarks to fit within this refined polygon and adjusts the tour to ensure the 'start' landmark is at the
beginning. It also checks if the final polygon is simple and rearranges the tour if necessary.
Args:
tour (list[Landmark]): list of landmarks representing the current tour path.
Returns:
list[Landmark]: Refined list of landmarks in the order of visit to produce a better tour path.
"""
coords = []
coords_dict = {}
for landmark in tour :
coords.append(landmark.location)
if landmark.name != 'finish' :
coords_dict[landmark.location] = landmark
tour_poly = Polygon(coords)
better_tour_poly = tour_poly.buffer(0)
try :
xs, ys = better_tour_poly.exterior.xy
if len(xs) != len(tour) :
better_tour_poly = concave_hull(MultiPoint(coords)) # Create concave hull with "core" of tour leaving out start and finish
xs, ys = better_tour_poly.exterior.xy
except :
better_tour_poly = concave_hull(MultiPoint(coords)) # Create concave hull with "core" of tour leaving out start and finish
xs, ys = better_tour_poly.exterior.xy
# reverse the xs and ys
xs.reverse()
ys.reverse()
better_tour = [] # list of ordered visit
name_index = {} # Maps the name of a landmark to its index in the concave polygon
# Loop through the polygon and generate the better (ordered) tour
for i,x in enumerate(xs[:-1]) :
y = ys[i]
better_tour.append(coords_dict[tuple((x,y))])
name_index[coords_dict[tuple((x,y))].name] = i
# Scroll the list to have start in front again
start_index = name_index['start']
better_tour = better_tour[start_index:] + better_tour[:start_index]
# Append the finish back and correct the time to reach
better_tour.append(tour[-1])
# Rearrange only if polygon still not simple
if not better_tour_poly.is_simple :
better_tour = self.rearrange(better_tour)
return better_tour
def refine_optimization(
self,
all_landmarks: list[Landmark],
base_tour: list[Landmark],
max_time: int,
detour: int
) -> list[Landmark]:
"""
This is the second stage of the optimization. It refines the initial tour path by considering additional minor landmarks and optimizes the path.
This method evaluates the need for further optimization based on the initial tour. If a detour is required
it adds minor landmarks around the initial predicted path and solves a new optimization problem to find a potentially better
tour. It then links the new tour and adjusts it using a nearest neighbor heuristic and polygon-based methods to
ensure a valid path. The final tour is chosen based on the shortest distance.
Args:
all_landmarks (list[Landmark]): The full list of landmarks available for the optimization.
base_tour (list[Landmark]): The initial tour path to be refined.
max_time (int): The maximum time available for the tour in minutes.
detour (int): The maximum detour time allowed for the tour in minutes.
Returns:
list[Landmark]: The refined list of landmarks representing the optimized tour path.
"""
# No need to refine if no detour is taken
if detour == 0:
return base_tour
minor_landmarks = self.get_minor_landmarks(all_landmarks, base_tour, self.detour_corridor_width)
self.logger.info(f"Using {len(minor_landmarks)} minor landmarks around the predicted path")
# full set of visitable landmarks
full_set = base_tour[:-1] + minor_landmarks # create full set of possible landmarks (without finish)
full_set.append(base_tour[-1]) # add finish back
# get a new tour
new_tour = self.optimizer.solve_optimization(
max_time = max_time + detour,
landmarks = full_set
)
if new_tour is None:
self.logger.warning("No solution found for the refined tour. Returning the initial tour.")
new_tour = base_tour
# Find shortest path using the nearest neighbor heuristic
better_tour, better_poly = self.find_shortest_path_through_all_landmarks(new_tour)
# Fix the tour using Polygons if the path looks weird
if base_tour[0].location == base_tour[-1].location and not better_poly.is_valid :
better_tour = self.fix_using_polygon(better_tour)
return better_tour

View File

@@ -0,0 +1,38 @@
from structs.landmark import Landmark
def take_most_important(landmarks: list[Landmark], N_important) -> list[Landmark] :
L = len(landmarks)
L_copy = []
L_clean = []
scores = [0]*len(landmarks)
names = []
name_id = {}
for i, elem in enumerate(landmarks) :
if elem.name not in names :
names.append(elem.name)
name_id[elem.name] = [i]
L_copy.append(elem)
else :
name_id[elem.name] += [i]
scores = []
for j in name_id[elem.name] :
scores.append(L[j].attractiveness)
best_id = max(range(len(scores)), key=scores.__getitem__)
t = name_id[elem.name][best_id]
if t == i :
for old in L_copy :
if old.name == elem.name :
old.attractiveness = L[t].attractiveness
scores = [0]*len(L_copy)
for i, elem in enumerate(L_copy) :
scores[i] = elem.attractiveness
res = sorted(range(len(scores)), key = lambda sub: scores[sub])[-(N_important-L):]
for i, elem in enumerate(L_copy) :
if i in res :
L_clean.append(elem)
return L_clean

17
frontend/README.md Normal file
View File

@@ -0,0 +1,17 @@
# Frontend
This is the frontend of the project. It is a Flutter application that is designed to run on both Android and iOS devices. The frontend is responsible for displaying the user interface and handling user input. It communicates with the backend to retrieve and send data.
## Getting Started
The flutter application is divided into multiple chunks of code.
- the `lib` directory contains the main code of the application.
- the `android` and `ios` directories contain platform-specific code.
- the root directory contains configuration files and metadata.
To run the application, you need to have the Flutter SDK installed. You can find instructions on how to do this [here](https://flutter.dev/docs/get-started/install).
Once you have the Flutter SDK installed, you can locally install the dependencies by running:
```bash
flutter pub get
```

View File

@@ -4,6 +4,7 @@ gradle-wrapper.jar
/gradlew
/gradlew.bat
/local.properties
/secrets.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.

View File

@@ -0,0 +1,48 @@
## Android Setup
### Keystore setup
```bash
keytool -genkey -v -keystore release.keystore -keyalg RSA -keysize 2048 -validity 10000 -alias release
```
- This is required to store local credentials securely (not used for now).
- But necesseary in order to restrict the particular api key to a particular app (through the sha1 of the associated keystore).
### Building and secret credentials
Following the guide under [https://developers.google.com/maps/flutter-package/config#android_1](https://developers.google.com/maps/flutter-package/config#android_1).
- Add the following to `android/build.gradle`:
```gradle
buildscript {
dependencies {
classpath "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1"
}
}
```
- Add the following to `android/app/build.gradle`:
```gradle
plugins {
// ...
id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'
}
```
- Add the credentials to `android/secrets.properties`:
```properties
MAPS_API_KEY=YOUR_API_KEY
```
- Reference the credentials in `android/app/src/main/AndroidManifest.xml`:
```xml
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="${MAPS_API_KEY}" />
```
### Using the credentials in CI
- Add the base64 encoded credentials to the repository secrets (e.g. `ANDROID_SECRETS`).
```bash
base64 -i android/secrets.properties
```
- Use the following in the CI script:
```bash
echo {{ secrets.ANDROID_SECRETS }} | base64 -d > android/secrets.properties
```

View File

@@ -2,14 +2,19 @@ plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'
// last is probably not needed
}
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
def localProperties = new Properties()
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
} else {
throw new GradleException("local.properties not found")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
@@ -22,6 +27,20 @@ if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
def secretPropertiesFile = rootProject.file('secrets.properties')
def secretProperties = new Properties()
if (secretPropertiesFile.exists()) {
secretPropertiesFile.withReader('UTF-8') { reader ->
secretProperties.load(reader)
}
} else {
throw new GradleException("Secrets file secrets.properties not found")
}
android {
namespace "com.example.fast_network_navigation"
compileSdk flutter.compileSdkVersion
@@ -52,6 +71,9 @@ android {
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
// // Placeholders of keys that are replaced by the build system.
manifestPlaceholders += ['MAPS_API_KEY': secretProperties.getProperty('MAPS_API_KEY')]
}
buildTypes {

View File

@@ -1,4 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Required to fetch data from the internet. -->
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:label="fast_network_navigation"
android:name="${applicationName}"
@@ -32,11 +35,10 @@
/>
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="AIzaSyCeWk_D2xvfOHLidvV56EZeQCUybypEntw"
android:value="${MAPS_API_KEY}"
/>
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility?hl=en and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
@@ -48,7 +50,4 @@
<data android:mimeType="text/plain"/>
</intent>
</queries>
<!-- Required to fetch data from the internet. -->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -16,3 +16,14 @@ subprojects {
tasks.register("clean", Delete) {
delete rootProject.buildDir
}
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1"
}
}

View File

@@ -0,0 +1 @@
MAPS_API_KEY=Key

View File

@@ -2,6 +2,7 @@ import 'package:fast_network_navigation/modules/trips_overview.dart';
import 'package:fast_network_navigation/pages/new_trip.dart';
import 'package:fast_network_navigation/pages/tutorial.dart';
import 'package:fast_network_navigation/structs/trip.dart';
import 'package:fast_network_navigation/utils/load_trips.dart';
import 'package:flutter/material.dart';
import 'package:fast_network_navigation/pages/overview.dart';
@@ -11,10 +12,13 @@ import 'package:fast_network_navigation/pages/profile.dart';
// A side drawer is used to switch between pages
class BasePage extends StatefulWidget {
final String mainScreen;
final String currentMap;
final List<Trip> trips;
final Future<Trip>? trip;
const BasePage({super.key, required this.mainScreen, this.currentMap = "map", this.trips = const []});
const BasePage({
super.key,
required this.mainScreen,
this.trip
});
@override
State<BasePage> createState() => _BasePageState();
@@ -22,12 +26,14 @@ class BasePage extends StatefulWidget {
class _BasePageState extends State<BasePage> {
@override
Widget build(BuildContext context) {
Widget currentView = const Text("loading...");
Future<List<Trip>> trips = loadTrips();
if (widget.mainScreen == "map") {
currentView = NavigationOverview();
currentView = NavigationOverview(trip: widget.trip ?? getFirstTrip(trips));
} else if (widget.mainScreen == "tutorial") {
currentView = TutorialPage();
} else if (widget.mainScreen == "profile") {
@@ -40,9 +46,6 @@ class _BasePageState extends State<BasePage> {
appBar: AppBar(title: Text("City Nav")),
body: Center(child: currentView),
drawer: Drawer(
// Add a ListView to the drawer. This ensures the user can scroll
// through the options in the drawer if there isn't enough vertical
// space to fit everything.
child: Column(
children: [
DrawerHeader(
@@ -61,20 +64,10 @@ class _BasePageState extends State<BasePage> {
),
),
ListTile(
title: const Text('Start'),
title: const Text('Your Trips'),
leading: const Icon(Icons.map),
selected: widget.mainScreen == "map",
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => BasePage(mainScreen: "map")
)
);
},
),
ListTile(
title: const Text('Trip Overview'),
leading: const Icon(Icons.list),
onTap: () {},
trailing: ElevatedButton(
onPressed: () {
Navigator.of(context).push(
@@ -86,7 +79,19 @@ class _BasePageState extends State<BasePage> {
child: const Text('New'),
),
),
Expanded(child: TripsOverview()),
// Adds a ListView to the drawer. This ensures the user can scroll
// through the options in the drawer if there isn't enough vertical
// space to fit everything.
Expanded(
child: TripsOverview(trips: trips),
),
ElevatedButton(
onPressed: () async {
removeAllTripsFromPrefs();
},
child: const Text('Clear trips'),
),
const Divider(),
ListTile(
title: const Text('How to use'),
@@ -101,6 +106,7 @@ class _BasePageState extends State<BasePage> {
},
),
// settings in the bottom of the drawer
ListTile(
title: const Text('Settings'),
leading: const Icon(Icons.settings),
@@ -113,7 +119,6 @@ class _BasePageState extends State<BasePage> {
);
},
),
// settings in the bottom of the drawer
],
),
),
@@ -121,3 +126,9 @@ class _BasePageState extends State<BasePage> {
}
}
Future<Trip> getFirstTrip (Future<List<Trip>> trips) async {
List<Trip> tripsf = await trips;
return tripsf[0];
}

View File

@@ -17,4 +17,3 @@ class App extends StatelessWidget {
);
}
}

View File

@@ -1,44 +1,73 @@
import 'package:fast_network_navigation/structs/trip.dart';
import 'package:flutter/material.dart';
Widget Greeter (ThemeData theme, {bool full = false}) {
String greeterText = "";
try {
String cityName = getCityName();
greeterText = "Welcome to $cityName!";
} catch (e) {
greeterText = "Welcome ...";
}
class Greeter extends StatefulWidget {
final Future<Trip> trip;
final bool standalone;
Widget topGreeter = Text(
greeterText,
style: TextStyle(color: theme.primaryColor, fontSize: 24.0, fontWeight: FontWeight.bold),
maxLines: 1,
);
Greeter({
required this.standalone,
required this.trip
});
Widget bottomGreeter = Container();
if (full) {
bottomGreeter = Text(
"Busy day ahead? Here is how to make the most of it!",
style: TextStyle(color: Colors.black, fontSize: 18),
textAlign: TextAlign.center,
);
}
Widget greeter = Center(
child: Column(
children: [
if (!full) Padding(padding: EdgeInsets.only(top: 24.0)),
topGreeter,
if (full) bottomGreeter,
Padding(padding: EdgeInsets.only(bottom: 24.0)),
],
),
);
return greeter;
@override
State<Greeter> createState() => _GreeterState();
}
String getCityName() {
return "Paris";
class _GreeterState extends State<Greeter> {
Widget greeterBuild (BuildContext context, AsyncSnapshot<Trip> snapshot) {
ThemeData theme = Theme.of(context);
String cityName = "";
if (snapshot.hasData) {
cityName = snapshot.data?.cityName ?? '...';
} else if (snapshot.hasError) {
cityName = "error";
} else { // still awaiting the cityname
cityName = "...";
}
Widget topGreeter = Text(
'Welcome to $cityName!',
style: TextStyle(color: theme.primaryColor, fontWeight: FontWeight.bold, fontSize: 24),
);
if (widget.standalone) {
return Center(
child: Padding(
padding: EdgeInsets.only(top: 24.0),
child: topGreeter,
),
);
} else {
return Center(
child: Column(
children: [
Padding(padding: EdgeInsets.only(top: 24.0)),
topGreeter,
bottomGreeter,
Padding(padding: EdgeInsets.only(bottom: 24.0)),
],
)
);
}
}
Widget bottomGreeter = const Text(
"Busy day ahead? Here is how to make the most of it!",
style: TextStyle(color: Colors.black, fontSize: 18),
textAlign: TextAlign.center,
);
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: widget.trip,
builder: greeterBuild,
);
}
}

View File

@@ -15,59 +15,66 @@ class _LandmarkCardState extends State<LandmarkCard> {
@override
Widget build(BuildContext context) {
ThemeData theme = Theme.of(context);
return Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15.0),
),
child: Row(
children: [
Container(
width: 160,
height: 160,
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(15.0),
bottomLeft: Radius.circular(15.0),
),
image: DecorationImage(
image: NetworkImage('https://upload.wikimedia.org/wikipedia/commons/thumb/a/a8/Tour_Eiffel_Wikimedia_Commons.jpg/1037px-Tour_Eiffel_Wikimedia_Commons.jpg'),
return Container(
height: 160,
child: Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15.0),
),
elevation: 5,
clipBehavior: Clip.antiAliasWithSaveLayer,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container( // the image on the left
// inherit the height of the parent container
height: double.infinity,
// force a fixed width
width: 160,
child: Image.network(
widget.landmark.imageURL ?? '',
errorBuilder: (context, error, stackTrace) => Icon(Icons.question_mark_outlined),
// TODO: make this a switch statement to load a placeholder if null
// cover the whole container meaning the image will be cropped
fit: BoxFit.cover,
),
),
),
Padding(
padding: EdgeInsets.all(10),
child: Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
widget.landmark.name,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
SizedBox(height: 5),
Text(
"${widget.landmark.name} (${widget.landmark.type.name})",
style: TextStyle(fontSize: 14),
),
],
Flexible(
child: Padding(
padding: EdgeInsets.all(10),
child: Column(
children: [
Row(
children: [
Flexible(
child: Text(
widget.landmark.name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
maxLines: 2,
),
)
],
),
Row(
children: [
Flexible(
child: Text(
"${widget.landmark.name} (${widget.landmark.type.name})",
style: const TextStyle(fontSize: 14),
),
)
]
),
],
),
),
),
),
// Align(
// alignment: Alignment.topRight,
// child: Icon(Icons.push_pin, color: theme.primaryColor),
// ),
],
],
),
),
);
}
}
}

View File

@@ -1,32 +1,38 @@
import 'dart:collection';
import 'package:fast_network_navigation/modules/landmark_card.dart';
import 'package:fast_network_navigation/structs/landmark.dart';
import 'package:fast_network_navigation/utils/get_landmarks.dart';
import 'package:fast_network_navigation/structs/trip.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class loadLandmarksOverview extends StatefulWidget {
const loadLandmarksOverview({super.key});
class LandmarksOverview extends StatefulWidget {
final Future<Trip>? trip;
const LandmarksOverview({super.key, this.trip});
@override
State<loadLandmarksOverview> createState() => _loadLandmarksOverviewState();
State<LandmarksOverview> createState() => _LandmarksOverviewState();
}
class _loadLandmarksOverviewState extends State<loadLandmarksOverview> {
final Future<List<Landmark>> _landmarks = fetchLandmarks();
class _LandmarksOverviewState extends State<LandmarksOverview> {
// final Future<List<Landmark>> _landmarks = fetchLandmarks();
@override
Widget build(BuildContext context) {
final Future<LinkedList<Landmark>> _landmarks = getLandmarks(widget.trip);
return DefaultTextStyle(
style: Theme.of(context).textTheme.displayMedium!,
textAlign: TextAlign.center,
child: FutureBuilder<List<Landmark>>(
child: FutureBuilder<LinkedList<Landmark>>(
future: _landmarks,
builder: (BuildContext context, AsyncSnapshot<List<Landmark>> snapshot) {
builder: (BuildContext context, AsyncSnapshot<LinkedList<Landmark>> snapshot) {
List<Widget> children;
if (snapshot.hasData) {
children = [landmarksWithSteps(snapshot.data!)];
children = [landmarksWithSteps(snapshot.data!), saveButton()];
} else if (snapshot.hasError) {
children = <Widget>[
const Icon(
@@ -36,7 +42,7 @@ class _loadLandmarksOverviewState extends State<loadLandmarksOverview> {
),
Padding(
padding: const EdgeInsets.only(top: 16),
child: Text('Error: ${snapshot.error}'),
child: Text('Error: ${snapshot.error}', style: TextStyle(fontSize: 12)),
),
];
} else {
@@ -52,50 +58,73 @@ class _loadLandmarksOverviewState extends State<loadLandmarksOverview> {
),
);
}
Widget saveButton() => ElevatedButton(
onPressed: () async {
Trip? trip = await widget.trip;
SharedPreferences prefs = await SharedPreferences.getInstance();
trip?.toPrefs(prefs);
},
child: const Text('Save'),
);
}
Widget landmarksWithSteps(List<Landmark> landmarks) {
Widget landmarksWithSteps(LinkedList<Landmark> landmarks) {
List<Widget> children = [];
for (Landmark landmark in landmarks) {
children.add(LandmarkCard(landmark));
children.add(stepBetweenLandmarks());
if (landmark.next != null) {
Widget step = stepBetweenLandmarks(landmark, landmark.next!);
children.add(step);
}
}
return Column(
children: children.sublist(0, children.length - 1)
children: children
);
}
Widget stepBetweenLandmarks() {
Widget stepBetweenLandmarks(Landmark before, Landmark after) {
// This is a simple widget that draws a line between landmark-cards
// It's a vertical dotted line
// Next to the line is the icon for the mode of transport (walking for now) and the estimated time
// There is also a button to open the navigation instructions as a new intent
return Row(
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
border: Border(
left: BorderSide(width: 1.0, color: Colors.black),
),
),
child: Column(
return Container(
margin: EdgeInsets.all(10),
padding: EdgeInsets.all(10),
decoration: BoxDecoration(
border: Border(
left: BorderSide(width: 3.0, color: Colors.black),
),
// gradient: LinearGradient(
// begin: Alignment.topLeft,
// end: Alignment.bottomRight,
// colors: [Colors.grey, Colors.white, Colors.white],
// ),
),
child: Row(
children: [
Column(
children: [
Icon(Icons.directions_walk),
Text("5 min", style: TextStyle(fontSize: 10)),
],
),
),
Spacer(),
ElevatedButton(
onPressed: () {
// Open navigation instructions
},
child: Text("Navigate"),
),
],
Spacer(),
ElevatedButton(
onPressed: () {
// Open navigation instructions
},
child: Text("Navigate"),
),
],
),
);
}
}
Future<LinkedList<Landmark>> getLandmarks (Future<Trip>? trip) async {
Trip tripf = await trip!;
return tripf.landmarks;
}

View File

@@ -1,9 +1,18 @@
import 'dart:collection';
import 'package:fast_network_navigation/structs/landmark.dart';
import 'package:fast_network_navigation/utils/get_landmarks.dart';
import 'package:fast_network_navigation/structs/trip.dart';
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
class MapWidget extends StatefulWidget {
final Future<Trip>? trip;
MapWidget({
this.trip
});
@override
State<MapWidget> createState() => _MapWidgetState();
}
@@ -18,12 +27,18 @@ class _MapWidgetState extends State<MapWidget> {
Set<Marker> markers = <Marker>{};
void _onMapCreated(GoogleMapController controller) {
void _onMapCreated(GoogleMapController controller) async {
mapController = controller;
Trip? trip = await widget.trip;
List<double>? newLocation = trip?.landmarks.first.location;
if (newLocation != null) {
CameraUpdate update = CameraUpdate.newLatLng(LatLng(newLocation[0], newLocation[1]));
controller.moveCamera(update);
}
drawLandmarks();
}
void _onCameraIdle() {
// print(mapController.getLatLng(ScreenCoordinate(x: 0, y: 0)));
}
@@ -31,16 +46,19 @@ class _MapWidgetState extends State<MapWidget> {
void drawLandmarks() async {
// (re)draws landmarks on the map
List<Landmark> landmarks = await fetchLandmarks();
setState(() {
for (Landmark landmark in landmarks) {
markers.add(Marker(
markerId: MarkerId(landmark.name),
position: LatLng(landmark.location[0], landmark.location[1]),
infoWindow: InfoWindow(title: landmark.name, snippet: landmark.type.name),
));
}
});
Trip? trip = await widget.trip;
LinkedList<Landmark>? landmarks = trip?.landmarks;
if (landmarks != null){
setState(() {
for (Landmark landmark in landmarks) {
markers.add(Marker(
markerId: MarkerId(landmark.name),
position: LatLng(landmark.location[0], landmark.location[1]),
infoWindow: InfoWindow(title: landmark.name, snippet: landmark.type.name),
));
}
});
}
}
@@ -52,7 +70,7 @@ class _MapWidgetState extends State<MapWidget> {
onCameraIdle: _onCameraIdle,
// onLongPress: ,
markers: markers,
cloudMapId: '41c21ac9b81dbfd8',
);
}
}

View File

@@ -2,19 +2,21 @@ import 'package:flutter/material.dart';
import 'package:fast_network_navigation/layout.dart';
import 'package:fast_network_navigation/structs/trip.dart';
import 'package:fast_network_navigation/utils/get_trips.dart';
class TripsOverview extends StatefulWidget {
const TripsOverview({super.key});
final Future<List<Trip>> trips;
const TripsOverview({
super.key,
required this.trips,
});
@override
State<TripsOverview> createState() => _TripsOverviewState();
}
class _TripsOverviewState extends State<TripsOverview> {
final Future<List<Trip>> _trips = loadTrips();
// final Future<List<Trip>> _trips = loadTrips();
Widget listBuild (BuildContext context, AsyncSnapshot<List<Trip>> snapshot) {
@@ -23,12 +25,12 @@ class _TripsOverviewState extends State<TripsOverview> {
children = List<Widget>.generate(snapshot.data!.length, (index) {
Trip trip = snapshot.data![index];
return ListTile(
title: Text("Trip to ${trip.cityName} (${trip.landmarks.length} stops)"),
title: Text("Trip to ${trip.cityName}"),
leading: Icon(Icons.pin_drop),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => BasePage(mainScreen: "map") //, trip: trip)
builder: (context) => BasePage(mainScreen: "map", trip: Future.value(trip))
)
);
},
@@ -58,7 +60,7 @@ class _TripsOverviewState extends State<TripsOverview> {
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: _trips,
future: widget.trips,
builder: listBuild,
);
}

View File

@@ -1,13 +1,21 @@
import 'package:fast_network_navigation/modules/greeter.dart';
import 'package:flutter/material.dart';
import 'package:sliding_up_panel/sliding_up_panel.dart';
import 'package:fast_network_navigation/structs/trip.dart';
import 'package:fast_network_navigation/modules/landmarks_overview.dart';
import 'package:fast_network_navigation/modules/map.dart';
import 'package:fast_network_navigation/modules/greeter.dart';
class NavigationOverview extends StatefulWidget {
final Future<Trip> trip;
NavigationOverview({
required this.trip
});
@override
State<NavigationOverview> createState() => _NavigationOverviewState();
}
@@ -18,16 +26,16 @@ class _NavigationOverviewState extends State<NavigationOverview> {
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
return SlidingUpPanel(
renderPanelSheet: false,
panel: _floatingPanel(theme),
collapsed: _floatingCollapsed(theme),
body: MapWidget()
panel: _floatingPanel(),
collapsed: _floatingCollapsed(),
body: MapWidget(trip: widget.trip)
);
}
Widget _floatingCollapsed(ThemeData theme){
Widget _floatingCollapsed(){
final ThemeData theme = Theme.of(context);
return Container(
decoration: BoxDecoration(
color: theme.canvasColor,
@@ -35,11 +43,12 @@ class _NavigationOverviewState extends State<NavigationOverview> {
boxShadow: []
),
child: Greeter(theme)
child: Greeter(standalone: true, trip: widget.trip)
);
}
Widget _floatingPanel(ThemeData theme){
Widget _floatingPanel(){
final ThemeData theme = Theme.of(context);
return Container(
decoration: BoxDecoration(
color: Colors.white,
@@ -57,8 +66,8 @@ class _NavigationOverviewState extends State<NavigationOverview> {
child: SingleChildScrollView(
child: Column(
children: <Widget>[
Greeter(theme, full: true),
loadLandmarksOverview(),
Greeter(standalone: false, trip: widget.trip),
LandmarksOverview(trip: widget.trip),
],
),
),

View File

@@ -53,12 +53,6 @@ class _ImportanceSlidersState extends State<ImportanceSliders> {
UserPreferences _prefs = UserPreferences();
@override
void initState() {
_prefs.load();
super.initState();
}
List<Card> _createSliders() {
List<Card> sliders = [];
for (SinglePreference pref in _prefs.preferences) {
@@ -80,7 +74,7 @@ class _ImportanceSlidersState extends State<ImportanceSliders> {
},
)
),
margin: EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 0),
margin: const EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 0),
shadowColor: Colors.grey,
));
}
@@ -89,6 +83,7 @@ class _ImportanceSlidersState extends State<ImportanceSliders> {
@override
Widget build(BuildContext context) {
return Column(children: _createSliders());
}
}

View File

@@ -1,54 +1,88 @@
class Landmark {
final String name;
final List location;
final LandmarkType type;
// final String description;
// final Duration duration;
// final bool visited;
import 'dart:collection';
import 'dart:convert';
const Landmark({
import 'package:shared_preferences/shared_preferences.dart';
final class Landmark extends LinkedListEntry<Landmark>{
// A linked node of a list of Landmarks
final String uuid;
final String name;
final List<double> location;
final LandmarkType type;
final bool? isSecondary;
// description to be shown in the overview
final String? imageURL;
final String? description;
final Duration? duration;
final bool? visited;
// Next node
// final Landmark? next;
final Duration? tripTime;
Landmark({
required this.uuid,
required this.name,
required this.location,
required this.type,
// required this.description,
// required this.duration,
// required this.visited,
this.isSecondary,
this.imageURL,
this.description,
this.duration,
this.visited,
// this.next,
this.tripTime,
});
factory Landmark.fromJson(Map<String, dynamic> json) {
return switch (json) {
{
'loc': List location,
'name': String name,
'type': String type,
// 'description': String description,
// 'duration': int duration,
// 'visited': bool visited
} =>
Landmark(
name: name,
location: location,
type: LandmarkType(name: type)
// description: description,
// duration: Duration(minutes: duration),
// visited: visited
),
_ => throw const FormatException('Failed to load destination.'),
};
factory Landmark.fromJson(Map<String, dynamic> json) {
if (json
case { // automatically match all the non-optionals and cast them to the right type
'uuid': String uuid,
'name': String name,
'location': List<dynamic> location,
'type': String type,
}) {
// refine the parsing on a few
List<double> locationFixed = List<double>.from(location);
// parse the rest separately, they could be missing
LandmarkType typeFixed = LandmarkType(name: type);
final isSecondary = json['is_secondary'] as bool?;
final imageURL = json['image_url'] as String?;
final description = json['description'] as String?;
var duration = Duration(minutes: json['duration'] ?? 0) as Duration?;
if (duration == const Duration()) {duration = null;};
final visited = json['visited'] as bool?;
return Landmark(
uuid: uuid, name: name, location: locationFixed, type: typeFixed, isSecondary: isSecondary, imageURL: imageURL, description: description, duration: duration, visited: visited);
} else {
throw FormatException('Invalid JSON: $json');
}
}
Map<String, dynamic> toJson() {
return {
'name': name,
'location': location,
'type': type.name,
// 'description': description,
// 'duration': duration.inMinutes,
// 'visited': visited
};
}
@override
bool operator ==(Object other) {
return other is Landmark && uuid == other.uuid;
}
Map<String, dynamic> toJson() => {
'uuid': uuid,
'name': name,
'location': location,
'type': type.name,
'is_secondary': isSecondary,
'image_url': imageURL,
'description': description,
'duration': duration?.inMinutes,
'visited': visited
};
}
@@ -65,3 +99,18 @@ class LandmarkType {
}
// Helpers
// Handling the landmarks requires a little bit of special care because the linked list is not directly representable in json
(Landmark, String?) getLandmarkFromPrefs(SharedPreferences prefs, String uuid) {
String? content = prefs.getString('landmark_$uuid');
Map<String, dynamic> json = jsonDecode(content!);
String? nextUUID = json['next_uuid'];
return (Landmark.fromJson(json), nextUUID);
}
void landmarkToPrefs(SharedPreferences prefs, Landmark current, Landmark? next) {
Map<String, dynamic> json = current.toJson();
json['next_uuid'] = next?.uuid;
prefs.setString('landmark_${current.uuid}', jsonEncode(json));
}

View File

@@ -0,0 +1,46 @@
// import "package:fast_network_navigation/structs/landmark.dart";
// class Linked<Landmark> {
// Landmark? head;
// Linked();
// // class methods
// bool get isEmpty => head == null;
// // Add a new node to the end of the list
// void add(Landmark value) {
// if (isEmpty) {
// // If the list is empty, set the new node as the head
// head = value;
// } else {
// Landmark? current = head;
// while (current!.next != null) {
// // Traverse the list to find the last node
// current = current.next;
// }
// current.next = value; // Set the new node as the next node of the last node
// }
// }
// // Remove the first node with the given value
// void remove(Landmark value) {
// if (isEmpty) return;
// // If the value is in the head node, update the head to the next node
// if (head! == value) {
// head = head.next;
// return;
// }
// var current = head;
// while (current!.next != null) {
// if (current.next! == value) {
// // If the value is found in the next node, skip the next node
// current.next = current.next.next;
// return;
// }
// current = current.next;
// }
// }
// }

View File

@@ -67,16 +67,16 @@ class UserPreferences {
void save() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
for (SinglePreference pref in preferences) {
prefs.setInt(pref.key, pref.value);
sharedPrefs.setInt(pref.key, pref.value);
}
}
void load() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
for (SinglePreference pref in preferences) {
pref.value = prefs.getInt(pref.key) ?? 0;
pref.value = sharedPrefs.getInt(pref.key) ?? 0;
}
}
}

View File

@@ -5,7 +5,7 @@ class Route {
final String name;
final Duration duration;
final List<Landmark> landmarks;
Route({
required this.name,
required this.duration,

View File

@@ -1,25 +1,75 @@
// Represents a collection of landmarks that represent a journey
// Different instances of a Trip can be saved and loaded by the user
import 'dart:collection';
import 'dart:convert';
import 'package:fast_network_navigation/structs/landmark.dart';
import 'package:shared_preferences/shared_preferences.dart';
class Trip {
final String uuid;
final String cityName;
final List<Landmark> landmarks;
// TODO: cityName should be inferred from coordinates of the Landmarks
final LinkedList<Landmark> landmarks;
// could be empty as well
Trip({required this.uuid, required this.cityName, required this.landmarks});
Trip({
required this.uuid,
required this.cityName,
required this.landmarks,
});
factory Trip.fromJson(Map<String, dynamic> json) {
List<Landmark> landmarks = [];
for (var landmark in json['landmarks']) {
landmarks.add(Landmark.fromJson(landmark));
}
return Trip(
uuid: json['uuid'],
cityName: json['cityName'],
landmarks: landmarks,
cityName: json['city_name'],
landmarks: LinkedList()
);
}
}
factory Trip.fromPrefs(SharedPreferences prefs, String uuid) {
String? content = prefs.getString('trip_$uuid');
Map<String, dynamic> json = jsonDecode(content!);
Trip trip = Trip.fromJson(json);
String? firstUUID = json['entry_uuid'];
readLandmarks(trip.landmarks, prefs, firstUUID);
return trip;
}
Map<String, dynamic> toJson() => {
'uuid': uuid,
'city_name': cityName,
'entry_uuid': landmarks.first?.uuid ?? ''
};
void toPrefs(SharedPreferences prefs){
Map<String, dynamic> json = toJson();
prefs.setString('trip_$uuid', jsonEncode(json));
for (Landmark landmark in landmarks) {
landmarkToPrefs(prefs, landmark, landmark.next);
}
}
}
// Helper
readLandmarks(LinkedList<Landmark> landmarks, SharedPreferences prefs, String? firstUUID) {
while (firstUUID != null) {
var (head, nextUUID) = getLandmarkFromPrefs(prefs, firstUUID);
landmarks.add(head);
firstUUID = nextUUID;
}
}
void removeAllTripsFromPrefs () async {
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.clear();
}

View File

@@ -0,0 +1,52 @@
// import "package:fast_network_navigation/structs/landmark.dart";
// import 'package:http/http.dart' as http;
// Future<List<Landmark>> fetchLandmarks() async {
// // final response = await http
// // .get(Uri.parse('https://nav.kluster.moll.re/v1/destination/1'));
// // if (response.statusCode == 200) {
// // If the server did return a 200 OK response,
// // then parse the JSON.
// List<Landmark> landmarks = [
// // 48°5129.6″N 2°1740.2″E
// Landmark(
// name: "Eiffel Tower",
// location: [48.51296, 2.17402],
// type: LandmarkType(name: "Tower"),
// imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a8/Tour_Eiffel_Wikimedia_Commons.jpg/1037px-Tour_Eiffel_Wikimedia_Commons.jpg"
// ),
// Landmark(
// name: "Notre Dame Cathedral",
// location: [48.8530, 2.3498],
// type: LandmarkType(name: "Monument"),
// imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f7/Notre-Dame_de_Paris%2C_4_October_2017.jpg/440px-Notre-Dame_de_Paris%2C_4_October_2017.jpg"
// ),
// Landmark(
// name: "Louvre palace",
// location: [48.8606, 2.3376],
// type: LandmarkType(name: "Museum"),
// imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/6/66/Louvre_Museum_Wikimedia_Commons.jpg/540px-Louvre_Museum_Wikimedia_Commons.jpg"
// ),
// Landmark(
// name: "Pont-des-arts",
// location: [48.5130, 2.2015],
// type: LandmarkType(name: "Bridge"),
// imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d1/Pont_des_Arts%2C_6e_Arrondissement%2C_Paris_%28HDR%29_20140320_1.jpg/560px-Pont_des_Arts%2C_6e_Arrondissement%2C_Paris_%28HDR%29_20140320_1.jpg"),
// Landmark(
// name: "Panthéon",
// location: [48.5046, 2.2046],
// type: LandmarkType(name: "Monument"),
// imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/8/80/Pantheon_of_Paris_007.JPG/1280px-Pantheon_of_Paris_007.JPG"
// ),
// ];
// // sleep 10 seconds
// await Future.delayed(Duration(seconds: 5));
// return landmarks;
// // } else {
// // // If the server did not return a 200 OK response,
// // // then throw an exception.
// // throw Exception('Failed to load destination');
// // }
// }

View File

@@ -1,27 +0,0 @@
import "package:fast_network_navigation/structs/landmark.dart";
import 'package:http/http.dart' as http;
Future<List<Landmark>> fetchLandmarks() async {
// final response = await http
// .get(Uri.parse('https://nav.kluster.moll.re/v1/destination/1'));
// if (response.statusCode == 200) {
// If the server did return a 200 OK response,
// then parse the JSON.
List<Landmark> landmarks = [
Landmark(name: "Landmark 1", location: [48.85, 2.35], type: LandmarkType(name: "Type 1")),
Landmark(name: "Landmark 2", location: [48.86, 2.36], type: LandmarkType(name: "Type 2")),
Landmark(name: "Landmark 3", location: [48.75, 2.3], type: LandmarkType(name: "Type 3")),
Landmark(name: "Landmark 4", location: [48.9, 2.4], type: LandmarkType(name: "Type 4")),
Landmark(name: "Landmark 5", location: [48.91, 2.45], type: LandmarkType(name: "Type 5")),
];
// sleep 10 seconds
await Future.delayed(Duration(seconds: 10));
return landmarks;
// } else {
// // If the server did not return a 200 OK response,
// // then throw an exception.
// throw Exception('Failed to load destination');
// }
}

View File

@@ -1,37 +0,0 @@
import 'dart:convert';
import 'package:fast_network_navigation/structs/trip.dart';
import 'package:fast_network_navigation/structs/landmark.dart';
import 'package:shared_preferences/shared_preferences.dart';
Future<List<Trip>> loadTrips() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
Set<String> keys = prefs.getKeys();
List<Trip> itineraries = [];
for (String key in keys) {
if (key.startsWith("itinerary_")) {
String json = prefs.getString(key)!;
itineraries.add(Trip.fromJson(jsonDecode(json)));
}
}
itineraries.add(Trip(uuid: "1", cityName: "Paris", landmarks: [
Landmark(name: "Landmark 1", location: [48.85, 2.35], type: LandmarkType(name: "Type 1")),
Landmark(name: "Landmark 2", location: [48.86, 2.36], type: LandmarkType(name: "Type 2")),
Landmark(name: "Landmark 3", location: [48.75, 2.3], type: LandmarkType(name: "Type 3")),
Landmark(name: "Landmark 4", location: [48.9, 2.4], type: LandmarkType(name: "Type 4")),
Landmark(name: "Landmark 5", location: [48.91, 2.45], type: LandmarkType(name: "Type 5")),
]));
itineraries.add(Trip(uuid: "2", cityName: "Vienna", landmarks: []));
itineraries.add(Trip(uuid: "3", cityName: "London", landmarks: []));
itineraries.add(Trip(uuid: "4", cityName: "Madrid", landmarks: []));
itineraries.add(Trip(uuid: "5", cityName: "Tokyo", landmarks: []));
itineraries.add(Trip(uuid: "6", cityName: "New York", landmarks: []));
itineraries.add(Trip(uuid: "7", cityName: "Los Angeles", landmarks: []));
itineraries.add(Trip(uuid: "8", cityName: "Zurich", landmarks: []));
itineraries.add(Trip(uuid: "9", cityName: "Orschwiller", landmarks: []));
await Future.delayed(Duration(seconds: 3));
return itineraries;
}

View File

@@ -0,0 +1,119 @@
import 'dart:collection';
import 'package:fast_network_navigation/structs/trip.dart';
import 'package:fast_network_navigation/structs/landmark.dart';
import 'package:shared_preferences/shared_preferences.dart';
Future<List<Trip>> loadTrips() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
List<Trip> trips = [];
Set<String> keys = prefs.getKeys();
for (String key in keys) {
if (key.startsWith('trip_')) {
String uuid = key.replaceFirst('trip_', '');
trips.add(Trip.fromPrefs(prefs, uuid));
}
}
if (trips.isEmpty) {
Trip t1 = Trip(uuid: '1', cityName: 'Paris', landmarks: LinkedList<Landmark>());
t1.landmarks.add(
Landmark(
uuid: '1',
name: "Eiffel Tower",
location: [48.859, 2.295],
type: LandmarkType(name: "Tower"),
imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a8/Tour_Eiffel_Wikimedia_Commons.jpg/1037px-Tour_Eiffel_Wikimedia_Commons.jpg"
),
);
t1.landmarks.add(
Landmark(
uuid: "2",
name: "Notre Dame Cathedral",
location: [48.8530, 2.3498],
type: LandmarkType(name: "Monument"),
imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f7/Notre-Dame_de_Paris%2C_4_October_2017.jpg/440px-Notre-Dame_de_Paris%2C_4_October_2017.jpg"
),
);
t1.landmarks.add(
Landmark(
uuid: "3",
name: "Louvre palace",
location: [48.8606, 2.3376],
type: LandmarkType(name: "Museum"),
imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/6/66/Louvre_Museum_Wikimedia_Commons.jpg/540px-Louvre_Museum_Wikimedia_Commons.jpg"
),
);
t1.landmarks.add(
Landmark(
uuid: "4",
name: "Pont-des-arts",
location: [48.8585, 2.3376],
type: LandmarkType(name: "Bridge"),
imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d1/Pont_des_Arts%2C_6e_Arrondissement%2C_Paris_%28HDR%29_20140320_1.jpg/560px-Pont_des_Arts%2C_6e_Arrondissement%2C_Paris_%28HDR%29_20140320_1.jpg"
),
);
t1.landmarks.add(
Landmark(
uuid: "5",
name: "Panthéon",
location: [48.847, 2.347],
type: LandmarkType(name: "Monument"),
imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/8/80/Pantheon_of_Paris_007.JPG/1280px-Pantheon_of_Paris_007.JPG"
),
);
trips.add(t1);
Trip t2 = Trip(uuid: '2', cityName: 'Vienna', landmarks: LinkedList<Landmark>());
t2.landmarks.add(
Landmark(
uuid: '21',
name: "St. Charles's Church",
location: [48.1924563,16.3334399],
type: LandmarkType(name: "Monument"),
imageURL: "https://lh5.googleusercontent.com/p/AF1QipNNmA76Ps71NCL9rOOFoyheCEOyXWdHcUgQx9jd=w408-h305-k-no"
),
);
t2.landmarks.add(
Landmark(
uuid: "22",
name: "Vienna State Opera",
location: [48.1949124,16.3483292],
type: LandmarkType(name: "Culture"),
imageURL: "https://lh5.googleusercontent.com/p/AF1QipMOx398kcoeDXFruSHNsb4lmZtdT8vibtK0cLi-=w408-h306-k-no"
),
);
t2.landmarks.add(
Landmark(
uuid: "23",
name: "Belvedere-Schlossgarten",
location: [48.1956427,16.3711521],
type: LandmarkType(name: "Nature"),
imageURL: "https://lh5.googleusercontent.com/p/AF1QipNcI5LImH2Qdzx0GmF-5CY1wRKINFZ7HkahPEy1=w408-h306-k-no"
),
);
t2.landmarks.add(
Landmark(
uuid: "24",
name: "Kunsthistorisches Museum Wien",
location: [48.2047501,16.3581904],
type: LandmarkType(name: "Museum"),
imageURL: "https://lh5.googleusercontent.com/p/AF1QipPuDu-kCCowO4TcawjziE8AhDVAANagVtRYBjlv=w408-h450-k-no"
),
);
t2.landmarks.add(
Landmark(
uuid: "25",
name: "Salztorbrücke",
location: [48.2132382,16.369051],
type: LandmarkType(name: "Bridge"),
),
);
trips.add(t2);
}
return trips;
}

3
renovate.json Normal file
View File

@@ -0,0 +1,3 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
}