Merge pull request 'UI elements using the new structs' (#8) from feature/unify-api-frontend into main
Reviewed-on: remoll/fast-network-navigation#8
25
.drone.yml
@ -1,25 +0,0 @@
|
|||||||
kind: pipeline
|
|
||||||
type: kubernetes
|
|
||||||
name: build-apk
|
|
||||||
|
|
||||||
clone:
|
|
||||||
depth: 1
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: build
|
|
||||||
image: cirrusci/flutter:stable
|
|
||||||
pull: true
|
|
||||||
commands:
|
|
||||||
- flutter packages get
|
|
||||||
- flutter build apk --release --split-per-abi
|
|
||||||
- mkdir apks
|
|
||||||
- mv build/app/outputs/apk/*/*/*.apk apks
|
|
||||||
|
|
||||||
- name: Publish APK
|
|
||||||
image: plugins/gitea-release
|
|
||||||
settings:
|
|
||||||
api_key:
|
|
||||||
from_secret: GITEA_TOKEN
|
|
||||||
trigger:
|
|
||||||
event:
|
|
||||||
- push
|
|
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.GITEA_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
|
@ -1,42 +0,0 @@
|
|||||||
on: push
|
|
||||||
name: Test, Build and Release apk
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build APK
|
|
||||||
runs-on: k8s
|
|
||||||
steps:
|
|
||||||
|
|
||||||
- name: Install prerequisites
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y xz-utils unzip
|
|
||||||
- uses: https://gitea.com/actions/checkout@v4
|
|
||||||
|
|
||||||
- uses: https://github.com/actions/setup-java@v4
|
|
||||||
with:
|
|
||||||
java-version: '17'
|
|
||||||
distribution: 'zulu'
|
|
||||||
|
|
||||||
- uses: https://github.com/subosito/flutter-action@v2
|
|
||||||
with:
|
|
||||||
channel: stable
|
|
||||||
flutter-version: 3.19.6
|
|
||||||
cache: true
|
|
||||||
- name: Setup Android SDK
|
|
||||||
uses: https://github.com/android-actions/setup-android@v3
|
|
||||||
|
|
||||||
- run: flutter pub get
|
|
||||||
# - run: flutter test
|
|
||||||
- run: flutter build apk --debug --split-per-abi
|
|
||||||
|
|
||||||
- name: Release APK
|
|
||||||
uses: https://gitea.com/akkuman/gitea-release-action@v1
|
|
||||||
with:
|
|
||||||
files: build/app/outputs/flutter-apk/*.apk
|
|
||||||
tag: testing
|
|
||||||
release_name: Testing release
|
|
||||||
release_body: "This is a testing release."
|
|
||||||
prerelease: true
|
|
||||||
token: ${{ secrets.GITEA_TOKEN }}
|
|
||||||
env:
|
|
||||||
NODE_OPTIONS: '--experimental-fetch'
|
|
65
.gitea/workflows/frontend_build-android.yaml
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- frontend/**
|
||||||
|
|
||||||
|
|
||||||
|
name: Build and release APK
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build APK
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
|
||||||
|
- name: Install prerequisites
|
||||||
|
run: |
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y jq
|
||||||
|
|
||||||
|
- uses: https://gitea.com/actions/checkout@v4
|
||||||
|
|
||||||
|
|
||||||
|
- uses: https://github.com/actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
java-version: '17'
|
||||||
|
distribution: 'zulu'
|
||||||
|
|
||||||
|
- name: Fix flutter SDK folder permission
|
||||||
|
run: git config --global --add safe.directory "*"
|
||||||
|
|
||||||
|
- uses: https://github.com/subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
channel: stable
|
||||||
|
flutter-version: 3.22.0
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: Setup Android SDK
|
||||||
|
uses: https://github.com/android-actions/setup-android@v3
|
||||||
|
|
||||||
|
- run: flutter pub get
|
||||||
|
working-directory: ./frontend
|
||||||
|
|
||||||
|
- name: Add required secrets
|
||||||
|
run: |
|
||||||
|
echo ${{ secrets.ANDROID_SECRETS_BASE64 }} | base64 -d > ./android/secrets.properties
|
||||||
|
working-directory: ./frontend
|
||||||
|
|
||||||
|
- name: Sanity check
|
||||||
|
run: |
|
||||||
|
ls
|
||||||
|
ls -lah android
|
||||||
|
working-directory: ./frontend
|
||||||
|
|
||||||
|
- run: flutter build apk --release --split-per-abi --build-number=${{ gitea.run_number }}
|
||||||
|
working-directory: ./frontend
|
||||||
|
|
||||||
|
- name: Upload APKs to artifacts
|
||||||
|
uses: https://gitea.com/actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: app-release
|
||||||
|
path: frontend/build/app/outputs/flutter-apk/
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 15
|
@ -1,21 +1,34 @@
|
|||||||
on: push
|
on:
|
||||||
name: Test, Build and Release web
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- frontend/**
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y xz-utils
|
sudo apt-get install -y xz-utils
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- 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.19.6
|
||||||
cache: true
|
cache: true
|
||||||
- run: flutter pub get
|
|
||||||
# - run: flutter test
|
|
||||||
|
|
||||||
|
- run: flutter pub get
|
||||||
|
working-directory: ./frontend
|
||||||
|
|
||||||
|
- run: flutter build web
|
||||||
|
working-directory: ./frontend
|
28
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
// 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": [
|
||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"cwd": "frontend",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "frontend (profile mode)",
|
||||||
|
"cwd": "frontend",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"flutterMode": "profile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "frontend (release mode)",
|
||||||
|
"cwd": "frontend",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"flutterMode": "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"]
|
12
backend/Pipfile
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
[[source]]
|
||||||
|
url = "https://pypi.org/simple"
|
||||||
|
verify_ssl = true
|
||||||
|
name = "pypi"
|
||||||
|
|
||||||
|
[packages]
|
||||||
|
numpy = "*"
|
||||||
|
scipy = "*"
|
||||||
|
fastapi = "*"
|
||||||
|
osmpythontools = "*"
|
||||||
|
|
||||||
|
[dev-packages]
|
1485
backend/Pipfile.lock
generated
Normal file
0
backend/app/__init__.py
Normal file
BIN
backend/app/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
backend/app/__pycache__/main.cpython-310.pyc
Normal file
1
backend/app/dependencies.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
import app.src
|
34
backend/app/main.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
from .src.optimizer import solve_optimization
|
||||||
|
from .src.landmarks_manager import Landmark
|
||||||
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/optimize/{max_steps}/{print_details}")
|
||||||
|
def main(max_steps: int, print_details: bool):
|
||||||
|
|
||||||
|
# CONSTRAINT TO RESPECT MAX NUMBER OF STEPS
|
||||||
|
#max_steps = 16
|
||||||
|
|
||||||
|
|
||||||
|
# Initialize all landmarks (+ start and goal). Order matters here
|
||||||
|
landmarks = []
|
||||||
|
landmarks.append(Landmark("départ", -1, (0, 0)))
|
||||||
|
landmarks.append(Landmark("tour eiffel", 99, (0,2))) # PUT IN JSON
|
||||||
|
landmarks.append(Landmark("arc de triomphe", 99, (0,4)))
|
||||||
|
landmarks.append(Landmark("louvre", 99, (0,6)))
|
||||||
|
landmarks.append(Landmark("montmartre", 99, (0,10)))
|
||||||
|
landmarks.append(Landmark("concorde", 99, (0,8)))
|
||||||
|
landmarks.append(Landmark("arrivée", -1, (0, 0)))
|
||||||
|
|
||||||
|
|
||||||
|
visiting_order = solve_optimization(landmarks, max_steps, print_details)
|
||||||
|
|
||||||
|
#return visiting_order
|
||||||
|
|
||||||
|
return("max steps :", max_steps, "\n", visiting_order)
|
||||||
|
|
||||||
|
|
||||||
|
"""if __name__ == "__main__":
|
||||||
|
main()"""
|
0
backend/app/src/__init__.py
Normal file
BIN
backend/app/src/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
backend/app/src/__pycache__/optimizer.cpython-310.pyc
Normal file
57
backend/app/src/landmarks_manager.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
from OSMPythonTools.api import Api
|
||||||
|
from OSMPythonTools.overpass import Overpass
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
# Defines the landmark class (aka some place there is to visit)
|
||||||
|
@dataclass
|
||||||
|
class Landmarkkkk :
|
||||||
|
name : str
|
||||||
|
attractiveness : int
|
||||||
|
id : int
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Landmark :
|
||||||
|
name : str
|
||||||
|
attractiveness : int
|
||||||
|
loc : tuple
|
||||||
|
|
||||||
|
# Converts a OSM id to a landmark
|
||||||
|
def add_from_id(id: int, score: int) :
|
||||||
|
|
||||||
|
try :
|
||||||
|
s = 'way/' + str(id) # prepare string for query
|
||||||
|
obj = api.query(s) # object to add
|
||||||
|
except :
|
||||||
|
s = 'relation/' + str(id) # prepare string for query
|
||||||
|
obj = api.query(s) # object to add
|
||||||
|
|
||||||
|
return Landmarkkkk(obj.tag('name:fr'), score, id) # create Landmark out of it
|
||||||
|
|
||||||
|
|
||||||
|
# take a lsit of tuples (id, score) to generate a list of landmarks
|
||||||
|
def generate_landmarks(ids_and_scores: list) :
|
||||||
|
|
||||||
|
L = []
|
||||||
|
for tup in ids_and_scores :
|
||||||
|
L.append(add_from_id(tup[0], tup[1]))
|
||||||
|
|
||||||
|
return L
|
||||||
|
|
||||||
|
api = Api()
|
||||||
|
|
||||||
|
|
||||||
|
l = (7515426, 70)
|
||||||
|
t = (5013364, 100)
|
||||||
|
n = (201611261, 99)
|
||||||
|
a = (226413508, 50)
|
||||||
|
m = (23762981, 30)
|
||||||
|
|
||||||
|
|
||||||
|
ids_and_scores = [t, l, n, a, m]
|
||||||
|
|
||||||
|
landmarks = generate_landmarks(ids_and_scores)
|
||||||
|
|
||||||
|
|
||||||
|
for obj in landmarks :
|
||||||
|
print(obj)
|
23
backend/app/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()
|
323
backend/app/src/optimizer.py
Normal file
@ -0,0 +1,323 @@
|
|||||||
|
from scipy.optimize import linprog
|
||||||
|
import numpy as np
|
||||||
|
from scipy.linalg import block_diag
|
||||||
|
|
||||||
|
|
||||||
|
# landmarks = [Landmark_1, Landmark_2, ...]
|
||||||
|
|
||||||
|
# Convert the solution of the optimization into the list of edges to follow. Order is taken into account
|
||||||
|
def untangle(resx: list) :
|
||||||
|
N = len(resx) # length of res
|
||||||
|
L = int(np.sqrt(N)) # number of landmarks. CAST INTO INT but should not be a problem because N = L**2 by def.
|
||||||
|
n_edges = resx.sum() # number of edges
|
||||||
|
|
||||||
|
order = []
|
||||||
|
nonzeroind = np.nonzero(resx)[0] # the return is a little funny so I use the [0]
|
||||||
|
|
||||||
|
nonzero_tup = np.unravel_index(nonzeroind, (L,L))
|
||||||
|
|
||||||
|
indx = nonzero_tup[0].tolist()
|
||||||
|
indy = nonzero_tup[1].tolist()
|
||||||
|
|
||||||
|
vert = (indx[0], indy[0])
|
||||||
|
|
||||||
|
order.append(vert[0])
|
||||||
|
order.append(vert[1])
|
||||||
|
|
||||||
|
while len(order) < n_edges + 1 :
|
||||||
|
ind = indx.index(vert[1])
|
||||||
|
|
||||||
|
vert = (indx[ind], indy[ind])
|
||||||
|
|
||||||
|
order.append(vert[1])
|
||||||
|
|
||||||
|
return order
|
||||||
|
|
||||||
|
# Just to print the result
|
||||||
|
def print_res(res, landmarks: list, P) :
|
||||||
|
X = abs(res.x)
|
||||||
|
order = untangle(X)
|
||||||
|
things = []
|
||||||
|
|
||||||
|
"""N = int(np.sqrt(len(X)))
|
||||||
|
for i in range(N):
|
||||||
|
print(X[i*N:i*N+N])
|
||||||
|
print("Optimal value:", -res.fun) # Minimization, so we negate to get the maximum
|
||||||
|
print("Optimal point:", res.x)
|
||||||
|
for i,x in enumerate(X) : X[i] = round(x,0)
|
||||||
|
print(order)"""
|
||||||
|
|
||||||
|
if (X.sum()+1)**2 == len(X) :
|
||||||
|
print('\nAll landmarks can be visited within max_steps, the following order is suggested : ')
|
||||||
|
else :
|
||||||
|
print('Could not visit all the landmarks, the following order is suggested : ')
|
||||||
|
|
||||||
|
for idx in order :
|
||||||
|
print('- ' + landmarks[idx].name)
|
||||||
|
things.append(landmarks[idx].name)
|
||||||
|
|
||||||
|
steps = path_length(P, abs(res.x))
|
||||||
|
print("\nSteps walked : " + str(steps))
|
||||||
|
|
||||||
|
return things
|
||||||
|
|
||||||
|
# Checks for cases of circular symmetry in the result
|
||||||
|
def has_circle(resx: list) :
|
||||||
|
N = len(resx) # length of res
|
||||||
|
L = int(np.sqrt(N)) # number of landmarks. CAST INTO INT but should not be a problem because N = L**2 by def.
|
||||||
|
n_edges = resx.sum() # number of edges
|
||||||
|
|
||||||
|
|
||||||
|
nonzeroind = np.nonzero(resx)[0] # the return is a little funny so I use the [0]
|
||||||
|
|
||||||
|
nonzero_tup = np.unravel_index(nonzeroind, (L,L))
|
||||||
|
|
||||||
|
indx = nonzero_tup[0].tolist()
|
||||||
|
indy = nonzero_tup[1].tolist()
|
||||||
|
|
||||||
|
|
||||||
|
verts = []
|
||||||
|
|
||||||
|
for i, x in enumerate(indx) :
|
||||||
|
verts.append((x, indy[i]))
|
||||||
|
|
||||||
|
|
||||||
|
for vert in verts :
|
||||||
|
visited = []
|
||||||
|
visited.append(vert)
|
||||||
|
|
||||||
|
while len(visited) < n_edges + 1 :
|
||||||
|
|
||||||
|
try :
|
||||||
|
ind = indx.index(vert[1])
|
||||||
|
|
||||||
|
vert = (indx[ind], indy[ind])
|
||||||
|
|
||||||
|
if vert in visited :
|
||||||
|
return visited
|
||||||
|
else :
|
||||||
|
visited.append(vert)
|
||||||
|
except :
|
||||||
|
break
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Constraint to not have d14 and d41 simultaneously. Does not prevent circular symmetry with more elements
|
||||||
|
def break_sym(landmarks, A_ub, b_ub):
|
||||||
|
L = len(landmarks)
|
||||||
|
upper_ind = np.triu_indices(L,0,L)
|
||||||
|
|
||||||
|
up_ind_x = upper_ind[0]
|
||||||
|
up_ind_y = upper_ind[1]
|
||||||
|
|
||||||
|
for i, _ in enumerate(up_ind_x) :
|
||||||
|
l = [0]*L*L
|
||||||
|
if up_ind_x[i] != up_ind_y[i] :
|
||||||
|
l[up_ind_x[i]*L + up_ind_y[i]] = 1
|
||||||
|
l[up_ind_y[i]*L + up_ind_x[i]] = 1
|
||||||
|
|
||||||
|
A_ub = np.vstack((A_ub,l))
|
||||||
|
b_ub.append(1)
|
||||||
|
|
||||||
|
"""for i in range(7):
|
||||||
|
print(l[i*7:i*7+7])
|
||||||
|
print("\n")"""
|
||||||
|
|
||||||
|
return A_ub, b_ub
|
||||||
|
|
||||||
|
# Constraint to not have circular paths. Want to go from start -> finish without unconnected loops
|
||||||
|
def break_circle(landmarks, A_ub, b_ub, circle) :
|
||||||
|
N = len(landmarks)
|
||||||
|
l = [0]*N*N
|
||||||
|
|
||||||
|
for index in circle :
|
||||||
|
x = index[0]
|
||||||
|
y = index[1]
|
||||||
|
l[x*N+y] = 1
|
||||||
|
|
||||||
|
A_ub = np.vstack((A_ub,l))
|
||||||
|
b_ub.append(len(circle)-1)
|
||||||
|
|
||||||
|
"""print("\n\nPREVENT CIRCLE")
|
||||||
|
for i in range(7):
|
||||||
|
print(l[i*7:i*7+7])
|
||||||
|
print("\n")"""
|
||||||
|
|
||||||
|
return A_ub, b_ub
|
||||||
|
|
||||||
|
# Constraint to respect max number of travels
|
||||||
|
def respect_number(landmarks, A_ub, b_ub):
|
||||||
|
h = []
|
||||||
|
for i in range(len(landmarks)) : h.append([1]*len(landmarks))
|
||||||
|
T = block_diag(*h)
|
||||||
|
"""for l in T :
|
||||||
|
for i in range(7):
|
||||||
|
print(l[i*7:i*7+7])
|
||||||
|
print("\n")"""
|
||||||
|
return np.vstack((A_ub, T)), b_ub + [1]*len(landmarks)
|
||||||
|
|
||||||
|
# Constraint to tie the problem together. Necessary but not sufficient to avoid circles
|
||||||
|
def respect_order(landmarks: list, A_eq, b_eq):
|
||||||
|
N = len(landmarks)
|
||||||
|
for i in range(N-1) : # Prevent stacked ones
|
||||||
|
if i == 0 :
|
||||||
|
continue
|
||||||
|
else :
|
||||||
|
l = [0]*N
|
||||||
|
l[i] = -1
|
||||||
|
l = l*N
|
||||||
|
for j in range(N) :
|
||||||
|
l[i*N + j] = 1
|
||||||
|
|
||||||
|
A_eq = np.vstack((A_eq,l))
|
||||||
|
b_eq.append(0)
|
||||||
|
|
||||||
|
"""for i in range(7):
|
||||||
|
print(l[i*7:i*7+7])
|
||||||
|
print("\n")"""
|
||||||
|
|
||||||
|
return A_eq, b_eq
|
||||||
|
|
||||||
|
# Compute manhattan distance between 2 locations
|
||||||
|
def manhattan_distance(loc1: tuple, loc2: tuple):
|
||||||
|
x1, y1 = loc1
|
||||||
|
x2, y2 = loc2
|
||||||
|
return abs(x1 - x2) + abs(y1 - y2)
|
||||||
|
|
||||||
|
# Constraint to not stay in position
|
||||||
|
def init_eq_not_stay(landmarks):
|
||||||
|
L = len(landmarks)
|
||||||
|
l = [0]*L*L
|
||||||
|
|
||||||
|
|
||||||
|
for i in range(L) :
|
||||||
|
for j in range(L) :
|
||||||
|
if j == i :
|
||||||
|
l[j + i*L] = 1
|
||||||
|
l[L-1] = 1 # cannot skip from start to finish
|
||||||
|
#A_eq = np.array([np.array(xi) for xi in A_eq]) # Must convert A_eq into an np array
|
||||||
|
l = np.array(np.array(l))
|
||||||
|
|
||||||
|
"""for i in range(7):
|
||||||
|
print(l[i*7:i*7+7])"""
|
||||||
|
|
||||||
|
return [l], [0]
|
||||||
|
|
||||||
|
# Initialize A and c. Compute the distances from all landmarks to each other and store attractiveness
|
||||||
|
# We want to maximize the sightseeing : max(c) st. A*x < b and A_eq*x = b_eq
|
||||||
|
def init_ub_dist(landmarks: list, max_steps: int):
|
||||||
|
# Objective function coefficients. a*x1 + b*x2 + c*x3 + ...
|
||||||
|
c = []
|
||||||
|
# Coefficients of inequality constraints (left-hand side)
|
||||||
|
A = []
|
||||||
|
for i, spot1 in enumerate(landmarks) :
|
||||||
|
dist_table = [0]*len(landmarks)
|
||||||
|
c.append(-spot1.attractiveness)
|
||||||
|
for j, spot2 in enumerate(landmarks) :
|
||||||
|
dist_table[j] = manhattan_distance(spot1.loc, spot2.loc)
|
||||||
|
A.append(dist_table)
|
||||||
|
c = c*len(landmarks)
|
||||||
|
A_ub = []
|
||||||
|
for line in A :
|
||||||
|
#print(line)
|
||||||
|
A_ub += line
|
||||||
|
return c, A_ub, [max_steps]
|
||||||
|
|
||||||
|
# Go through the landmarks and force the optimizer to use landmarks where attractiveness is set to -1
|
||||||
|
def respect_user_mustsee(landmarks: list, A_eq: list, b_eq: list) :
|
||||||
|
L = len(landmarks)
|
||||||
|
H = 0 # sort of heuristic to get an idea of the number of steps needed
|
||||||
|
for i in landmarks :
|
||||||
|
if i.name == "départ" : elem_prev = i # list of all matches
|
||||||
|
for i, elem in enumerate(landmarks) :
|
||||||
|
if elem.attractiveness == -1 :
|
||||||
|
l = [0]*L*L
|
||||||
|
if elem.name != "arrivée" :
|
||||||
|
for j in range(L) :
|
||||||
|
l[j +i*L] = 1
|
||||||
|
|
||||||
|
else : # This ensures we go to goal
|
||||||
|
for k in range(L-1) :
|
||||||
|
l[k*L+L-1] = 1
|
||||||
|
|
||||||
|
H += manhattan_distance(elem.loc, elem_prev.loc)
|
||||||
|
elem_prev = elem
|
||||||
|
|
||||||
|
"""for i in range(7):
|
||||||
|
print(l[i*7:i*7+7])
|
||||||
|
print("\n")"""
|
||||||
|
|
||||||
|
A_eq = np.vstack((A_eq,l))
|
||||||
|
b_eq.append(1)
|
||||||
|
return A_eq, b_eq, H
|
||||||
|
|
||||||
|
# Computes the path length given path matrix (dist_table) and a result
|
||||||
|
def path_length(P: list, resx: list) :
|
||||||
|
return np.dot(P, resx)
|
||||||
|
|
||||||
|
# Main optimization pipeline
|
||||||
|
def solve_optimization (landmarks, max_steps, printing_details) :
|
||||||
|
|
||||||
|
# SET CONSTRAINTS FOR INEQUALITY
|
||||||
|
c, A_ub, b_ub = init_ub_dist(landmarks, max_steps) # Add the distances from each landmark to the other
|
||||||
|
P = A_ub # store the paths for later. Needed to compute path length
|
||||||
|
A_ub, b_ub = respect_number(landmarks, A_ub, b_ub) # Respect max number of visits.
|
||||||
|
|
||||||
|
# TODO : Problems with circular symmetry
|
||||||
|
A_ub, b_ub = break_sym(landmarks, A_ub, b_ub) # break the symmetry. Only use the upper diagonal values
|
||||||
|
|
||||||
|
# SET CONSTRAINTS FOR EQUALITY
|
||||||
|
A_eq, b_eq = init_eq_not_stay(landmarks) # Force solution not to stay in same place
|
||||||
|
A_eq, b_eq, H = respect_user_mustsee(landmarks, A_eq, b_eq) # Check if there are user_defined must_see. Also takes care of start/goal
|
||||||
|
|
||||||
|
A_eq, b_eq = respect_order(landmarks, A_eq, b_eq) # Respect order of visit (only works when max_steps is limiting factor)
|
||||||
|
|
||||||
|
# Bounds for variables (x can only be 0 or 1)
|
||||||
|
x_bounds = [(0, 1)] * len(c)
|
||||||
|
|
||||||
|
# Solve linear programming problem
|
||||||
|
|
||||||
|
res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq = b_eq, bounds=x_bounds, method='highs', integrality=3)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Raise error if no solution is found
|
||||||
|
if not res.success :
|
||||||
|
|
||||||
|
# Override the max_steps using the heuristic
|
||||||
|
for i, val in enumerate(b_ub) :
|
||||||
|
if val == max_steps : b_ub[i] = H
|
||||||
|
|
||||||
|
# Solve problem again :
|
||||||
|
res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq = b_eq, bounds=x_bounds, method='highs', integrality=3)
|
||||||
|
|
||||||
|
if not res.success :
|
||||||
|
s = "No solution could be found, even when increasing max_steps using the heuristic"
|
||||||
|
return s
|
||||||
|
#raise ValueError("No solution could be found, even when increasing max_steps using the heuristic")
|
||||||
|
|
||||||
|
# If there is a solution, we're good to go, just check for
|
||||||
|
else :
|
||||||
|
circle = has_circle(res.x)
|
||||||
|
i = 0
|
||||||
|
|
||||||
|
# Break the circular symmetry if needed
|
||||||
|
while len(circle) != 0 :
|
||||||
|
A_ub, b_ub = break_circle(landmarks, A_ub, b_ub, circle)
|
||||||
|
res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq = b_eq, bounds=x_bounds, method='highs', integrality=3)
|
||||||
|
circle = has_circle(res.x)
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
if printing_details is True :
|
||||||
|
if i != 0 :
|
||||||
|
print(f"Neded to recompute paths {i} times because of unconnected loops...")
|
||||||
|
X = print_res(res, landmarks, P)
|
||||||
|
return X
|
||||||
|
else :
|
||||||
|
return untangle(res.x)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
0
deployment/kustomization.yaml
Normal file
0
.gitignore → frontend/.gitignore
vendored
@ -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.
|
48
frontend/android/README.md
Normal 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
|
||||||
|
```
|
@ -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 {
|
@ -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.
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 544 B |
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 442 B |
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 721 B |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
@ -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"
|
||||||
|
}
|
||||||
|
}
|
1
frontend/android/fallback.properties
Normal file
@ -0,0 +1 @@
|
|||||||
|
MAPS_API_KEY=Key
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 295 B |
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 406 B |
Before Width: | Height: | Size: 450 B After Width: | Height: | Size: 450 B |
Before Width: | Height: | Size: 282 B After Width: | Height: | Size: 282 B |
Before Width: | Height: | Size: 462 B After Width: | Height: | Size: 462 B |
Before Width: | Height: | Size: 704 B After Width: | Height: | Size: 704 B |
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 406 B |
Before Width: | Height: | Size: 586 B After Width: | Height: | Size: 586 B |
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 862 B |
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 862 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 762 B After Width: | Height: | Size: 762 B |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 68 B After Width: | Height: | Size: 68 B |
Before Width: | Height: | Size: 68 B After Width: | Height: | Size: 68 B |
Before Width: | Height: | Size: 68 B After Width: | Height: | Size: 68 B |
134
frontend/lib/layout.dart
Normal 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];
|
||||||
|
}
|
@ -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),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
73
frontend/lib/modules/greeter.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
80
frontend/lib/modules/landmark_card.dart
Normal 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),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
130
frontend/lib/modules/landmarks_overview.dart
Normal 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;
|
||||||
|
}
|
||||||
|
|
75
frontend/lib/modules/map.dart
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
67
frontend/lib/modules/trips_overview.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
30
frontend/lib/pages/new_trip.dart
Normal 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',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
79
frontend/lib/pages/overview.dart
Normal 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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
89
frontend/lib/pages/profile.dart
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
27
frontend/lib/pages/tutorial.dart
Normal 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.',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
116
frontend/lib/structs/landmark.dart
Normal 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));
|
||||||
|
}
|
46
frontend/lib/structs/linked_landmarks.dart
Normal 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;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
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 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
frontend/lib/structs/route.dart
Normal 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
|
||||||
|
});
|
||||||
|
}
|