80 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
d5e0b7d51a Beginning to use different contexts
Some checks failed
Build and push docker image / Build (pull_request) Failing after 2m49s
Build and release APK / Build APK (pull_request) Successful in 5m48s
Build web / Build Web (pull_request) Successful in 1m32s
2024-06-03 13:51:01 +02:00
ebaec40d6b Update frontend/lib/pages/profile.dart
Some checks failed
Build and push docker image / Build (pull_request) Failing after 2m3s
Build and release APK / Build APK (pull_request) Successful in 5m18s
Build web / Build Web (pull_request) Successful in 1m12s
2024-06-02 19:38:46 +00:00
c1d74ab227 use structs to draw custom map pointers
Some checks failed
Build and push docker image / Build (pull_request) Failing after 2m10s
Build and release APK / Build APK (pull_request) Successful in 5m22s
Build web / Build Web (pull_request) Successful in 1m14s
2024-06-01 19:58:25 +02:00
8bc7da0b3e first ui elements using the new structs
Some checks failed
Build and push docker image / Build (pull_request) Failing after 41s
Build and release APK / Build APK (pull_request) Successful in 5m25s
Build web / Build Web (pull_request) Successful in 1m17s
2024-05-31 21:33:04 +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
7e4538a1bf Merge branch 'feature/use-google-maps'
Some checks failed
Build and push docker image / Build (pull_request) Failing after 2m11s
Build and release APK / Build APK (pull_request) Successful in 5m9s
Build web / Build Web (pull_request) Successful in 1m26s
2024-05-29 18:25:06 +02:00
80b04905de Merge branch 'feature/optimizer-use-fastapi' 2024-05-29 18:23:34 +02:00
ae9860cc8e proper fast api suage
Some checks failed
Test code / Test code (push) Has been cancelled
Build and release APK / Build APK (pull_request) Has been cancelled
Build web / Build Web (pull_request) Has been cancelled
Test code / Test code (pull_request) Has been cancelled
2024-05-29 18:14:40 +02:00
3029fb8537 some preference improvements
Some checks failed
Build and release APK / Build APK (pull_request) Successful in 7m27s
Test code / Test code (push) Has been cancelled
Build web / Build Web (pull_request) Has been cancelled
Test code / Test code (pull_request) Has been cancelled
2024-05-25 18:55:58 +02:00
f292b2a375 Update .gitea/workflows/backed_build-image.yaml 2024-05-25 12:23:22 +00:00
6dc5d560ae Merge pull request 'Automated docker builds' (#5) from feature/docker-cicd into main
Reviewed-on: remoll/fast-network-navigation#5
2024-05-25 12:20:19 +00:00
cb33f315a0 final building cleanup
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) Failing after 1m42s
2024-05-25 14:19:57 +02:00
99dc8b5e67 added OSM api 2024-05-23 12:20:32 +02:00
d8d425a922 added dataclass
Some checks failed
Test code / Test code (push) Failing after 3s
Build and release APK / Build APK (pull_request) Has been cancelled
Build web / Build Web (pull_request) Has been cancelled
Test code / Test code (pull_request) Has been cancelled
2024-05-23 10:39:14 +02:00
70cebc0aa1 fixed return order
Some checks failed
Test code / Test code (push) Failing after 2s
2024-05-22 15:12:54 +02:00
101af0ebc6 included fastapi
Some checks failed
Test code / Test code (push) Failing after 3s
2024-05-22 15:01:46 +02:00
2b31ce5f6b Cleanup and created main
Some checks failed
Test code / Test code (push) Failing after 2s
2024-05-22 10:16:33 +02:00
82a864e57f fixed circular symmetry
Some checks failed
Test code / Test code (push) Failing after 2s
2024-05-22 00:40:57 +02:00
07830de1b2 Update .gitea/workflows/frontend_build-android.yaml
Some checks failed
Test code / Test code (push) Failing after 1s
Test code / Test code (pull_request) Failing after 0s
Build web / Build Web (pull_request) Has been cancelled
Build and release APK / Build APK (pull_request) Successful in 8m7s
2024-05-21 06:29:10 +00:00
3688229d7b Update frontend/android/app/src/main/AndroidManifest.xml
Some checks failed
Test code / Test code (push) Waiting to run
Build and release APK / Build APK (pull_request) Successful in 5m50s
Build web / Build Web (pull_request) Failing after 1s
Test code / Test code (pull_request) Failing after 0s
2024-05-20 22:18:08 +00:00
6405f33a34 update flutter builder
Some checks failed
Test code / Test code (push) Waiting to run
Build and release APK / Build APK (pull_request) Failing after 5m18s
Build web / Build Web (pull_request) Failing after 0s
Test code / Test code (pull_request) Failing after 0s
2024-05-20 21:38:20 +02:00
f6e3cfc8a0 build like this
Some checks failed
Test code / Test code (push) Has been cancelled
Test code / Test code (pull_request) Has been cancelled
Build web / Build Web (pull_request) Has been cancelled
Build and release APK / Build APK (pull_request) Failing after 4m19s
2024-05-20 12:30:33 +02:00
e600f40c1a corrected symmetry cosntraint
Some checks failed
Test code / Test code (push) Failing after 26s
2024-05-20 01:37:35 +02:00
66fc4e7d33 update deps
Some checks failed
Test code / Test code (push) Failing after 21s
Build and release APK / Build APK (pull_request) Failing after 4m59s
Build web / Build Web (pull_request) Failing after 51s
Test code / Test code (pull_request) Failing after 17s
2024-05-18 10:56:04 +02:00
3854cef54a adding google maps baby!
Some checks failed
Test code / Test code (push) Failing after 24s
Build web / Build Web (pull_request) Failing after 3m10s
Test code / Test code (pull_request) Failing after 44s
Build and release APK / Build APK (pull_request) Failing after 15m48s
2024-05-17 20:12:59 +02:00
66 changed files with 5323 additions and 1760 deletions

View File

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

View File

