Implement common base structure #7
31
.gitea/workflows/backed_build-image.yaml
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
name: Build and push docker image
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
|
||||||
|
- uses: https://gitea.com/actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Login to Docker Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: git.kluster.moll.re
|
||||||
|
username: ${{ gitea.repository_owner }}
|
||||||
|
password: ${{ secrets.DOCKER_PUSH_TOKEN }}
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: backend
|
||||||
|
tags: git.kluster.moll.re/remoll/fast_network_navigation/backend:latest
|
||||||
|
push: true
|
@ -2,44 +2,53 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
paths:
|
||||||
|
- frontend/**
|
||||||
|
|
||||||
|
|
||||||
name: Build and release APK
|
name: Build and release APK
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build APK
|
name: Build APK
|
||||||
runs-on: k8s
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- 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
|
||||||
uses: https://github.com/android-actions/setup-android@v3
|
uses: https://github.com/android-actions/setup-android@v3
|
||||||
|
|
||||||
- run: flutter pub get
|
- run: flutter pub get
|
||||||
|
working-directory: ./frontend
|
||||||
|
|
||||||
- run: flutter build apk --debug --split-per-abi
|
- run: flutter build apk --release --split-per-abi
|
||||||
|
working-directory: ./frontend
|
||||||
|
|
||||||
- name: Release APK
|
- name: Release APK
|
||||||
uses: https://gitea.com/akkuman/gitea-release-action@v1
|
uses: https://gitea.com/akkuman/gitea-release-action@v1
|
||||||
with:
|
with:
|
||||||
files: build/app/outputs/flutter-apk/*.apk
|
files: ./frontend/build/app/outputs/flutter-apk/*.apk
|
||||||
name: Testing release
|
name: Testing release
|
||||||
release_name: testing
|
release_name: testing
|
||||||
tag: testing
|
tag: testing
|
||||||
@ -49,3 +58,4 @@ jobs:
|
|||||||
token: ${{ secrets.GITEA_TOKEN }}
|
token: ${{ secrets.GITEA_TOKEN }}
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: '--experimental-fetch'
|
NODE_OPTIONS: '--experimental-fetch'
|
||||||
|
|
@ -2,13 +2,16 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
paths:
|
||||||
|
- frontend/**
|
||||||
|
|
||||||
|
|
||||||
name: Build web
|
name: Build web
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build Web
|
name: Build Web
|
||||||
runs-on: k8s
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: Install prerequisites
|
- name: Install prerequisites
|
||||||
@ -25,6 +28,7 @@ jobs:
|
|||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
- run: flutter pub get
|
- run: flutter pub get
|
||||||
|
working-directory: ./frontend
|
||||||
|
|
||||||
- run: flutter build web
|
- run: flutter build web
|
||||||
|
working-directory: ./frontend
|
@ -1,31 +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
|
|
||||||
|
|
||||||
- run: flutter test
|
|
44
.gitignore
vendored
@ -1,43 +1 @@
|
|||||||
# Miscellaneous
|
cache/
|
||||||
*.class
|
|
||||||
*.log
|
|
||||||
*.pyc
|
|
||||||
*.swp
|
|
||||||
.DS_Store
|
|
||||||
.atom/
|
|
||||||
.buildlog/
|
|
||||||
.history
|
|
||||||
.svn/
|
|
||||||
migrate_working_dir/
|
|
||||||
|
|
||||||
# IntelliJ related
|
|
||||||
*.iml
|
|
||||||
*.ipr
|
|
||||||
*.iws
|
|
||||||
.idea/
|
|
||||||
|
|
||||||
# The .vscode folder contains launch configuration and tasks you configure in
|
|
||||||
# VS Code which you may wish to be included in version control, so this line
|
|
||||||
# is commented out by default.
|
|
||||||
#.vscode/
|
|
||||||
|
|
||||||
# Flutter/Dart/Pub related
|
|
||||||
**/doc/api/
|
|
||||||
**/ios/Flutter/.last_build_id
|
|
||||||
.dart_tool/
|
|
||||||
.flutter-plugins
|
|
||||||
.flutter-plugins-dependencies
|
|
||||||
.pub-cache/
|
|
||||||
.pub/
|
|
||||||
/build/
|
|
||||||
|
|
||||||
# Symbolication related
|
|
||||||
app.*.symbols
|
|
||||||
|
|
||||||
# Obfuscation related
|
|
||||||
app.*.map.json
|
|
||||||
|
|
||||||
# Android Studio will place build artifacts here
|
|
||||||
/android/app/debug
|
|
||||||
/android/app/profile
|
|
||||||
/android/app/release
|
|
||||||
|
164
backend/.gitignore
vendored
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
# osm-cache
|
||||||
|
cache/
|
||||||
|
# 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/
|
11
backend/Dockerfile
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
FROM python:3
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY Pipfile Pipfile.lock .
|
||||||
|
|
||||||
|
RUN pip install pipenv
|
||||||
|
RUN pipenv install --deploy --system
|
||||||
|
|
||||||
|
COPY . /src
|
||||||
|
|
||||||
|
CMD ["pipenv", "run", "python", "/app/src/main.py"]
|
14
backend/Pipfile
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
[[source]]
|
||||||
|
url = "https://pypi.org/simple"
|
||||||
|
verify_ssl = true
|
||||||
|
name = "pypi"
|
||||||
|
|
||||||
|
[packages]
|
||||||
|
numpy = "*"
|
||||||
|
scipy = "*"
|
||||||
|
fastapi = "*"
|
||||||
|
osmpythontools = "*"
|
||||||
|
pydantic = "*"
|
||||||
|
shapely = "*"
|
||||||
|
|
||||||
|
[dev-packages]
|
1543
backend/Pipfile.lock
generated
Normal file
11
backend/src/amenities/nature.am
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
'leisure'='park'
|
||||||
|
geological
|
||||||
|
'natural'='geyser'
|
||||||
|
'natural'='hot_spring'
|
||||||
|
'natural'='arch'
|
||||||
|
'natural'='volcano'
|
||||||
|
'natural'='stone'
|
||||||
|
'tourism'='alpine_hut'
|
||||||
|
'tourism'='viewpoint'
|
||||||
|
'tourism'='zoo'
|
||||||
|
'waterway'='waterfall'
|
2
backend/src/amenities/shopping.am
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
'shop'='department_store'
|
||||||
|
'shop'='mall'
|
8
backend/src/amenities/sightseeing.am
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
'tourism'='museum'
|
||||||
|
'tourism'='attraction'
|
||||||
|
'tourism'='gallery'
|
||||||
|
historic
|
||||||
|
'amenity'='planetarium'
|
||||||
|
'amenity'='place_of_worship'
|
||||||
|
'amenity'='fountain'
|
||||||
|
'water'='reflecting_pool'
|
365
backend/src/landmarks_manager.py
Normal file
@ -0,0 +1,365 @@
|
|||||||
|
import math as m
|
||||||
|
import json, os
|
||||||
|
|
||||||
|
from typing import List, Tuple, Optional
|
||||||
|
from OSMPythonTools.overpass import Overpass, overpassQueryBuilder
|
||||||
|
|
||||||
|
from structs.landmarks import Landmark, LandmarkType
|
||||||
|
from structs.preferences import Preferences, Preference
|
||||||
|
|
||||||
|
|
||||||
|
SIGHTSEEING = LandmarkType(landmark_type='sightseeing')
|
||||||
|
NATURE = LandmarkType(landmark_type='nature')
|
||||||
|
SHOPPING = LandmarkType(landmark_type='shopping')
|
||||||
|
|
||||||
|
|
||||||
|
# Include the json here
|
||||||
|
# Create a list of all things to visit given some preferences and a city. Ready for the optimizer
|
||||||
|
def generate_landmarks(preferences: Preferences, coordinates: Tuple[float, float]) :
|
||||||
|
|
||||||
|
l_sights, l_nature, l_shop = get_amenities()
|
||||||
|
L = []
|
||||||
|
|
||||||
|
# List for sightseeing
|
||||||
|
if preferences.sightseeing.score != 0 :
|
||||||
|
L1 = get_landmarks(l_sights, SIGHTSEEING, coordinates=coordinates)
|
||||||
|
correct_score(L1, preferences.sightseeing)
|
||||||
|
L += L1
|
||||||
|
|
||||||
|
# List for nature
|
||||||
|
if preferences.nature.score != 0 :
|
||||||
|
L2 = get_landmarks(l_nature, NATURE, coordinates=coordinates)
|
||||||
|
correct_score(L2, preferences.nature)
|
||||||
|
L += L2
|
||||||
|
|
||||||
|
# List for shopping
|
||||||
|
if preferences.shopping.score != 0 :
|
||||||
|
L3 = get_landmarks(l_shop, SHOPPING, coordinates=coordinates)
|
||||||
|
correct_score(L3, preferences.shopping)
|
||||||
|
L += L3
|
||||||
|
|
||||||
|
L = remove_duplicates(L)
|
||||||
|
|
||||||
|
return L, take_most_important(L)
|
||||||
|
|
||||||
|
|
||||||
|
"""def generate_landmarks(preferences: Preferences, city_country: str = None, coordinates: Tuple[float, float] = None) -> Tuple[List[Landmark], List[Landmark]] :
|
||||||
|
|
||||||
|
l_sights, l_nature, l_shop = get_amenities()
|
||||||
|
L = []
|
||||||
|
|
||||||
|
# List for sightseeing
|
||||||
|
if preferences.sightseeing.score != 0 :
|
||||||
|
L1 = get_landmarks(l_sights, SIGHTSEEING, city_country=city_country, coordinates=coordinates)
|
||||||
|
correct_score(L1, preferences.sightseeing)
|
||||||
|
L += L1
|
||||||
|
|
||||||
|
# List for nature
|
||||||
|
if preferences.nature.score != 0 :
|
||||||
|
L2 = get_landmarks(l_nature, NATURE, city_country=city_country, coordinates=coordinates)
|
||||||
|
correct_score(L2, preferences.nature)
|
||||||
|
L += L2
|
||||||
|
|
||||||
|
# List for shopping
|
||||||
|
if preferences.shopping.score != 0 :
|
||||||
|
L3 = get_landmarks(l_shop, SHOPPING, city_country=city_country, coordinates=coordinates)
|
||||||
|
correct_score(L3, preferences.shopping)
|
||||||
|
L += L3
|
||||||
|
|
||||||
|
return remove_duplicates(L), take_most_important(L)
|
||||||
|
"""
|
||||||
|
# Helper function to gather the amenities list
|
||||||
|
def get_amenities() -> List[List[str]] :
|
||||||
|
|
||||||
|
# Get the list of amenities from the files
|
||||||
|
sightseeing = get_list('/amenities/sightseeing.am')
|
||||||
|
nature = get_list('/amenities/nature.am')
|
||||||
|
shopping = get_list('/amenities/shopping.am')
|
||||||
|
|
||||||
|
return sightseeing, nature, shopping
|
||||||
|
|
||||||
|
|
||||||
|
# Helper function to read a .am file and generate the corresponding list
|
||||||
|
def get_list(path: str) -> List[str] :
|
||||||
|
|
||||||
|
with open(os.path.dirname(os.path.abspath(__file__)) + path) as f :
|
||||||
|
content = f.readlines()
|
||||||
|
|
||||||
|
amenities = []
|
||||||
|
for line in content :
|
||||||
|
amenities.append(line.strip('\n'))
|
||||||
|
|
||||||
|
return amenities
|
||||||
|
|
||||||
|
|
||||||
|
# Take the most important landmarks from the list
|
||||||
|
def take_most_important(L: List[Landmark], N = 0) -> List[Landmark] :
|
||||||
|
|
||||||
|
# Read the parameters from the file
|
||||||
|
with open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/landmarks_manager.params', "r") as f :
|
||||||
|
parameters = json.loads(f.read())
|
||||||
|
N_important = parameters['N important']
|
||||||
|
|
||||||
|
L_copy = []
|
||||||
|
L_clean = []
|
||||||
|
scores = [0]*len(L)
|
||||||
|
names = []
|
||||||
|
name_id = {}
|
||||||
|
|
||||||
|
for i, elem in enumerate(L) :
|
||||||
|
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-N):]
|
||||||
|
|
||||||
|
for i, elem in enumerate(L_copy) :
|
||||||
|
if i in res :
|
||||||
|
L_clean.append(elem)
|
||||||
|
|
||||||
|
return L_clean
|
||||||
|
|
||||||
|
|
||||||
|
# Remove duplicate elements and elements with low score
|
||||||
|
def remove_duplicates(L: List[Landmark]) -> List[Landmark] :
|
||||||
|
"""
|
||||||
|
Removes duplicate landmarks based on their names from the given list.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
L (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 L :
|
||||||
|
if landmark.name in names:
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
else :
|
||||||
|
names.append(landmark.name)
|
||||||
|
L_clean.append(landmark)
|
||||||
|
|
||||||
|
return L_clean
|
||||||
|
|
||||||
|
|
||||||
|
# Correct the score of a list of landmarks by taking into account preference settings
|
||||||
|
def correct_score(L: List[Landmark], preference: Preference) :
|
||||||
|
|
||||||
|
if len(L) == 0 :
|
||||||
|
return
|
||||||
|
|
||||||
|
if L[0].type != preference.type :
|
||||||
|
raise TypeError(f"LandmarkType {preference.type} does not match the type of Landmark {L[0].name}")
|
||||||
|
|
||||||
|
for elem in L :
|
||||||
|
elem.attractiveness = int(elem.attractiveness*preference.score/500) # arbitrary computation
|
||||||
|
|
||||||
|
|
||||||
|
# Function to count elements within a certain radius of a location
|
||||||
|
def count_elements_within_radius(coordinates: Tuple[float, float], radius: int) -> int:
|
||||||
|
|
||||||
|
lat = coordinates[0]
|
||||||
|
lon = coordinates[1]
|
||||||
|
|
||||||
|
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 :
|
||||||
|
overpass = Overpass()
|
||||||
|
radius_result = overpass.query(radius_query)
|
||||||
|
return radius_result.countElements()
|
||||||
|
|
||||||
|
except :
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# Creates a bounding box around given coordinates
|
||||||
|
def create_bbox(coordinates: Tuple[float, float], side_length: int) -> Tuple[float, float, float, float]:
|
||||||
|
|
||||||
|
lat = coordinates[0]
|
||||||
|
lon = coordinates[1]
|
||||||
|
|
||||||
|
# Half the side length in km (since it's a square bbox)
|
||||||
|
half_side_length_km = side_length / 2.0
|
||||||
|
|
||||||
|
# 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 get_landmarks(list_amenity: list, landmarktype: LandmarkType, coordinates: Tuple[float, float]) -> List[Landmark] :
|
||||||
|
|
||||||
|
# Read the parameters from the file
|
||||||
|
with open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/landmarks_manager.params', "r") as f :
|
||||||
|
parameters = json.loads(f.read())
|
||||||
|
tag_coeff = parameters['tag coeff']
|
||||||
|
park_coeff = parameters['park coeff']
|
||||||
|
church_coeff = parameters['church coeff']
|
||||||
|
radius = parameters['radius close to']
|
||||||
|
bbox_side = parameters['city bbox side']
|
||||||
|
|
||||||
|
# Create bbox around start location
|
||||||
|
bbox = create_bbox(coordinates, bbox_side)
|
||||||
|
|
||||||
|
# Initialize some variables
|
||||||
|
N = 0
|
||||||
|
L = []
|
||||||
|
overpass = Overpass()
|
||||||
|
|
||||||
|
for amenity in list_amenity :
|
||||||
|
query = overpassQueryBuilder(bbox=bbox, elementType=['way', 'relation'], selector=amenity, includeCenter=True, out='body')
|
||||||
|
result = overpass.query(query)
|
||||||
|
N += result.countElements()
|
||||||
|
|
||||||
|
for elem in result.elements():
|
||||||
|
|
||||||
|
name = elem.tag('name') # Add name
|
||||||
|
location = (elem.centerLat(), elem.centerLon()) # Add coordinates (lat, lon)
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
else :
|
||||||
|
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
|
||||||
|
|
||||||
|
# Add score of given landmark based on the number of surrounding elements. Penalty for churches as there are A LOT
|
||||||
|
if amenity == "'amenity'='place_of_worship'" :
|
||||||
|
score = int((count_elements_within_radius(location, radius) + n_tags*tag_coeff )*church_coeff)
|
||||||
|
elif amenity == "'leisure'='park'" :
|
||||||
|
score = int((count_elements_within_radius(location, radius) + n_tags*tag_coeff )*park_coeff)
|
||||||
|
else :
|
||||||
|
score = count_elements_within_radius(location, radius) + n_tags*tag_coeff
|
||||||
|
|
||||||
|
if score is not None :
|
||||||
|
# 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=n_tags)
|
||||||
|
L.append(landmark)
|
||||||
|
|
||||||
|
return L
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"""def get_landmarks(list_amenity: list, landmarktype: LandmarkType, city_country: str = None, coordinates: Tuple[float, float] = None) -> List[Landmark] :
|
||||||
|
|
||||||
|
if city_country is None and coordinates is None :
|
||||||
|
raise ValueError("Either one of 'city_country' and 'coordinates' arguments must be specified")
|
||||||
|
|
||||||
|
if city_country is not None and coordinates is not None :
|
||||||
|
raise ValueError("Cannot specify both 'city_country' and 'coordinates' at the same time, please choose either one")
|
||||||
|
|
||||||
|
# Read the parameters from the file
|
||||||
|
with open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/landmarks_manager.params', "r") as f :
|
||||||
|
parameters = json.loads(f.read())
|
||||||
|
tag_coeff = parameters['tag coeff']
|
||||||
|
park_coeff = parameters['park coeff']
|
||||||
|
church_coeff = parameters['church coeff']
|
||||||
|
radius = parameters['radius close to']
|
||||||
|
bbox_side = parameters['city bbox side']
|
||||||
|
|
||||||
|
# If city_country is specified :
|
||||||
|
if city_country is not None :
|
||||||
|
nominatim = Nominatim()
|
||||||
|
areaId = nominatim.query(city_country).areaId()
|
||||||
|
bbox = None
|
||||||
|
|
||||||
|
# If coordinates are specified :
|
||||||
|
elif coordinates is not None :
|
||||||
|
bbox = create_bbox(coordinates, bbox_side)
|
||||||
|
areaId = None
|
||||||
|
|
||||||
|
else :
|
||||||
|
raise ValueError("Argument number is not corresponding.")
|
||||||
|
|
||||||
|
# Initialize some variables
|
||||||
|
N = 0
|
||||||
|
L = []
|
||||||
|
overpass = Overpass()
|
||||||
|
|
||||||
|
for amenity in list_amenity :
|
||||||
|
query = overpassQueryBuilder(area=areaId, bbox=bbox, elementType=['way', 'relation'], selector=amenity, includeCenter=True, out='body')
|
||||||
|
result = overpass.query(query)
|
||||||
|
N += result.countElements()
|
||||||
|
|
||||||
|
for elem in result.elements():
|
||||||
|
|
||||||
|
name = elem.tag('name') # Add name
|
||||||
|
location = (elem.centerLat(), elem.centerLon()) # Add coordinates (lat, lon)
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
else :
|
||||||
|
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
|
||||||
|
|
||||||
|
# Add score of given landmark based on the number of surrounding elements. Penalty for churches as there are A LOT
|
||||||
|
if amenity == "'amenity'='place_of_worship'" :
|
||||||
|
score = int((count_elements_within_radius(location, radius) + n_tags*tag_coeff )*church_coeff)
|
||||||
|
elif amenity == "'leisure'='park'" :
|
||||||
|
score = int((count_elements_within_radius(location, radius) + n_tags*tag_coeff )*park_coeff)
|
||||||
|
else :
|
||||||
|
score = count_elements_within_radius(location, radius) + n_tags*tag_coeff
|
||||||
|
|
||||||
|
if score is not None :
|
||||||
|
# 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=n_tags)
|
||||||
|
L.append(landmark)
|
||||||
|
|
||||||
|
return L
|
||||||
|
"""
|
70
backend/src/main.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
from optimizer import solve_optimization
|
||||||
|
from refiner import refine_optimization
|
||||||
|
from landmarks_manager import generate_landmarks
|
||||||
|
from structs.landmarks import Landmark
|
||||||
|
from structs.landmarktype import LandmarkType
|
||||||
|
from structs.preferences import Preferences, Preference
|
||||||
|
from fastapi import FastAPI, Query, Body
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
|
||||||
|
# Assuming frontend is calling like this :
|
||||||
|
#"http://127.0.0.1:8000/process?param1={param1}¶m2={param2}"
|
||||||
|
@app.post("/optimizer_coords/{start_lat}/{start_lon}/{finish_lat}/{finish_lon}")
|
||||||
|
def main1(start_lat: float, start_lon: float, preferences: Preferences = Body(...), finish_lat: float = None, finish_lon: float = None) -> List[Landmark]:
|
||||||
|
|
||||||
|
if preferences is None :
|
||||||
|
raise ValueError("Please provide preferences in the form of a 'Preference' BaseModel class.")
|
||||||
|
if bool(start_lat) ^ bool(start_lon) :
|
||||||
|
raise ValueError("Please provide both latitude and longitude for the starting point")
|
||||||
|
if bool(finish_lat) ^ bool(finish_lon) :
|
||||||
|
raise ValueError("Please provide both latitude and longitude for the finish point")
|
||||||
|
|
||||||
|
start = Landmark(name='start', type=LandmarkType(landmark_type='start'), location=(start_lat, start_lon), osm_type='start', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
|
||||||
|
|
||||||
|
if bool(finish_lat) and bool(finish_lon) :
|
||||||
|
finish = Landmark(name='finish', type=LandmarkType(landmark_type='finish'), location=(finish_lat, finish_lon), osm_type='finish', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
|
||||||
|
else :
|
||||||
|
finish = Landmark(name='finish', type=LandmarkType(landmark_type='finish'), location=(start_lat, start_lon), 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.8375946, 2.2949904), 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.8375946, 2.2949904), osm_type='finish', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
|
||||||
|
|
||||||
|
# Generate the landmarks from the start location
|
||||||
|
landmarks, landmarks_short = generate_landmarks(preferences=preferences, coordinates=start.location)
|
||||||
|
|
||||||
|
# insert start and finish to the landmarks list
|
||||||
|
landmarks_short.insert(0, start)
|
||||||
|
landmarks_short.append(finish)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO use these parameters in another way
|
||||||
|
max_walking_time = 4 # hours
|
||||||
|
detour = 30 # minutes
|
||||||
|
|
||||||
|
# First stage optimization
|
||||||
|
base_tour = solve_optimization(landmarks_short, max_walking_time*60, True)
|
||||||
|
|
||||||
|
# Second stage optimization
|
||||||
|
refined_tour = refine_optimization(landmarks, base_tour, max_walking_time*60+detour, True)
|
||||||
|
|
||||||
|
return refined_tour
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# input city, country in the form of 'Paris, France'
|
||||||
|
@app.post("/test2/{city_country}")
|
||||||
|
def test2(city_country: str, preferences: Preferences = Body(...)) -> List[Landmark]:
|
||||||
|
|
||||||
|
landmarks = generate_landmarks(city_country, preferences)
|
||||||
|
|
||||||
|
max_steps = 9000000
|
||||||
|
|
||||||
|
visiting_order = solve_optimization(landmarks, max_steps, True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
23
backend/src/main_example.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import fastapi
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Destination:
|
||||||
|
name: str
|
||||||
|
location: tuple
|
||||||
|
attractiveness: int
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
d = Destination()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def get_route() -> list[Destination]:
|
||||||
|
return {"route": "Hello World"}
|
||||||
|
|
||||||
|
endpoint = ("/get_route", get_route)
|
||||||
|
end
|
||||||
|
if __name__ == "__main__":
|
||||||
|
fastapi.run()
|
411
backend/src/optimizer.py
Normal file
@ -0,0 +1,411 @@
|
|||||||
|
import numpy as np
|
||||||
|
import json, os
|
||||||
|
|
||||||
|
from typing import List, Tuple
|
||||||
|
from scipy.optimize import linprog
|
||||||
|
from math import radians, sin, cos, acos
|
||||||
|
from shapely import Polygon
|
||||||
|
|
||||||
|
from structs.landmarks import Landmark
|
||||||
|
|
||||||
|
|
||||||
|
# Function to print the result
|
||||||
|
def print_res(L: List[Landmark], L_tot):
|
||||||
|
|
||||||
|
if len(L) == L_tot:
|
||||||
|
print('\nAll landmarks can be visited within max_steps, the following order is suggested : ')
|
||||||
|
else :
|
||||||
|
print('Could not visit all the landmarks, the following order is suggested : ')
|
||||||
|
|
||||||
|
dist = 0
|
||||||
|
for elem in L :
|
||||||
|
if elem.name != 'start' :
|
||||||
|
print('- ' + elem.name + ', time to reach = ' + str(elem.time_to_reach))
|
||||||
|
dist += elem.time_to_reach
|
||||||
|
else :
|
||||||
|
print('- ' + elem.name)
|
||||||
|
|
||||||
|
print("\nMinutes walked : " + str(dist))
|
||||||
|
print(f"Visited {len(L)-2} out of {L_tot-2} landmarks")
|
||||||
|
|
||||||
|
|
||||||
|
# Prevent the use of a particular solution
|
||||||
|
def prevent_config(resx, A_ub, b_ub) -> bool:
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
A_ub = np.vstack((A_ub, h))
|
||||||
|
b_ub.append(len(vertices_visited)-1)
|
||||||
|
|
||||||
|
return A_ub, b_ub
|
||||||
|
|
||||||
|
|
||||||
|
# Prevent the possibility of a given solution bit
|
||||||
|
def break_cricle(circle_vertices: list, L: int, A_ub: list, b_ub: list) -> bool:
|
||||||
|
|
||||||
|
if L-1 in circle_vertices :
|
||||||
|
circle_vertices.remove(L-1)
|
||||||
|
|
||||||
|
h = [0]*L*L
|
||||||
|
for i in range(L) :
|
||||||
|
if i in circle_vertices :
|
||||||
|
h[i*L:i*L+L] = [1]*L
|
||||||
|
|
||||||
|
A_ub = np.vstack((A_ub, h))
|
||||||
|
b_ub.append(len(circle_vertices)-1)
|
||||||
|
|
||||||
|
return A_ub, b_ub
|
||||||
|
|
||||||
|
|
||||||
|
# Checks if the path is connected, returns a circle if it finds one and the RESULT
|
||||||
|
def is_connected(resx) -> bool:
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
n_edges = resx.sum() # number of edges
|
||||||
|
|
||||||
|
nonzeroind = np.nonzero(resx)[0] # the return is a little funny so I use the [0]
|
||||||
|
|
||||||
|
nonzero_tup = np.unravel_index(nonzeroind, (L,L))
|
||||||
|
|
||||||
|
ind_a = nonzero_tup[0].tolist()
|
||||||
|
ind_b = nonzero_tup[1].tolist()
|
||||||
|
|
||||||
|
edges = []
|
||||||
|
edges_visited = []
|
||||||
|
vertices_visited = []
|
||||||
|
|
||||||
|
edge1 = (ind_a[0], ind_b[0])
|
||||||
|
edges_visited.append(edge1)
|
||||||
|
vertices_visited.append(edge1[0])
|
||||||
|
|
||||||
|
for i, a in enumerate(ind_a) :
|
||||||
|
edges.append((a, ind_b[i])) # Create the list of edges
|
||||||
|
|
||||||
|
remaining = edges
|
||||||
|
remaining.remove(edge1)
|
||||||
|
|
||||||
|
break_flag = False
|
||||||
|
while len(remaining) > 0 and not break_flag:
|
||||||
|
for edge2 in remaining :
|
||||||
|
if edge2[0] == edge1[1] :
|
||||||
|
if edge1[1] in vertices_visited :
|
||||||
|
edges_visited.append(edge2)
|
||||||
|
break_flag = True
|
||||||
|
break
|
||||||
|
else :
|
||||||
|
vertices_visited.append(edge1[1])
|
||||||
|
edges_visited.append(edge2)
|
||||||
|
remaining.remove(edge2)
|
||||||
|
edge1 = edge2
|
||||||
|
|
||||||
|
elif edge1[1] == L-1 or edge1[1] in vertices_visited:
|
||||||
|
break_flag = True
|
||||||
|
break
|
||||||
|
|
||||||
|
vertices_visited.append(edge1[1])
|
||||||
|
|
||||||
|
|
||||||
|
if len(vertices_visited) == n_edges +1 :
|
||||||
|
return vertices_visited, []
|
||||||
|
else:
|
||||||
|
return vertices_visited, edges_visited
|
||||||
|
|
||||||
|
|
||||||
|
# Function that returns the distance in meters from one location to another
|
||||||
|
def get_distance(p1: Tuple[float, float], p2: Tuple[float, float], detour: float, speed: float) :
|
||||||
|
|
||||||
|
# Compute the straight-line distance in km
|
||||||
|
if p1 == p2 :
|
||||||
|
return 0, 0
|
||||||
|
else:
|
||||||
|
dist = 6371.01 * acos(sin(radians(p1[0]))*sin(radians(p2[0])) + cos(radians(p1[0]))*cos(radians(p2[0]))*cos(radians(p1[1]) - radians(p2[1])))
|
||||||
|
|
||||||
|
# Consider the detour factor for average cityto deterline walking distance (in km)
|
||||||
|
walk_dist = dist*detour
|
||||||
|
|
||||||
|
# Time to walk this distance (in minutes)
|
||||||
|
walk_time = walk_dist/speed*60
|
||||||
|
|
||||||
|
if walk_time > 15 :
|
||||||
|
walk_time = 5*round(walk_time/5)
|
||||||
|
else :
|
||||||
|
walk_time = round(walk_time)
|
||||||
|
|
||||||
|
|
||||||
|
return round(walk_dist, 1), walk_time
|
||||||
|
|
||||||
|
|
||||||
|
# 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[Landmark], max_steps: int):
|
||||||
|
|
||||||
|
with open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/optimizer.params', "r") as f :
|
||||||
|
parameters = json.loads(f.read())
|
||||||
|
detour = parameters['detour factor']
|
||||||
|
speed = parameters['average walking speed']
|
||||||
|
|
||||||
|
# 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_distance(spot1.location, spot2.location, detour, speed)[1]
|
||||||
|
dist_table[j] = t
|
||||||
|
A_ub += dist_table
|
||||||
|
c = c*len(landmarks)
|
||||||
|
|
||||||
|
return c, A_ub, [max_steps]
|
||||||
|
|
||||||
|
|
||||||
|
# Constraint to respect only one travel per landmark. Also caps the total number of visited landmarks
|
||||||
|
def respect_number(L:int, A_ub, b_ub):
|
||||||
|
|
||||||
|
ones = [1]*L
|
||||||
|
zeros = [0]*L
|
||||||
|
for i in range(L) :
|
||||||
|
h = zeros*i + ones + zeros*(L-1-i)
|
||||||
|
A_ub = np.vstack((A_ub, h))
|
||||||
|
b_ub.append(1)
|
||||||
|
|
||||||
|
# Read the parameters from the file
|
||||||
|
with open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/optimizer.params', "r") as f :
|
||||||
|
parameters = json.loads(f.read())
|
||||||
|
max_landmarks = parameters['max landmarks']
|
||||||
|
|
||||||
|
A_ub = np.vstack((A_ub, ones*L))
|
||||||
|
b_ub.append(max_landmarks+1)
|
||||||
|
|
||||||
|
return A_ub, b_ub
|
||||||
|
|
||||||
|
|
||||||
|
# Constraint to not have d14 and d41 simultaneously. Does not prevent circular symmetry with more elements
|
||||||
|
def break_sym(L, A_ub, b_ub):
|
||||||
|
upper_ind = np.triu_indices(L,0,L)
|
||||||
|
|
||||||
|
up_ind_x = upper_ind[0]
|
||||||
|
up_ind_y = upper_ind[1]
|
||||||
|
|
||||||
|
for i, _ in enumerate(up_ind_x) :
|
||||||
|
l = [0]*L*L
|
||||||
|
if up_ind_x[i] != up_ind_y[i] :
|
||||||
|
l[up_ind_x[i]*L + up_ind_y[i]] = 1
|
||||||
|
l[up_ind_y[i]*L + up_ind_x[i]] = 1
|
||||||
|
|
||||||
|
A_ub = np.vstack((A_ub,l))
|
||||||
|
b_ub.append(1)
|
||||||
|
|
||||||
|
return A_ub, b_ub
|
||||||
|
|
||||||
|
|
||||||
|
# Constraint to not stay in position. Removes d11, d22, d33, etc.
|
||||||
|
def init_eq_not_stay(L: int):
|
||||||
|
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))
|
||||||
|
|
||||||
|
return [l], [0]
|
||||||
|
|
||||||
|
|
||||||
|
# Go through the landmarks and force the optimizer to use landmarks where attractiveness is set to -1
|
||||||
|
def respect_user_mustsee(landmarks: List[Landmark], A_eq: list, b_eq: list) :
|
||||||
|
L = len(landmarks)
|
||||||
|
|
||||||
|
for i, elem in enumerate(landmarks) :
|
||||||
|
if elem.must_do is True and elem.name not in ['finish', 'start']:
|
||||||
|
l = [0]*L*L
|
||||||
|
for j in range(L) : # sets the horizontal ones (go from)
|
||||||
|
l[j +i*L] = 1 # sets the vertical ones (go to) double check if good
|
||||||
|
|
||||||
|
for k in range(L-1) :
|
||||||
|
l[k*L+L-1] = 1
|
||||||
|
|
||||||
|
A_eq = np.vstack((A_eq,l))
|
||||||
|
b_eq.append(2)
|
||||||
|
|
||||||
|
return A_eq, b_eq
|
||||||
|
|
||||||
|
|
||||||
|
# Constraint to ensure start at start and finish at goal
|
||||||
|
def respect_start_finish(L: int, A_eq: list, b_eq: list):
|
||||||
|
ls = [1]*L + [0]*L*(L-1) # sets only horizontal ones for start (go from)
|
||||||
|
ljump = [0]*L*L
|
||||||
|
ljump[L-1] = 1 # Prevent start finish jump
|
||||||
|
lg = [0]*L*L
|
||||||
|
ll = [0]*L*(L-1) + [1]*L
|
||||||
|
for k in range(L-1) : # sets only vertical ones for goal (go to)
|
||||||
|
ll[k*L] = 1
|
||||||
|
if k != 0 : # Prevent the shortcut start -> finish
|
||||||
|
lg[k*L+L-1] = 1
|
||||||
|
|
||||||
|
|
||||||
|
A_eq = np.vstack((A_eq,ls))
|
||||||
|
A_eq = np.vstack((A_eq,ljump))
|
||||||
|
A_eq = np.vstack((A_eq,lg))
|
||||||
|
A_eq = np.vstack((A_eq,ll))
|
||||||
|
b_eq.append(1)
|
||||||
|
b_eq.append(0)
|
||||||
|
b_eq.append(1)
|
||||||
|
b_eq.append(0)
|
||||||
|
|
||||||
|
return A_eq, b_eq
|
||||||
|
|
||||||
|
|
||||||
|
# Constraint to tie the problem together. Necessary but not sufficient to avoid circles
|
||||||
|
def respect_order(N: int, A_eq, b_eq):
|
||||||
|
for i in range(N-1) : # Prevent stacked ones
|
||||||
|
if i == 0 or i == N-1: # Don't touch start or finish
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
# Computes the time to reach from each landmark to the next
|
||||||
|
def add_time_to_reach(order: List[int], landmarks: List[Landmark])->List[Landmark] :
|
||||||
|
|
||||||
|
# Read the parameters from the file
|
||||||
|
with open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/optimizer.params', "r") as f :
|
||||||
|
parameters = json.loads(f.read())
|
||||||
|
detour_factor = parameters['detour factor']
|
||||||
|
speed = parameters['average walking speed']
|
||||||
|
|
||||||
|
j = 0
|
||||||
|
L = []
|
||||||
|
prev = landmarks[0]
|
||||||
|
|
||||||
|
while(len(L) != len(order)) :
|
||||||
|
|
||||||
|
elem = landmarks[order[j]]
|
||||||
|
if elem != prev :
|
||||||
|
elem.time_to_reach = get_distance(elem.location, prev.location, detour_factor, speed)[1]
|
||||||
|
elem.must_do = True
|
||||||
|
L.append(elem)
|
||||||
|
prev = elem
|
||||||
|
j += 1
|
||||||
|
|
||||||
|
return L
|
||||||
|
|
||||||
|
|
||||||
|
def add_time_to_reach_simple(ordered_visit: List[Landmark])-> List[Landmark] :
|
||||||
|
|
||||||
|
# Read the parameters from the file
|
||||||
|
with open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/optimizer.params', "r") as f :
|
||||||
|
parameters = json.loads(f.read())
|
||||||
|
detour_factor = parameters['detour factor']
|
||||||
|
speed = parameters['average walking speed']
|
||||||
|
|
||||||
|
L = []
|
||||||
|
prev = ordered_visit[0]
|
||||||
|
L.append(prev)
|
||||||
|
total_dist = 0
|
||||||
|
|
||||||
|
for elem in ordered_visit[1:] :
|
||||||
|
elem.time_to_reach = get_distance(elem.location, prev.location, detour_factor, speed)[1]
|
||||||
|
elem.must_do = True
|
||||||
|
L.append(elem)
|
||||||
|
prev = elem
|
||||||
|
total_dist += get_distance(elem.location, prev.location, detour_factor, speed)[1]
|
||||||
|
|
||||||
|
|
||||||
|
return L, total_dist
|
||||||
|
|
||||||
|
|
||||||
|
# Main optimization pipeline
|
||||||
|
def solve_optimization (landmarks :List[Landmark], max_steps: int, printing_details: bool) :
|
||||||
|
|
||||||
|
L = len(landmarks)
|
||||||
|
|
||||||
|
# SET CONSTRAINTS FOR INEQUALITY
|
||||||
|
c, A_ub, b_ub = init_ub_dist(landmarks, max_steps) # Add the distances from each landmark to the other
|
||||||
|
A_ub, b_ub = respect_number(L, A_ub, b_ub) # Respect max number of visits (no more possible stops than landmarks).
|
||||||
|
A_ub, b_ub = break_sym(L, A_ub, b_ub) # break the 'zig-zag' symmetry
|
||||||
|
|
||||||
|
# SET CONSTRAINTS FOR EQUALITY
|
||||||
|
A_eq, b_eq = init_eq_not_stay(L) # 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 = respect_start_finish(L, A_eq, b_eq) # Force start and finish positions
|
||||||
|
A_eq, b_eq = respect_order(L, A_eq, b_eq) # Respect order of visit (only works when max_steps is limiting factor)
|
||||||
|
|
||||||
|
# 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
|
||||||
|
else :
|
||||||
|
order, circle = is_connected(res.x)
|
||||||
|
i = 0
|
||||||
|
timeout = 80
|
||||||
|
while len(circle) != 0 and i < timeout:
|
||||||
|
A_ub, b_ub = prevent_config(res.x, A_ub, b_ub)
|
||||||
|
A_ub, b_ub = break_cricle(order, len(landmarks), A_ub, b_ub)
|
||||||
|
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)
|
||||||
|
order, circle = is_connected(res.x)
|
||||||
|
if len(circle) == 0 :
|
||||||
|
break
|
||||||
|
print(i)
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
if i == timeout :
|
||||||
|
raise TimeoutError(f"Optimization took too long. No solution found after {timeout} iterations.")
|
||||||
|
|
||||||
|
# Add the times to reach and stop optimizing
|
||||||
|
L = add_time_to_reach(order, landmarks)
|
||||||
|
|
||||||
|
if printing_details is True :
|
||||||
|
if i != 0 :
|
||||||
|
print(f"Neded to recompute paths {i} times because of unconnected loops...")
|
||||||
|
print_res(L, len(landmarks))
|
||||||
|
print("\nTotal score : " + str(int(-res.fun)))
|
||||||
|
|
||||||
|
return L
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
8
backend/src/parameters/landmarks_manager.params
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"city bbox side" : 10,
|
||||||
|
"radius close to" : 27.5,
|
||||||
|
"church coeff" : 0.6,
|
||||||
|
"park coeff" : 1.5,
|
||||||
|
"tag coeff" : 100,
|
||||||
|
"N important" : 40
|
||||||
|
}
|
5
backend/src/parameters/optimizer.params
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"detour factor" : 1.4,
|
||||||
|
"average walking speed" : 4.8,
|
||||||
|
"max landmarks" : 10
|
||||||
|
}
|
293
backend/src/refiner.py
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
from heapq import heappop, heappush
|
||||||
|
from itertools import permutations
|
||||||
|
import os, json
|
||||||
|
|
||||||
|
from shapely import buffer, LineString, Point, Polygon, MultiPoint, convex_hull, concave_hull, LinearRing
|
||||||
|
from typing import List, Tuple
|
||||||
|
from math import pi
|
||||||
|
|
||||||
|
from structs.landmarks import Landmark
|
||||||
|
from landmarks_manager import take_most_important
|
||||||
|
from optimizer import solve_optimization, add_time_to_reach_simple, print_res, get_distance
|
||||||
|
|
||||||
|
|
||||||
|
def create_corridor(landmarks: List[Landmark], width: float) :
|
||||||
|
|
||||||
|
corrected_width = (180*width)/(6371000*pi)
|
||||||
|
|
||||||
|
path = create_linestring(landmarks)
|
||||||
|
obj = buffer(path, corrected_width, join_style="mitre", cap_style="square", mitre_limit=2)
|
||||||
|
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
def create_linestring(landmarks: List[Landmark])->List[Point] :
|
||||||
|
|
||||||
|
points = []
|
||||||
|
|
||||||
|
for landmark in landmarks :
|
||||||
|
points.append(Point(landmark.location))
|
||||||
|
|
||||||
|
return LineString(points)
|
||||||
|
|
||||||
|
|
||||||
|
def is_in_area(area: Polygon, coordinates) -> bool :
|
||||||
|
point = Point(coordinates)
|
||||||
|
return point.within(area)
|
||||||
|
|
||||||
|
|
||||||
|
def is_close_to(location1: Tuple[float], location2: Tuple[float]):
|
||||||
|
"""Determine if two locations are close by rounding their coordinates to 3 decimals."""
|
||||||
|
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(landmarks: List[Landmark]) -> List[Landmark]:
|
||||||
|
|
||||||
|
i = 1
|
||||||
|
while i < len(landmarks):
|
||||||
|
j = i+1
|
||||||
|
while j < len(landmarks):
|
||||||
|
if is_close_to(landmarks[i].location, landmarks[j].location) and landmarks[i].name not in ['start', 'finish'] and landmarks[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:
|
||||||
|
landmarks.insert(i + 1, landmarks.pop(j))
|
||||||
|
break # Move to the next i-th element after rearrangement
|
||||||
|
j += 1
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
return landmarks
|
||||||
|
|
||||||
|
"""
|
||||||
|
def find_shortest_path(landmarks: List[Landmark]) -> List[Landmark]:
|
||||||
|
|
||||||
|
# Read from data
|
||||||
|
with open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/optimizer.params', "r") as f :
|
||||||
|
parameters = json.loads(f.read())
|
||||||
|
detour = parameters['detour factor']
|
||||||
|
speed = parameters['average walking speed']
|
||||||
|
|
||||||
|
# Step 1: Build the graph
|
||||||
|
graph = defaultdict(list)
|
||||||
|
for i in range(len(landmarks)):
|
||||||
|
for j in range(len(landmarks)):
|
||||||
|
if i != j:
|
||||||
|
distance = get_distance(landmarks[i].location, landmarks[j].location, detour, speed)[1]
|
||||||
|
graph[i].append((distance, j))
|
||||||
|
|
||||||
|
# Step 2: Dijkstra's algorithm to find the shortest path from start to finish
|
||||||
|
start_idx = next(i for i, lm in enumerate(landmarks) if lm.name == 'start')
|
||||||
|
finish_idx = next(i for i, lm in enumerate(landmarks) if lm.name == 'finish')
|
||||||
|
|
||||||
|
distances = {i: float('inf') for i in range(len(landmarks))}
|
||||||
|
previous_nodes = {i: None for i in range(len(landmarks))}
|
||||||
|
distances[start_idx] = 0
|
||||||
|
priority_queue = [(0, start_idx)]
|
||||||
|
|
||||||
|
while priority_queue:
|
||||||
|
current_distance, current_index = heappop(priority_queue)
|
||||||
|
|
||||||
|
if current_distance > distances[current_index]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for neighbor_distance, neighbor_index in graph[current_index]:
|
||||||
|
distance = current_distance + neighbor_distance
|
||||||
|
|
||||||
|
if distance < distances[neighbor_index]:
|
||||||
|
distances[neighbor_index] = distance
|
||||||
|
previous_nodes[neighbor_index] = current_index
|
||||||
|
heappush(priority_queue, (distance, neighbor_index))
|
||||||
|
|
||||||
|
# Step 3: Backtrack from finish to start to find the path
|
||||||
|
path = []
|
||||||
|
current_index = finish_idx
|
||||||
|
while current_index is not None:
|
||||||
|
path.append(landmarks[current_index])
|
||||||
|
current_index = previous_nodes[current_index]
|
||||||
|
path.reverse()
|
||||||
|
|
||||||
|
return path
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
def total_path_distance(path: List[Landmark], detour, speed) -> float:
|
||||||
|
total_distance = 0
|
||||||
|
for i in range(len(path) - 1):
|
||||||
|
total_distance += get_distance(path[i].location, path[i + 1].location, detour, speed)[1]
|
||||||
|
return total_distance
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def find_shortest_path_through_all_landmarks(landmarks: List[Landmark]) -> List[Landmark]:
|
||||||
|
|
||||||
|
# Read from data
|
||||||
|
with open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/optimizer.params', "r") as f :
|
||||||
|
parameters = json.loads(f.read())
|
||||||
|
detour = parameters['detour factor']
|
||||||
|
speed = parameters['average walking speed']
|
||||||
|
|
||||||
|
# Step 1: Find 'start' and 'finish' landmarks
|
||||||
|
start_idx = next(i for i, lm in enumerate(landmarks) if lm.name == 'start')
|
||||||
|
finish_idx = next(i for i, lm in enumerate(landmarks) if lm.name == '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_distance(current_landmark.location, lm.location, detour, speed)[1])
|
||||||
|
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
|
||||||
|
|
||||||
|
def get_minor_landmarks(all_landmarks: List[Landmark], visited_landmarks: List[Landmark], width: float) -> List[Landmark] :
|
||||||
|
|
||||||
|
second_order_landmarks = []
|
||||||
|
visited_names = []
|
||||||
|
area = create_corridor(visited_landmarks, width)
|
||||||
|
|
||||||
|
for visited in visited_landmarks :
|
||||||
|
visited_names.append(visited.name)
|
||||||
|
|
||||||
|
for landmark in all_landmarks :
|
||||||
|
if is_in_area(area, landmark.location) and landmark.name not in visited_names:
|
||||||
|
second_order_landmarks.append(landmark)
|
||||||
|
|
||||||
|
return take_most_important(second_order_landmarks, len(visited_landmarks))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"""def refine_optimization(landmarks: List[Landmark], base_tour: List[Landmark], max_time: int, print_infos: bool) -> List[Landmark] :
|
||||||
|
|
||||||
|
minor_landmarks = get_minor_landmarks(landmarks, base_tour, 200)
|
||||||
|
|
||||||
|
if print_infos : print("There are " + str(len(minor_landmarks)) + " minor landmarks around the predicted path")
|
||||||
|
|
||||||
|
full_set = base_tour[:-1] + minor_landmarks # create full set of possible landmarks (without finish)
|
||||||
|
full_set.append(base_tour[-1]) # add finish back
|
||||||
|
|
||||||
|
new_tour = solve_optimization(full_set, max_time, print_infos)
|
||||||
|
|
||||||
|
return new_tour"""
|
||||||
|
|
||||||
|
|
||||||
|
def refine_optimization(landmarks: List[Landmark], base_tour: List[Landmark], max_time: int, print_infos: bool) -> List[Landmark] :
|
||||||
|
|
||||||
|
# Read from the file
|
||||||
|
with open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/optimizer.params', "r") as f :
|
||||||
|
parameters = json.loads(f.read())
|
||||||
|
max_landmarks = parameters['max landmarks']
|
||||||
|
|
||||||
|
if len(base_tour)-2 >= max_landmarks :
|
||||||
|
return base_tour
|
||||||
|
|
||||||
|
|
||||||
|
minor_landmarks = get_minor_landmarks(landmarks, base_tour, 200)
|
||||||
|
|
||||||
|
if print_infos : print("Using " + str(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 = solve_optimization(full_set, max_time, False)
|
||||||
|
new_tour, new_dist = add_time_to_reach_simple(new_tour)
|
||||||
|
|
||||||
|
"""#if base_tour[0].location == base_tour[-1].location :
|
||||||
|
if False :
|
||||||
|
coords = [] # Coordinates of the new tour
|
||||||
|
coords_dict = {} # maps the location of an element to the element itself. Used to access the elements back once we get the geometry
|
||||||
|
|
||||||
|
# Iterate through the new tour without finish
|
||||||
|
for elem in new_tour[:-1] :
|
||||||
|
coords.append(Point(elem.location))
|
||||||
|
coords_dict[elem.location] = elem # if start = goal, only finish remains
|
||||||
|
|
||||||
|
# Create a concave polygon using the coordinates
|
||||||
|
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]) :
|
||||||
|
better_tour.append(coords_dict[tuple((x,ys[i]))])
|
||||||
|
name_index[coords_dict[tuple((x,ys[i]))].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(new_tour[-1])
|
||||||
|
|
||||||
|
# Rearrange only if polygon
|
||||||
|
better_tour = rearrange(better_tour)
|
||||||
|
|
||||||
|
# Add the time to reach
|
||||||
|
better_tour = add_time_to_reach_simple(better_tour)
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not better_poly.is_simple :
|
||||||
|
|
||||||
|
coords_dict = {}
|
||||||
|
better_tour2 = []
|
||||||
|
for elem in better_tour :
|
||||||
|
coords_dict[elem.location] = elem
|
||||||
|
|
||||||
|
better_poly2 = better_poly.buffer(0)
|
||||||
|
new_coords = better_poly2.exterior.coords[:]
|
||||||
|
start_coords = base_tour[0].location
|
||||||
|
start_index = new_coords.
|
||||||
|
|
||||||
|
#for point in new_coords :
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
better_tour, better_poly = find_shortest_path_through_all_landmarks(new_tour)
|
||||||
|
better_tour, better_dist = add_time_to_reach_simple(better_tour)
|
||||||
|
|
||||||
|
if new_dist < better_dist :
|
||||||
|
final_tour = new_tour
|
||||||
|
else :
|
||||||
|
final_tour = better_tour
|
||||||
|
|
||||||
|
if print_infos :
|
||||||
|
print("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n")
|
||||||
|
print("\nRefined tour (result of second stage optimization): ")
|
||||||
|
print_res(final_tour, len(full_set))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return final_tour
|
0
backend/src/structs/__init__.py
Normal file
20
backend/src/structs/landmarks.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from typing import Optional
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from .landmarktype import LandmarkType
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Output to frontend
|
||||||
|
class Landmark(BaseModel) :
|
||||||
|
name : str
|
||||||
|
type: LandmarkType # De facto mapping depending on how the query was executed with overpass. Should still EXACTLY correspond to the preferences
|
||||||
|
location : tuple
|
||||||
|
osm_type : str
|
||||||
|
osm_id : int
|
||||||
|
attractiveness : int
|
||||||
|
must_do : bool
|
||||||
|
n_tags : int
|
||||||
|
time_to_reach : Optional[int] = 0
|
||||||
|
|
||||||
|
|
4
backend/src/structs/landmarktype.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
class LandmarkType(BaseModel):
|
||||||
|
landmark_type: str
|
28
backend/src/structs/preferences.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
from .landmarktype import LandmarkType
|
||||||
|
|
||||||
|
class Preference(BaseModel) :
|
||||||
|
name: str
|
||||||
|
type: LandmarkType # should match the attributes of the Preferences class
|
||||||
|
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
|
||||||
|
|
||||||
|
""" # Food (price low or high. Combien on veut dépenser pour manger à midi/soir)
|
||||||
|
food_budget : Preference
|
||||||
|
|
||||||
|
# Tolérance au détour (ce qui détermine (+ ou -) le chemin emprunté)
|
||||||
|
detour_tol : Preference"""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
116
backend/src/tester.py
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from typing import List
|
||||||
|
from landmarks_manager import generate_landmarks
|
||||||
|
from fastapi.encoders import jsonable_encoder
|
||||||
|
|
||||||
|
from optimizer import solve_optimization
|
||||||
|
from refiner import refine_optimization
|
||||||
|
from structs.landmarks import Landmark
|
||||||
|
from structs.landmarktype import LandmarkType
|
||||||
|
from structs.preferences import Preferences, Preference
|
||||||
|
|
||||||
|
|
||||||
|
# Helper function to create a .txt file with results
|
||||||
|
def write_data(L: List[Landmark], file_name: str):
|
||||||
|
|
||||||
|
data = pd.DataFrame()
|
||||||
|
i = 0
|
||||||
|
|
||||||
|
for landmark in L :
|
||||||
|
data[i] = jsonable_encoder(landmark)
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
data.to_json(file_name, indent = 2, force_ascii=False)
|
||||||
|
|
||||||
|
def test3(city_country: str) -> List[Landmark]:
|
||||||
|
|
||||||
|
|
||||||
|
preferences = Preferences(
|
||||||
|
sightseeing=Preference(
|
||||||
|
name='sightseeing',
|
||||||
|
type=LandmarkType(landmark_type='sightseeing'),
|
||||||
|
score = 5),
|
||||||
|
nature=Preference(
|
||||||
|
name='nature',
|
||||||
|
type=LandmarkType(landmark_type='nature'),
|
||||||
|
score = 0),
|
||||||
|
shopping=Preference(
|
||||||
|
name='shopping',
|
||||||
|
type=LandmarkType(landmark_type='shopping'),
|
||||||
|
score = 5))
|
||||||
|
|
||||||
|
coordinates = None
|
||||||
|
|
||||||
|
landmarks, landmarks_short = generate_landmarks(preferences=preferences, city_country=city_country, coordinates=coordinates)
|
||||||
|
|
||||||
|
#write_data(landmarks)
|
||||||
|
|
||||||
|
start = Landmark(name='start', type=LandmarkType(landmark_type='start'), location=(48.2044576, 16.3870242), 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.2044576, 16.3870242), osm_type='finish', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
|
||||||
|
|
||||||
|
|
||||||
|
test = landmarks_short
|
||||||
|
|
||||||
|
test.insert(0, start)
|
||||||
|
test.append(finish)
|
||||||
|
|
||||||
|
max_walking_time = 2 # hours
|
||||||
|
|
||||||
|
visiting_list = solve_optimization(test, max_walking_time*60, True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test4(coordinates: tuple[float, float]) -> List[Landmark]:
|
||||||
|
|
||||||
|
|
||||||
|
preferences = Preferences(
|
||||||
|
sightseeing=Preference(
|
||||||
|
name='sightseeing',
|
||||||
|
type=LandmarkType(landmark_type='sightseeing'),
|
||||||
|
score = 5),
|
||||||
|
nature=Preference(
|
||||||
|
name='nature',
|
||||||
|
type=LandmarkType(landmark_type='nature'),
|
||||||
|
score = 5),
|
||||||
|
shopping=Preference(
|
||||||
|
name='shopping',
|
||||||
|
type=LandmarkType(landmark_type='shopping'),
|
||||||
|
score = 5))
|
||||||
|
|
||||||
|
|
||||||
|
# Create start and finish
|
||||||
|
start = Landmark(name='start', type=LandmarkType(landmark_type='start'), location=coordinates, osm_type='start', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
|
||||||
|
finish = Landmark(name='finish', type=LandmarkType(landmark_type='finish'), location=coordinates, 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.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 = generate_landmarks(preferences=preferences, coordinates=start.location)
|
||||||
|
#write_data(landmarks, "landmarks.txt")
|
||||||
|
|
||||||
|
# Insert start and finish to the landmarks list
|
||||||
|
landmarks_short.insert(0, start)
|
||||||
|
landmarks_short.append(finish)
|
||||||
|
|
||||||
|
# TODO use these parameters in another way
|
||||||
|
max_walking_time = 2 # hours
|
||||||
|
detour = 30 # minutes
|
||||||
|
|
||||||
|
# First stage optimization
|
||||||
|
base_tour = solve_optimization(landmarks_short, max_walking_time*60, True)
|
||||||
|
|
||||||
|
# Second stage optimization
|
||||||
|
refined_tour = refine_optimization(landmarks, base_tour, max_walking_time*60+detour, True)
|
||||||
|
|
||||||
|
return refined_tour
|
||||||
|
|
||||||
|
|
||||||
|
#test4(tuple((48.8344400, 2.3220540))) # Café Chez César
|
||||||
|
#test4(tuple((48.8375946, 2.2949904))) # Point random
|
||||||
|
test4(tuple((47.377859, 8.540585))) # Zurich HB
|
||||||
|
#test3('Vienna, Austria')
|
0
deployment/kustomization.yaml
Normal file
43
frontend/.gitignore
vendored
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
#.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
**/doc/api/
|
||||||
|
**/ios/Flutter/.last_build_id
|
||||||
|
.dart_tool/
|
||||||
|
.flutter-plugins
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
.pub-cache/
|
||||||
|
.pub/
|
||||||
|
/build/
|
||||||
|
|
||||||
|
# Symbolication related
|
||||||
|
app.*.symbols
|
||||||
|
|
||||||
|
# Obfuscation related
|
||||||
|
app.*.map.json
|
||||||
|
|
||||||
|
# Android Studio will place build artifacts here
|
||||||
|
/android/app/debug
|
||||||
|
/android/app/profile
|
||||||
|
/android/app/release
|
@ -45,6 +45,9 @@ 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()
|
@ -28,7 +28,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="AIzaSyCeWk_D2xvfOHLidvV56EZeQCUybypEntw"
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
</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
|
||||||
@ -41,4 +48,7 @@
|
|||||||
<data android:mimeType="text/plain"/>
|
<data android:mimeType="text/plain"/>
|
||||||
</intent>
|
</intent>
|
||||||
</queries>
|
</queries>
|
||||||
|
|
||||||
|
<!-- Required to fetch data from the internet. -->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
</manifest>
|
</manifest>
|
Before ![]() (image error) Size: 544 B After ![]() (image error) Size: 544 B ![]() ![]() |
Before ![]() (image error) Size: 442 B After ![]() (image error) Size: 442 B ![]() ![]() |
Before ![]() (image error) Size: 721 B After ![]() (image error) Size: 721 B ![]() ![]() |
Before ![]() (image error) Size: 1.0 KiB After ![]() (image error) Size: 1.0 KiB ![]() ![]() |
Before ![]() (image error) Size: 1.4 KiB After ![]() (image error) Size: 1.4 KiB ![]() ![]() |
0
android/app/src/profile/AndroidManifest.xml → frontend/android/app/src/profile/AndroidManifest.xml
0
ios/.gitignore → frontend/ios/.gitignore
vendored
0
ios/Runner.xcworkspace/contents.xcworkspacedata → frontend/ios/Runner.xcworkspace/contents.xcworkspacedata
generated
Before ![]() (image error) Size: 11 KiB After ![]() (image error) Size: 11 KiB ![]() ![]() |
Before ![]() (image error) Size: 295 B After ![]() (image error) Size: 295 B ![]() ![]() |
Before ![]() (image error) Size: 406 B After ![]() (image error) Size: 406 B ![]() ![]() |
Before ![]() (image error) Size: 450 B After ![]() (image error) Size: 450 B ![]() ![]() |
Before ![]() (image error) Size: 282 B After ![]() (image error) Size: 282 B ![]() ![]() |
Before ![]() (image error) Size: 462 B After ![]() (image error) Size: 462 B ![]() ![]() |
Before ![]() (image error) Size: 704 B After ![]() (image error) Size: 704 B ![]() ![]() |
Before ![]() (image error) Size: 406 B After ![]() (image error) Size: 406 B ![]() ![]() |
Before ![]() (image error) Size: 586 B After ![]() (image error) Size: 586 B ![]() ![]() |
Before ![]() (image error) Size: 862 B After ![]() (image error) Size: 862 B ![]() ![]() |
Before ![]() (image error) Size: 862 B After ![]() (image error) Size: 862 B ![]() ![]() |
Before ![]() (image error) Size: 1.6 KiB After ![]() (image error) Size: 1.6 KiB ![]() ![]() |
Before ![]() (image error) Size: 762 B After ![]() (image error) Size: 762 B ![]() ![]() |
Before ![]() (image error) Size: 1.2 KiB After ![]() (image error) Size: 1.2 KiB ![]() ![]() |
Before ![]() (image error) Size: 1.4 KiB After ![]() (image error) Size: 1.4 KiB ![]() ![]() |
Before ![]() (image error) Size: 68 B After ![]() (image error) Size: 68 B ![]() ![]() |
Before ![]() (image error) Size: 68 B After ![]() (image error) Size: 68 B ![]() ![]() |
Before ![]() (image error) Size: 68 B After ![]() (image error) Size: 68 B ![]() ![]() |
@ -4,9 +4,10 @@ import 'package:fast_network_navigation/modules/overview.dart';
|
|||||||
import 'package:fast_network_navigation/modules/profile.dart';
|
import 'package:fast_network_navigation/modules/profile.dart';
|
||||||
|
|
||||||
|
|
||||||
|
// BasePage is the scaffold that holds all other pages
|
||||||
|
// A side drawer is used to switch between pages
|
||||||
class BasePage extends StatefulWidget {
|
class BasePage extends StatefulWidget {
|
||||||
const BasePage({super.key, required this.title});
|
const BasePage({super.key, required this.title});
|
||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -22,7 +23,7 @@ class _BasePageState extends State<BasePage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget currentView = MapPage();
|
Widget currentView = NavigationOverview();
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ThemeData theme = Theme.of(context);
|
final ThemeData theme = Theme.of(context);
|
||||||
@ -51,7 +52,7 @@ class _BasePageState extends State<BasePage> {
|
|||||||
// Update the state of the app
|
// Update the state of the app
|
||||||
_onItemTapped(0);
|
_onItemTapped(0);
|
||||||
// Then close the drawer
|
// Then close the drawer
|
||||||
currentView = MapPage();
|
currentView = NavigationOverview();
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -87,3 +88,4 @@ class _BasePageState extends State<BasePage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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());
|
||||||
|
|
81
frontend/lib/modules/map.dart
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||||
|
|
||||||
|
class MapWidget extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
State<MapWidget> createState() => _MapWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MapWidgetState extends State<MapWidget> {
|
||||||
|
late GoogleMapController mapController;
|
||||||
|
|
||||||
|
final LatLng _center = const LatLng(45.521563, -122.677433);
|
||||||
|
|
||||||
|
void _onMapCreated(GoogleMapController controller) {
|
||||||
|
mapController = controller;
|
||||||
|
}
|
||||||
|
void _onCameraIdle() {
|
||||||
|
// print(mapController.getLatLng());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GoogleMap(
|
||||||
|
onMapCreated: _onMapCreated,
|
||||||
|
initialCameraPosition: CameraPosition(
|
||||||
|
target: _center,
|
||||||
|
zoom: 11.0,
|
||||||
|
),
|
||||||
|
onCameraIdle: _onCameraIdle,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// GeoCode geoCode = GeoCode();
|
||||||
|
// String _currentCityName = "...";
|
||||||
|
// final Debounce _debounce = Debounce(Duration(seconds: 3));
|
||||||
|
|
||||||
|
// final LatLng _center = const LatLng(45.521563, -122.677433);
|
||||||
|
// late GoogleMapController mapController;
|
||||||
|
|
||||||
|
// void _onMapCreated(GoogleMapController controller) {
|
||||||
|
// mapController = controller;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 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();});
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
99
frontend/lib/modules/overview.dart
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:sliding_up_panel/sliding_up_panel.dart';
|
||||||
|
import 'package:geocode/geocode.dart';
|
||||||
|
import 'dart:async';
|
||||||
|
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||||
|
|
||||||
|
import 'package:fast_network_navigation/modules/navigation.dart';
|
||||||
|
import 'package:fast_network_navigation/modules/map.dart';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class NavigationOverview extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
State<NavigationOverview> createState() => _NavigationOverviewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Debounce {
|
||||||
|
Duration delay;
|
||||||
|
Timer? _timer;
|
||||||
|
|
||||||
|
Debounce(
|
||||||
|
this.delay,
|
||||||
|
);
|
||||||
|
|
||||||
|
call(void Function() callback) {
|
||||||
|
_timer?.cancel();
|
||||||
|
_timer = Timer(delay, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
_timer?.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class _NavigationOverviewState extends State<NavigationOverview> {
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final ThemeData theme = Theme.of(context);
|
||||||
|
return SlidingUpPanel(
|
||||||
|
renderPanelSheet: false,
|
||||||
|
panel: _floatingPanel(theme),
|
||||||
|
collapsed: _floatingCollapsed(theme),
|
||||||
|
body: MapWidget()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 #todo",
|
||||||
|
style: TextStyle(color: theme.primaryColor, fontSize: 24.0, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
91
frontend/lib/modules/profile.dart
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
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),
|
||||||
|
),
|
||||||
|
|
||||||
|
Text('Please rate your preferences for the following activities:'),
|
||||||
|
|
||||||
|
// Now the sliders
|
||||||
|
ImportanceSliders()
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ImportanceSliders extends StatefulWidget {
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ImportanceSliders> createState() => _ImportanceSlidersState();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class _ImportanceSlidersState extends State<ImportanceSliders> {
|
||||||
|
|
||||||
|
UserPreferences _prefs = UserPreferences();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_prefs.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
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: EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 0),
|
||||||
|
shadowColor: Colors.grey,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return sliders;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(children: _createSliders());
|
||||||
|
}
|
||||||
|
}
|
62
frontend/lib/structs/destination.dart
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import "package:flutter/material.dart";
|
||||||
|
|
||||||
|
class Destination {
|
||||||
|
final double latitude;
|
||||||
|
final double longitude;
|
||||||
|
final String name;
|
||||||
|
final String description;
|
||||||
|
// final DestinationType type;
|
||||||
|
final Duration duration;
|
||||||
|
final bool visited;
|
||||||
|
|
||||||
|
const Destination({
|
||||||
|
required this.latitude,
|
||||||
|
required this.longitude,
|
||||||
|
required this.name,
|
||||||
|
required this.description,
|
||||||
|
// required this.type,
|
||||||
|
required this.duration,
|
||||||
|
required this.visited,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory Destination.fromJson(Map<String, dynamic> json) {
|
||||||
|
return switch (json) {
|
||||||
|
{
|
||||||
|
'lat': double latitude,
|
||||||
|
'lon': double longitude,
|
||||||
|
'name': String name,
|
||||||
|
'description': String description,
|
||||||
|
// 'type': String type,
|
||||||
|
'duration': int duration,
|
||||||
|
'visited': bool visited
|
||||||
|
|
||||||
|
} =>
|
||||||
|
Destination(
|
||||||
|
latitude: latitude,
|
||||||
|
longitude: longitude,
|
||||||
|
name: name,
|
||||||
|
description: description,
|
||||||
|
// type: "DestinationType.values.firstWhere((element) => element.name == type)",
|
||||||
|
duration: Duration(minutes: duration),
|
||||||
|
visited: visited
|
||||||
|
),
|
||||||
|
_ => throw const FormatException('Failed to load destination.'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DestinationType {
|
||||||
|
final String name;
|
||||||
|
final String description;
|
||||||
|
final Icon icon;
|
||||||
|
|
||||||
|
const DestinationType({
|
||||||
|
required this.name,
|
||||||
|
required this.description,
|
||||||
|
required this.icon,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
82
frontend/lib/structs/preferences.dart
Normal 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 prefs = await SharedPreferences.getInstance();
|
||||||
|
for (SinglePreference pref in preferences) {
|
||||||
|
prefs.setInt(pref.key, pref.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void load() async {
|
||||||
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
for (SinglePreference pref in preferences) {
|
||||||
|
pref.value = prefs.getInt(pref.key) ?? 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
frontend/lib/structs/route.dart
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import "package:fast_network_navigation/structs/destination.dart";
|
||||||
|
|
||||||
|
|
||||||
|
class Route {
|
||||||
|
final String name;
|
||||||
|
final Duration duration;
|
||||||
|
final List<Destination> destinations;
|
||||||
|
|
||||||
|
Route({
|
||||||
|
required this.name,
|
||||||
|
required this.duration,
|
||||||
|
required this.destinations
|
||||||
|
});
|
||||||
|
}
|
18
frontend/lib/utils/get_route.dart
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import "package:fast_network_navigation/structs/destination.dart";
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
Future<Destination> fetchDestination() 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.
|
||||||
|
return Destination.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
|
||||||
|
} else {
|
||||||
|
// If the server did not return a 200 OK response,
|
||||||
|
// then throw an exception.
|
||||||
|
throw Exception('Failed to load destination');
|
||||||
|
}
|
||||||
|
}
|