UI elements using the new structs #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
|
||||
name: Test, Build and Release web
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- frontend/**
|
||||
|
||||
|
||||
name: Build web
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Web
|
||||
runs-on: k8s
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Install prerequisites
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y xz-utils
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: https://github.com/subosito/flutter-action@v2
|
||||
with:
|
||||
channel: stable
|
||||
flutter-version: 3.19.6
|
||||
cache: true
|
||||
- run: flutter pub get
|
||||
# - 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.bat
|
||||
/local.properties
|
||||
/secrets.properties
|
||||
GeneratedPluginRegistrant.java
|
||||
|
||||
# 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 "kotlin-android"
|
||||
id "dev.flutter.flutter-gradle-plugin"
|
||||
id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'
|
||||
// last is probably not needed
|
||||
}
|
||||
|
||||
def localProperties = new Properties()
|
||||
def localPropertiesFile = rootProject.file('local.properties')
|
||||
def localProperties = new Properties()
|
||||
|
||||
if (localPropertiesFile.exists()) {
|
||||
localPropertiesFile.withReader('UTF-8') { reader ->
|
||||
localProperties.load(reader)
|
||||
}
|
||||
} else {
|
||||
throw new GradleException("local.properties not found")
|
||||
}
|
||||
|
||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||
@ -22,6 +27,20 @@ if (flutterVersionName == null) {
|
||||
flutterVersionName = '1.0'
|
||||
}
|
||||
|
||||
|
||||
|
||||
def secretPropertiesFile = rootProject.file('secrets.properties')
|
||||
def secretProperties = new Properties()
|
||||
|
||||
if (secretPropertiesFile.exists()) {
|
||||
secretPropertiesFile.withReader('UTF-8') { reader ->
|
||||
secretProperties.load(reader)
|
||||
}
|
||||
} else {
|
||||
throw new GradleException("Secrets file secrets.properties not found")
|
||||
}
|
||||
|
||||
|
||||
android {
|
||||
namespace "com.example.fast_network_navigation"
|
||||
compileSdk flutter.compileSdkVersion
|
||||
@ -45,10 +64,16 @@ android {
|
||||
applicationId "com.example.fast_network_navigation"
|
||||
// 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.
|
||||
// Minimum Android version for Google Maps SDK
|
||||
// https://developers.google.com/maps/flutter-package/config#android
|
||||
minSdk = 21
|
||||
minSdkVersion flutter.minSdkVersion
|
||||
targetSdkVersion flutter.targetSdkVersion
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
// // Placeholders of keys that are replaced by the build system.
|
||||
manifestPlaceholders += ['MAPS_API_KEY': secretProperties.getProperty('MAPS_API_KEY')]
|
||||
|
||||
}
|
||||
|
||||
buildTypes {
|
@ -1,4 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- Required to fetch data from the internet. -->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
|
||||
<application
|
||||
android:label="fast_network_navigation"
|
||||
android:name="${applicationName}"
|
||||
@ -28,8 +31,14 @@
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
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>
|
||||
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility?hl=en and
|
||||
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
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) {
|
||||
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:fast_network_navigation/modules/scaffold.dart';
|
||||
import 'package:fast_network_navigation/layout.dart';
|
||||
|
||||
void main() => runApp(const App());
|
||||
|
||||
@ -12,9 +12,8 @@ class App extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: appTitle,
|
||||
home: BasePage(title: appTitle),
|
||||
home: BasePage(mainScreen: "map"),
|
||||
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
|
||||
});
|
||||
}
|