@@ -2,6 +2,9 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
paths:
- frontend/**
name: Build and release APK name: Build and release APK
@@ -13,20 +16,24 @@ jobs:
- name: Install prerequisites - name: Install prerequisites
run: | run: |
sudo apt-get update apt-get update
sudo apt-get install -y xz-utils unzip apt-get install -y jq
- uses: https://gitea.com/actions/checkout@v4 - uses: https://gitea.com/actions/checkout@v4
- uses: https://github.com/actions/setup-java@v4 - uses: https://github.com/actions/setup-java@v4
with: with:
java-version: '17' java-version: '17'
distribution: 'zulu' distribution: 'zulu'
- name: Fix flutter SDK folder permission
run: git config --global --add safe.directory "*"
- uses: https://github.com/subosito/flutter-action@v2 - uses: https://github.com/subosito/flutter-action@v2
with: with:
channel: stable channel: stable
flutter-version: 3.19.6 flutter-version: 3.22.0
cache: true cache: true
- name: Setup Android SDK - name: Setup Android SDK
@@ -35,21 +42,24 @@ jobs:
- run: flutter pub get - run: flutter pub get
working-directory: ./frontend working-directory: ./frontend
- run: flutter build apk --debug --split-per-abi - name: Add required secrets
run: |
echo ${{ secrets.ANDROID_SECRETS_PROPERTIES }} > ./android/secrets.properties
working-directory: ./frontend working-directory: ./frontend
- name: Sanity check
run: |
ls
ls -lah android
working-directory: ./frontend
- name: Release APK - run: flutter build apk --release --split-per-abi --build-number=${{ gitea.run_number }}
uses: https://gitea.com/akkuman/gitea-release-action@v1 working-directory: ./frontend
- name: Upload APKs to artifacts
uses: https://gitea.com/actions/upload-artifact@v3
with: with:
files: ./frontendbuild/app/outputs/flutter-apk/*.apk name: app-release
name: Testing release path: frontend/build/app/outputs/flutter-apk/
release_name: testing if-no-files-found: error
tag: testing retention-days: 15
tag_name: testing
release_body: "This is a testing release."
prerelease: true
token: ${{ secrets.GITEA_TOKEN }}
env:
NODE_OPTIONS: '--experimental-fetch'

View File

@@ -1,32 +1,34 @@
on: # on:
pull_request: # pull_request:
branches: # branches:
- main # - main
# paths:
name: Build web # - frontend/**
jobs:
build:
name: Build Web
runs-on: k8s
steps:
- name: Install prerequisites
run: |
sudo apt-get update
sudo apt-get install -y xz-utils
- uses: actions/checkout@v4
- 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 build web # name: Build web
working-directory: ./frontend
# 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
# - uses: actions/checkout@v4
# - 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 build web
# working-directory: ./frontend

View File

@@ -1,33 +0,0 @@
on:
push:
pull_request:
branches:
- main
name: Test code
jobs:
test:
name: Test code
runs-on: k8s
steps:
- name: Install prerequisites
run: |
sudo apt-get update
sudo apt-get install -y xz-utils
- uses: actions/checkout@v4
- 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 test
working-directory: ./frontend

1
.gitignore vendored Normal file
View File

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

52
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,52 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
// backend - python using fastapi
{
"name": "Backend - debug",
"type": "debugpy",
"request": "launch",
"module": "uvicorn",
"env": {
"DEBUG": "true"
},
"args": [
"--app-dir",
"src",
"main:app",
"--reload",
],
"jinja": true,
"cwd": "${workspaceFolder}/backend"
},
{
"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",
"request": "launch",
"program": "lib/main.dart",
"cwd": "${workspaceFolder}/frontend"
},
{
"name": "Frontend - profile",
"type": "dart",
"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 ## 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) Official releases will be made available on the Google Play Store and the Apple App Store.
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials, ## Roadmap
samples, guidance on mobile development, and a full API reference. 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%">

165
backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,165 @@
# osm-cache
cache/
apicache/
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

View File

@@ -1,4 +1,4 @@
FROM python:3 FROM python:3.11-slim
WORKDIR /app WORKDIR /app
COPY Pipfile Pipfile.lock . COPY Pipfile Pipfile.lock .
@@ -6,6 +6,12 @@ COPY Pipfile Pipfile.lock .
RUN pip install pipenv RUN pip install pipenv
RUN pipenv install --deploy --system 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

@@ -1,14 +1,16 @@
[[source]] [[source]]
url = "https://pypi.org/simple" url = "https://pypi.org/simple"
verify_ssl = true verify_ssl = true
name = "pypi" name = "pypi"
[packages] [dev-packages]
numpy = "*"
scipy = "*" [packages]
fastapi = "*" numpy = "*"
fastapi = "*"
[dev-packages] pydantic = "*"
geopy = "*"
[requires] shapely = "*"
python_version = "3.12" scipy = "*"
osmpythontools = "*"
pywikibot = "*"

2553
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

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

View File

@@ -1,23 +1,68 @@
import fastapi import logging
from dataclasses import dataclass from fastapi import FastAPI, Query, Body
from structs.landmark import Landmark
@dataclass from structs.preferences import Preferences
class Destination: from structs.linked_landmarks import LinkedLandmarks
name: str from utils.landmarks_manager import LandmarkManager
location: tuple from utils.optimizer import Optimizer
attractiveness: int from utils.refiner import Refiner
logger = logging.getLogger(__name__)
d = Destination()
app = FastAPI()
manager = LandmarkManager()
optimizer = Optimizer()
def get_route() -> list[Destination]: refiner = Refiner(optimizer=optimizer)
return {"route": "Hello World"}
endpoint = ("/get_route", get_route) @app.post("/route/new")
end def get_route(preferences: Preferences, start: tuple[float, float], end: tuple[float, float] | None = None) -> str:
if __name__ == "__main__": '''
fastapi.run() 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

@@ -1,206 +0,0 @@
from scipy.optimize import linprog
import numpy as np
from scipy.linalg import block_diag
# Defines the landmark class (aka some place there is to visit)
class landmark :
def __init__(self, name: str, attractiveness: int, loc: tuple):
self.name = name
self.attractiveness = attractiveness
self.loc = loc
# Convert the result (edges from j to k like d_25 = edge between vertex 2 and vertex 5) into the list of indices corresponding to the landmarks
def untangle(res: list) :
N = len(res) # 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_landmarks = res.sum() # number of visited landmarks
visit_order = []
cnt = 0
if n_landmarks % 2 == 1 : # if odd number of visited checkpoints
for i in range(L) :
for j in range(L) :
if res[i*L + j] == 1 : # if index is 1
cnt += 1 # increment counter
if cnt % 2 == 1 : # if counter odd
visit_order.append(i)
visit_order.append(j)
else : # if even number of ones
for i in range(L) :
for j in range(L) :
if res[i*L + j] == 1 : # if index is one
cnt += 1 # increment counter
if j % (L-1) == 0 : # if last node
visit_order.append(j) # append only the last index
return visit_order # return
if cnt % 2 == 1 :
visit_order.append(i)
visit_order.append(j)
return visit_order
# Just to print the result
def print_res(res: list, P) :
X = abs(res.x)
order = untangle(X)
# print("Optimal value:", -res.fun) # Minimization, so we negate to get the maximum
# print("Optimal point:", res.x)
# N = int(np.sqrt(len(X)))
# for i in range(N):
# print(X[i*N:i*N+N])
# print(order)
if (X.sum()+1)**2 == len(X) :
print('\nAll landmarks can be visited within max_steps, the following order is most likely not the fastest')
else :
print('Could not visit all the landmarks, the following order could be the fastest but not sure')
print("Order of visit :")
for i, elem in enumerate(landmarks) :
if i in order : print('- ' + elem.name)
steps = path_length(P, abs(res.x))
print("\nSteps walked : " + str(steps))
# Constraint to use only the upper triangular indices for travel
def break_sym(landmarks, A_eq, b_eq):
L = len(landmarks)
l = [0]*L*L
for i in range(L) :
for j in range(L) :
if i >= j :
l[j+i*L] = 1
A_eq = np.vstack((A_eq,l))
b_eq.append(0)
return A_eq, b_eq
# 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)
return np.vstack((A_ub, T)), b_ub + [1]*len(landmarks)
# Constraint to tie the problem together and have a connected path
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)
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
#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))
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 :
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)
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
A_eq = np.vstack((A_eq,l))
b_eq.append(1)
return A_eq, b_eq
# Computes the path length given path matrix (dist_table) and a result
def path_length(P: list, resx: list) :
return np.dot(P, resx)
# Initialize all landmarks (+ start and goal). Order matters here
landmarks = []
landmarks.append(landmark("départ", -1, (0, 0)))
landmarks.append(landmark("concorde", -1, (5,5)))
landmarks.append(landmark("tour eiffel", 99, (1,1))) # PUT IN JSON
landmarks.append(landmark("arc de triomphe", 99, (2,3)))
landmarks.append(landmark("louvre", 70, (4,2)))
landmarks.append(landmark("montmartre", 20, (0,2)))
landmarks.append(landmark("arrivée", -1, (0, 0)))
# CONSTRAINT TO RESPECT MAX NUMBER OF STEPS
max_steps = 25
# 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.
# 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 = 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 = break_sym(landmarks, A_eq, b_eq) # break the symmetry. Only use the upper diagonal values
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 :
raise ValueError("No solution has been found, please adapt your max steps")
# Print result
print_res(res, P)

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

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
/gradlew.bat /gradlew.bat
/local.properties /local.properties
/secrets.properties
GeneratedPluginRegistrant.java GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore. # 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 "com.android.application"
id "kotlin-android" id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin" 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 localPropertiesFile = rootProject.file('local.properties')
def localProperties = new Properties()
if (localPropertiesFile.exists()) { if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader -> localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader) localProperties.load(reader)
} }
} else {
throw new GradleException("local.properties not found")
} }
def flutterVersionCode = localProperties.getProperty('flutter.versionCode') def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
@@ -22,6 +27,20 @@ if (flutterVersionName == null) {
flutterVersionName = '1.0' 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 { android {
namespace "com.example.fast_network_navigation" namespace "com.example.fast_network_navigation"
compileSdk flutter.compileSdkVersion compileSdk flutter.compileSdkVersion
@@ -45,10 +64,16 @@ android {
applicationId "com.example.fast_network_navigation" applicationId "com.example.fast_network_navigation"
// You can update the following values to match your application needs. // You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
// Minimum Android version for Google Maps SDK
// https://developers.google.com/maps/flutter-package/config#android
minSdk = 21
minSdkVersion flutter.minSdkVersion minSdkVersion flutter.minSdkVersion
targetSdkVersion flutter.targetSdkVersion targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
// // Placeholders of keys that are replaced by the build system.
manifestPlaceholders += ['MAPS_API_KEY': secretProperties.getProperty('MAPS_API_KEY')]
} }
buildTypes { buildTypes {

View File

@@ -1,4 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <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 <application
android:label="fast_network_navigation" android:label="fast_network_navigation"
android:name="${applicationName}" android:name="${applicationName}"
@@ -28,8 +31,14 @@
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data <meta-data
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2"
/>
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="${MAPS_API_KEY}"
/>
</application> </application>
<!-- Required to query activities that can process text, see: <!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility?hl=en and https://developer.android.com/training/package-visibility?hl=en and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT. https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.

View File

@@ -16,3 +16,14 @@ subprojects {
tasks.register("clean", Delete) { tasks.register("clean", Delete) {
delete rootProject.buildDir 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

134
frontend/lib/layout.dart Normal file
View File

@@ -0,0 +1,134 @@
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';
import 'package:fast_network_navigation/pages/profile.dart';
// BasePage is the scaffold that holds all other pages
// A side drawer is used to switch between pages
class BasePage extends StatefulWidget {
final String mainScreen;
final Future<Trip>? trip;
const BasePage({
super.key,
required this.mainScreen,
this.trip
});
@override
State<BasePage> createState() => _BasePageState();
}
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(trip: widget.trip ?? getFirstTrip(trips));
} else if (widget.mainScreen == "tutorial") {
currentView = TutorialPage();
} else if (widget.mainScreen == "profile") {
currentView = ProfilePage();
}
final ThemeData theme = Theme.of(context);
return Scaffold(
appBar: AppBar(title: Text("City Nav")),
body: Center(child: currentView),
drawer: Drawer(
child: Column(
children: [
DrawerHeader(
decoration: BoxDecoration(
gradient: LinearGradient(colors: [Colors.cyan, theme.primaryColor])
),
child: Center(
child: Text(
'City Nav',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
),
),
ListTile(
title: const Text('Your Trips'),
leading: const Icon(Icons.map),
selected: widget.mainScreen == "map",
onTap: () {},
trailing: ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const NewTripPage()
)
);
},
child: const Text('New'),
),
),
// 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'),
leading: Icon(Icons.help),
selected: widget.mainScreen == "tutorial",
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => BasePage(mainScreen: "tutorial")
)
);
},
),
// settings in the bottom of the drawer
ListTile(
title: const Text('Settings'),
leading: const Icon(Icons.settings),
selected: widget.mainScreen == "profile",
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => BasePage(mainScreen: "profile")
)
);
},
),
],
),
),
);
}
}
Future<Trip> getFirstTrip (Future<List<Trip>> trips) async {
List<Trip> tripsf = await trips;
return tripsf[0];
}

View File

@@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fast_network_navigation/modules/scaffold.dart'; import 'package:fast_network_navigation/layout.dart';
void main() => runApp(const App()); void main() => runApp(const App());
@@ -12,9 +12,8 @@ class App extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(
title: appTitle, title: appTitle,
home: BasePage(title: appTitle), home: BasePage(mainScreen: "map"),
theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.green), theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.green),
); );
} }
} }

View File

@@ -1,40 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class DestinationCard extends StatefulWidget {
final String title;
final String description;
final String image;
bool visited;
@override
_DestinationCardState createState() => _DestinationCardState();
DestinationCard(this.title, this.description, this.image, this.visited);
Widget build() {
return Card(
child: ListTile(
leading: Icon(Icons.location_on),
title: Text(title),
subtitle: Text(description),
onTap: () {
// Navigator.pushNamed(context, '/destination');
},
),
);
}
}
class _DestinationCardState extends State<DestinationCard> {
@override
Widget build(BuildContext context) {
return Card();
}
}

View File

@@ -0,0 +1,73 @@
import 'package:fast_network_navigation/structs/trip.dart';
import 'package:flutter/material.dart';
class Greeter extends StatefulWidget {
final Future<Trip> trip;
final bool standalone;
Greeter({
required this.standalone,
required this.trip
});
@override
State<Greeter> createState() => _GreeterState();
}
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

@@ -0,0 +1,80 @@
import 'package:fast_network_navigation/structs/landmark.dart';
import 'package:flutter/material.dart';
class LandmarkCard extends StatefulWidget {
final Landmark landmark;
@override
_LandmarkCardState createState() => _LandmarkCardState();
LandmarkCard(this.landmark);
}
class _LandmarkCardState extends State<LandmarkCard> {
@override
Widget build(BuildContext context) {
ThemeData theme = Theme.of(context);
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,
),
),
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),
),
)
]
),
],
),
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,130 @@
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/structs/trip.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class LandmarksOverview extends StatefulWidget {
final Future<Trip>? trip;
const LandmarksOverview({super.key, this.trip});
@override
State<LandmarksOverview> createState() => _LandmarksOverviewState();
}
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<LinkedList<Landmark>>(
future: _landmarks,
builder: (BuildContext context, AsyncSnapshot<LinkedList<Landmark>> snapshot) {
List<Widget> children;
if (snapshot.hasData) {
children = [landmarksWithSteps(snapshot.data!), saveButton()];
} else if (snapshot.hasError) {
children = <Widget>[
const Icon(
Icons.error_outline,
color: Colors.red,
size: 60,
),
Padding(
padding: const EdgeInsets.only(top: 16),
child: Text('Error: ${snapshot.error}', style: TextStyle(fontSize: 12)),
),
];
} else {
children = [Center(child: CircularProgressIndicator())];
}
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: children,
),
);
},
),
);
}
Widget saveButton() => ElevatedButton(
onPressed: () async {
Trip? trip = await widget.trip;
SharedPreferences prefs = await SharedPreferences.getInstance();
trip?.toPrefs(prefs);
},
child: const Text('Save'),
);
}
Widget landmarksWithSteps(LinkedList<Landmark> landmarks) {
List<Widget> children = [];
for (Landmark landmark in landmarks) {
children.add(LandmarkCard(landmark));
if (landmark.next != null) {
Widget step = stepBetweenLandmarks(landmark, landmark.next!);
children.add(step);
}
}
return Column(
children: children
);
}
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 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"),
),
],
),
);
}
Future<LinkedList<Landmark>> getLandmarks (Future<Trip>? trip) async {
Trip tripf = await trip!;
return tripf.landmarks;
}

View File

@@ -0,0 +1,76 @@
import 'dart:collection';
import 'package:fast_network_navigation/structs/landmark.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();
}
class _MapWidgetState extends State<MapWidget> {
late GoogleMapController mapController;
// coordinates of Paris
CameraPosition _cameraPosition = CameraPosition(
target: LatLng(48.8566, 2.3522),
zoom: 11.0,
);
Set<Marker> markers = <Marker>{};
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)));
}
void drawLandmarks() async {
// (re)draws landmarks on the map
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),
));
}
});
}
}
@override
Widget build(BuildContext context) {
return GoogleMap(
onMapCreated: _onMapCreated,
initialCameraPosition: _cameraPosition,
onCameraIdle: _onCameraIdle,
// onLongPress: ,
markers: markers,
cloudMapId: '41c21ac9b81dbfd8',
);
}
}

View File

@@ -1,63 +0,0 @@
import 'package:fast_network_navigation/modules/destination_card.dart';
import 'package:flutter/material.dart';
List<Widget> loadDestinations() {
List<Widget> cities = [
singleDestination(
"New York",
"The Big Apple",
"https://upload.wikimedia.org/wikipedia/commons/thumb/3/34/View_of_New_York_City.jpg/800px-View_of_New_York_City.jpg"
),
singleDestination(
"Los Angeles",
"City of Angels",
"https://upload.wikimedia.org/wikipedia/commons/thumb/8/8d/Los_Angeles_City_Hall_2013.jpg/800px-Los_Angeles_City_Hall_2013.jpg"
),
singleDestination(
"Chicago",
"The Windy City",
"https://upload.wikimedia.org/wikipedia/commons/thumb/6/6f/Chicago_skyline%2C_viewed_from_John_Hancock_Center.jpg/800px-Chicago_skyline%2C_viewed_from_John_Hancock_Center.jpg"
),
singleDestination(
"San Francisco",
"The Golden City",
"https://upload.wikimedia.org/wikipedia/commons/thumb/6/6d/San_Francisco_City_Hall_2013.jpg/800px-San_Francisco_City_Hall_2013.jpg"
),
singleDestination(
"Miami",
"The Magic City",
"https://upload.wikimedia.org/wikipedia/commons/thumb/6/6e/Miami_collage.jpg/800px-Miami_collage.jpg"
),
singleDestination(
"Las Vegas",
"Sin City",
"https://upload.wikimedia.org/wikipedia/commons/thumb/6/6e/Las_Vegas_Strip.jpg/800px-Las_Vegas_Strip.jpg"
),
singleDestination(
"Seattle",
"Emerald City",
"https://upload.wikimedia.org/wikipedia/commons/thumb/2/2f/Seattle_Kerry_Park_Skyline.jpg/800px-Seattle_Kerry_Park_Skyline.jpg"
),
singleDestination(
"Boston",
"Beantown",
"https://upload.wikimedia.org/wikipedia/commons/thumb/0/0f/Boston_skyline_from_Longfellow_Bridge_September_2017_panorama_2.jpg/800px-Boston"
)
];
cities.shuffle();
return cities;
}
Widget singleDestination(String title, String description, String image) {
return Card(
child: ListTile(
leading: Icon(Icons.location_on),
title: Text(title),
subtitle: Text(description),
onTap: () {
// Navigator.pushNamed(context, '/destination');
},
),
);
}

View File

@@ -1,153 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:sliding_up_panel/sliding_up_panel.dart';
import 'package:latlong2/latlong.dart';
import 'package:geocoding/geocoding.dart';
import 'package:geocode/geocode.dart';
import 'dart:async';
import 'package:fast_network_navigation/modules/navigation.dart';
class MapPage extends StatefulWidget {
@override
_MapPageState createState() => _MapPageState();
}
class Debounce {
Duration delay;
Timer? _timer;
Debounce(
this.delay,
);
call(void Function() callback) {
_timer?.cancel();
_timer = Timer(delay, callback);
}
dispose() {
_timer?.cancel();
}
}
class _MapPageState extends State<MapPage> {
GeoCode geoCode = GeoCode();
final mapController = MapController();
String _currentCityName = "...";
final Debounce _debounce = Debounce(Duration(seconds: 3));
void _setCurrentCityName() async {
if (mapController.camera.zoom < 9) {
return; // Don't bother if the view is too wide
}
var currentCoordinates = mapController.camera.center;
String? city;
try{
List<Placemark> placemarks = await placemarkFromCoordinates(currentCoordinates.latitude, currentCoordinates.longitude);
city = placemarks[0].locality.toString();
} catch (e) {
debugPrint("Error: $e");
try {
Address address = await geoCode.reverseGeocoding(latitude: currentCoordinates.latitude, longitude: currentCoordinates.longitude);
if (address.city == null || address.city.toString().contains("Throttled!")){
throw Exception("Probably rate limited");
}
city = address.city.toString();
} catch (e) {
debugPrint("Error: $e");
}
}
if (city != null) {
setState(() {
_currentCityName = city!;
});
} else {
_debounce(() async {_setCurrentCityName();});
}
}
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
return SlidingUpPanel(
renderPanelSheet: false,
panel: _floatingPanel(theme),
collapsed: _floatingCollapsed(theme),
body: FlutterMap(
mapController: mapController,
options: MapOptions(
initialZoom: 11,
initialCenter: LatLng(51.509364, -0.128928),
onMapReady: () {
mapController.mapEventStream.listen((evt) {_debounce(() async {_setCurrentCityName();});});
// And any other `MapController` dependent non-movement methods
},
),
children: [
openStreetMapTileLayer,
],
),
);
}
Widget _floatingCollapsed(ThemeData theme){
return Container(
decoration: BoxDecoration(
color: theme.canvasColor,
borderRadius: BorderRadius.only(topLeft: Radius.circular(24.0), topRight: Radius.circular(24.0)),
),
child: Greeting(theme)
);
}
Widget _floatingPanel(ThemeData theme){
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(24.0)),
boxShadow: [
BoxShadow(
blurRadius: 20.0,
color: theme.shadowColor,
),
]
),
child: Center(
child: Padding(
padding: EdgeInsets.all(8.0),
child: SingleChildScrollView(
child: Column(
children: <Widget>[
Greeting(theme),
Text("Got a lot to do today! Here is a rundown:"),
...loadDestinations(),
],
),
),
),
),
);
}
Widget Greeting (ThemeData theme) {
return Center(
child: Text(
"Explore ${_currentCityName}",
style: TextStyle(color: theme.primaryColor, fontSize: 24.0, fontWeight: FontWeight.bold),
),
);
}
}
TileLayer get openStreetMapTileLayer => TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'flutter_map',
);
// Add a pin to the map

View File

@@ -1,55 +0,0 @@
import 'package:flutter/material.dart';
class ProfilePage extends StatefulWidget {
@override
_ProfilePageState createState() => _ProfilePageState();
}
class _ProfilePageState extends State<ProfilePage> {
double value = 0.0;
void onChanged(double newValue) {
setState(() {
value = newValue;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Profile'),
),
body: Padding(
padding: EdgeInsets.all(8.0),
child: Column(
children: <Widget>[
Card(
child: ListTile(
leading: Icon(Icons.notifications_sharp),
title: Text('Notification 1'),
subtitle: Text('This is a first notification'),
),
),
Card(
child: ListTile(
leading: Icon(Icons.notifications_sharp),
title: Text('Notification 2'),
subtitle: Text('This is a notification'),
),
),
Card(
child: ListTile(
leading: Icon(Icons.outdoor_grill),
title: Text("Eating preference"),
subtitle: Slider.adaptive(value: value, onChanged: onChanged, min: 0, max: 5, divisions: 5, label: value.toInt().toString(),)
)
)
],
),
)
);
}
}

View File

@@ -1,89 +0,0 @@
import 'package:flutter/material.dart';
import 'package:fast_network_navigation/modules/overview.dart';
import 'package:fast_network_navigation/modules/profile.dart';
class BasePage extends StatefulWidget {
const BasePage({super.key, required this.title});
final String title;
@override
State<BasePage> createState() => _BasePageState();
}
class _BasePageState extends State<BasePage> {
int _selectedIndex = 0;
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
Widget currentView = MapPage();
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
return Scaffold(
appBar: AppBar(title: Text(widget.title)),
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: ListView(
// Important: Remove any padding from the ListView.
padding: EdgeInsets.zero,
children: [
DrawerHeader(
decoration: BoxDecoration(
gradient: LinearGradient(colors: [Colors.cyan, theme.primaryColor])
),
child: const Text('The fanciest navigation!'),
),
ListTile(
title: const Text('Home'),
selected: _selectedIndex == 0,
onTap: () {
// Update the state of the app
_onItemTapped(0);
// Then close the drawer
currentView = MapPage();
Navigator.pop(context);
},
),
ListTile(
title: const Text('Business'),
selected: _selectedIndex == 1,
onTap: () {
// Update the state of the app
_onItemTapped(1);
currentView = const Text("ghfhggfhgf");
// Then close the drawer
Navigator.pop(context);
},
),
// add a whitespace so that the settings are at the bottom
const Divider(),
ListTile(
title: const Text('Settings'),
leading: const Icon(Icons.settings),
selected: _selectedIndex == 2,
onTap: () {
_onItemTapped(2);
currentView = ProfilePage();
Navigator.pop(context);
},
),
// settings in the bottom of the drawer
],
),
),
);
}
}

View File

@@ -0,0 +1,67 @@
import 'package:flutter/material.dart';
import 'package:fast_network_navigation/layout.dart';
import 'package:fast_network_navigation/structs/trip.dart';
class TripsOverview extends StatefulWidget {
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();
Widget listBuild (BuildContext context, AsyncSnapshot<List<Trip>> snapshot) {
List<Widget> children;
if (snapshot.hasData) {
children = List<Widget>.generate(snapshot.data!.length, (index) {
Trip trip = snapshot.data![index];
return ListTile(
title: Text("Trip to ${trip.cityName}"),
leading: Icon(Icons.pin_drop),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => BasePage(mainScreen: "map", trip: Future.value(trip))
)
);
},
);
});
} else if (snapshot.hasError) {
children = [
const Icon(
Icons.error_outline,
color: Colors.red,
size: 60,
),
Padding(
padding: const EdgeInsets.only(top: 16),
child: Text('Error: ${snapshot.error}'),
),
];
} else {
children = [Center(child: CircularProgressIndicator())];
}
return ListView(
children: children,
);
}
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: widget.trips,
builder: listBuild,
);
}
}

View File

@@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
class NewTripPage extends StatefulWidget {
const NewTripPage({Key? key}) : super(key: key);
@override
_NewTripPageState createState() => _NewTripPageState();
}
class _NewTripPageState extends State<NewTripPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('New Trip'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'Create a new trip',
),
],
),
),
);
}
}

View File

@@ -0,0 +1,79 @@
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();
}
class _NavigationOverviewState extends State<NavigationOverview> {
@override
Widget build(BuildContext context) {
return SlidingUpPanel(
renderPanelSheet: false,
panel: _floatingPanel(),
collapsed: _floatingCollapsed(),
body: MapWidget(trip: widget.trip)
);
}
Widget _floatingCollapsed(){
final ThemeData theme = Theme.of(context);
return Container(
decoration: BoxDecoration(
color: theme.canvasColor,
borderRadius: BorderRadius.only(topLeft: Radius.circular(24.0), topRight: Radius.circular(24.0)),
boxShadow: []
),
child: Greeter(standalone: true, trip: widget.trip)
);
}
Widget _floatingPanel(){
final ThemeData theme = Theme.of(context);
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(24.0)),
boxShadow: [
BoxShadow(
blurRadius: 20.0,
color: theme.shadowColor,
),
]
),
child: Center(
child: Padding(
padding: EdgeInsets.all(8.0),
child: SingleChildScrollView(
child: Column(
children: <Widget>[
Greeter(standalone: false, trip: widget.trip),
LandmarksOverview(trip: widget.trip),
],
),
),
),
),
);
}
}

View File

@@ -0,0 +1,89 @@
import 'package:fast_network_navigation/structs/preferences.dart';
import 'package:flutter/material.dart';
class ProfilePage extends StatefulWidget {
@override
_ProfilePageState createState() => _ProfilePageState();
}
class _ProfilePageState extends State<ProfilePage> {
@override
Widget build(BuildContext context) {
return ListView(
children: [
// First a round, centered image
Center(
child: CircleAvatar(
radius: 100,
child: Icon(Icons.person, size: 100),
)
),
Center(
child: Text('Curious traveler', style: TextStyle(fontSize: 24))
),
Padding(padding: EdgeInsets.all(10)),
Divider(indent: 25, endIndent: 25),
Padding(padding: EdgeInsets.all(10)),
Padding(
padding: EdgeInsets.only(left: 10, right: 10, top: 0, bottom: 10),
child: Text('Please rate your personal preferences so that we can taylor your experience.', style: TextStyle(fontSize: 18))
),
// Now the sliders
ImportanceSliders()
]
);
}
}
class ImportanceSliders extends StatefulWidget {
@override
State<ImportanceSliders> createState() => _ImportanceSlidersState();
}
class _ImportanceSlidersState extends State<ImportanceSliders> {
UserPreferences _prefs = UserPreferences();
List<Card> _createSliders() {
List<Card> sliders = [];
for (SinglePreference pref in _prefs.preferences) {
sliders.add(Card(
child: ListTile(
leading: pref.icon,
title: Text(pref.name),
subtitle: Slider(
value: pref.value.toDouble(),
min: 0,
max: 10,
divisions: 10,
label: pref.value.toString(),
onChanged: (double newValue) {
setState(() {
pref.value = newValue.toInt();
_prefs.save();
});
},
)
),
margin: const EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 0),
shadowColor: Colors.grey,
));
}
return sliders;
}
@override
Widget build(BuildContext context) {
return Column(children: _createSliders());
}
}

View File

@@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
class TutorialPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Tutorial"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'Welcome to the tutorial page!',
),
Text(
'This is where you will learn how to use the app.',
),
],
),
),
);
}
}

View File

@@ -1,31 +0,0 @@
class Destination {
final double latitude;
final double longitude;
final String name;
final String description;
final DestinationType type;
final Duration duration;
final bool visited;
Destination({
required this.latitude,
required this.longitude,
required this.name,
required this.description,
required this.type,
required this.duration,
required this.visited,
});
}
class DestinationType {
final String name;
final String description;
DestinationType({
required this.name,
required this.description,
});
}

View File

@@ -0,0 +1,116 @@
import 'dart:collection';
import 'dart:convert';
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,
this.isSecondary,
this.imageURL,
this.description,
this.duration,
this.visited,
// this.next,
this.tripTime,
});
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');
}
}
@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
};
}
class LandmarkType {
final String name;
// final String description;
// final Icon icon;
const LandmarkType({
required this.name,
// required this.description,
// required this.icon,
});
}
// 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

@@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class SinglePreference {
String name;
String description;
int value;
Icon icon;
String key;
SinglePreference({
required this.name,
required this.description,
required this.value,
required this.icon,
required this.key,
});
}
class UserPreferences {
List<SinglePreference> preferences = [
SinglePreference(
name: "Sightseeing",
description: "How much do you like sightseeing?",
value: 0,
icon: Icon(Icons.church),
key: "sightseeing",
),
SinglePreference(
name: "Shopping",
description: "How much do you like shopping?",
value: 0,
icon: Icon(Icons.shopping_bag),
key: "shopping",
),
SinglePreference(
name: "Foods & Drinks",
description: "How much do you like eating?",
value: 0,
icon: Icon(Icons.restaurant),
key: "eating",
),
SinglePreference(
name: "Nightlife",
description: "How much do you like nightlife?",
value: 0,
icon: Icon(Icons.wine_bar),
key: "nightlife",
),
SinglePreference(
name: "Nature",
description: "How much do you like nature?",
value: 0,
icon: Icon(Icons.landscape),
key: "nature",
),
SinglePreference(
name: "Culture",
description: "How much do you like culture?",
value: 0,
icon: Icon(Icons.palette),
key: "culture",
),
];
void save() async {
SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
for (SinglePreference pref in preferences) {
sharedPrefs.setInt(pref.key, pref.value);
}
}
void load() async {
SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
for (SinglePreference pref in preferences) {
pref.value = sharedPrefs.getInt(pref.key) ?? 0;
}
}
}

View File

@@ -0,0 +1,14 @@
import "package:fast_network_navigation/structs/landmark.dart";
class Route {
final String name;
final Duration duration;
final List<Landmark> landmarks;
Route({
required this.name,
required this.duration,
required this.landmarks
});
}

View File

@@ -0,0 +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;
// 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,
});
factory Trip.fromJson(Map<String, dynamic> json) {
return Trip(
uuid: json['uuid'],
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

@@ -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;
}

View File

@@ -5,6 +5,8 @@
import FlutterMacOS import FlutterMacOS
import Foundation import Foundation
import shared_preferences_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
} }

View File

@@ -41,6 +41,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.18.0" version: "1.18.0"
csslib:
dependency: transitive
description:
name: csslib
sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
cupertino_icons: cupertino_icons:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -57,6 +65,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.1" version: "1.3.1"
ffi:
dependency: transitive
description:
name: ffi
sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
file:
dependency: transitive
description:
name: file
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@@ -66,65 +90,86 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: flutter_lints name: flutter_lints
sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.2" version: "4.0.0"
flutter_map: flutter_plugin_android_lifecycle:
dependency: "direct main" dependency: transitive
description: description:
name: flutter_map name: flutter_plugin_android_lifecycle
sha256: cda8d72135b697f519287258b5294a57ce2f2a5ebf234f0e406aad4dc14c9399 sha256: "8cf40eebf5dec866a6d1956ad7b4f7016e6c0cc69847ab946833b7d43743809f"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.1.0" version: "2.0.19"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
geocode: flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
google_maps:
dependency: transitive
description:
name: google_maps
sha256: "47eef3836b49bb030d5cb3afc60b8451408bf34cf753e571b645d6529eb4251a"
url: "https://pub.dev"
source: hosted
version: "7.1.0"
google_maps_flutter:
dependency: "direct main" dependency: "direct main"
description: description:
name: geocode name: google_maps_flutter
sha256: cf9727c369bb3703b97d6e440225962dc27b7f3c686662fe3cdcc91cbfb7074d sha256: c1972cbad779bc5346c49045f26ae45550a0958b1cbca5b524dd3c8954995d28
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.3" version: "2.6.1"
geocoding: google_maps_flutter_android:
dependency: "direct main"
description:
name: geocoding
sha256: d580c801cba9386b4fac5047c4c785a4e19554f46be42f4f5e5b7deacd088a66
url: "https://pub.dev"
source: hosted
version: "3.0.0"
geocoding_android:
dependency: transitive dependency: transitive
description: description:
name: geocoding_android name: google_maps_flutter_android
sha256: "1b13eca79b11c497c434678fed109c2be020b158cec7512c848c102bc7232603" sha256: "0bcadb80eba39afda77dede89a6caafd3b68f2786b90491eceea4a01c3db181c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.3.1" version: "2.8.0"
geocoding_ios: google_maps_flutter_ios:
dependency: transitive dependency: transitive
description: description:
name: geocoding_ios name: google_maps_flutter_ios
sha256: "94ddba60387501bd1c11e18dca7c5a9e8c645d6e3da9c38b9762434941870c24" sha256: e5132d17f051600d90d79d9f574b177c24231da702453a036db2490f9ced4646
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.1" version: "2.6.0"
geocoding_platform_interface: google_maps_flutter_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: geocoding_platform_interface name: google_maps_flutter_platform_interface
sha256: "8c2c8226e5c276594c2e18bfe88b19110ed770aeb7c1ab50ede570be8b92229b" sha256: "167af879da4d004cd58771f1469b91dcc3b9b0a2c5334cc6bf71fd41d4b35403"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.2.0" version: "2.6.0"
google_maps_flutter_web:
dependency: transitive
description:
name: google_maps_flutter_web
sha256: "0c0d5c723d94b295cf86dd1c45ff91d2ac1fff7c05ddca4f01bef9fa0a014690"
url: "https://pub.dev"
source: hosted
version: "0.5.7"
html:
dependency: transitive
description:
name: html
sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a"
url: "https://pub.dev"
source: hosted
version: "0.15.4"
http: http:
dependency: transitive dependency: "direct main"
description: description:
name: http name: http
sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938"
@@ -139,22 +184,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.2" version: "4.0.2"
intl: js:
dependency: transitive dependency: transitive
description: description:
name: intl name: js
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.19.0" version: "0.6.7"
latlong2: js_wrapping:
dependency: "direct main" dependency: transitive
description: description:
name: latlong2 name: js_wrapping
sha256: "98227922caf49e6056f91b6c56945ea1c7b166f28ffcd5fb8e72fc0b453cc8fe" sha256: e385980f7c76a8c1c9a560dfb623b890975841542471eade630b2871d243851c
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.9.1" version: "0.7.4"
leak_tracker: leak_tracker:
dependency: transitive dependency: transitive
description: description:
@@ -183,26 +228,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: lints name: lints
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.0" version: "4.0.0"
lists:
dependency: transitive
description:
name: lists
sha256: "4ca5c19ae4350de036a7e996cdd1ee39c93ac0a2b840f4915459b7d0a7d4ab27"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
logger:
dependency: transitive
description:
name: logger
sha256: "8c94b8c219e7e50194efc8771cd0e9f10807d8d3e219af473d89b06cc2ee4e04"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@@ -227,14 +256,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.12.0" version: "1.12.0"
mgrs_dart:
dependency: transitive
description:
name: mgrs_dart
sha256: fb89ae62f05fa0bb90f70c31fc870bcbcfd516c843fb554452ab3396f78586f7
url: "https://pub.dev"
source: hosted
version: "2.0.0"
path: path:
dependency: transitive dependency: transitive
description: description:
@@ -243,6 +264,38 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.0" version: "1.9.0"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
url: "https://pub.dev"
source: hosted
version: "2.2.1"
platform:
dependency: transitive
description:
name: platform
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
url: "https://pub.dev"
source: hosted
version: "3.1.4"
plugin_platform_interface: plugin_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -251,22 +304,70 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.8" version: "2.1.8"
polylabel: sanitize_html:
dependency: transitive dependency: transitive
description: description:
name: polylabel name: sanitize_html
sha256: "41b9099afb2aa6c1730bdd8a0fab1400d287694ec7615dd8516935fa3144214b" sha256: "12669c4a913688a26555323fb9cec373d8f9fbe091f2d01c40c723b33caa8989"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
proj4dart:
dependency: transitive
description:
name: proj4dart
sha256: c8a659ac9b6864aa47c171e78d41bbe6f5e1d7bd790a5814249e6b68bc44324e
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.0" version: "2.1.0"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180
url: "https://pub.dev"
source: hosted
version: "2.2.3"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2"
url: "https://pub.dev"
source: hosted
version: "2.2.2"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a"
url: "https://pub.dev"
source: hosted
version: "2.3.0"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@@ -304,6 +405,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.2" version: "2.1.2"
stream_transform:
dependency: transitive
description:
name: stream_transform
sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
string_scanner: string_scanner:
dependency: transitive dependency: transitive
description: description:
@@ -336,14 +445,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.2" version: "1.3.2"
unicode:
dependency: transitive
description:
name: unicode
sha256: "0f69e46593d65245774d4f17125c6084d2c20b4e473a983f6e21b7d7762218f1"
url: "https://pub.dev"
source: hosted
version: "0.3.1"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
@@ -368,14 +469,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.1" version: "0.5.1"
wkt_parser: win32:
dependency: transitive dependency: transitive
description: description:
name: wkt_parser name: win32
sha256: "8a555fc60de3116c00aad67891bcab20f81a958e4219cc106e3c037aa3937f13" sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" version: "5.5.1"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
url: "https://pub.dev"
source: hosted
version: "1.0.4"
sdks: sdks:
dart: ">=3.3.4 <4.0.0" dart: ">=3.4.0 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54" flutter: ">=3.19.0"

View File

@@ -35,11 +35,10 @@ dependencies:
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.6 cupertino_icons: ^1.0.6
flutter_map: ^6.1.0
sliding_up_panel: ^2.0.0+1 sliding_up_panel: ^2.0.0+1
latlong2: ^0.9.1 google_maps_flutter: ^2.6.1
geocoding: ^3.0.0 http: ^1.2.1
geocode: ^1.0.3 shared_preferences: ^2.2.3
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@@ -50,7 +49,7 @@ dev_dependencies:
# activated in the `analysis_options.yaml` file located at the root of your # activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint # package. See that file for information about deactivating specific lint
# rules and activating additional ones. # rules and activating additional ones.
flutter_lints: ^3.0.0 flutter_lints: ^4.0.0
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec # following page: https://dart.dev/tools/pub/pubspec

View File

@@ -9,16 +9,15 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
// import 'package:fast_network_navigation/main.dart'; // import 'package:fast_network_navigation/main.dart';
import 'package:fast_network_navigation/modules/scaffold.dart'; import 'package:fast_network_navigation/layout.dart';
void main() { void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async { testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame. // Build our app and trigger a frame.
await tester.pumpWidget(BasePage(title: "City Nav")); await tester.pumpWidget(BasePage(mainScreen: "map",));
// Verify that our counter starts at 0. // Verfiy that the title is displayed
expect(find.text('0'), findsOneWidget); expect(find.text('City Nav'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame. // Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add)); await tester.tap(find.byIcon(Icons.add));

View File

@@ -14,6 +14,7 @@
This is a placeholder for base href that will be replaced by the value of This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`. the `--base-href` argument provided to `flutter build`.
--> -->
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyCeWk_D2xvfOHLidvV56EZeQCUybypEntw&libraries=drawing"></script>
<base href="$FLUTTER_BASE_HREF"> <base href="$FLUTTER_BASE_HREF">
<meta charset="UTF-8"> <meta charset="UTF-8">

3
renovate.json Normal file
View File

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