Compare commits
27 Commits
feature/fr
...
fix/missin
| Author | SHA1 | Date | |
|---|---|---|---|
| c3e72001be | |||
| f09836c470 | |||
| 9e90aed957 | |||
| d176416b15 | |||
| 5063bc2b69 | |||
| e95088ad25 | |||
| 313bdede03 | |||
| 8ceaf549e4 | |||
| 63d3ff63e7 | |||
| e1dbbbb274 | |||
| 139555dc8e | |||
| 1785e125c7 | |||
| 82d37288d6 | |||
| 9b0821926c | |||
| 0514fa063f | |||
| 9ccf68d983 | |||
| 132aa5a19b | |||
| 19b0c37a97 | |||
| ecdef605a7 | |||
| e2a918112b | |||
| 96b0718081 | |||
| d9e5d9dac6 | |||
| b0f9d31ee2 | |||
| 54bc9028ad | |||
| 37926e68ec | |||
| e2d3d29956 | |||
| 6921ab57f8 |
@@ -15,18 +15,10 @@ jobs:
|
||||
|
||||
- uses: https://gitea.com/actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
- name: Install pylint
|
||||
run: |
|
||||
apt-get update && apt-get install -y python3 python3-pip
|
||||
pip install pipenv
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
ls -la
|
||||
# only install dev-packages
|
||||
pipenv install --categories=dev-packages
|
||||
working-directory: backend
|
||||
apt-get update && apt-get install -y python3 python3-pip pylint
|
||||
|
||||
- name: Run linter
|
||||
run: pipenv run pylint src --fail-under=9
|
||||
run: pylint src --fail-under=9
|
||||
working-directory: backend
|
||||
|
||||
@@ -15,20 +15,18 @@ jobs:
|
||||
|
||||
- uses: https://gitea.com/actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
- name: Install uv (manually)
|
||||
run: |
|
||||
apt-get update && apt-get install -y python3 python3-pip
|
||||
pip install pipenv
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
ls -la
|
||||
# install all packages, including dev-packages
|
||||
pipenv install --dev
|
||||
- name: Install dependencies
|
||||
working-directory: backend
|
||||
run: |
|
||||
uv sync --frozen --no-cache --no-dev
|
||||
|
||||
- name: Run Tests
|
||||
run: pipenv run pytest src --html=report.html --self-contained-html --log-cli-level=DEBUG
|
||||
run: uv run pytest src --html=report.html --self-contained-html --log-cli-level=DEBUG
|
||||
working-directory: backend
|
||||
|
||||
- name: Upload HTML report
|
||||
|
||||
@@ -37,8 +37,8 @@ jobs:
|
||||
|
||||
- uses: https://github.com/actions/setup-java@v4
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'oracle'
|
||||
java-version: '17'
|
||||
distribution: 'zulu'
|
||||
|
||||
- name: Setup Android SDK
|
||||
uses: https://github.com/android-actions/setup-android@v3
|
||||
|
||||
5
.vscode/launch.json
vendored
5
.vscode/launch.json
vendored
@@ -33,11 +33,8 @@
|
||||
"request": "launch",
|
||||
"program": "lib/main.dart",
|
||||
"cwd": "${workspaceFolder}/frontend",
|
||||
"args": [
|
||||
"--dart-define-from-file=secrets.json"
|
||||
],
|
||||
"env": {
|
||||
"ANDROID_GOOGLE_MAPS_API_KEY": "AIzaSyD6RK_pzKFc8T-t1t0jiC3PNRZwNXECFG4"
|
||||
"GOOGLE_MAPS_API_KEY": "testing"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
5
backend/.gitignore
vendored
5
backend/.gitignore
vendored
@@ -15,6 +15,9 @@ __pycache__/
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Pytest reports
|
||||
report.html
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
@@ -131,7 +134,7 @@ celerybeat.pid
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
*.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
|
||||
1
backend/.python-version
Normal file
1
backend/.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.12.9
|
||||
@@ -1,11 +1,29 @@
|
||||
FROM python:3.11-slim
|
||||
FROM python:3.12-slim-bookworm
|
||||
|
||||
# The installer requires curl (and certificates) to download the release archive
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates
|
||||
|
||||
# Download the latest installer
|
||||
ADD https://astral.sh/uv/install.sh /uv-installer.sh
|
||||
|
||||
# Run the installer then remove it
|
||||
RUN sh /uv-installer.sh && rm /uv-installer.sh
|
||||
|
||||
# Ensure the installed binary is on the `PATH`
|
||||
ENV PATH="/root/.local/bin/:$PATH"
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /app
|
||||
COPY Pipfile Pipfile.lock .
|
||||
|
||||
RUN pip install pipenv
|
||||
RUN pipenv install --deploy --system
|
||||
# Copy uv files
|
||||
COPY pyproject.toml pyproject.toml
|
||||
COPY uv.lock uv.lock
|
||||
COPY .python-version .python-version
|
||||
|
||||
# Sync the venv
|
||||
RUN uv sync --frozen --no-cache --no-dev
|
||||
|
||||
# Copy application files
|
||||
COPY src src
|
||||
|
||||
EXPOSE 8000
|
||||
@@ -17,4 +35,4 @@ ENV MEMCACHED_HOST_PATH=none
|
||||
ENV LOKI_URL=none
|
||||
|
||||
# explicitly use a string instead of an argument list to force a shell and variable expansion
|
||||
CMD fastapi run src/main.py --port 8000 --workers $NUM_WORKERS
|
||||
CMD uv run fastapi run src/main.py --port 8000 --workers $NUM_WORKERS
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[dev-packages]
|
||||
pylint = "*"
|
||||
pytest = "*"
|
||||
tomli = "*"
|
||||
httpx = "*"
|
||||
exceptiongroup = "*"
|
||||
pytest-html = "*"
|
||||
typing-extensions = "*"
|
||||
dill = "*"
|
||||
|
||||
[packages]
|
||||
numpy = "*"
|
||||
fastapi = "*"
|
||||
pydantic = "*"
|
||||
shapely = "*"
|
||||
pymemcache = "*"
|
||||
fastapi-cli = "*"
|
||||
scikit-learn = "*"
|
||||
loki-logger-handler = "*"
|
||||
pulp = "*"
|
||||
scipy = "*"
|
||||
requests = "*"
|
||||
1246
backend/Pipfile.lock
generated
1246
backend/Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,363 +0,0 @@
|
||||
[
|
||||
{
|
||||
"name": "Chinatown",
|
||||
"type": "shopping",
|
||||
"location": [
|
||||
45.7554934,
|
||||
4.8444852
|
||||
],
|
||||
"osm_type": "way",
|
||||
"osm_id": 996515596,
|
||||
"attractiveness": 129,
|
||||
"n_tags": 0,
|
||||
"image_url": null,
|
||||
"website_url": null,
|
||||
"wiki_url": null,
|
||||
"keywords": {},
|
||||
"description": null,
|
||||
"duration": 30,
|
||||
"name_en": null,
|
||||
"uuid": "285d159c-68ee-4b37-8d71-f27ee3d38b02",
|
||||
"must_do": false,
|
||||
"must_avoid": false,
|
||||
"is_secondary": false,
|
||||
"time_to_reach_next": 0,
|
||||
"next_uuid": null,
|
||||
"is_viewpoint": false,
|
||||
"is_place_of_worship": false
|
||||
},
|
||||
{
|
||||
"name": "Galeries Lafayette",
|
||||
"type": "shopping",
|
||||
"location": [
|
||||
45.7627107,
|
||||
4.8556833
|
||||
],
|
||||
"osm_type": "way",
|
||||
"osm_id": 1069872743,
|
||||
"attractiveness": 197,
|
||||
"n_tags": 11,
|
||||
"image_url": null,
|
||||
"website_url": "http://www.galerieslafayette.com/",
|
||||
"wiki_url": null,
|
||||
"keywords": null,
|
||||
"description": null,
|
||||
"duration": 30,
|
||||
"name_en": null,
|
||||
"uuid": "28f1bc30-10d3-4944-8861-0ed9abca012d",
|
||||
"must_do": false,
|
||||
"must_avoid": false,
|
||||
"is_secondary": false,
|
||||
"time_to_reach_next": 0,
|
||||
"next_uuid": null,
|
||||
"is_viewpoint": false,
|
||||
"is_place_of_worship": false
|
||||
},
|
||||
{
|
||||
"name": "Muji",
|
||||
"type": "shopping",
|
||||
"location": [
|
||||
45.7615971,
|
||||
4.8543781
|
||||
],
|
||||
"osm_type": "way",
|
||||
"osm_id": 1044165817,
|
||||
"attractiveness": 259,
|
||||
"n_tags": 14,
|
||||
"image_url": null,
|
||||
"website_url": "https://www.muji.com/fr/",
|
||||
"wiki_url": null,
|
||||
"keywords": null,
|
||||
"description": null,
|
||||
"duration": 30,
|
||||
"name_en": "Muji",
|
||||
"uuid": "957f86a5-6c00-41a2-815d-d6f739052be4",
|
||||
"must_do": false,
|
||||
"must_avoid": false,
|
||||
"is_secondary": false,
|
||||
"time_to_reach_next": 0,
|
||||
"next_uuid": null,
|
||||
"is_viewpoint": false,
|
||||
"is_place_of_worship": false
|
||||
},
|
||||
{
|
||||
"name": "HEMA",
|
||||
"type": "shopping",
|
||||
"location": [
|
||||
45.7619133,
|
||||
4.8565239
|
||||
],
|
||||
"osm_type": "way",
|
||||
"osm_id": 1069872750,
|
||||
"attractiveness": 156,
|
||||
"n_tags": 9,
|
||||
"image_url": null,
|
||||
"website_url": "https://fr.westfield.com/lapartdieu/store/HEMA/www.hema.fr",
|
||||
"wiki_url": null,
|
||||
"keywords": null,
|
||||
"description": null,
|
||||
"duration": 30,
|
||||
"name_en": null,
|
||||
"uuid": "8dae9d3e-e4c4-4e80-941d-0b106e22c85b",
|
||||
"must_do": false,
|
||||
"must_avoid": false,
|
||||
"is_secondary": false,
|
||||
"time_to_reach_next": 0,
|
||||
"next_uuid": null,
|
||||
"is_viewpoint": false,
|
||||
"is_place_of_worship": false
|
||||
},
|
||||
{
|
||||
"name": "Cordeliers",
|
||||
"type": "shopping",
|
||||
"location": [
|
||||
45.7622752,
|
||||
4.8337998
|
||||
],
|
||||
"osm_type": "node",
|
||||
"osm_id": 5545183519,
|
||||
"attractiveness": 813,
|
||||
"n_tags": 0,
|
||||
"image_url": null,
|
||||
"website_url": null,
|
||||
"wiki_url": null,
|
||||
"keywords": {},
|
||||
"description": null,
|
||||
"duration": 30,
|
||||
"name_en": null,
|
||||
"uuid": "ba02adb5-e28f-4645-8c2d-25ead6232379",
|
||||
"must_do": false,
|
||||
"must_avoid": false,
|
||||
"is_secondary": false,
|
||||
"time_to_reach_next": 0,
|
||||
"next_uuid": null,
|
||||
"is_viewpoint": false,
|
||||
"is_place_of_worship": false
|
||||
},
|
||||
{
|
||||
"name": "Halles de Lyon Paul Bocuse",
|
||||
"type": "shopping",
|
||||
"location": [
|
||||
45.7628282,
|
||||
4.8505601
|
||||
],
|
||||
"osm_type": "relation",
|
||||
"osm_id": 971529,
|
||||
"attractiveness": 272,
|
||||
"n_tags": 12,
|
||||
"image_url": null,
|
||||
"website_url": "https://www.halles-de-lyon-paulbocuse.com/",
|
||||
"wiki_url": "fr:Halles de Lyon-Paul Bocuse",
|
||||
"keywords": {
|
||||
"importance": "national",
|
||||
"height": null,
|
||||
"place_type": "marketplace",
|
||||
"date": null
|
||||
},
|
||||
"description": "Halles de Lyon Paul Bocuse is a marketplace of national importance.",
|
||||
"duration": 30,
|
||||
"name_en": null,
|
||||
"uuid": "bbd50de3-aa91-425d-90c2-d4abfd1b4abe",
|
||||
"must_do": false,
|
||||
"must_avoid": false,
|
||||
"is_secondary": false,
|
||||
"time_to_reach_next": 0,
|
||||
"next_uuid": null,
|
||||
"is_viewpoint": false,
|
||||
"is_place_of_worship": false
|
||||
},
|
||||
{
|
||||
"name": "Grand Bazar",
|
||||
"type": "shopping",
|
||||
"location": [
|
||||
45.7632141,
|
||||
4.8361975
|
||||
],
|
||||
"osm_type": "way",
|
||||
"osm_id": 82399951,
|
||||
"attractiveness": 93,
|
||||
"n_tags": 7,
|
||||
"image_url": null,
|
||||
"website_url": null,
|
||||
"wiki_url": null,
|
||||
"keywords": null,
|
||||
"description": null,
|
||||
"duration": 30,
|
||||
"name_en": null,
|
||||
"uuid": "3de9131c-87c5-4efb-9fa8-064896fb8b29",
|
||||
"must_do": false,
|
||||
"must_avoid": false,
|
||||
"is_secondary": false,
|
||||
"time_to_reach_next": 0,
|
||||
"next_uuid": null,
|
||||
"is_viewpoint": false,
|
||||
"is_place_of_worship": false
|
||||
},
|
||||
{
|
||||
"name": "Shopping Area",
|
||||
"type": "shopping",
|
||||
"location": [
|
||||
45.7673452,
|
||||
4.8438683
|
||||
],
|
||||
"osm_type": "node",
|
||||
"osm_id": 0,
|
||||
"attractiveness": 156,
|
||||
"n_tags": 0,
|
||||
"image_url": null,
|
||||
"website_url": null,
|
||||
"wiki_url": null,
|
||||
"keywords": {},
|
||||
"description": null,
|
||||
"duration": 30,
|
||||
"name_en": null,
|
||||
"uuid": "df2482a8-7e2e-4536-aad3-564899b2fa65",
|
||||
"must_do": false,
|
||||
"must_avoid": false,
|
||||
"is_secondary": false,
|
||||
"time_to_reach_next": 0,
|
||||
"next_uuid": null,
|
||||
"is_viewpoint": false,
|
||||
"is_place_of_worship": false
|
||||
},
|
||||
{
|
||||
"name": "Cour Oxyg\u00e8ne",
|
||||
"type": "shopping",
|
||||
"location": [
|
||||
45.7620905,
|
||||
4.8568873
|
||||
],
|
||||
"osm_type": "way",
|
||||
"osm_id": 132673030,
|
||||
"attractiveness": 63,
|
||||
"n_tags": 5,
|
||||
"image_url": null,
|
||||
"website_url": null,
|
||||
"wiki_url": null,
|
||||
"keywords": null,
|
||||
"description": null,
|
||||
"duration": 30,
|
||||
"name_en": null,
|
||||
"uuid": "ed134f76-9a02-4bee-9c10-78454f7bc4ce",
|
||||
"must_do": false,
|
||||
"must_avoid": false,
|
||||
"is_secondary": false,
|
||||
"time_to_reach_next": 0,
|
||||
"next_uuid": null,
|
||||
"is_viewpoint": false,
|
||||
"is_place_of_worship": false
|
||||
},
|
||||
{
|
||||
"name": "P\u00f4le de Commerces et de Loisirs Confluence",
|
||||
"type": "shopping",
|
||||
"location": [
|
||||
45.7410414,
|
||||
4.8171031
|
||||
],
|
||||
"osm_type": "way",
|
||||
"osm_id": 440270633,
|
||||
"attractiveness": 259,
|
||||
"n_tags": 14,
|
||||
"image_url": null,
|
||||
"website_url": "https://www.confluence.fr/",
|
||||
"wiki_url": null,
|
||||
"keywords": null,
|
||||
"description": null,
|
||||
"duration": 30,
|
||||
"name_en": null,
|
||||
"uuid": "dd7e2f5f-0e60-4560-b903-e5ded4b6e36a",
|
||||
"must_do": false,
|
||||
"must_avoid": false,
|
||||
"is_secondary": false,
|
||||
"time_to_reach_next": 0,
|
||||
"next_uuid": null,
|
||||
"is_viewpoint": false,
|
||||
"is_place_of_worship": false
|
||||
},
|
||||
{
|
||||
"name": "Grand H\u00f4tel-Dieu",
|
||||
"type": "shopping",
|
||||
"location": [
|
||||
45.7586955,
|
||||
4.8364597
|
||||
],
|
||||
"osm_type": "relation",
|
||||
"osm_id": 300128,
|
||||
"attractiveness": 546,
|
||||
"n_tags": 22,
|
||||
"image_url": null,
|
||||
"website_url": "https://grand-hotel-dieu.com",
|
||||
"wiki_url": "fr:H\u00f4tel-Dieu de Lyon",
|
||||
"keywords": {
|
||||
"importance": "international",
|
||||
"height": null,
|
||||
"place_type": "building",
|
||||
"date": "C17"
|
||||
},
|
||||
"description": "Grand H\u00f4tel-Dieu is an internationally famous building. It was constructed in C17.",
|
||||
"duration": 30,
|
||||
"name_en": null,
|
||||
"uuid": "a91265a8-ffbd-44f7-a7ab-3ff75f08fbab",
|
||||
"must_do": false,
|
||||
"must_avoid": false,
|
||||
"is_secondary": false,
|
||||
"time_to_reach_next": 0,
|
||||
"next_uuid": null,
|
||||
"is_viewpoint": false,
|
||||
"is_place_of_worship": false
|
||||
},
|
||||
{
|
||||
"name": "Westfield La Part-Dieu",
|
||||
"type": "shopping",
|
||||
"location": [
|
||||
45.761331,
|
||||
4.855676
|
||||
],
|
||||
"osm_type": "way",
|
||||
"osm_id": 62338376,
|
||||
"attractiveness": 546,
|
||||
"n_tags": 22,
|
||||
"image_url": null,
|
||||
"website_url": "https://fr.westfield.com/lapartdieu",
|
||||
"wiki_url": "fr:La Part-Dieu (centre commercial)",
|
||||
"keywords": null,
|
||||
"description": null,
|
||||
"duration": 30,
|
||||
"name_en": null,
|
||||
"uuid": "7d60316f-d689-4fcf-be68-ffc09353b826",
|
||||
"must_do": false,
|
||||
"must_avoid": false,
|
||||
"is_secondary": false,
|
||||
"time_to_reach_next": 0,
|
||||
"next_uuid": null,
|
||||
"is_viewpoint": false,
|
||||
"is_place_of_worship": false
|
||||
},
|
||||
{
|
||||
"name": "Ainay",
|
||||
"type": "shopping",
|
||||
"location": [
|
||||
45.7553105,
|
||||
4.8312084
|
||||
],
|
||||
"osm_type": "node",
|
||||
"osm_id": 5545126047,
|
||||
"attractiveness": 132,
|
||||
"n_tags": 0,
|
||||
"image_url": null,
|
||||
"website_url": null,
|
||||
"wiki_url": null,
|
||||
"keywords": {},
|
||||
"description": null,
|
||||
"duration": 30,
|
||||
"name_en": null,
|
||||
"uuid": "ad214f3d-a4b9-4078-876a-446caa7ab01c",
|
||||
"must_do": false,
|
||||
"must_avoid": false,
|
||||
"is_secondary": false,
|
||||
"time_to_reach_next": 0,
|
||||
"next_uuid": null,
|
||||
"is_viewpoint": false,
|
||||
"is_place_of_worship": false
|
||||
}
|
||||
]
|
||||
6
backend/main.py
Normal file
6
backend/main.py
Normal file
@@ -0,0 +1,6 @@
|
||||
def main():
|
||||
print("Hello from backend!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
55
backend/pyproject.toml
Normal file
55
backend/pyproject.toml
Normal file
@@ -0,0 +1,55 @@
|
||||
[project]
|
||||
name = "backend"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"annotated-types==0.7.0 ; python_full_version >= '3.8'",
|
||||
"anyio==4.8.0 ; python_full_version >= '3.9'",
|
||||
"certifi==2024.12.14 ; python_full_version >= '3.6'",
|
||||
"charset-normalizer==3.4.1 ; python_full_version >= '3.7'",
|
||||
"click==8.1.8 ; python_full_version >= '3.7'",
|
||||
"fastapi==0.115.7 ; python_full_version >= '3.8'",
|
||||
"fastapi-cli==0.0.7 ; python_full_version >= '3.8'",
|
||||
"h11==0.14.0 ; python_full_version >= '3.7'",
|
||||
"httptools==0.6.4",
|
||||
"idna==3.10 ; python_full_version >= '3.6'",
|
||||
"joblib==1.4.2 ; python_full_version >= '3.8'",
|
||||
"loki-logger-handler==1.1.0 ; python_full_version >= '2.7'",
|
||||
"markdown-it-py==3.0.0 ; python_full_version >= '3.8'",
|
||||
"mdurl==0.1.2 ; python_full_version >= '3.7'",
|
||||
"numpy==2.2.2 ; python_full_version >= '3.10'",
|
||||
"pulp==2.9.0 ; python_full_version >= '3.7'",
|
||||
"pydantic==2.10.6 ; python_full_version >= '3.8'",
|
||||
"pydantic-core==2.27.2 ; python_full_version >= '3.8'",
|
||||
"pygments==2.19.1 ; python_full_version >= '3.8'",
|
||||
"pymemcache==4.0.0 ; python_full_version >= '3.7'",
|
||||
"python-dotenv==1.0.1",
|
||||
"pyyaml==6.0.2",
|
||||
"requests==2.32.3 ; python_full_version >= '3.8'",
|
||||
"rich==13.9.4 ; python_full_version >= '3.8'",
|
||||
"rich-toolkit==0.13.2 ; python_full_version >= '3.8'",
|
||||
"scikit-learn==1.6.1 ; python_full_version >= '3.9'",
|
||||
"scipy==1.15.1 ; python_full_version >= '3.10'",
|
||||
"shapely==2.0.6 ; python_full_version >= '3.7'",
|
||||
"shellingham==1.5.4 ; python_full_version >= '3.7'",
|
||||
"sniffio==1.3.1 ; python_full_version >= '3.7'",
|
||||
"starlette==0.45.3 ; python_full_version >= '3.9'",
|
||||
"threadpoolctl==3.5.0 ; python_full_version >= '3.8'",
|
||||
"typer==0.15.1 ; python_full_version >= '3.7'",
|
||||
"typing-extensions==4.12.2 ; python_full_version >= '3.8'",
|
||||
"urllib3==2.3.0 ; python_full_version >= '3.9'",
|
||||
"uvicorn[standard]==0.34.0 ; python_full_version >= '3.9'",
|
||||
"uvloop==0.21.0",
|
||||
"watchfiles==1.0.4",
|
||||
"websockets==14.2",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"httpx>=0.28.1",
|
||||
"ipykernel>=6.30.0",
|
||||
"pytest>=8.4.1",
|
||||
"pytest-html>=4.1.1",
|
||||
]
|
||||
File diff suppressed because one or more lines are too long
@@ -102,52 +102,54 @@ class ClusterManager:
|
||||
selector = sel,
|
||||
out = out
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Error fetching clusters: {e}")
|
||||
|
||||
if result is None :
|
||||
self.logger.debug(f"Found no {cluster_type} clusters, overpass query returned no datapoints.")
|
||||
self.valid = False
|
||||
|
||||
else :
|
||||
points = []
|
||||
for elem in result:
|
||||
osm_type = elem.get('type')
|
||||
|
||||
# Get coordinates and append them to the points list
|
||||
_, coords = get_base_info(elem, osm_type)
|
||||
if coords is not None :
|
||||
points.append(coords)
|
||||
|
||||
if points :
|
||||
self.all_points = np.array(points)
|
||||
|
||||
# Apply DBSCAN to find clusters. Choose different settings for different cities.
|
||||
if self.cluster_type == 'shopping' and len(self.all_points) > 200 :
|
||||
dbscan = DBSCAN(eps=0.00118, min_samples=15, algorithm='kd_tree') # for large cities
|
||||
elif self.cluster_type == 'sightseeing' :
|
||||
dbscan = DBSCAN(eps=0.0025, min_samples=15, algorithm='kd_tree') # for historic neighborhoods
|
||||
else :
|
||||
dbscan = DBSCAN(eps=0.00075, min_samples=10, algorithm='kd_tree') # for small cities
|
||||
|
||||
labels = dbscan.fit_predict(self.all_points)
|
||||
|
||||
# Check that there are is least 1 cluster
|
||||
if len(set(labels)) > 1 :
|
||||
self.logger.info(f"Found {len(set(labels))} different {cluster_type} clusters.")
|
||||
# Separate clustered points and noise points
|
||||
self.cluster_points = self.all_points[labels != -1]
|
||||
self.cluster_labels = labels[labels != -1]
|
||||
self.filter_clusters() # ValueError here sometimes. I dont know why. # Filter the clusters to keep only the largest ones.
|
||||
self.valid = True
|
||||
|
||||
else :
|
||||
self.logger.info(f"Found 0 {cluster_type} clusters.")
|
||||
self.valid = False
|
||||
if result is None :
|
||||
self.logger.debug(f"Found no {cluster_type} clusters, overpass query returned no datapoints.")
|
||||
self.valid = False
|
||||
|
||||
else :
|
||||
self.logger.debug(f"Detected 0 {cluster_type} clusters.")
|
||||
self.valid = False
|
||||
points = []
|
||||
for elem in result:
|
||||
osm_type = elem.get('type')
|
||||
|
||||
# Get coordinates and append them to the points list
|
||||
_, coords = get_base_info(elem, osm_type)
|
||||
if coords is not None :
|
||||
points.append(coords)
|
||||
|
||||
if points :
|
||||
self.all_points = np.array(points)
|
||||
|
||||
# Apply DBSCAN to find clusters. Choose different settings for different cities.
|
||||
if self.cluster_type == 'shopping' and len(self.all_points) > 200 :
|
||||
dbscan = DBSCAN(eps=0.00118, min_samples=15, algorithm='kd_tree') # for large cities
|
||||
elif self.cluster_type == 'sightseeing' :
|
||||
dbscan = DBSCAN(eps=0.0025, min_samples=15, algorithm='kd_tree') # for historic neighborhoods
|
||||
else :
|
||||
dbscan = DBSCAN(eps=0.00075, min_samples=10, algorithm='kd_tree') # for small cities
|
||||
|
||||
labels = dbscan.fit_predict(self.all_points)
|
||||
|
||||
# Check that there are is least 1 cluster
|
||||
if len(set(labels)) > 1 :
|
||||
self.logger.info(f"Found {len(set(labels))} different {cluster_type} clusters.")
|
||||
# Separate clustered points and noise points
|
||||
self.cluster_points = self.all_points[labels != -1]
|
||||
self.cluster_labels = labels[labels != -1]
|
||||
self.filter_clusters() # ValueError here sometimes. I dont know why. # Filter the clusters to keep only the largest ones.
|
||||
self.valid = True
|
||||
|
||||
else :
|
||||
self.logger.info(f"Found 0 {cluster_type} clusters.")
|
||||
self.valid = False
|
||||
|
||||
else :
|
||||
self.logger.debug(f"Found 0 {cluster_type} clusters.")
|
||||
self.valid = False
|
||||
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Could not fetch clusters: {e}")
|
||||
self.valid = False
|
||||
|
||||
|
||||
|
||||
def generate_clusters(self) -> list[Landmark]:
|
||||
|
||||
@@ -4,7 +4,6 @@ import yaml
|
||||
|
||||
from ..structs.preferences import Preferences
|
||||
from ..structs.landmark import Landmark
|
||||
from ..utils.take_most_important import take_most_important
|
||||
from .cluster_manager import ClusterManager
|
||||
from ..overpass.overpass import Overpass, get_base_info
|
||||
from ..utils.bbox import create_bbox
|
||||
@@ -23,7 +22,7 @@ class LandmarkManager:
|
||||
church_coeff: float # coeff to adjsut score of churches
|
||||
nature_coeff: float # coeff to adjust score of parks
|
||||
overall_coeff: float # coeff to adjust weight of tags
|
||||
n_important: int # number of important landmarks to consider
|
||||
# n_important: int # number of important landmarks to consider
|
||||
|
||||
|
||||
def __init__(self) -> None:
|
||||
@@ -42,7 +41,7 @@ class LandmarkManager:
|
||||
self.wikipedia_bonus = parameters['wikipedia_bonus']
|
||||
self.viewpoint_bonus = parameters['viewpoint_bonus']
|
||||
self.pay_bonus = parameters['pay_bonus']
|
||||
self.n_important = parameters['N_important']
|
||||
# self.n_important = parameters['N_important']
|
||||
|
||||
with OPTIMIZER_PARAMETERS_PATH.open('r') as f:
|
||||
parameters = yaml.safe_load(f)
|
||||
@@ -55,7 +54,12 @@ class LandmarkManager:
|
||||
self.logger.info('LandmakManager successfully initialized.')
|
||||
|
||||
|
||||
def generate_landmarks_list(self, center_coordinates: tuple[float, float], preferences: Preferences) -> tuple[list[Landmark], list[Landmark]]:
|
||||
def generate_landmarks_list(
|
||||
self,
|
||||
center_coordinates: tuple[float, float],
|
||||
preferences: Preferences,
|
||||
allow_clusters: bool = True
|
||||
) -> list[Landmark] :
|
||||
"""
|
||||
Generate and prioritize a list of landmarks based on user preferences.
|
||||
|
||||
@@ -63,16 +67,17 @@ class LandmarkManager:
|
||||
and current location. It scores and corrects these landmarks, removes duplicates, and then selects the most important
|
||||
landmarks based on a predefined criterion.
|
||||
|
||||
Args:
|
||||
center_coordinates (tuple[float, float]): The latitude and longitude of the center location around which to search.
|
||||
preferences (Preferences): The user's preference settings that influence the landmark selection.
|
||||
Parameters :
|
||||
center_coordinates (tuple[float, float]): The latitude and longitude of the center location around which to search.
|
||||
preferences (Preferences): The user's preference settings that influence the landmark selection.
|
||||
allow_clusters (bool, optional) : If set to False, no clusters will be fetched. Mainly used for the option to fetch landmarks nearby.
|
||||
|
||||
Returns:
|
||||
tuple[list[Landmark], list[Landmark]]:
|
||||
- A list of all existing landmarks.
|
||||
- A list of the most important landmarks based on the user's preferences.
|
||||
"""
|
||||
self.logger.debug('Starting to fetch landmarks...')
|
||||
self.logger.info(f'Starting to fetch landmarks around {center_coordinates}...')
|
||||
max_walk_dist = int((preferences.max_time_minute/2)/60*self.walking_speed*1000/self.detour_factor)
|
||||
radius = min(max_walk_dist, int(self.max_bbox_side/2))
|
||||
|
||||
@@ -89,10 +94,11 @@ class LandmarkManager:
|
||||
all_landmarks.update(current_landmarks)
|
||||
self.logger.info(f'Found {len(current_landmarks)} sightseeing landmarks')
|
||||
|
||||
if allow_clusters :
|
||||
# special pipeline for historic neighborhoods
|
||||
neighborhood_manager = ClusterManager(bbox, 'sightseeing')
|
||||
historic_clusters = neighborhood_manager.generate_clusters()
|
||||
all_landmarks.update(historic_clusters)
|
||||
neighborhood_manager = ClusterManager(bbox, 'sightseeing')
|
||||
historic_clusters = neighborhood_manager.generate_clusters()
|
||||
all_landmarks.update(historic_clusters)
|
||||
|
||||
# list for nature
|
||||
if preferences.nature.score != 0:
|
||||
@@ -113,16 +119,19 @@ class LandmarkManager:
|
||||
landmark.duration = 30
|
||||
all_landmarks.update(current_landmarks)
|
||||
|
||||
# special pipeline for shopping malls
|
||||
shopping_manager = ClusterManager(bbox, 'shopping')
|
||||
shopping_clusters = shopping_manager.generate_clusters()
|
||||
all_landmarks.update(shopping_clusters)
|
||||
if allow_clusters :
|
||||
# special pipeline for shopping malls
|
||||
shopping_manager = ClusterManager(bbox, 'shopping')
|
||||
shopping_clusters = shopping_manager.generate_clusters()
|
||||
all_landmarks.update(shopping_clusters)
|
||||
|
||||
|
||||
landmarks_constrained = take_most_important(all_landmarks, self.n_important)
|
||||
# DETAILS HERE
|
||||
# self.logger.info(f'All landmarks generated : {len(all_landmarks)} landmarks around {center_coordinates}, and constrained to {len(landmarks_constrained)} most important ones.')
|
||||
self.logger.info(f'Found {len(all_landmarks)} landmarks in total.')
|
||||
|
||||
return all_landmarks, landmarks_constrained
|
||||
return sorted(all_landmarks, key=lambda x: x.attractiveness, reverse=True)
|
||||
|
||||
|
||||
def set_landmark_score(self, landmark: Landmark, landmarktype: str, preference_level: int) :
|
||||
"""
|
||||
@@ -236,6 +245,17 @@ class LandmarkManager:
|
||||
continue
|
||||
|
||||
tags = elem.get('tags')
|
||||
n_tags=len(tags)
|
||||
|
||||
# Skip this landmark if not suitable
|
||||
if tags.get('building:part') is not None :
|
||||
continue
|
||||
if tags.get('disused') is not None :
|
||||
continue
|
||||
if tags.get('boundary') is not None :
|
||||
continue
|
||||
if tags.get('shop') is not None and landmarktype != 'shopping' :
|
||||
continue
|
||||
|
||||
# Convert this to Landmark object
|
||||
landmark = Landmark(name=name,
|
||||
@@ -244,180 +264,36 @@ class LandmarkManager:
|
||||
osm_id=id,
|
||||
osm_type=osm_type,
|
||||
attractiveness=0,
|
||||
n_tags=len(tags))
|
||||
n_tags=n_tags)
|
||||
|
||||
# Browse through tags to add information to landmark.
|
||||
for key, value in tags.items():
|
||||
# Extract useful information for score calculation later down the road.
|
||||
landmark.image_url = tags.get('image')
|
||||
landmark.website_url = tags.get('website')
|
||||
landmark.wiki_url = tags.get('wikipedia')
|
||||
landmark.name_en = tags.get('name:en')
|
||||
|
||||
# Skip this landmark if not suitable.
|
||||
if key == 'building:part' and value == 'yes' :
|
||||
break
|
||||
if 'disused:' in key :
|
||||
break
|
||||
if 'boundary:' in key :
|
||||
break
|
||||
if 'shop' in key and landmarktype != 'shopping' :
|
||||
break
|
||||
# if value == 'apartments' :
|
||||
# break
|
||||
|
||||
# Fill in the other attributes.
|
||||
if key == 'image' :
|
||||
landmark.image_url = value
|
||||
if key == 'website' :
|
||||
landmark.website_url = value
|
||||
if value == 'place_of_worship' :
|
||||
# Check for place of worship
|
||||
if tags.get('place_of_worship') is not None :
|
||||
landmark.is_place_of_worship = True
|
||||
if key == 'wikipedia' :
|
||||
landmark.wiki_url = value
|
||||
if key == 'name:en' :
|
||||
landmark.name_en = value
|
||||
if 'building:' in key or 'pay' in key :
|
||||
landmark.n_tags -= 1
|
||||
landmark.name_en = tags.get('place_of_worship')
|
||||
|
||||
# Set the duration. Needed for the optimization.
|
||||
if tags.get('amenity') in ['aquarium', 'planetarium'] or tags.get('tourism') in ['aquarium', 'museum', 'zoo']:
|
||||
landmark.duration = 60
|
||||
elif tags.get('tourism') == 'viewpoint' :
|
||||
landmark.is_viewpoint = True
|
||||
landmark.duration = 10
|
||||
elif tags.get('building') == 'cathedral' :
|
||||
landmark.is_place_of_worship = False
|
||||
landmark.duration = 10
|
||||
|
||||
# Set the duration.
|
||||
if value in ['museum', 'aquarium', 'planetarium'] :
|
||||
landmark.duration = 60
|
||||
elif value == 'viewpoint' :
|
||||
landmark.is_viewpoint = True
|
||||
landmark.duration = 10
|
||||
elif value == 'cathedral' :
|
||||
landmark.is_place_of_worship = False
|
||||
landmark.duration = 10
|
||||
|
||||
landmark.description, landmark.keywords = self.description_and_keywords(tags)
|
||||
# Compute the score and add landmark to the list.
|
||||
self.set_landmark_score(landmark, landmarktype, preference_level)
|
||||
landmarks.append(landmark)
|
||||
|
||||
continue
|
||||
|
||||
|
||||
return landmarks
|
||||
|
||||
|
||||
def description_and_keywords(self, tags: dict):
|
||||
"""
|
||||
Generates a description and a set of keywords for a given landmark based on its tags.
|
||||
|
||||
Params:
|
||||
tags (dict): A dictionary containing metadata about the landmark, including its name,
|
||||
importance, height, date of construction, and visitor information.
|
||||
|
||||
Returns:
|
||||
description (str): A string description of the landmark.
|
||||
keywords (dict): A dictionary of keywords with fields such as 'importance', 'height',
|
||||
'place_type', and 'date'.
|
||||
"""
|
||||
# Extract relevant fields
|
||||
name = tags.get('name')
|
||||
importance = tags.get('importance', None)
|
||||
n_visitors = tags.get('tourism:visitors', None)
|
||||
height = tags.get('height')
|
||||
place_type = self.get_place_type(tags)
|
||||
date = self.get_date(tags)
|
||||
|
||||
if place_type is None :
|
||||
return None, None
|
||||
|
||||
# Start the description.
|
||||
if importance is None :
|
||||
if len(tags.keys()) < 5 :
|
||||
return None, None
|
||||
if len(tags.keys()) < 10 :
|
||||
description = f"{name} is a well known {place_type}."
|
||||
elif len(tags.keys()) < 17 :
|
||||
importance = 'national'
|
||||
description = f"{name} is a {place_type} of national importance."
|
||||
else :
|
||||
importance = 'international'
|
||||
description = f"{name} is an internationally famous {place_type}."
|
||||
else :
|
||||
description = f"{name} is a {place_type} of {importance} importance."
|
||||
|
||||
if height is not None and date is not None :
|
||||
description += f" This {place_type} was constructed in {date} and is ca. {height} meters high."
|
||||
elif height is not None :
|
||||
description += f" This {place_type} stands ca. {height} meters tall."
|
||||
elif date is not None:
|
||||
description += f" It was constructed in {date}."
|
||||
|
||||
# Format the visitor number
|
||||
if n_visitors is not None :
|
||||
n_visitors = int(n_visitors)
|
||||
if n_visitors < 1000000 :
|
||||
description += f" It welcomes {int(n_visitors/1000)} thousand visitors every year."
|
||||
else :
|
||||
description += f" It welcomes {round(n_visitors/1000000, 1)} million visitors every year."
|
||||
|
||||
# Set the keywords.
|
||||
keywords = {"importance": importance,
|
||||
"height": height,
|
||||
"place_type": place_type,
|
||||
"date": date}
|
||||
|
||||
return description, keywords
|
||||
|
||||
|
||||
def get_place_type(self, data):
|
||||
"""
|
||||
Determines the type of the place based on available tags such as 'amenity', 'building',
|
||||
'historic', and 'leisure'. The priority order is: 'historic' > 'building' (if not generic) >
|
||||
'amenity' > 'leisure'.
|
||||
|
||||
Params:
|
||||
data (dict): A dictionary containing metadata about the place.
|
||||
|
||||
Returns:
|
||||
place_type (str): The determined type of the place, or None if no relevant type is found.
|
||||
"""
|
||||
amenity = data.get('amenity', None)
|
||||
building = data.get('building', None)
|
||||
historic = data.get('historic', None)
|
||||
leisure = data.get('leisure')
|
||||
|
||||
if historic and historic != "yes":
|
||||
return historic
|
||||
if building and building not in ["yes", "civic", "government", "apartments", "residential", "commericial", "industrial", "retail", "religious", "public", "service"]:
|
||||
return building
|
||||
if amenity:
|
||||
return amenity
|
||||
if leisure:
|
||||
return leisure
|
||||
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_date(self, data):
|
||||
"""
|
||||
Extracts the most relevant date from the available tags, prioritizing 'construction_date',
|
||||
'start_date', 'year_of_construction', and 'opening_date' in that order.
|
||||
|
||||
Params:
|
||||
data (dict): A dictionary containing metadata about the place.
|
||||
|
||||
Returns:
|
||||
date (str): The most relevant date found, or None if no date is available.
|
||||
"""
|
||||
construction_date = data.get('construction_date', None)
|
||||
opening_date = data.get('opening_date', None)
|
||||
start_date = data.get('start_date', None)
|
||||
year_of_construction = data.get('year_of_construction', None)
|
||||
|
||||
# Prioritize based on availability
|
||||
if construction_date:
|
||||
return construction_date
|
||||
if start_date:
|
||||
return start_date
|
||||
if year_of_construction:
|
||||
return year_of_construction
|
||||
if opening_date:
|
||||
return opening_date
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def dict_to_selector_list(d: dict) -> list:
|
||||
"""
|
||||
Convert a dictionary of key-value pairs to a list of Overpass query strings.
|
||||
|
||||
123
backend/src/landmarks/landmarks_router.py
Normal file
123
backend/src/landmarks/landmarks_router.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""Main app for backend api"""
|
||||
import logging
|
||||
import time
|
||||
import random
|
||||
from fastapi import HTTPException, APIRouter
|
||||
|
||||
from ..structs.landmark import Landmark
|
||||
from ..structs.preferences import Preferences, Preference
|
||||
from .landmarks_manager import LandmarkManager
|
||||
|
||||
|
||||
# Setup the logger and the Landmarks Manager
|
||||
logger = logging.getLogger(__name__)
|
||||
manager = LandmarkManager()
|
||||
|
||||
|
||||
# Initialize the API router
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/get/landmarks")
|
||||
def get_landmarks(
|
||||
preferences: Preferences,
|
||||
start: tuple[float, float],
|
||||
) -> list[Landmark]:
|
||||
"""
|
||||
Function that returns all available landmarks given some preferences and a start position.
|
||||
|
||||
Args:
|
||||
preferences : the preferences specified by the user as the post body
|
||||
start : the coordinates of the starting point
|
||||
Returns:
|
||||
list[Landmark] : The full list of fetched landmarks
|
||||
"""
|
||||
if preferences is None:
|
||||
raise HTTPException(status_code=406, detail="Preferences not provided or incomplete.")
|
||||
if (preferences.shopping.score == 0 and
|
||||
preferences.sightseeing.score == 0 and
|
||||
preferences.nature.score == 0) :
|
||||
raise HTTPException(status_code=406, detail="All preferences are 0.")
|
||||
if start is None:
|
||||
raise HTTPException(status_code=406, detail="Start coordinates not provided")
|
||||
if not (-90 <= start[0] <= 90 or -180 <= start[1] <= 180):
|
||||
raise HTTPException(status_code=422, detail="Start coordinates not in range")
|
||||
|
||||
logger.info(f"Requested new trip generation. Details:\n\tCoordinates: {start}\n\tTime: {preferences.max_time_minute}\n\tSightseeing: {preferences.sightseeing.score}\n\tNature: {preferences.nature.score}\n\tShopping: {preferences.shopping.score}")
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
# Generate the landmarks from the start location
|
||||
landmarks = manager.generate_landmarks_list(
|
||||
center_coordinates = start,
|
||||
preferences = preferences
|
||||
)
|
||||
|
||||
if len(landmarks) == 0 :
|
||||
raise HTTPException(status_code=500, detail="No landmarks were found.")
|
||||
|
||||
t_generate_landmarks = time.time() - start_time
|
||||
logger.info(f'Fetched {len(landmarks)} landmarks in \t: {round(t_generate_landmarks,3)} seconds')
|
||||
|
||||
return landmarks
|
||||
|
||||
|
||||
@router.post("/get-nearby/landmarks/{lat}/{lon}")
|
||||
def get_landmarks_nearby(
|
||||
lat: float,
|
||||
lon: float
|
||||
) -> list[Landmark] :
|
||||
"""
|
||||
Suggests nearby landmarks based on a given latitude and longitude.
|
||||
|
||||
This endpoint returns a curated list of up to 5 landmarks around the given geographical coordinates. It uses fixed preferences for
|
||||
sightseeing, shopping, and nature, with a maximum time constraint of 30 minutes to limit the number of landmarks returned.
|
||||
|
||||
Args:
|
||||
lat (float): Latitude of the user's current location.
|
||||
lon (float): Longitude of the user's current location.
|
||||
|
||||
Returns:
|
||||
list[Landmark]: A list of selected nearby landmarks.
|
||||
"""
|
||||
logger.info(f'Fetching landmarks nearby ({lat}, {lon}).')
|
||||
|
||||
# Define fixed preferences:
|
||||
prefs = Preferences(
|
||||
sightseeing = Preference(
|
||||
type='sightseeing',
|
||||
score=5
|
||||
),
|
||||
shopping = Preference(
|
||||
type='shopping',
|
||||
score=2
|
||||
),
|
||||
nature = Preference(
|
||||
type='nature',
|
||||
score=5
|
||||
),
|
||||
max_time_minute=30,
|
||||
detour_tolerance_minute=0,
|
||||
)
|
||||
|
||||
# Find the landmarks around the location
|
||||
landmarks_around = manager.generate_landmarks_list(
|
||||
center_coordinates = (lat, lon),
|
||||
preferences = prefs,
|
||||
allow_clusters=False,
|
||||
)
|
||||
|
||||
if len(landmarks_around) == 0 :
|
||||
raise HTTPException(status_code=500, detail="No landmarks were found.")
|
||||
|
||||
# select 8 - 12 landmarks from there
|
||||
if len(landmarks_around) > 8 :
|
||||
n_imp = random.randint(2,5)
|
||||
rest = random.randint(8 - n_imp, min(12, len(landmarks_around))-n_imp)
|
||||
|
||||
print(f'len = {len(landmarks_around)}\nn_imp = {n_imp}\nrest = {rest}')
|
||||
landmarks_around = landmarks_around[:n_imp] + random.sample(landmarks_around[n_imp:], rest)
|
||||
|
||||
logger.info(f'Found {len(landmarks_around)} landmarks to suggest nearby ({lat}, {lon}).')
|
||||
# logger.debug('Suggested landmarks :\n\t' + '\n\t'.join(f'{landmark}' for landmark in landmarks_around))
|
||||
return landmarks_around
|
||||
@@ -33,14 +33,14 @@ def configure_logging():
|
||||
# silence the chatty logs loki generates itself
|
||||
logging.getLogger('urllib3.connectionpool').setLevel(logging.WARNING)
|
||||
# no need for time since it's added by loki or can be shown in kube logs
|
||||
logging_format = '%(name)s - %(levelname)s - %(message)s'
|
||||
logging_format = '%(name)-55s - %(levelname)-7s - %(message)s'
|
||||
|
||||
else:
|
||||
# if we are in a debug (local) session, set verbose and rich logging
|
||||
from rich.logging import RichHandler
|
||||
logging_handlers = [RichHandler()]
|
||||
logging_level = logging.DEBUG if is_debug else logging.INFO
|
||||
logging_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
logging_format = '%(asctime)s - %(name)-55s - %(levelname)-7s - %(message)s'
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Main app for backend api"""
|
||||
import logging
|
||||
import yaml
|
||||
import time
|
||||
from contextlib import asynccontextmanager
|
||||
from fastapi import FastAPI, HTTPException, BackgroundTasks
|
||||
@@ -9,12 +10,15 @@ from .structs.landmark import Landmark
|
||||
from .structs.preferences import Preferences
|
||||
from .structs.linked_landmarks import LinkedLandmarks
|
||||
from .structs.trip import Trip
|
||||
from .overpass.overpass import fill_cache
|
||||
from .landmarks.landmarks_manager import LandmarkManager
|
||||
from .toilets.toilet_routes import router as toilets_router
|
||||
from .toilets.toilets_router import router as toilets_router
|
||||
from .optimization.optimization_router import router as optimization_router
|
||||
from .landmarks.landmarks_router import router as landmarks_router, get_landmarks_nearby
|
||||
from .optimization.optimizer import Optimizer
|
||||
from .optimization.refiner import Refiner
|
||||
from .overpass.overpass import fill_cache
|
||||
from .cache import client as cache_client
|
||||
from .constants import OPTIMIZER_PARAMETERS_PATH
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -37,9 +41,24 @@ app = FastAPI(lifespan=lifespan)
|
||||
|
||||
|
||||
|
||||
# Fetches the global list of landmarks given preferences and start/end coordinates. Two routes
|
||||
# Call with "/get/landmarks/" for main entry point of the trip generation pipeline.
|
||||
# Call with "/get-nearby/landmarks/" for the NEARBY feature.
|
||||
app.include_router(landmarks_router)
|
||||
|
||||
|
||||
# Optimizes the trip given preferences. Second step in the main trip generation pipeline
|
||||
# Call with "/optimize/trip"
|
||||
app.include_router(optimization_router)
|
||||
|
||||
|
||||
# Fetches toilets near given coordinates.
|
||||
# Call with "/get/toilets" for fetching toilets around coordinates
|
||||
app.include_router(toilets_router)
|
||||
|
||||
|
||||
|
||||
###### TO REMOVE ONCE THE FRONTEND IS UP TO DATE ######
|
||||
@app.post("/trip/new")
|
||||
def new_trip(preferences: Preferences,
|
||||
start: tuple[float, float],
|
||||
@@ -65,51 +84,61 @@ def new_trip(preferences: Preferences,
|
||||
raise HTTPException(status_code=406, detail="Start coordinates not provided")
|
||||
if not (-90 <= start[0] <= 90 or -180 <= start[1] <= 180):
|
||||
raise HTTPException(status_code=422, detail="Start coordinates not in range")
|
||||
if end is None:
|
||||
end = start
|
||||
logger.info("No end coordinates provided. Using start=end.")
|
||||
|
||||
logger.info(f"Requested new trip generation. Details:\n\tCoordinates: {start}\n\tTime: {preferences.max_time_minute}\n\tSightseeing: {preferences.sightseeing.score}\n\tNature: {preferences.nature.score}\n\tShopping: {preferences.shopping.score}")
|
||||
|
||||
start_landmark = Landmark(name='start',
|
||||
type='start',
|
||||
location=(start[0], start[1]),
|
||||
osm_type='start',
|
||||
osm_id=0,
|
||||
attractiveness=0,
|
||||
duration=0,
|
||||
must_do=True,
|
||||
n_tags = 0)
|
||||
|
||||
end_landmark = Landmark(name='finish',
|
||||
type='finish',
|
||||
location=(end[0], end[1]),
|
||||
osm_type='end',
|
||||
osm_id=0,
|
||||
attractiveness=0,
|
||||
duration=0,
|
||||
must_do=True,
|
||||
n_tags=0)
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
# Generate the landmarks from the start location
|
||||
landmarks, landmarks_short = manager.generate_landmarks_list(
|
||||
landmarks = manager.generate_landmarks_list(
|
||||
center_coordinates = start,
|
||||
preferences = preferences
|
||||
)
|
||||
|
||||
if len(landmarks) == 0 :
|
||||
raise HTTPException(status_code=500, detail="No landmarks were found.")
|
||||
|
||||
# insert start and finish to the landmarks list
|
||||
landmarks_short.insert(0, start_landmark)
|
||||
landmarks_short.append(end_landmark)
|
||||
|
||||
t_generate_landmarks = time.time() - start_time
|
||||
logger.info(f'Fetched {len(landmarks)} landmarks in \t: {round(t_generate_landmarks,3)} seconds')
|
||||
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
logger.info(f"Requested new trip generation. Details:\n\tCoordinates: {start}\n\tTime: {preferences.max_time_minute}\n\tSightseeing: {preferences.sightseeing.score}\n\tNature: {preferences.nature.score}\n\tShopping: {preferences.shopping.score}")
|
||||
|
||||
start_landmark = Landmark(
|
||||
name='start',
|
||||
type='start',
|
||||
location=(start[0], start[1]),
|
||||
osm_type='start',
|
||||
osm_id=0,
|
||||
attractiveness=0,
|
||||
duration=0,
|
||||
must_do=True,
|
||||
n_tags = 0
|
||||
)
|
||||
|
||||
end_landmark = Landmark(
|
||||
name='finish',
|
||||
type='finish',
|
||||
location=(end[0], end[1]),
|
||||
osm_type='end',
|
||||
osm_id=0,
|
||||
attractiveness=0,
|
||||
duration=0,
|
||||
must_do=True,
|
||||
n_tags=0
|
||||
)
|
||||
|
||||
# From the parameters load the length at which to truncate the landmarks list.
|
||||
with OPTIMIZER_PARAMETERS_PATH.open('r') as f:
|
||||
parameters = yaml.safe_load(f)
|
||||
n_important = parameters['N_important']
|
||||
|
||||
# Truncate to the most important landmarks for a shorter list
|
||||
landmarks_short = landmarks[:n_important]
|
||||
|
||||
# insert start and finish to the shorter landmarks list
|
||||
landmarks_short.insert(0, start_landmark)
|
||||
landmarks_short.append(end_landmark)
|
||||
|
||||
# First stage optimization
|
||||
try:
|
||||
base_tour = optimizer.solve_optimization(preferences.max_time_minute, landmarks_short)
|
||||
@@ -121,11 +150,12 @@ def new_trip(preferences: Preferences,
|
||||
start_time = time.time()
|
||||
|
||||
# Second stage optimization
|
||||
# TODO : only if necessary (not enough landmarks for ex.)
|
||||
try :
|
||||
refined_tour = refiner.refine_optimization(landmarks, base_tour,
|
||||
preferences.max_time_minute,
|
||||
preferences.detour_tolerance_minute)
|
||||
refined_tour = refiner.refine_optimization(
|
||||
landmarks, base_tour,
|
||||
preferences.max_time_minute,
|
||||
preferences.detour_tolerance_minute
|
||||
)
|
||||
except Exception as exc :
|
||||
logger.warning(f"Refiner failed. Proceeding with base trip {str(exc)}")
|
||||
refined_tour = base_tour
|
||||
@@ -139,14 +169,16 @@ def new_trip(preferences: Preferences,
|
||||
|
||||
# upon creation of the trip, persistence of both the trip and its landmarks is ensured.
|
||||
trip = Trip.from_linked_landmarks(linked_tour, cache_client)
|
||||
logger.info(f'Generated a trip of {trip.total_time} minutes with {len(refined_tour)} landmarks in {round(t_generate_landmarks + t_first_stage + t_second_stage,3)} seconds.')
|
||||
logger.debug('Detailed trip :\n\t' + '\n\t'.join(f'{landmark}' for landmark in refined_tour))
|
||||
logger.info(f'Optimized a trip of {trip.total_time} minutes with {len(refined_tour)} landmarks in {round(t_first_stage + t_second_stage,3)} seconds.')
|
||||
logger.info('Detailed trip :\n\t' + '\n\t'.join(f'{landmark}' for landmark in refined_tour))
|
||||
|
||||
background_tasks.add_task(fill_cache)
|
||||
|
||||
return trip
|
||||
|
||||
|
||||
|
||||
|
||||
#### For already existing trips/landmarks
|
||||
@app.get("/trip/{trip_uuid}")
|
||||
def get_trip(trip_uuid: str) -> Trip:
|
||||
@@ -224,3 +256,4 @@ def update_trip_time(trip_uuid: str, removed_landmark_uuid: str) -> Trip:
|
||||
trip = Trip.from_linked_landmarks(linked_tour, cache_client)
|
||||
|
||||
return trip
|
||||
|
||||
|
||||
141
backend/src/optimization/optimization_router.py
Normal file
141
backend/src/optimization/optimization_router.py
Normal file
@@ -0,0 +1,141 @@
|
||||
"""API entry point for the trip optimization."""
|
||||
|
||||
import logging
|
||||
import time
|
||||
import yaml
|
||||
from fastapi import HTTPException, APIRouter, BackgroundTasks
|
||||
|
||||
from .optimizer import Optimizer
|
||||
from .refiner import Refiner
|
||||
from ..structs.landmark import Landmark
|
||||
from ..structs.preferences import Preferences
|
||||
from ..structs.linked_landmarks import LinkedLandmarks
|
||||
from ..structs.trip import Trip
|
||||
from ..overpass.overpass import fill_cache
|
||||
from ..cache import client as cache_client
|
||||
from ..constants import OPTIMIZER_PARAMETERS_PATH
|
||||
|
||||
|
||||
# Setup the Logger, Optimizer and Refiner
|
||||
logger = logging.getLogger(__name__)
|
||||
optimizer = Optimizer()
|
||||
refiner = Refiner(optimizer=optimizer)
|
||||
|
||||
|
||||
# Initialize the API router
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/optimize/trip")
|
||||
def optimize_trip(
|
||||
preferences: Preferences,
|
||||
landmarks: list[Landmark],
|
||||
start: tuple[float, float],
|
||||
end: tuple[float, float] | None = None,
|
||||
background_tasks: BackgroundTasks = None
|
||||
) -> Trip:
|
||||
"""
|
||||
Main function to call the optimizer.
|
||||
|
||||
Args:
|
||||
preferences (Preferences) : the preferences specified by the user as the post body.
|
||||
start (tuple[float, float]) : the coordinates of the starting point.
|
||||
end tuple[float, float] : the coordinates of the finishing point.
|
||||
backgroud_tasks (BackgroundTasks) : necessary to fill the cache after the trip has been returned.
|
||||
Returns:
|
||||
(uuid) : The uuid of the first landmark in the optimized route
|
||||
"""
|
||||
if preferences is None:
|
||||
raise HTTPException(status_code=406, detail="Preferences not provided or incomplete.")
|
||||
if len(landmarks) == 0 :
|
||||
raise HTTPException(status_code=406, detail="No landmarks provided for computing the trip.")
|
||||
if (preferences.shopping.score == 0 and
|
||||
preferences.sightseeing.score == 0 and
|
||||
preferences.nature.score == 0) :
|
||||
raise HTTPException(status_code=406, detail="All preferences are 0.")
|
||||
if start is None:
|
||||
raise HTTPException(status_code=406, detail="Start coordinates not provided")
|
||||
if not (-90 <= start[0] <= 90 or -180 <= start[1] <= 180):
|
||||
raise HTTPException(status_code=422, detail="Start coordinates not in range")
|
||||
if end is None:
|
||||
end = start
|
||||
logger.info("No end coordinates provided. Using start=end.")
|
||||
|
||||
# Start the timer
|
||||
start_time = time.time()
|
||||
|
||||
logger.info(f"Requested new trip generation. Details:\n\tCoordinates: {start}\n\tTime: {preferences.max_time_minute}\n\tSightseeing: {preferences.sightseeing.score}\n\tNature: {preferences.nature.score}\n\tShopping: {preferences.shopping.score}")
|
||||
|
||||
start_landmark = Landmark(
|
||||
name='start',
|
||||
type='start',
|
||||
location=(start[0], start[1]),
|
||||
osm_type='start',
|
||||
osm_id=0,
|
||||
attractiveness=0,
|
||||
duration=0,
|
||||
must_do=True,
|
||||
n_tags = 0
|
||||
)
|
||||
|
||||
end_landmark = Landmark(
|
||||
name='finish',
|
||||
type='finish',
|
||||
location=(end[0], end[1]),
|
||||
osm_type='end',
|
||||
osm_id=0,
|
||||
attractiveness=0,
|
||||
duration=0,
|
||||
must_do=True,
|
||||
n_tags=0
|
||||
)
|
||||
|
||||
# From the parameters load the length at which to truncate the landmarks list.
|
||||
with OPTIMIZER_PARAMETERS_PATH.open('r') as f:
|
||||
parameters = yaml.safe_load(f)
|
||||
n_important = parameters['N_important']
|
||||
|
||||
# Truncate to the most important landmarks for a shorter list
|
||||
landmarks_short = landmarks[:n_important]
|
||||
|
||||
# insert start and finish to the shorter landmarks list
|
||||
landmarks_short.insert(0, start_landmark)
|
||||
landmarks_short.append(end_landmark)
|
||||
|
||||
# First stage optimization
|
||||
try:
|
||||
base_tour = optimizer.solve_optimization(preferences.max_time_minute, landmarks_short)
|
||||
except Exception as exc:
|
||||
logger.error(f"Trip generation failed: {str(exc)}")
|
||||
raise HTTPException(status_code=500, detail=f"Optimization failed: {str(exc)}") from exc
|
||||
|
||||
t_first_stage = time.time() - start_time
|
||||
start_time = time.time()
|
||||
|
||||
# Second stage optimization
|
||||
try :
|
||||
refined_tour = refiner.refine_optimization(
|
||||
landmarks, base_tour,
|
||||
preferences.max_time_minute,
|
||||
preferences.detour_tolerance_minute
|
||||
)
|
||||
except Exception as exc :
|
||||
logger.warning(f"Refiner failed. Proceeding with base trip {str(exc)}")
|
||||
refined_tour = base_tour
|
||||
|
||||
t_second_stage = time.time() - start_time
|
||||
|
||||
logger.debug(f'First stage optimization\t: {round(t_first_stage,3)} seconds')
|
||||
logger.debug(f'Second stage optimization\t: {round(t_second_stage,3)} seconds')
|
||||
logger.info(f'Total computation time\t: {round(t_first_stage + t_second_stage,3)} seconds')
|
||||
linked_tour = LinkedLandmarks(refined_tour)
|
||||
|
||||
# upon creation of the trip, persistence of both the trip and its landmarks is ensured.
|
||||
trip = Trip.from_linked_landmarks(linked_tour, cache_client)
|
||||
logger.info(f'Optimized a trip of {trip.total_time} minutes with {len(refined_tour)} landmarks in {round(t_first_stage + t_second_stage,3)} seconds.')
|
||||
logger.info('Detailed trip :\n\t' + '\n\t'.join(f'{landmark}' for landmark in refined_tour))
|
||||
|
||||
background_tasks.add_task(fill_cache)
|
||||
|
||||
return trip
|
||||
|
||||
@@ -6,7 +6,6 @@ from shapely import buffer, LineString, Point, Polygon, MultiPoint, concave_hull
|
||||
|
||||
from ..structs.landmark import Landmark
|
||||
from ..utils.get_time_distance import get_time
|
||||
from ..utils.take_most_important import take_most_important
|
||||
from .optimizer import Optimizer
|
||||
from ..constants import OPTIMIZER_PARAMETERS_PATH
|
||||
|
||||
@@ -238,7 +237,7 @@ class Refiner :
|
||||
if self.is_in_area(area, landmark.location) and landmark.name not in visited_names:
|
||||
second_order_landmarks.append(landmark)
|
||||
|
||||
return take_most_important(second_order_landmarks, int(self.max_landmarks_refiner*0.75))
|
||||
return sorted(second_order_landmarks, key=lambda x: x.attractiveness, reverse=True)[:int(self.max_landmarks_refiner*0.75)]
|
||||
|
||||
|
||||
# Try fix the shortest path using shapely
|
||||
|
||||
@@ -402,6 +402,8 @@ def fill_cache():
|
||||
n_files = 0
|
||||
total = 0
|
||||
|
||||
overpass.logger.info('Trip successfully returned, starting to fill cache.')
|
||||
|
||||
with os.scandir(OSM_CACHE_DIR) as it:
|
||||
for entry in it:
|
||||
if entry.is_file() and entry.name.startswith('hollow_'):
|
||||
|
||||
@@ -7,5 +7,4 @@ tag_exponent: 1.15
|
||||
image_bonus: 1.1
|
||||
viewpoint_bonus: 10
|
||||
wikipedia_bonus: 1.25
|
||||
N_important: 60
|
||||
pay_bonus: -1
|
||||
|
||||
@@ -6,4 +6,5 @@ max_landmarks_refiner: 20
|
||||
overshoot: 0.0016
|
||||
time_limit: 1
|
||||
gap_rel: 0.025
|
||||
max_iter: 80
|
||||
max_iter: 80
|
||||
N_important: 60
|
||||
|
||||
@@ -49,8 +49,8 @@ class Landmark(BaseModel) :
|
||||
image_url : Optional[str] = None
|
||||
website_url : Optional[str] = None
|
||||
wiki_url : Optional[str] = None
|
||||
keywords: Optional[dict] = {}
|
||||
description : Optional[str] = None
|
||||
# keywords: Optional[dict] = {}
|
||||
# description : Optional[str] = None
|
||||
duration : Optional[int] = 5
|
||||
name_en : Optional[str] = None
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from .landmark import Landmark
|
||||
from ..utils.get_time_distance import get_time
|
||||
from ..utils.description import description_and_keywords
|
||||
|
||||
class LinkedLandmarks:
|
||||
"""
|
||||
@@ -35,18 +36,23 @@ class LinkedLandmarks:
|
||||
Create the links between the landmarks in the list by setting their
|
||||
.next_uuid and the .time_to_next attributes.
|
||||
"""
|
||||
|
||||
# Mark secondary landmarks as such
|
||||
self.update_secondary_landmarks()
|
||||
|
||||
|
||||
for i, landmark in enumerate(self._landmarks[:-1]):
|
||||
# Set uuid of the next landmark
|
||||
landmark.next_uuid = self._landmarks[i + 1].uuid
|
||||
|
||||
# Adjust time to reach and total time
|
||||
time_to_next = get_time(landmark.location, self._landmarks[i + 1].location)
|
||||
landmark.time_to_reach_next = time_to_next
|
||||
self.total_time += time_to_next
|
||||
self.total_time += landmark.duration
|
||||
|
||||
# Fill in the keywords and description. GOOD IDEA, BAD EXECUTION, tags aren't available anymore at this stage
|
||||
# landmark.description, landmark.keywords = description_and_keywords(tags)
|
||||
|
||||
|
||||
self._landmarks[-1].next_uuid = None
|
||||
self._landmarks[-1].time_to_reach_next = 0
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Defines the Preferences used as input for trip generation."""
|
||||
|
||||
from typing import Optional, Literal
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, field_validator
|
||||
|
||||
|
||||
class Preference(BaseModel) :
|
||||
@@ -15,6 +15,13 @@ class Preference(BaseModel) :
|
||||
type: Literal['sightseeing', 'nature', 'shopping', 'start', 'finish']
|
||||
score: int # score could be from 1 to 5
|
||||
|
||||
@field_validator("type")
|
||||
@classmethod
|
||||
def validate_type(cls, v):
|
||||
if v not in {'sightseeing', 'nature', 'shopping', 'start', 'finish'}:
|
||||
raise ValueError(f"Invalid type: {v}")
|
||||
return v
|
||||
|
||||
|
||||
# Input for optimization
|
||||
class Preferences(BaseModel) :
|
||||
|
||||
@@ -19,30 +19,50 @@ def invalid_client():
|
||||
([48.8566, 2.3522], {}, 422),
|
||||
|
||||
# Invalid cases: incomplete preferences.
|
||||
([48.084588, 7.280405], {"sightseeing": {"type": "nature", "score": 5}, # no shopping
|
||||
([48.084588, 7.280405], {"sightseeing": {"type": "sightseeing", "score": 5}, # no shopping pref
|
||||
"nature": {"type": "nature", "score": 5},
|
||||
}, 422),
|
||||
([48.084588, 7.280405], {"sightseeing": {"type": "nature", "score": 5}, # no nature
|
||||
([48.084588, 7.280405], {"sightseeing": {"type": "sightseeing", "score": 5}, # no nature pref
|
||||
"shopping": {"type": "shopping", "score": 5},
|
||||
}, 422),
|
||||
([48.084588, 7.280405], {"nature": {"type": "nature", "score": 5}, # no sightseeing
|
||||
([48.084588, 7.280405], {"nature": {"type": "nature", "score": 5}, # no sightseeing pref
|
||||
"shopping": {"type": "shopping", "score": 5},
|
||||
}, 422),
|
||||
([48.084588, 7.280405], {"sightseeing": {"type": "nature", "score": 1}, # mixed up preferences types. TODO: i suggest reducing the complexity by remove the Preference object.
|
||||
"nature": {"type": "shopping", "score": 1},
|
||||
"shopping": {"type": "shopping", "score": 1},
|
||||
}, 422),
|
||||
([48.084588, 7.280405], {"doesnotexist": {"type": "sightseeing", "score": 2}, # non-existing preferences types
|
||||
"nature": {"type": "nature", "score": 2},
|
||||
"shopping": {"type": "shopping", "score": 2},
|
||||
}, 422),
|
||||
([48.084588, 7.280405], {"sightseeing": {"type": "sightseeing", "score": 3}, # non-existing preferences types
|
||||
"nature": {"type": "doesntexisteither", "score": 3},
|
||||
"shopping": {"type": "shopping", "score": 3},
|
||||
}, 422),
|
||||
([48.084588, 7.280405], {"sightseeing": {"type": "sightseeing", "score": -1}, # negative preference value
|
||||
"nature": {"type": "doesntexisteither", "score": 4},
|
||||
"shopping": {"type": "shopping", "score": 4},
|
||||
}, 422),
|
||||
([48.084588, 7.280405], {"sightseeing": {"type": "sightseeing", "score": 10}, # too high preference value
|
||||
"nature": {"type": "doesntexisteither", "score": 4},
|
||||
"shopping": {"type": "shopping", "score": 4},
|
||||
}, 422),
|
||||
|
||||
# Invalid cases: unexisting coords
|
||||
([91, 181], {"sightseeing": {"type": "nature", "score": 5},
|
||||
([91, 181], {"sightseeing": {"type": "sightseeing", "score": 5},
|
||||
"nature": {"type": "nature", "score": 5},
|
||||
"shopping": {"type": "shopping", "score": 5},
|
||||
}, 422),
|
||||
([-91, 181], {"sightseeing": {"type": "nature", "score": 5},
|
||||
([-91, 181], {"sightseeing": {"type": "sightseeing", "score": 5},
|
||||
"nature": {"type": "nature", "score": 5},
|
||||
"shopping": {"type": "shopping", "score": 5},
|
||||
}, 422),
|
||||
([91, -181], {"sightseeing": {"type": "nature", "score": 5},
|
||||
([91, -181], {"sightseeing": {"type": "sightseeing", "score": 5},
|
||||
"nature": {"type": "nature", "score": 5},
|
||||
"shopping": {"type": "shopping", "score": 5},
|
||||
}, 422),
|
||||
([-91, -181], {"sightseeing": {"type": "nature", "score": 5},
|
||||
([-91, -181], {"sightseeing": {"type": "sightseeing", "score": 5},
|
||||
"nature": {"type": "nature", "score": 5},
|
||||
"shopping": {"type": "shopping", "score": 5},
|
||||
}, 422),
|
||||
@@ -53,8 +73,8 @@ def test_input(invalid_client, start, preferences, status_code): # pylint: dis
|
||||
Test new trip creation with different sets of preferences and locations.
|
||||
"""
|
||||
response = invalid_client.post(
|
||||
"/trip/new",
|
||||
json={
|
||||
"/get/landmarks",
|
||||
json ={
|
||||
"preferences": preferences,
|
||||
"start": start
|
||||
}
|
||||
|
||||
@@ -1,343 +0,0 @@
|
||||
"""Collection of tests to ensure correct implementation and track progress. """
|
||||
import time
|
||||
from fastapi.testclient import TestClient
|
||||
import pytest
|
||||
|
||||
from .test_utils import load_trip_landmarks, log_trip_details
|
||||
from ..main import app
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def client():
|
||||
"""Client used to call the app."""
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
def test_turckheim(client, request): # pylint: disable=redefined-outer-name
|
||||
"""
|
||||
Test n°1 : Custom test in Turckheim to ensure small villages are also supported.
|
||||
|
||||
Args:
|
||||
client:
|
||||
request:
|
||||
"""
|
||||
start_time = time.time() # Start timer
|
||||
duration_minutes = 20
|
||||
|
||||
response = client.post(
|
||||
"/trip/new",
|
||||
json={
|
||||
"preferences": {"sightseeing": {"type": "sightseeing", "score": 5},
|
||||
"nature": {"type": "nature", "score": 0},
|
||||
"shopping": {"type": "shopping", "score": 0},
|
||||
"max_time_minute": duration_minutes,
|
||||
"detour_tolerance_minute": 0},
|
||||
"start": [48.084588, 7.280405]
|
||||
# "start": [45.74445023349939, 4.8222687890538865]
|
||||
# "start": [45.75156398104873, 4.827154464827647]
|
||||
}
|
||||
)
|
||||
result = response.json()
|
||||
landmarks = load_trip_landmarks(client, result['first_landmark_uuid'])
|
||||
|
||||
|
||||
# Get computation time
|
||||
comp_time = time.time() - start_time
|
||||
|
||||
# Add details to report
|
||||
log_trip_details(request, landmarks, result['total_time'], duration_minutes)
|
||||
|
||||
|
||||
# checks :
|
||||
assert response.status_code == 200 # check for successful planning
|
||||
assert isinstance(landmarks, list) # check that the return type is a list
|
||||
assert len(landmarks) > 2 # check that there is something to visit
|
||||
assert comp_time < 30, f"Computation time exceeded 30 seconds: {comp_time:.2f} seconds"
|
||||
assert duration_minutes*0.8 < result['total_time'], f"Trip too short: {result['total_time']} instead of {duration_minutes}"
|
||||
assert duration_minutes*1.2 > result['total_time'], f"Trip too long: {result['total_time']} instead of {duration_minutes}"
|
||||
# assert 2!= 3
|
||||
|
||||
def test_bellecour(client, request) : # pylint: disable=redefined-outer-name
|
||||
"""
|
||||
Test n°2 : Custom test in Lyon centre to ensure proper decision making in crowded area.
|
||||
|
||||
Args:
|
||||
client:
|
||||
request:
|
||||
"""
|
||||
start_time = time.time() # Start timer
|
||||
duration_minutes = 120
|
||||
|
||||
|
||||
response = client.post(
|
||||
"/trip/new",
|
||||
json={
|
||||
"preferences": {"sightseeing": {"type": "sightseeing", "score": 5},
|
||||
"nature": {"type": "nature", "score": 5},
|
||||
"shopping": {"type": "shopping", "score": 5},
|
||||
"max_time_minute": duration_minutes,
|
||||
"detour_tolerance_minute": 0},
|
||||
"start": [45.7576485, 4.8330241]
|
||||
}
|
||||
)
|
||||
result = response.json()
|
||||
landmarks = load_trip_landmarks(client, result['first_landmark_uuid'])
|
||||
|
||||
# Get computation time
|
||||
comp_time = time.time() - start_time
|
||||
|
||||
# Add details to report
|
||||
log_trip_details(request, landmarks, result['total_time'], duration_minutes)
|
||||
|
||||
# for elem in landmarks :
|
||||
# print(elem)
|
||||
|
||||
# checks :
|
||||
assert response.status_code == 200 # check for successful planning
|
||||
assert comp_time < 30, f"Computation time exceeded 30 seconds: {comp_time:.2f} seconds"
|
||||
assert duration_minutes*0.8 < result['total_time'], f"Trip too short: {result['total_time']} instead of {duration_minutes}"
|
||||
assert duration_minutes*1.2 > result['total_time'], f"Trip too long: {result['total_time']} instead of {duration_minutes}"
|
||||
|
||||
def test_cologne(client, request) : # pylint: disable=redefined-outer-name
|
||||
"""
|
||||
Test n°3 : Custom test in Cologne to ensure proper decision making in crowded area.
|
||||
|
||||
Args:
|
||||
client:
|
||||
request:
|
||||
"""
|
||||
start_time = time.time() # Start timer
|
||||
duration_minutes = 240
|
||||
|
||||
response = client.post(
|
||||
"/trip/new",
|
||||
json={
|
||||
"preferences": {"sightseeing": {"type": "sightseeing", "score": 5},
|
||||
"nature": {"type": "nature", "score": 5},
|
||||
"shopping": {"type": "shopping", "score": 5},
|
||||
"max_time_minute": duration_minutes,
|
||||
"detour_tolerance_minute": 0},
|
||||
"start": [50.942352665, 6.957777972392]
|
||||
}
|
||||
)
|
||||
result = response.json()
|
||||
landmarks = load_trip_landmarks(client, result['first_landmark_uuid'])
|
||||
|
||||
# Get computation time
|
||||
comp_time = time.time() - start_time
|
||||
|
||||
# Add details to report
|
||||
log_trip_details(request, landmarks, result['total_time'], duration_minutes)
|
||||
|
||||
# for elem in landmarks :
|
||||
# print(elem)
|
||||
|
||||
# checks :
|
||||
assert response.status_code == 200 # check for successful planning
|
||||
assert comp_time < 30, f"Computation time exceeded 30 seconds: {comp_time:.2f} seconds"
|
||||
assert duration_minutes*0.8 < result['total_time'], f"Trip too short: {result['total_time']} instead of {duration_minutes}"
|
||||
assert duration_minutes*1.2 > result['total_time'], f"Trip too long: {result['total_time']} instead of {duration_minutes}"
|
||||
|
||||
|
||||
def test_strasbourg(client, request) : # pylint: disable=redefined-outer-name
|
||||
"""
|
||||
Test n°4 : Custom test in Strasbourg to ensure proper decision making in crowded area.
|
||||
|
||||
Args:
|
||||
client:
|
||||
request:
|
||||
"""
|
||||
start_time = time.time() # Start timer
|
||||
duration_minutes = 180
|
||||
|
||||
response = client.post(
|
||||
"/trip/new",
|
||||
json={
|
||||
"preferences": {"sightseeing": {"type": "sightseeing", "score": 5},
|
||||
"nature": {"type": "nature", "score": 5},
|
||||
"shopping": {"type": "shopping", "score": 5},
|
||||
"max_time_minute": duration_minutes,
|
||||
"detour_tolerance_minute": 0},
|
||||
"start": [48.5846589226, 7.74078715721]
|
||||
}
|
||||
)
|
||||
result = response.json()
|
||||
landmarks = load_trip_landmarks(client, result['first_landmark_uuid'])
|
||||
|
||||
# Get computation time
|
||||
comp_time = time.time() - start_time
|
||||
|
||||
# Add details to report
|
||||
log_trip_details(request, landmarks, result['total_time'], duration_minutes)
|
||||
|
||||
# for elem in landmarks :
|
||||
# print(elem)
|
||||
|
||||
# checks :
|
||||
assert response.status_code == 200 # check for successful planning
|
||||
assert comp_time < 30, f"Computation time exceeded 30 seconds: {comp_time:.2f} seconds"
|
||||
assert duration_minutes*0.8 < result['total_time'], f"Trip too short: {result['total_time']} instead of {duration_minutes}"
|
||||
assert duration_minutes*1.2 > result['total_time'], f"Trip too long: {result['total_time']} instead of {duration_minutes}"
|
||||
|
||||
|
||||
def test_zurich(client, request) : # pylint: disable=redefined-outer-name
|
||||
"""
|
||||
Test n°5 : Custom test in Zurich to ensure proper decision making in crowded area.
|
||||
|
||||
Args:
|
||||
client:
|
||||
request:
|
||||
"""
|
||||
start_time = time.time() # Start timer
|
||||
duration_minutes = 180
|
||||
|
||||
response = client.post(
|
||||
"/trip/new",
|
||||
json={
|
||||
"preferences": {"sightseeing": {"type": "sightseeing", "score": 5},
|
||||
"nature": {"type": "nature", "score": 5},
|
||||
"shopping": {"type": "shopping", "score": 5},
|
||||
"max_time_minute": duration_minutes,
|
||||
"detour_tolerance_minute": 0},
|
||||
"start": [47.377884227, 8.5395114066]
|
||||
}
|
||||
)
|
||||
result = response.json()
|
||||
landmarks = load_trip_landmarks(client, result['first_landmark_uuid'])
|
||||
|
||||
# Get computation time
|
||||
comp_time = time.time() - start_time
|
||||
|
||||
# Add details to report
|
||||
log_trip_details(request, landmarks, result['total_time'], duration_minutes)
|
||||
|
||||
# for elem in landmarks :
|
||||
# print(elem)
|
||||
|
||||
# checks :
|
||||
assert response.status_code == 200 # check for successful planning
|
||||
assert comp_time < 30, f"Computation time exceeded 30 seconds: {comp_time:.2f} seconds"
|
||||
assert duration_minutes*0.8 < result['total_time'], f"Trip too short: {result['total_time']} instead of {duration_minutes}"
|
||||
assert duration_minutes*1.2 > result['total_time'], f"Trip too long: {result['total_time']} instead of {duration_minutes}"
|
||||
|
||||
|
||||
def test_paris(client, request) : # pylint: disable=redefined-outer-name
|
||||
"""
|
||||
Test n°6 : Custom test in Paris (les Halles) centre to ensure proper decision making in crowded area.
|
||||
|
||||
Args:
|
||||
client:
|
||||
request:
|
||||
"""
|
||||
start_time = time.time() # Start timer
|
||||
duration_minutes = 200
|
||||
|
||||
response = client.post(
|
||||
"/trip/new",
|
||||
json={
|
||||
"preferences": {"sightseeing": {"type": "sightseeing", "score": 5},
|
||||
"nature": {"type": "nature", "score": 0},
|
||||
"shopping": {"type": "shopping", "score": 5},
|
||||
"max_time_minute": duration_minutes,
|
||||
"detour_tolerance_minute": 0},
|
||||
"start": [48.85468881798671, 2.3423925755998374]
|
||||
}
|
||||
)
|
||||
result = response.json()
|
||||
landmarks = load_trip_landmarks(client, result['first_landmark_uuid'])
|
||||
|
||||
# Get computation time
|
||||
comp_time = time.time() - start_time
|
||||
|
||||
# Add details to report
|
||||
log_trip_details(request, landmarks, result['total_time'], duration_minutes)
|
||||
|
||||
# for elem in landmarks :
|
||||
# print(elem)
|
||||
|
||||
# checks :
|
||||
assert response.status_code == 200 # check for successful planning
|
||||
assert comp_time < 30, f"Computation time exceeded 30 seconds: {comp_time:.2f} seconds"
|
||||
assert duration_minutes*0.8 < result['total_time'], f"Trip too short: {result['total_time']} instead of {duration_minutes}"
|
||||
assert duration_minutes*1.2 > result['total_time'], f"Trip too long: {result['total_time']} instead of {duration_minutes}"
|
||||
|
||||
|
||||
def test_new_york(client, request) : # pylint: disable=redefined-outer-name
|
||||
"""
|
||||
Test n°7 : Custom test in New York to ensure proper decision making in crowded area.
|
||||
|
||||
Args:
|
||||
client:
|
||||
request:
|
||||
"""
|
||||
start_time = time.time() # Start timer
|
||||
duration_minutes = 600
|
||||
|
||||
response = client.post(
|
||||
"/trip/new",
|
||||
json={
|
||||
"preferences": {"sightseeing": {"type": "sightseeing", "score": 5},
|
||||
"nature": {"type": "nature", "score": 5},
|
||||
"shopping": {"type": "shopping", "score": 5},
|
||||
"max_time_minute": duration_minutes,
|
||||
"detour_tolerance_minute": 0},
|
||||
"start": [40.72592726802, -73.9920434795]
|
||||
}
|
||||
)
|
||||
result = response.json()
|
||||
landmarks = load_trip_landmarks(client, result['first_landmark_uuid'])
|
||||
|
||||
# Get computation time
|
||||
comp_time = time.time() - start_time
|
||||
|
||||
# Add details to report
|
||||
log_trip_details(request, landmarks, result['total_time'], duration_minutes)
|
||||
|
||||
# for elem in landmarks :
|
||||
# print(elem)
|
||||
|
||||
# checks :
|
||||
assert response.status_code == 200 # check for successful planning
|
||||
assert comp_time < 30, f"Computation time exceeded 30 seconds: {comp_time:.2f} seconds"
|
||||
assert duration_minutes*0.8 < result['total_time'], f"Trip too short: {result['total_time']} instead of {duration_minutes}"
|
||||
assert duration_minutes*1.2 > result['total_time'], f"Trip too long: {result['total_time']} instead of {duration_minutes}"
|
||||
|
||||
|
||||
def test_shopping(client, request) : # pylint: disable=redefined-outer-name
|
||||
"""
|
||||
Test n°8 : Custom test in Lyon centre to ensure shopping clusters are found.
|
||||
|
||||
Args:
|
||||
client:
|
||||
request:
|
||||
"""
|
||||
start_time = time.time() # Start timer
|
||||
duration_minutes = 240
|
||||
|
||||
response = client.post(
|
||||
"/trip/new",
|
||||
json={
|
||||
"preferences": {"sightseeing": {"type": "sightseeing", "score": 0},
|
||||
"nature": {"type": "nature", "score": 0},
|
||||
"shopping": {"type": "shopping", "score": 5},
|
||||
"max_time_minute": duration_minutes,
|
||||
"detour_tolerance_minute": 0},
|
||||
"start": [45.7576485, 4.8330241]
|
||||
}
|
||||
)
|
||||
result = response.json()
|
||||
landmarks = load_trip_landmarks(client, result['first_landmark_uuid'])
|
||||
|
||||
# Get computation time
|
||||
comp_time = time.time() - start_time
|
||||
|
||||
# Add details to report
|
||||
log_trip_details(request, landmarks, result['total_time'], duration_minutes)
|
||||
|
||||
# for elem in landmarks :
|
||||
# print(elem)
|
||||
|
||||
# checks :
|
||||
assert response.status_code == 200 # check for successful planning
|
||||
assert comp_time < 30, f"Computation time exceeded 30 seconds: {comp_time:.2f} seconds"
|
||||
assert duration_minutes*0.8 < result['total_time'], f"Trip too short: {result['total_time']} instead of {duration_minutes}"
|
||||
assert duration_minutes*1.2 > result['total_time'], f"Trip too long: {result['total_time']} instead of {duration_minutes}"
|
||||
46
backend/src/tests/test_nearby.py
Normal file
46
backend/src/tests/test_nearby.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""Collection of tests to ensure correct implementation and track progress of the get_landmarks_nearby feature. """
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
import pytest
|
||||
|
||||
from ..main import app
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def client():
|
||||
"""Client used to call the app."""
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"location,status_code",
|
||||
[
|
||||
([45.7576485, 4.8330241], 200), # Lyon, France
|
||||
([41.4020572, 2.1818985], 200), # Barcelona, Spain
|
||||
([59.3293, 18.0686], 200), # Stockholm, Sweden
|
||||
([43.6532, -79.3832], 200), # Toronto, Canada
|
||||
([38.7223, -9.1393], 200), # Lisbon, Portugal
|
||||
([6.5244, 3.3792], 200), # Lagos, Nigeria
|
||||
([17.3850, 78.4867], 200), # Hyderabad, India
|
||||
([30.0444, 31.2357], 200), # Cairo, Egypt
|
||||
([50.8503, 4.3517], 200), # Brussels, Belgium
|
||||
([35.2271, -80.8431], 200), # Charlotte, USA
|
||||
([10.4806, -66.9036], 200), # Caracas, Venezuela
|
||||
([9.51074, -13.71118], 200), # Conakry, Guinea
|
||||
]
|
||||
)
|
||||
def test_nearby(client, location, status_code): # pylint: disable=redefined-outer-name
|
||||
"""
|
||||
Test n°1 : Verify handling of invalid input.
|
||||
|
||||
Args:
|
||||
client:
|
||||
request:
|
||||
"""
|
||||
response = client.post(f"/get-nearby/landmarks/{location[0]}/{location[1]}")
|
||||
suggestions = response.json()
|
||||
|
||||
# checks :
|
||||
assert response.status_code == status_code # check for successful planning
|
||||
assert isinstance(suggestions, list) # check that the return type is a list
|
||||
assert len(suggestions) > 0
|
||||
@@ -18,7 +18,7 @@ def client():
|
||||
[
|
||||
({}, None, 422), # Invalid case: no location at all.
|
||||
([443], None, 422), # Invalid cases: invalid location.
|
||||
([443, 433], None, 422), # Invalid cases: invalid location.
|
||||
([443, 433], None, 422), # Invalid cases: invalid location.
|
||||
]
|
||||
)
|
||||
def test_invalid_input(client, location, radius, status_code): # pylint: disable=redefined-outer-name
|
||||
@@ -30,7 +30,7 @@ def test_invalid_input(client, location, radius, status_code): # pylint: disa
|
||||
request:
|
||||
"""
|
||||
response = client.post(
|
||||
"/toilets/new",
|
||||
"/get/toilets",
|
||||
params={
|
||||
"location": location,
|
||||
"radius": radius
|
||||
@@ -58,7 +58,7 @@ def test_no_toilets(client, location, status_code): # pylint: disable=redefin
|
||||
request:
|
||||
"""
|
||||
response = client.post(
|
||||
"/toilets/new",
|
||||
"/get/toilets",
|
||||
params={
|
||||
"location": location
|
||||
}
|
||||
@@ -87,7 +87,7 @@ def test_toilets(client, location, status_code): # pylint: disable=redefined-
|
||||
request:
|
||||
"""
|
||||
response = client.post(
|
||||
"/toilets/new",
|
||||
"/get/toilets",
|
||||
params={
|
||||
"location": location,
|
||||
"radius" : 600
|
||||
|
||||
81
backend/src/tests/test_trip_generation.py
Normal file
81
backend/src/tests/test_trip_generation.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""Collection of tests to ensure correct implementation and track progress."""
|
||||
import time
|
||||
from fastapi.testclient import TestClient
|
||||
import pytest
|
||||
|
||||
from .test_utils import load_trip_landmarks, log_trip_details
|
||||
from ..structs.preferences import Preferences, Preference
|
||||
from ..main import app
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def client():
|
||||
"""Client used to call the app."""
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"sightseeing, shopping, nature, max_time_minute, start_coords, end_coords",
|
||||
[
|
||||
# Edge cases
|
||||
(0, 0, 5, 240, [45.7576485, 4.8330241], None), # Lyon, Bellecour - test shopping only
|
||||
|
||||
# Realistic
|
||||
(5, 0, 0, 20, [48.0845881, 7.2804050], None), # Turckheim
|
||||
(5, 5, 5, 120, [45.7576485, 4.8330241], None), # Lyon, Bellecour
|
||||
(5, 2, 5, 240, [50.9423526, 6.9577780], None), # Cologne, centre
|
||||
(3, 5, 0, 180, [48.5846589226, 7.74078715721], None), # Strasbourg, centre
|
||||
(2, 4, 5, 180, [47.377884227, 8.5395114066], None), # Zurich, centre
|
||||
(5, 0, 5, 200, [48.85468881798671, 2.3423925755998374], None), # Paris, centre
|
||||
(5, 5, 5, 600, [40.72592726802, -73.9920434795], None), # New York, Lower Manhattan
|
||||
]
|
||||
)
|
||||
def test_trip(client, request, sightseeing, shopping, nature, max_time_minute, start_coords, end_coords):
|
||||
|
||||
start_time = time.time() # Start timer
|
||||
|
||||
prefs = Preferences(
|
||||
sightseeing=Preference(type='sightseeing', score=sightseeing),
|
||||
shopping=Preference(type='shopping', score=shopping),
|
||||
nature=Preference(type='nature', score=nature),
|
||||
max_time_minute=max_time_minute,
|
||||
detour_tolerance_minute=0,
|
||||
)
|
||||
start = start_coords
|
||||
end = end_coords
|
||||
|
||||
# Step 1: request the list of landmarks in the vicinty of the starting point
|
||||
response = client.post(
|
||||
"/get/landmarks",
|
||||
json={
|
||||
"preferences": prefs.model_dump(),
|
||||
"start": start_coords,
|
||||
"end": end_coords,
|
||||
}
|
||||
)
|
||||
landmarks = response.json()
|
||||
|
||||
# Step 2: Feed the landmarks to the optimizer to compute the trip
|
||||
response = client.post(
|
||||
"/optimize/trip",
|
||||
json={
|
||||
"preferences": prefs.model_dump(),
|
||||
"landmarks": landmarks,
|
||||
"start": start,
|
||||
"end": end,
|
||||
}
|
||||
)
|
||||
result = response.json()
|
||||
landmarks = load_trip_landmarks(client, result['first_landmark_uuid'])
|
||||
|
||||
# Get computation time
|
||||
comp_time = time.time() - start_time
|
||||
|
||||
# Add details to report
|
||||
log_trip_details(request, landmarks, result['total_time'], prefs.max_time_minute)
|
||||
|
||||
# checks :
|
||||
assert response.status_code == 200 # check for successful planning
|
||||
assert comp_time < 30, f"Computation time exceeded 30 seconds: {comp_time:.2f} seconds"
|
||||
assert prefs.max_time_minute*0.8 < result['total_time'], f"Trip too short: {result['total_time']} instead of {prefs.max_time_minute}"
|
||||
assert prefs.max_time_minute*1.2 > result['total_time'], f"Trip too long: {result['total_time']} instead of {prefs.max_time_minute}"
|
||||
@@ -1,9 +1,12 @@
|
||||
"""Helper methods for testing."""
|
||||
import time
|
||||
import logging
|
||||
from functools import wraps
|
||||
from fastapi import HTTPException
|
||||
|
||||
from ..structs.landmark import Landmark
|
||||
from ..cache import client as cache_client
|
||||
from ..structs.landmark import Landmark
|
||||
from ..structs.preferences import Preferences, Preference
|
||||
|
||||
|
||||
def landmarks_to_osmid(landmarks: list[Landmark]) -> list[int] :
|
||||
@@ -91,3 +94,34 @@ def log_trip_details(request, landmarks: list[Landmark], duration: int, target_d
|
||||
request.node.trip_details = trip_string
|
||||
request.node.trip_duration = str(duration) # result['total_time']
|
||||
request.node.target_duration = str(target_duration)
|
||||
|
||||
|
||||
|
||||
|
||||
def trip_params(
|
||||
sightseeing: int,
|
||||
shopping: int,
|
||||
nature: int,
|
||||
max_time_minute: int,
|
||||
start_coords: tuple[float, float] = None,
|
||||
end_coords: tuple[float, float] = None,
|
||||
):
|
||||
def decorator(test_func):
|
||||
@wraps(test_func)
|
||||
def wrapper(client, request):
|
||||
prefs = Preferences(
|
||||
sightseeing=Preference(type='sightseeing', score=sightseeing),
|
||||
shopping=Preference(type='shopping', score=shopping),
|
||||
nature=Preference(type='nature', score=nature),
|
||||
max_time_minute=max_time_minute,
|
||||
detour_tolerance_minute=0,
|
||||
)
|
||||
|
||||
start = start_coords
|
||||
end = end_coords
|
||||
|
||||
# Inject into test function
|
||||
return test_func(client, request, prefs, start, end)
|
||||
|
||||
return wrapper
|
||||
return decorator
|
||||
@@ -70,6 +70,8 @@ class ToiletsManager:
|
||||
|
||||
toilets_list = self.to_toilets(result)
|
||||
|
||||
self.logger.debug(f'Found {len(toilets_list)} toilets around {self.location}')
|
||||
|
||||
return toilets_list
|
||||
|
||||
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
"""Defines the endpoint for fetching toilet locations."""
|
||||
"""API entry point for fetching toilet locations."""
|
||||
|
||||
from fastapi import HTTPException, APIRouter, Query
|
||||
|
||||
from ..structs.toilets import Toilets
|
||||
from .toilets_manager import ToiletsManager
|
||||
from ..structs.toilets import Toilets
|
||||
|
||||
|
||||
# Define the API router
|
||||
# Initialize the API router
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/toilets/new")
|
||||
def get_toilets(location: tuple[float, float] = Query(...), radius: int = 500) -> list[Toilets] :
|
||||
@router.post("/get/toilets")
|
||||
def get_toilets(
|
||||
location: tuple[float, float] = Query(...),
|
||||
radius: int = 500
|
||||
) -> list[Toilets] :
|
||||
"""
|
||||
Endpoint to find toilets within a specified radius from a given location.
|
||||
|
||||
@@ -34,5 +38,6 @@ def get_toilets(location: tuple[float, float] = Query(...), radius: int = 500) -
|
||||
toilets_list = toilets_manager.generate_toilet_list()
|
||||
except KeyError as exc:
|
||||
raise HTTPException(status_code=404, detail="No toilets found") from exc
|
||||
|
||||
|
||||
return toilets_list
|
||||
123
backend/src/utils/description.py
Normal file
123
backend/src/utils/description.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""Add more information about the landmarks by writing a short description and keywords. """
|
||||
|
||||
|
||||
def description_and_keywords(tags: dict):
|
||||
"""
|
||||
Generates a description and a set of keywords for a given landmark based on its tags.
|
||||
|
||||
Params:
|
||||
tags (dict): A dictionary containing metadata about the landmark, including its name,
|
||||
importance, height, date of construction, and visitor information.
|
||||
|
||||
Returns:
|
||||
description (str): A string description of the landmark.
|
||||
keywords (dict): A dictionary of keywords with fields such as 'importance', 'height',
|
||||
'place_type', and 'date'.
|
||||
"""
|
||||
# Extract relevant fields
|
||||
name = tags.get('name')
|
||||
importance = tags.get('importance', None)
|
||||
n_visitors = tags.get('tourism:visitors', None)
|
||||
height = tags.get('height')
|
||||
place_type = get_place_type(tags)
|
||||
date = get_date(tags)
|
||||
|
||||
if place_type is None :
|
||||
return None, None
|
||||
|
||||
# Start the description.
|
||||
if importance is None :
|
||||
if len(tags.keys()) < 5 :
|
||||
return None, None
|
||||
if len(tags.keys()) < 10 :
|
||||
description = f"{name} is a well known {place_type}."
|
||||
elif len(tags.keys()) < 17 :
|
||||
importance = 'national'
|
||||
description = f"{name} is a {place_type} of national importance."
|
||||
else :
|
||||
importance = 'international'
|
||||
description = f"{name} is an internationally famous {place_type}."
|
||||
else :
|
||||
description = f"{name} is a {place_type} of {importance} importance."
|
||||
|
||||
if height is not None and date is not None :
|
||||
description += f" This {place_type} was constructed in {date} and is ca. {height} meters high."
|
||||
elif height is not None :
|
||||
description += f" This {place_type} stands ca. {height} meters tall."
|
||||
elif date is not None:
|
||||
description += f" It was constructed in {date}."
|
||||
|
||||
# Format the visitor number
|
||||
if n_visitors is not None :
|
||||
n_visitors = int(n_visitors)
|
||||
if n_visitors < 1000000 :
|
||||
description += f" It welcomes {int(n_visitors/1000)} thousand visitors every year."
|
||||
else :
|
||||
description += f" It welcomes {round(n_visitors/1000000, 1)} million visitors every year."
|
||||
|
||||
# Set the keywords.
|
||||
keywords = {"importance": importance,
|
||||
"height": height,
|
||||
"place_type": place_type,
|
||||
"date": date}
|
||||
|
||||
return description, keywords
|
||||
|
||||
|
||||
def get_place_type(tags):
|
||||
"""
|
||||
Determines the type of the place based on available tags such as 'amenity', 'building',
|
||||
'historic', and 'leisure'. The priority order is: 'historic' > 'building' (if not generic) >
|
||||
'amenity' > 'leisure'.
|
||||
|
||||
Params:
|
||||
tags (dict): A dictionary containing metadata about the place.
|
||||
|
||||
Returns:
|
||||
place_type (str): The determined type of the place, or None if no relevant type is found.
|
||||
"""
|
||||
amenity = tags.get('amenity', None)
|
||||
building = tags.get('building', None)
|
||||
historic = tags.get('historic', None)
|
||||
leisure = tags.get('leisure')
|
||||
|
||||
if historic and historic != "yes":
|
||||
return historic
|
||||
if building and building not in ["yes", "civic", "government", "apartments", "residential", "commericial", "industrial", "retail", "religious", "public", "service"]:
|
||||
return building
|
||||
if amenity:
|
||||
return amenity
|
||||
if leisure:
|
||||
return leisure
|
||||
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_date(tags):
|
||||
"""
|
||||
Extracts the most relevant date from the available tags, prioritizing 'construction_date',
|
||||
'start_date', 'year_of_construction', and 'opening_date' in that order.
|
||||
|
||||
Params:
|
||||
tags (dict): A dictionary containing metadata about the place.
|
||||
|
||||
Returns:
|
||||
date (str): The most relevant date found, or None if no date is available.
|
||||
"""
|
||||
construction_date = tags.get('construction_date', None)
|
||||
opening_date = tags.get('opening_date', None)
|
||||
start_date = tags.get('start_date', None)
|
||||
year_of_construction = tags.get('year_of_construction', None)
|
||||
|
||||
# Prioritize based on availability
|
||||
if construction_date:
|
||||
return construction_date
|
||||
if start_date:
|
||||
return start_date
|
||||
if year_of_construction:
|
||||
return year_of_construction
|
||||
if opening_date:
|
||||
return opening_date
|
||||
|
||||
return None
|
||||
@@ -1,17 +0,0 @@
|
||||
"""Helper function to return only the major landmarks from a large list."""
|
||||
from ..structs.landmark import Landmark
|
||||
|
||||
def take_most_important(landmarks: list[Landmark], n_important) -> list[Landmark]:
|
||||
"""
|
||||
Given a list of landmarks, return the n_important most important landmarks
|
||||
Args:
|
||||
landmarks: list[Landmark] - list of landmarks
|
||||
n_important: int - number of most important landmarks to return
|
||||
Returns:
|
||||
list[Landmark] - list of the n_important most important landmarks
|
||||
"""
|
||||
|
||||
# Sort landmarks by attractiveness (descending)
|
||||
sorted_landmarks = sorted(landmarks, key=lambda x: x.attractiveness, reverse=True)
|
||||
|
||||
return sorted_landmarks[:n_important]
|
||||
1330
backend/uv.lock
generated
Normal file
1330
backend/uv.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
default.nix
19
default.nix
@@ -1,20 +1,17 @@
|
||||
{ pkgs ? import <nixpkgs> { config.android_sdk.accept_license = true; config.allowUnfree = true; } }:
|
||||
|
||||
pkgs.mkShell {
|
||||
|
||||
buildInputs = with pkgs; [
|
||||
flutter
|
||||
android-tools # for adb
|
||||
openjdk # required for Android builds
|
||||
# pkgs.androidenv.androidPkgs.androidsdk # The customized SDK that we've made above
|
||||
# androidenv.androidPkgs.ndk-bundle
|
||||
buildInputs = [
|
||||
pkgs.flutter
|
||||
#pkgs.android-tools # for adb
|
||||
#pkgs.openjdk # required for Android builds
|
||||
];
|
||||
|
||||
# Setting up android build environments on nix is a bit of a pain - immutable paths, etc.
|
||||
# I used a hacky workaround by manually downloading the SDK and NDK from the website and simply telling the shell where to find them
|
||||
ANDROID_HOME = "/scratch/remy/android";
|
||||
|
||||
# Set up Android SDK paths if needed
|
||||
shellHook = ''
|
||||
export ANDROID_SDK_ROOT=${pkgs.androidsdk}/libexec/android-sdk
|
||||
export PATH=$PATH:${pkgs.androidsdk}/libexec/android-sdk/platform-tools
|
||||
echo "Flutter dev environment ready. 'adb' and 'flutter' are available."
|
||||
'';
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ To truly deploy a new version of the application, i.e. to the official app store
|
||||
git tag -a v<name> -m "Release <name>"
|
||||
git push origin v<name>
|
||||
```
|
||||
We adhere to the [Semantic Versioning](https://semver.org/) standard, so the tag should be of the form `v0.1.8` for example.
|
||||
We adhere to the [Semantic Versioning](https://semver.org/) standard, so the tag should be of the form `v0.1.8` for example.
|
||||
|
||||
### Icons and logos
|
||||
The application uses a custom launcher icon and splash screen. These are managed platform-independently using the `flutter_launcher_icons` package.
|
||||
@@ -66,10 +66,3 @@ These are used by the CI/CD pipeline to deploy the application.
|
||||
- `IOS_ASC_KEY_ID` as well
|
||||
- `IOS_MATCH_PASSWORD` is used by fastlane match to download the certificates
|
||||
- `IOS_MATCH_REPO_SSH_KEY_BASE64` is used to authenticate with the git repository where the certificates are stored
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Android SDK&NDK setup
|
||||
|
||||
@@ -24,15 +24,5 @@ linter:
|
||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||
|
||||
# Exclude legacy/experimental code that is not part of the current
|
||||
# refactor work. This prevents the analyzer from failing on old files
|
||||
# in `lib/old` while we iterate on the new architecture in `lib/`.
|
||||
analyzer:
|
||||
exclude:
|
||||
- lib/old/**
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
|
||||
formatter:
|
||||
page_width: 200
|
||||
|
||||
@@ -49,8 +49,7 @@ if (secretPropertiesFile.exists()) {
|
||||
android {
|
||||
namespace "com.anydev.anyway"
|
||||
compileSdk flutter.compileSdkVersion
|
||||
ndkVersion = "27.0.12077973"
|
||||
// TODO - set back to ndkVersion flutter.ndkVersion once https://github.com/flutter/flutter/issues/139427 is resolved
|
||||
ndkVersion flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
@@ -66,7 +65,7 @@ android {
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
|
||||
|
||||
applicationId "com.anydev.anyway"
|
||||
// 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.
|
||||
|
||||
@@ -19,7 +19,7 @@ pluginManagement {
|
||||
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version "8.7.0" apply false
|
||||
id "com.android.application" version "8.1.0" apply false
|
||||
id "org.jetbrains.kotlin.android" version "2.0.20" apply false
|
||||
}
|
||||
|
||||
|
||||
@@ -26,13 +26,13 @@ ThemeData APP_THEME = ThemeData(
|
||||
cardColor: Colors.white,
|
||||
useMaterial3: true,
|
||||
|
||||
colorScheme: const ColorScheme.light(
|
||||
colorScheme: ColorScheme.light(
|
||||
primary: PRIMARY_COLOR,
|
||||
secondary: GRADIENT_END,
|
||||
surface: Colors.white,
|
||||
error: Colors.red,
|
||||
onPrimary: Colors.white,
|
||||
onSecondary: Color.fromARGB(255, 30, 22, 22),
|
||||
onSecondary: const Color.fromARGB(255, 30, 22, 22),
|
||||
onSurface: Colors.black,
|
||||
onError: Colors.white,
|
||||
brightness: Brightness.light,
|
||||
@@ -64,6 +64,12 @@ ThemeData APP_THEME = ThemeData(
|
||||
),
|
||||
|
||||
|
||||
cardTheme: const CardTheme(
|
||||
shadowColor: Colors.grey,
|
||||
elevation: 2,
|
||||
margin: EdgeInsets.all(10),
|
||||
),
|
||||
|
||||
sliderTheme: const SliderThemeData(
|
||||
trackHeight: 15,
|
||||
inactiveTrackColor: Colors.grey,
|
||||
@@ -77,4 +83,4 @@ const Gradient APP_GRADIENT = LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [GRADIENT_START, GRADIENT_END],
|
||||
);
|
||||
);
|
||||
@@ -1,83 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const String APP_NAME = 'AnyWay';
|
||||
|
||||
String API_URL_BASE = 'https://anyway.anydev.info';
|
||||
String API_URL_DEBUG = 'https://anyway-stg.anydev.info';
|
||||
String PRIVACY_URL = 'https://anydev.info/privacy';
|
||||
|
||||
const String MAP_ID = '41c21ac9b81dbfd8';
|
||||
|
||||
|
||||
const Color GRADIENT_START = Color(0xFFF9B208);
|
||||
const Color GRADIENT_END = Color(0xFFE72E77);
|
||||
|
||||
const Color PRIMARY_COLOR = Color(0xFFF38F1A);
|
||||
|
||||
|
||||
|
||||
const double TRIP_PANEL_MAX_HEIGHT = 0.8;
|
||||
const double TRIP_PANEL_MIN_HEIGHT = 0.12;
|
||||
|
||||
ThemeData APP_THEME = ThemeData(
|
||||
primaryColor: PRIMARY_COLOR,
|
||||
|
||||
scaffoldBackgroundColor: Colors.white,
|
||||
cardColor: Colors.white,
|
||||
useMaterial3: true,
|
||||
|
||||
colorScheme: const ColorScheme.light(
|
||||
primary: PRIMARY_COLOR,
|
||||
secondary: GRADIENT_END,
|
||||
surface: Colors.white,
|
||||
error: Colors.red,
|
||||
onPrimary: Colors.white,
|
||||
onSecondary: Color.fromARGB(255, 30, 22, 22),
|
||||
onSurface: Colors.black,
|
||||
onError: Colors.white,
|
||||
brightness: Brightness.light,
|
||||
),
|
||||
|
||||
|
||||
textButtonTheme: const TextButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
foregroundColor: WidgetStatePropertyAll(PRIMARY_COLOR),
|
||||
side: WidgetStatePropertyAll(
|
||||
BorderSide(
|
||||
color: PRIMARY_COLOR,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
|
||||
elevatedButtonTheme: const ElevatedButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
foregroundColor: WidgetStatePropertyAll(PRIMARY_COLOR),
|
||||
)
|
||||
),
|
||||
|
||||
outlinedButtonTheme: const OutlinedButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
foregroundColor: WidgetStatePropertyAll(PRIMARY_COLOR),
|
||||
)
|
||||
),
|
||||
|
||||
|
||||
sliderTheme: const SliderThemeData(
|
||||
trackHeight: 15,
|
||||
inactiveTrackColor: Colors.grey,
|
||||
thumbColor: PRIMARY_COLOR,
|
||||
activeTrackColor: GRADIENT_END
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
const Gradient APP_GRADIENT = LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [GRADIENT_START, GRADIENT_END],
|
||||
);
|
||||
|
||||
|
||||
final GlobalKey<ScaffoldMessengerState> rootScaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
|
||||
@@ -1,16 +0,0 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
class DioClient {
|
||||
final Dio dio;
|
||||
|
||||
DioClient({required String baseUrl}): dio = Dio(BaseOptions(
|
||||
baseUrl: baseUrl,
|
||||
connectTimeout: const Duration(seconds: 5),
|
||||
receiveTimeout: const Duration(seconds: 120),
|
||||
// also accept 500 errors, since we cannot rule out that the server is at fault. We still want to gracefully handle these errors
|
||||
validateStatus: (status) => status! <= 500,
|
||||
receiveDataWhenStatusError: true,
|
||||
contentType: Headers.jsonContentType,
|
||||
responseType: ResponseType.json,
|
||||
));
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// Defines the contract for persisting trip payloads locally.
|
||||
abstract class TripLocalDataSource {
|
||||
/// Returns all saved trip JSON payloads, newest first.
|
||||
Future<List<Map<String, dynamic>>> loadTrips();
|
||||
|
||||
/// Returns a single trip payload by uuid if present.
|
||||
/// TODO - should directly return Trip?
|
||||
Future<Map<String, dynamic>?> getTrip(String uuid);
|
||||
|
||||
/// Upserts the provided trip payload (also used for editing existing trips).
|
||||
Future<void> upsertTrip(Map<String, dynamic> tripJson);
|
||||
|
||||
/// Removes the trip with the matching uuid.
|
||||
Future<void> deleteTrip(String uuid);
|
||||
}
|
||||
|
||||
class TripLocalDataSourceImpl implements TripLocalDataSource {
|
||||
TripLocalDataSourceImpl({Future<SharedPreferences>? preferences})
|
||||
: _prefsFuture = preferences ?? SharedPreferences.getInstance();
|
||||
|
||||
static const String _storageKey = 'savedTrips';
|
||||
final Future<SharedPreferences> _prefsFuture;
|
||||
|
||||
@override
|
||||
Future<List<Map<String, dynamic>>> loadTrips() async {
|
||||
final prefs = await _prefsFuture;
|
||||
final stored = prefs.getStringList(_storageKey);
|
||||
if (stored == null) return [];
|
||||
return stored.map(_decodeTrip).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>?> getTrip(String uuid) async {
|
||||
final trips = await loadTrips();
|
||||
for (final trip in trips) {
|
||||
if (trip['uuid'] == uuid) {
|
||||
return Map<String, dynamic>.from(trip);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> upsertTrip(Map<String, dynamic> tripJson) async {
|
||||
final uuid = tripJson['uuid'];
|
||||
if (uuid is! String || uuid.isEmpty) {
|
||||
throw ArgumentError('Trip JSON must contain a uuid string');
|
||||
}
|
||||
|
||||
final trips = await loadTrips();
|
||||
trips.removeWhere((trip) => trip['uuid'] == uuid);
|
||||
trips.insert(0, Map<String, dynamic>.from(tripJson));
|
||||
await _persistTrips(trips);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteTrip(String uuid) async {
|
||||
final trips = await loadTrips();
|
||||
final updated = trips.where((trip) => trip['uuid'] != uuid).toList();
|
||||
if (updated.length == trips.length) {
|
||||
return;
|
||||
}
|
||||
await _persistTrips(updated);
|
||||
}
|
||||
|
||||
Future<void> _persistTrips(List<Map<String, dynamic>> trips) async {
|
||||
final prefs = await _prefsFuture;
|
||||
final payload = trips.map(jsonEncode).toList();
|
||||
await prefs.setStringList(_storageKey, payload);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _decodeTrip(String raw) {
|
||||
final decoded = jsonDecode(raw);
|
||||
if (decoded is! Map<String, dynamic>) {
|
||||
throw const FormatException('Saved trip entry is not a JSON object');
|
||||
}
|
||||
return Map<String, dynamic>.from(decoded);
|
||||
}
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
abstract class TripRemoteDataSource {
|
||||
/// Fetch available landmarks for the provided preferences/start payload.
|
||||
Future<List<Map<String, dynamic>>> fetchLandmarks(Map<String, dynamic> body);
|
||||
|
||||
/// Create a new trip using the optimizer payload (that includes landmarks).
|
||||
Future<Map<String, dynamic>> createTrip(Map<String, dynamic> body);
|
||||
|
||||
/// Fetch an existing trip by UUID
|
||||
Future<Map<String, dynamic>> fetchTrip(String uuid);
|
||||
}
|
||||
|
||||
class TripRemoteDataSourceImpl implements TripRemoteDataSource {
|
||||
final Dio dio;
|
||||
|
||||
TripRemoteDataSourceImpl({required this.dio});
|
||||
|
||||
@override
|
||||
Future<List<Map<String, dynamic>>> fetchLandmarks(Map<String, dynamic> body) async {
|
||||
final response = await dio.post('/get/landmarks', data: body);
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Server error fetching landmarks: ${response.statusCode}');
|
||||
}
|
||||
|
||||
if (response.data is! List) {
|
||||
throw Exception('Unexpected landmarks response format');
|
||||
}
|
||||
|
||||
return _normalizeLandmarks(List<dynamic>.from(response.data as List));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> createTrip(Map<String, dynamic> body) async {
|
||||
log('Creating trip with body: $body');
|
||||
// convert body to actual json
|
||||
String bodyJson = jsonEncode(body);
|
||||
log('Trip request JSON: $bodyJson');
|
||||
final response = await dio.post('/optimize/trip', data: body);
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Server error: ${response.statusCode}');
|
||||
}
|
||||
|
||||
if (response.data is! Map<String, dynamic>) {
|
||||
throw Exception('Unexpected response format');
|
||||
}
|
||||
|
||||
final Map<String, dynamic> json = Map<String, dynamic>.from(response.data as Map);
|
||||
|
||||
_ensureLandmarks(json);
|
||||
if (json.containsKey('landmarks') && json['landmarks'] is List) {
|
||||
return json;
|
||||
}
|
||||
|
||||
final String? firstUuid = json['first_landmark_uuid'] as String?;
|
||||
if (firstUuid == null) {
|
||||
return json;
|
||||
}
|
||||
|
||||
final List<Map<String, dynamic>> landmarks = [];
|
||||
String? next = firstUuid;
|
||||
while (next != null) {
|
||||
final lm = await _fetchLandmarkByUuid(next);
|
||||
landmarks.add(lm);
|
||||
final dynamic nxt = lm['next_uuid'];
|
||||
next = (nxt is String) ? nxt : null;
|
||||
}
|
||||
|
||||
json['landmarks'] = landmarks;
|
||||
return json;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> fetchTrip(String uuid) async {
|
||||
final response = await dio.get('/trip/$uuid');
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Server error: ${response.statusCode}');
|
||||
}
|
||||
if (response.data is! Map<String, dynamic>) {
|
||||
throw Exception('Unexpected response format');
|
||||
}
|
||||
|
||||
final Map<String, dynamic> json = Map<String, dynamic>.from(response.data as Map);
|
||||
// Normalize same as createTrip: if trip contains first_landmark_uuid, expand chain
|
||||
if (json.containsKey('landmarks') && json['landmarks'] is List) {
|
||||
_ensureLandmarks(json);
|
||||
return json;
|
||||
}
|
||||
|
||||
final String? firstUuid = json['first_landmark_uuid'] as String?;
|
||||
if (firstUuid == null) return json;
|
||||
|
||||
final List<Map<String, dynamic>> landmarks = [];
|
||||
String? next = firstUuid;
|
||||
while (next != null) {
|
||||
final lm = await _fetchLandmarkByUuid(next);
|
||||
landmarks.add(lm);
|
||||
final dynamic nxt = lm['next_uuid'];
|
||||
next = (nxt is String) ? nxt : null;
|
||||
}
|
||||
|
||||
json['landmarks'] = landmarks;
|
||||
return json;
|
||||
}
|
||||
|
||||
// Fetch a single landmark by uuid via the /landmark/{landmark_uuid} endpoint
|
||||
Future<Map<String, dynamic>> _fetchLandmarkByUuid(String uuid) async {
|
||||
final response = await dio.get('/landmark/$uuid');
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Failed to fetch landmark $uuid: ${response.statusCode}');
|
||||
}
|
||||
if (response.data is! Map<String, dynamic>) {
|
||||
throw Exception('Unexpected landmark response format');
|
||||
}
|
||||
return _normalizeLandmark(Map<String, dynamic>.from(response.data as Map));
|
||||
}
|
||||
|
||||
static void _ensureLandmarks(Map<String, dynamic> json) {
|
||||
final raw = json['landmarks'];
|
||||
if (raw is List) {
|
||||
json['landmarks'] = _normalizeLandmarks(raw);
|
||||
}
|
||||
}
|
||||
|
||||
static List<Map<String, dynamic>> _normalizeLandmarks(List<dynamic> rawList) {
|
||||
return rawList
|
||||
.map((item) => _normalizeLandmark(Map<String, dynamic>.from(item as Map)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
static Map<String, dynamic> _normalizeLandmark(Map<String, dynamic> source) {
|
||||
final normalized = Map<String, dynamic>.from(source);
|
||||
final typeValue = normalized['type'];
|
||||
if (typeValue is String) {
|
||||
normalized['type'] = {'type': typeValue};
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
// TODO - I have the feeling this file is outdated and not used anymore
|
||||
import 'package:anyway/domain/entities/landmark.dart';
|
||||
import 'package:anyway/domain/entities/landmark_description.dart';
|
||||
import 'package:anyway/domain/entities/landmark_type.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
part 'landmark_model.freezed.dart';
|
||||
part 'landmark_model.g.dart';
|
||||
|
||||
@freezed
|
||||
abstract class LandmarkModel with _$LandmarkModel {
|
||||
const factory LandmarkModel({
|
||||
required String uuid,
|
||||
required String name,
|
||||
required List<double> location,
|
||||
required String type,
|
||||
required bool isSecondary,
|
||||
required String description,
|
||||
}) = _LandmarkModel;
|
||||
|
||||
const LandmarkModel._();
|
||||
|
||||
factory LandmarkModel.fromJson(Map<String, dynamic> json) => _$LandmarkModelFromJson(json);
|
||||
|
||||
Landmark toEntity() => Landmark(
|
||||
uuid: uuid,
|
||||
name: name,
|
||||
location: location,
|
||||
type: LandmarkType(type: LandmarkTypeEnum.values.firstWhere((e) => e.value == type)),
|
||||
isSecondary: isSecondary,
|
||||
description: LandmarkDescription(description: description, tags: []),
|
||||
);
|
||||
}
|
||||
@@ -1,298 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'landmark_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$LandmarkModel {
|
||||
|
||||
String get uuid; String get name; List<double> get location; String get type; bool get isSecondary; String get description;
|
||||
/// Create a copy of LandmarkModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$LandmarkModelCopyWith<LandmarkModel> get copyWith => _$LandmarkModelCopyWithImpl<LandmarkModel>(this as LandmarkModel, _$identity);
|
||||
|
||||
/// Serializes this LandmarkModel to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is LandmarkModel&&(identical(other.uuid, uuid) || other.uuid == uuid)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other.location, location)&&(identical(other.type, type) || other.type == type)&&(identical(other.isSecondary, isSecondary) || other.isSecondary == isSecondary)&&(identical(other.description, description) || other.description == description));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,uuid,name,const DeepCollectionEquality().hash(location),type,isSecondary,description);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LandmarkModel(uuid: $uuid, name: $name, location: $location, type: $type, isSecondary: $isSecondary, description: $description)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $LandmarkModelCopyWith<$Res> {
|
||||
factory $LandmarkModelCopyWith(LandmarkModel value, $Res Function(LandmarkModel) _then) = _$LandmarkModelCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String uuid, String name, List<double> location, String type, bool isSecondary, String description
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$LandmarkModelCopyWithImpl<$Res>
|
||||
implements $LandmarkModelCopyWith<$Res> {
|
||||
_$LandmarkModelCopyWithImpl(this._self, this._then);
|
||||
|
||||
final LandmarkModel _self;
|
||||
final $Res Function(LandmarkModel) _then;
|
||||
|
||||
/// Create a copy of LandmarkModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? uuid = null,Object? name = null,Object? location = null,Object? type = null,Object? isSecondary = null,Object? description = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
uuid: null == uuid ? _self.uuid : uuid // ignore: cast_nullable_to_non_nullable
|
||||
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,location: null == location ? _self.location : location // ignore: cast_nullable_to_non_nullable
|
||||
as List<double>,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||
as String,isSecondary: null == isSecondary ? _self.isSecondary : isSecondary // ignore: cast_nullable_to_non_nullable
|
||||
as bool,description: null == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [LandmarkModel].
|
||||
extension LandmarkModelPatterns on LandmarkModel {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _LandmarkModel value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _LandmarkModel() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _LandmarkModel value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _LandmarkModel():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _LandmarkModel value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _LandmarkModel() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String uuid, String name, List<double> location, String type, bool isSecondary, String description)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _LandmarkModel() when $default != null:
|
||||
return $default(_that.uuid,_that.name,_that.location,_that.type,_that.isSecondary,_that.description);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String uuid, String name, List<double> location, String type, bool isSecondary, String description) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _LandmarkModel():
|
||||
return $default(_that.uuid,_that.name,_that.location,_that.type,_that.isSecondary,_that.description);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String uuid, String name, List<double> location, String type, bool isSecondary, String description)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _LandmarkModel() when $default != null:
|
||||
return $default(_that.uuid,_that.name,_that.location,_that.type,_that.isSecondary,_that.description);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _LandmarkModel extends LandmarkModel {
|
||||
const _LandmarkModel({required this.uuid, required this.name, required final List<double> location, required this.type, required this.isSecondary, required this.description}): _location = location,super._();
|
||||
factory _LandmarkModel.fromJson(Map<String, dynamic> json) => _$LandmarkModelFromJson(json);
|
||||
|
||||
@override final String uuid;
|
||||
@override final String name;
|
||||
final List<double> _location;
|
||||
@override List<double> get location {
|
||||
if (_location is EqualUnmodifiableListView) return _location;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_location);
|
||||
}
|
||||
|
||||
@override final String type;
|
||||
@override final bool isSecondary;
|
||||
@override final String description;
|
||||
|
||||
/// Create a copy of LandmarkModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$LandmarkModelCopyWith<_LandmarkModel> get copyWith => __$LandmarkModelCopyWithImpl<_LandmarkModel>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$LandmarkModelToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _LandmarkModel&&(identical(other.uuid, uuid) || other.uuid == uuid)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other._location, _location)&&(identical(other.type, type) || other.type == type)&&(identical(other.isSecondary, isSecondary) || other.isSecondary == isSecondary)&&(identical(other.description, description) || other.description == description));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,uuid,name,const DeepCollectionEquality().hash(_location),type,isSecondary,description);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LandmarkModel(uuid: $uuid, name: $name, location: $location, type: $type, isSecondary: $isSecondary, description: $description)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$LandmarkModelCopyWith<$Res> implements $LandmarkModelCopyWith<$Res> {
|
||||
factory _$LandmarkModelCopyWith(_LandmarkModel value, $Res Function(_LandmarkModel) _then) = __$LandmarkModelCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String uuid, String name, List<double> location, String type, bool isSecondary, String description
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$LandmarkModelCopyWithImpl<$Res>
|
||||
implements _$LandmarkModelCopyWith<$Res> {
|
||||
__$LandmarkModelCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _LandmarkModel _self;
|
||||
final $Res Function(_LandmarkModel) _then;
|
||||
|
||||
/// Create a copy of LandmarkModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? uuid = null,Object? name = null,Object? location = null,Object? type = null,Object? isSecondary = null,Object? description = null,}) {
|
||||
return _then(_LandmarkModel(
|
||||
uuid: null == uuid ? _self.uuid : uuid // ignore: cast_nullable_to_non_nullable
|
||||
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,location: null == location ? _self._location : location // ignore: cast_nullable_to_non_nullable
|
||||
as List<double>,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||
as String,isSecondary: null == isSecondary ? _self.isSecondary : isSecondary // ignore: cast_nullable_to_non_nullable
|
||||
as bool,description: null == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
@@ -1,29 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'landmark_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_LandmarkModel _$LandmarkModelFromJson(Map<String, dynamic> json) =>
|
||||
_LandmarkModel(
|
||||
uuid: json['uuid'] as String,
|
||||
name: json['name'] as String,
|
||||
location: (json['location'] as List<dynamic>)
|
||||
.map((e) => (e as num).toDouble())
|
||||
.toList(),
|
||||
type: json['type'] as String,
|
||||
isSecondary: json['isSecondary'] as bool,
|
||||
description: json['description'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$LandmarkModelToJson(_LandmarkModel instance) =>
|
||||
<String, dynamic>{
|
||||
'uuid': instance.uuid,
|
||||
'name': instance.name,
|
||||
'location': instance.location,
|
||||
'type': instance.type,
|
||||
'isSecondary': instance.isSecondary,
|
||||
'description': instance.description,
|
||||
};
|
||||
@@ -1,114 +0,0 @@
|
||||
import 'package:anyway/data/datasources/trip_local_datasource.dart';
|
||||
import 'package:anyway/data/datasources/trip_remote_datasource.dart';
|
||||
import 'package:anyway/domain/entities/landmark.dart';
|
||||
import 'package:anyway/domain/entities/preferences.dart';
|
||||
import 'package:anyway/domain/entities/trip.dart';
|
||||
import 'package:anyway/domain/repositories/trip_repository.dart';
|
||||
|
||||
class BackendTripRepository implements TripRepository {
|
||||
final TripRemoteDataSource remote;
|
||||
final TripLocalDataSource local;
|
||||
|
||||
BackendTripRepository({required this.remote, required this.local});
|
||||
|
||||
@override
|
||||
Future<Trip> getTrip({Preferences? preferences, String? tripUUID, List<Landmark>? landmarks}) async {
|
||||
try {
|
||||
Map<String, dynamic> json;
|
||||
|
||||
if (tripUUID != null) {
|
||||
json = await remote.fetchTrip(tripUUID);
|
||||
} else {
|
||||
if (preferences == null) {
|
||||
throw ArgumentError('Either preferences or tripUUID must be provided');
|
||||
}
|
||||
|
||||
final Map<String, dynamic> prefsPayload = _buildPreferencesPayload(preferences);
|
||||
List<Map<String, dynamic>> landmarkBodies = landmarks != null
|
||||
? landmarks.map((lm) => lm.toJson()).toList()
|
||||
: await _fetchLandmarkPayloads(prefsPayload, preferences.startLocation);
|
||||
|
||||
// TODO: remove
|
||||
// restrict the landmark list to 30 to iterate quickly
|
||||
landmarkBodies = landmarkBodies.take(30).toList();
|
||||
// change the json key because of backend inconsistency
|
||||
for (var lm in landmarkBodies) {
|
||||
if (lm.containsKey('type')) {
|
||||
lm['type'] = "sightseeing";
|
||||
}
|
||||
lm['osm_type'] = 'node';
|
||||
lm['osm_id'] = 1;
|
||||
}
|
||||
|
||||
final Map<String, dynamic> body = {
|
||||
'preferences': prefsPayload,
|
||||
'landmarks': landmarkBodies,
|
||||
'start': preferences.startLocation,
|
||||
};
|
||||
if (preferences.endLocation != null) {
|
||||
body['end'] = preferences.endLocation;
|
||||
}
|
||||
if (preferences.detourToleranceMinutes != null) {
|
||||
body['detour_tolerance_minute'] = preferences.detourToleranceMinutes;
|
||||
}
|
||||
|
||||
json = await remote.createTrip(body);
|
||||
}
|
||||
|
||||
return Trip.fromJson(json);
|
||||
} catch (e) {
|
||||
throw Exception('Failed to fetch trip: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// TODO - maybe shorten this
|
||||
@override
|
||||
Future<List<Landmark>> searchLandmarks(Preferences preferences) async {
|
||||
final Map<String, dynamic> prefsPayload = _buildPreferencesPayload(preferences);
|
||||
final List<Map<String, dynamic>> rawLandmarks = await _fetchLandmarkPayloads(prefsPayload, preferences.startLocation);
|
||||
return rawLandmarks.map((lmJson) => Landmark.fromJson(lmJson)).toList();
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> _fetchLandmarkPayloads(
|
||||
Map<String, dynamic> prefsPayload, List<double> startLocation) async {
|
||||
final Map<String, dynamic> landmarkRequest = {
|
||||
'preferences': prefsPayload,
|
||||
'start': startLocation,
|
||||
};
|
||||
|
||||
return await remote.fetchLandmarks(landmarkRequest);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _buildPreferencesPayload(Preferences preferences) {
|
||||
final Map<String, dynamic> prefsPayload = {};
|
||||
preferences.scores.forEach((type, score) {
|
||||
prefsPayload[type] = {'type': type, 'score': score};
|
||||
});
|
||||
prefsPayload['max_time_minute'] = preferences.maxTimeMinutes;
|
||||
return prefsPayload;
|
||||
}
|
||||
|
||||
// TODO - should this be moved to a separate local repository?
|
||||
@override
|
||||
Future<List<Trip>> getSavedTrips() async {
|
||||
final rawTrips = await local.loadTrips();
|
||||
return rawTrips.map(Trip.fromJson).toList(growable: false);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Trip?> getSavedTrip(String uuid) async {
|
||||
final json = await local.getTrip(uuid);
|
||||
return json == null ? null : Trip.fromJson(json);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> saveTrip(Trip trip) async {
|
||||
await local.upsertTrip(trip.toJson());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteSavedTrip(String uuid) async {
|
||||
await local.deleteTrip(uuid);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:anyway/domain/repositories/onboarding_repository.dart';
|
||||
|
||||
class LocalOnboardingRepository implements OnboardingRepository {
|
||||
static const _key = 'onboardingCompleted';
|
||||
|
||||
@override
|
||||
Future<bool> isOnboarded() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool(_key) ?? false;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setOnboarded(bool value) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_key, value);
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:anyway/domain/entities/preferences.dart';
|
||||
import 'package:anyway/domain/repositories/preferences_repository.dart';
|
||||
|
||||
class PreferencesRepositoryImpl implements PreferencesRepository {
|
||||
static const _key = 'userPreferences';
|
||||
|
||||
@override
|
||||
Future<Preferences> getPreferences() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final raw = prefs.getString(_key);
|
||||
if (raw == null) {
|
||||
// TODO - rethink this
|
||||
// return a sensible default
|
||||
return Preferences(
|
||||
scores: {
|
||||
'sightseeing': 0,
|
||||
'shopping': 0,
|
||||
'nature': 0,
|
||||
},
|
||||
maxTimeMinutes: 120,
|
||||
startLocation: const [48.8575, 2.3514],
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
final map = json.decode(raw) as Map<String, dynamic>;
|
||||
return Preferences.fromJson(map);
|
||||
} catch (_) {
|
||||
return Preferences(
|
||||
scores: {
|
||||
'sightseeing': 0,
|
||||
'shopping': 0,
|
||||
'nature': 0,
|
||||
},
|
||||
maxTimeMinutes: 120,
|
||||
startLocation: const [48.8575, 2.3514],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> savePreferences(Preferences preferences) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final raw = json.encode(preferences.toJson());
|
||||
await prefs.setString(_key, raw);
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
# Domain layer
|
||||
|
||||
## `entities` - Model definition
|
||||
|
||||
Since we follow the repository structure convention, in this folder we purely define data models, without providing any logic. To reduce boilerplate, we use the `freezed` package. This requires some code generation, which means that all model definitions have the following structure:
|
||||
```dart
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
// required: associates our `main.dart` with the code generated by Freezed
|
||||
part 'main.freezed.dart';
|
||||
// optional: Since our Person class is serializable, we must add this line.
|
||||
// But if Person was not serializable, we could skip it.
|
||||
part 'main.g.dart';
|
||||
```
|
||||
|
||||
This is required boilerplate for all models. Then, we define the model itself using the `@freezed` annotation:
|
||||
```dart
|
||||
@freezed
|
||||
...
|
||||
```
|
||||
|
||||
The `*.frozen.dart` and `*.g.dart` are pure boilerplate and should not be touched.
|
||||
|
||||
Note that the description of the data will losely follow the capabilities of the backend but does not need to reflect it exactly. That is where the `data` part is for: translating api calls into flutter objects.
|
||||
|
||||
To ensure the creation of the boilerplate code, run the following command in your terminal:
|
||||
|
||||
```bash
|
||||
flutter pub run build_runner build --delete-conflicting-outputs
|
||||
```
|
||||
or interactively:
|
||||
```
|
||||
dart run build_runner watch -d
|
||||
```
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import 'package:anyway/domain/entities/landmark_type.dart';
|
||||
import 'package:anyway/domain/entities/landmark_description.dart';
|
||||
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
part 'landmark.freezed.dart';
|
||||
part 'landmark.g.dart';
|
||||
|
||||
@unfreezed
|
||||
abstract class Landmark with _$Landmark {
|
||||
factory Landmark({
|
||||
required String uuid,
|
||||
required String name,
|
||||
required List<double> location,
|
||||
required LandmarkType type,
|
||||
|
||||
@JsonKey(name: 'is_secondary')
|
||||
bool? isSecondary,
|
||||
|
||||
/// Optional rich description object (may be null if API returns plain string)
|
||||
LandmarkDescription? description,
|
||||
|
||||
@JsonKey(name: 'name_en')
|
||||
String? nameEn,
|
||||
|
||||
@JsonKey(name: 'website_url')
|
||||
String? websiteUrl,
|
||||
|
||||
@JsonKey(name: 'image_url')
|
||||
String? imageUrl,
|
||||
|
||||
@JsonKey(name: 'attractiveness')
|
||||
int? attractiveness,
|
||||
|
||||
@JsonKey(name: 'n_tags')
|
||||
int? tagCount,
|
||||
|
||||
|
||||
|
||||
/// Duration at landmark in minutes
|
||||
@JsonKey(name: 'duration')
|
||||
int? durationMinutes,
|
||||
|
||||
bool? visited,
|
||||
|
||||
@JsonKey(name: 'time_to_reach_next')
|
||||
int? timeToReachNextMinutes,
|
||||
}) = _Landmark;
|
||||
|
||||
factory Landmark.fromJson(Map<String, Object?> json) => _$LandmarkFromJson(json);
|
||||
}
|
||||
|
||||
extension LandmarkVisitX on Landmark {
|
||||
bool get isVisited => visited ?? false;
|
||||
|
||||
set isVisited(bool value) => visited = value;
|
||||
}
|
||||
@@ -1,350 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'landmark.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$Landmark {
|
||||
|
||||
String get uuid; set uuid(String value); String get name; set name(String value); List<double> get location; set location(List<double> value); LandmarkType get type; set type(LandmarkType value);@JsonKey(name: 'is_secondary') bool? get isSecondary;@JsonKey(name: 'is_secondary') set isSecondary(bool? value);/// Optional rich description object (may be null if API returns plain string)
|
||||
LandmarkDescription? get description;/// Optional rich description object (may be null if API returns plain string)
|
||||
set description(LandmarkDescription? value);@JsonKey(name: 'name_en') String? get nameEn;@JsonKey(name: 'name_en') set nameEn(String? value);@JsonKey(name: 'website_url') String? get websiteUrl;@JsonKey(name: 'website_url') set websiteUrl(String? value);@JsonKey(name: 'image_url') String? get imageUrl;@JsonKey(name: 'image_url') set imageUrl(String? value);@JsonKey(name: 'attractiveness') int? get attractiveness;@JsonKey(name: 'attractiveness') set attractiveness(int? value);@JsonKey(name: 'n_tags') int? get tagCount;@JsonKey(name: 'n_tags') set tagCount(int? value);/// Duration at landmark in minutes
|
||||
@JsonKey(name: 'duration') int? get durationMinutes;/// Duration at landmark in minutes
|
||||
@JsonKey(name: 'duration') set durationMinutes(int? value); bool? get visited; set visited(bool? value);@JsonKey(name: 'time_to_reach_next') int? get timeToReachNextMinutes;@JsonKey(name: 'time_to_reach_next') set timeToReachNextMinutes(int? value);
|
||||
/// Create a copy of Landmark
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$LandmarkCopyWith<Landmark> get copyWith => _$LandmarkCopyWithImpl<Landmark>(this as Landmark, _$identity);
|
||||
|
||||
/// Serializes this Landmark to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Landmark(uuid: $uuid, name: $name, location: $location, type: $type, isSecondary: $isSecondary, description: $description, nameEn: $nameEn, websiteUrl: $websiteUrl, imageUrl: $imageUrl, attractiveness: $attractiveness, tagCount: $tagCount, durationMinutes: $durationMinutes, visited: $visited, timeToReachNextMinutes: $timeToReachNextMinutes)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $LandmarkCopyWith<$Res> {
|
||||
factory $LandmarkCopyWith(Landmark value, $Res Function(Landmark) _then) = _$LandmarkCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String uuid, String name, List<double> location, LandmarkType type,@JsonKey(name: 'is_secondary') bool? isSecondary, LandmarkDescription? description,@JsonKey(name: 'name_en') String? nameEn,@JsonKey(name: 'website_url') String? websiteUrl,@JsonKey(name: 'image_url') String? imageUrl,@JsonKey(name: 'attractiveness') int? attractiveness,@JsonKey(name: 'n_tags') int? tagCount,@JsonKey(name: 'duration') int? durationMinutes, bool? visited,@JsonKey(name: 'time_to_reach_next') int? timeToReachNextMinutes
|
||||
});
|
||||
|
||||
|
||||
$LandmarkTypeCopyWith<$Res> get type;$LandmarkDescriptionCopyWith<$Res>? get description;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$LandmarkCopyWithImpl<$Res>
|
||||
implements $LandmarkCopyWith<$Res> {
|
||||
_$LandmarkCopyWithImpl(this._self, this._then);
|
||||
|
||||
final Landmark _self;
|
||||
final $Res Function(Landmark) _then;
|
||||
|
||||
/// Create a copy of Landmark
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? uuid = null,Object? name = null,Object? location = null,Object? type = null,Object? isSecondary = freezed,Object? description = freezed,Object? nameEn = freezed,Object? websiteUrl = freezed,Object? imageUrl = freezed,Object? attractiveness = freezed,Object? tagCount = freezed,Object? durationMinutes = freezed,Object? visited = freezed,Object? timeToReachNextMinutes = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
uuid: null == uuid ? _self.uuid : uuid // ignore: cast_nullable_to_non_nullable
|
||||
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,location: null == location ? _self.location : location // ignore: cast_nullable_to_non_nullable
|
||||
as List<double>,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||
as LandmarkType,isSecondary: freezed == isSecondary ? _self.isSecondary : isSecondary // ignore: cast_nullable_to_non_nullable
|
||||
as bool?,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
||||
as LandmarkDescription?,nameEn: freezed == nameEn ? _self.nameEn : nameEn // ignore: cast_nullable_to_non_nullable
|
||||
as String?,websiteUrl: freezed == websiteUrl ? _self.websiteUrl : websiteUrl // ignore: cast_nullable_to_non_nullable
|
||||
as String?,imageUrl: freezed == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable
|
||||
as String?,attractiveness: freezed == attractiveness ? _self.attractiveness : attractiveness // ignore: cast_nullable_to_non_nullable
|
||||
as int?,tagCount: freezed == tagCount ? _self.tagCount : tagCount // ignore: cast_nullable_to_non_nullable
|
||||
as int?,durationMinutes: freezed == durationMinutes ? _self.durationMinutes : durationMinutes // ignore: cast_nullable_to_non_nullable
|
||||
as int?,visited: freezed == visited ? _self.visited : visited // ignore: cast_nullable_to_non_nullable
|
||||
as bool?,timeToReachNextMinutes: freezed == timeToReachNextMinutes ? _self.timeToReachNextMinutes : timeToReachNextMinutes // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
));
|
||||
}
|
||||
/// Create a copy of Landmark
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$LandmarkTypeCopyWith<$Res> get type {
|
||||
|
||||
return $LandmarkTypeCopyWith<$Res>(_self.type, (value) {
|
||||
return _then(_self.copyWith(type: value));
|
||||
});
|
||||
}/// Create a copy of Landmark
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$LandmarkDescriptionCopyWith<$Res>? get description {
|
||||
if (_self.description == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $LandmarkDescriptionCopyWith<$Res>(_self.description!, (value) {
|
||||
return _then(_self.copyWith(description: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [Landmark].
|
||||
extension LandmarkPatterns on Landmark {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _Landmark value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _Landmark() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _Landmark value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _Landmark():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _Landmark value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _Landmark() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String uuid, String name, List<double> location, LandmarkType type, @JsonKey(name: 'is_secondary') bool? isSecondary, LandmarkDescription? description, @JsonKey(name: 'name_en') String? nameEn, @JsonKey(name: 'website_url') String? websiteUrl, @JsonKey(name: 'image_url') String? imageUrl, @JsonKey(name: 'attractiveness') int? attractiveness, @JsonKey(name: 'n_tags') int? tagCount, @JsonKey(name: 'duration') int? durationMinutes, bool? visited, @JsonKey(name: 'time_to_reach_next') int? timeToReachNextMinutes)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _Landmark() when $default != null:
|
||||
return $default(_that.uuid,_that.name,_that.location,_that.type,_that.isSecondary,_that.description,_that.nameEn,_that.websiteUrl,_that.imageUrl,_that.attractiveness,_that.tagCount,_that.durationMinutes,_that.visited,_that.timeToReachNextMinutes);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String uuid, String name, List<double> location, LandmarkType type, @JsonKey(name: 'is_secondary') bool? isSecondary, LandmarkDescription? description, @JsonKey(name: 'name_en') String? nameEn, @JsonKey(name: 'website_url') String? websiteUrl, @JsonKey(name: 'image_url') String? imageUrl, @JsonKey(name: 'attractiveness') int? attractiveness, @JsonKey(name: 'n_tags') int? tagCount, @JsonKey(name: 'duration') int? durationMinutes, bool? visited, @JsonKey(name: 'time_to_reach_next') int? timeToReachNextMinutes) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _Landmark():
|
||||
return $default(_that.uuid,_that.name,_that.location,_that.type,_that.isSecondary,_that.description,_that.nameEn,_that.websiteUrl,_that.imageUrl,_that.attractiveness,_that.tagCount,_that.durationMinutes,_that.visited,_that.timeToReachNextMinutes);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String uuid, String name, List<double> location, LandmarkType type, @JsonKey(name: 'is_secondary') bool? isSecondary, LandmarkDescription? description, @JsonKey(name: 'name_en') String? nameEn, @JsonKey(name: 'website_url') String? websiteUrl, @JsonKey(name: 'image_url') String? imageUrl, @JsonKey(name: 'attractiveness') int? attractiveness, @JsonKey(name: 'n_tags') int? tagCount, @JsonKey(name: 'duration') int? durationMinutes, bool? visited, @JsonKey(name: 'time_to_reach_next') int? timeToReachNextMinutes)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _Landmark() when $default != null:
|
||||
return $default(_that.uuid,_that.name,_that.location,_that.type,_that.isSecondary,_that.description,_that.nameEn,_that.websiteUrl,_that.imageUrl,_that.attractiveness,_that.tagCount,_that.durationMinutes,_that.visited,_that.timeToReachNextMinutes);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _Landmark implements Landmark {
|
||||
_Landmark({required this.uuid, required this.name, required this.location, required this.type, @JsonKey(name: 'is_secondary') this.isSecondary, this.description, @JsonKey(name: 'name_en') this.nameEn, @JsonKey(name: 'website_url') this.websiteUrl, @JsonKey(name: 'image_url') this.imageUrl, @JsonKey(name: 'attractiveness') this.attractiveness, @JsonKey(name: 'n_tags') this.tagCount, @JsonKey(name: 'duration') this.durationMinutes, this.visited, @JsonKey(name: 'time_to_reach_next') this.timeToReachNextMinutes});
|
||||
factory _Landmark.fromJson(Map<String, dynamic> json) => _$LandmarkFromJson(json);
|
||||
|
||||
@override String uuid;
|
||||
@override String name;
|
||||
@override List<double> location;
|
||||
@override LandmarkType type;
|
||||
@override@JsonKey(name: 'is_secondary') bool? isSecondary;
|
||||
/// Optional rich description object (may be null if API returns plain string)
|
||||
@override LandmarkDescription? description;
|
||||
@override@JsonKey(name: 'name_en') String? nameEn;
|
||||
@override@JsonKey(name: 'website_url') String? websiteUrl;
|
||||
@override@JsonKey(name: 'image_url') String? imageUrl;
|
||||
@override@JsonKey(name: 'attractiveness') int? attractiveness;
|
||||
@override@JsonKey(name: 'n_tags') int? tagCount;
|
||||
/// Duration at landmark in minutes
|
||||
@override@JsonKey(name: 'duration') int? durationMinutes;
|
||||
@override bool? visited;
|
||||
@override@JsonKey(name: 'time_to_reach_next') int? timeToReachNextMinutes;
|
||||
|
||||
/// Create a copy of Landmark
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$LandmarkCopyWith<_Landmark> get copyWith => __$LandmarkCopyWithImpl<_Landmark>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$LandmarkToJson(this, );
|
||||
}
|
||||
|
||||
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Landmark(uuid: $uuid, name: $name, location: $location, type: $type, isSecondary: $isSecondary, description: $description, nameEn: $nameEn, websiteUrl: $websiteUrl, imageUrl: $imageUrl, attractiveness: $attractiveness, tagCount: $tagCount, durationMinutes: $durationMinutes, visited: $visited, timeToReachNextMinutes: $timeToReachNextMinutes)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$LandmarkCopyWith<$Res> implements $LandmarkCopyWith<$Res> {
|
||||
factory _$LandmarkCopyWith(_Landmark value, $Res Function(_Landmark) _then) = __$LandmarkCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String uuid, String name, List<double> location, LandmarkType type,@JsonKey(name: 'is_secondary') bool? isSecondary, LandmarkDescription? description,@JsonKey(name: 'name_en') String? nameEn,@JsonKey(name: 'website_url') String? websiteUrl,@JsonKey(name: 'image_url') String? imageUrl,@JsonKey(name: 'attractiveness') int? attractiveness,@JsonKey(name: 'n_tags') int? tagCount,@JsonKey(name: 'duration') int? durationMinutes, bool? visited,@JsonKey(name: 'time_to_reach_next') int? timeToReachNextMinutes
|
||||
});
|
||||
|
||||
|
||||
@override $LandmarkTypeCopyWith<$Res> get type;@override $LandmarkDescriptionCopyWith<$Res>? get description;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$LandmarkCopyWithImpl<$Res>
|
||||
implements _$LandmarkCopyWith<$Res> {
|
||||
__$LandmarkCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _Landmark _self;
|
||||
final $Res Function(_Landmark) _then;
|
||||
|
||||
/// Create a copy of Landmark
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? uuid = null,Object? name = null,Object? location = null,Object? type = null,Object? isSecondary = freezed,Object? description = freezed,Object? nameEn = freezed,Object? websiteUrl = freezed,Object? imageUrl = freezed,Object? attractiveness = freezed,Object? tagCount = freezed,Object? durationMinutes = freezed,Object? visited = freezed,Object? timeToReachNextMinutes = freezed,}) {
|
||||
return _then(_Landmark(
|
||||
uuid: null == uuid ? _self.uuid : uuid // ignore: cast_nullable_to_non_nullable
|
||||
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,location: null == location ? _self.location : location // ignore: cast_nullable_to_non_nullable
|
||||
as List<double>,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||
as LandmarkType,isSecondary: freezed == isSecondary ? _self.isSecondary : isSecondary // ignore: cast_nullable_to_non_nullable
|
||||
as bool?,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
||||
as LandmarkDescription?,nameEn: freezed == nameEn ? _self.nameEn : nameEn // ignore: cast_nullable_to_non_nullable
|
||||
as String?,websiteUrl: freezed == websiteUrl ? _self.websiteUrl : websiteUrl // ignore: cast_nullable_to_non_nullable
|
||||
as String?,imageUrl: freezed == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable
|
||||
as String?,attractiveness: freezed == attractiveness ? _self.attractiveness : attractiveness // ignore: cast_nullable_to_non_nullable
|
||||
as int?,tagCount: freezed == tagCount ? _self.tagCount : tagCount // ignore: cast_nullable_to_non_nullable
|
||||
as int?,durationMinutes: freezed == durationMinutes ? _self.durationMinutes : durationMinutes // ignore: cast_nullable_to_non_nullable
|
||||
as int?,visited: freezed == visited ? _self.visited : visited // ignore: cast_nullable_to_non_nullable
|
||||
as bool?,timeToReachNextMinutes: freezed == timeToReachNextMinutes ? _self.timeToReachNextMinutes : timeToReachNextMinutes // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of Landmark
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$LandmarkTypeCopyWith<$Res> get type {
|
||||
|
||||
return $LandmarkTypeCopyWith<$Res>(_self.type, (value) {
|
||||
return _then(_self.copyWith(type: value));
|
||||
});
|
||||
}/// Create a copy of Landmark
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$LandmarkDescriptionCopyWith<$Res>? get description {
|
||||
if (_self.description == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $LandmarkDescriptionCopyWith<$Res>(_self.description!, (value) {
|
||||
return _then(_self.copyWith(description: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// dart format on
|
||||
@@ -1,47 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'landmark.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_Landmark _$LandmarkFromJson(Map<String, dynamic> json) => _Landmark(
|
||||
uuid: json['uuid'] as String,
|
||||
name: json['name'] as String,
|
||||
location: (json['location'] as List<dynamic>)
|
||||
.map((e) => (e as num).toDouble())
|
||||
.toList(),
|
||||
type: LandmarkType.fromJson(json['type'] as Map<String, dynamic>),
|
||||
isSecondary: json['is_secondary'] as bool?,
|
||||
description: json['description'] == null
|
||||
? null
|
||||
: LandmarkDescription.fromJson(
|
||||
json['description'] as Map<String, dynamic>,
|
||||
),
|
||||
nameEn: json['name_en'] as String?,
|
||||
websiteUrl: json['website_url'] as String?,
|
||||
imageUrl: json['image_url'] as String?,
|
||||
attractiveness: (json['attractiveness'] as num?)?.toInt(),
|
||||
tagCount: (json['n_tags'] as num?)?.toInt(),
|
||||
durationMinutes: (json['duration'] as num?)?.toInt(),
|
||||
visited: json['visited'] as bool?,
|
||||
timeToReachNextMinutes: (json['time_to_reach_next'] as num?)?.toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$LandmarkToJson(_Landmark instance) => <String, dynamic>{
|
||||
'uuid': instance.uuid,
|
||||
'name': instance.name,
|
||||
'location': instance.location,
|
||||
'type': instance.type,
|
||||
'is_secondary': instance.isSecondary,
|
||||
'description': instance.description,
|
||||
'name_en': instance.nameEn,
|
||||
'website_url': instance.websiteUrl,
|
||||
'image_url': instance.imageUrl,
|
||||
'attractiveness': instance.attractiveness,
|
||||
'n_tags': instance.tagCount,
|
||||
'duration': instance.durationMinutes,
|
||||
'visited': instance.visited,
|
||||
'time_to_reach_next': instance.timeToReachNextMinutes,
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
part 'landmark_description.freezed.dart';
|
||||
part 'landmark_description.g.dart';
|
||||
|
||||
|
||||
@freezed
|
||||
abstract class LandmarkDescription with _$LandmarkDescription {
|
||||
const factory LandmarkDescription({
|
||||
required String description,
|
||||
required List<String> tags,
|
||||
}) = _LandmarkDescription;
|
||||
|
||||
factory LandmarkDescription.fromJson(Map<String, Object?> json) => _$LandmarkDescriptionFromJson(json);
|
||||
}
|
||||
|
||||
@@ -1,286 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'landmark_description.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$LandmarkDescription {
|
||||
|
||||
String get description; List<String> get tags;
|
||||
/// Create a copy of LandmarkDescription
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$LandmarkDescriptionCopyWith<LandmarkDescription> get copyWith => _$LandmarkDescriptionCopyWithImpl<LandmarkDescription>(this as LandmarkDescription, _$identity);
|
||||
|
||||
/// Serializes this LandmarkDescription to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is LandmarkDescription&&(identical(other.description, description) || other.description == description)&&const DeepCollectionEquality().equals(other.tags, tags));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,description,const DeepCollectionEquality().hash(tags));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LandmarkDescription(description: $description, tags: $tags)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $LandmarkDescriptionCopyWith<$Res> {
|
||||
factory $LandmarkDescriptionCopyWith(LandmarkDescription value, $Res Function(LandmarkDescription) _then) = _$LandmarkDescriptionCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String description, List<String> tags
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$LandmarkDescriptionCopyWithImpl<$Res>
|
||||
implements $LandmarkDescriptionCopyWith<$Res> {
|
||||
_$LandmarkDescriptionCopyWithImpl(this._self, this._then);
|
||||
|
||||
final LandmarkDescription _self;
|
||||
final $Res Function(LandmarkDescription) _then;
|
||||
|
||||
/// Create a copy of LandmarkDescription
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? description = null,Object? tags = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
description: null == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
||||
as String,tags: null == tags ? _self.tags : tags // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [LandmarkDescription].
|
||||
extension LandmarkDescriptionPatterns on LandmarkDescription {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _LandmarkDescription value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _LandmarkDescription() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _LandmarkDescription value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _LandmarkDescription():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _LandmarkDescription value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _LandmarkDescription() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String description, List<String> tags)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _LandmarkDescription() when $default != null:
|
||||
return $default(_that.description,_that.tags);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String description, List<String> tags) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _LandmarkDescription():
|
||||
return $default(_that.description,_that.tags);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String description, List<String> tags)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _LandmarkDescription() when $default != null:
|
||||
return $default(_that.description,_that.tags);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _LandmarkDescription implements LandmarkDescription {
|
||||
const _LandmarkDescription({required this.description, required final List<String> tags}): _tags = tags;
|
||||
factory _LandmarkDescription.fromJson(Map<String, dynamic> json) => _$LandmarkDescriptionFromJson(json);
|
||||
|
||||
@override final String description;
|
||||
final List<String> _tags;
|
||||
@override List<String> get tags {
|
||||
if (_tags is EqualUnmodifiableListView) return _tags;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_tags);
|
||||
}
|
||||
|
||||
|
||||
/// Create a copy of LandmarkDescription
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$LandmarkDescriptionCopyWith<_LandmarkDescription> get copyWith => __$LandmarkDescriptionCopyWithImpl<_LandmarkDescription>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$LandmarkDescriptionToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _LandmarkDescription&&(identical(other.description, description) || other.description == description)&&const DeepCollectionEquality().equals(other._tags, _tags));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,description,const DeepCollectionEquality().hash(_tags));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LandmarkDescription(description: $description, tags: $tags)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$LandmarkDescriptionCopyWith<$Res> implements $LandmarkDescriptionCopyWith<$Res> {
|
||||
factory _$LandmarkDescriptionCopyWith(_LandmarkDescription value, $Res Function(_LandmarkDescription) _then) = __$LandmarkDescriptionCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String description, List<String> tags
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$LandmarkDescriptionCopyWithImpl<$Res>
|
||||
implements _$LandmarkDescriptionCopyWith<$Res> {
|
||||
__$LandmarkDescriptionCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _LandmarkDescription _self;
|
||||
final $Res Function(_LandmarkDescription) _then;
|
||||
|
||||
/// Create a copy of LandmarkDescription
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? description = null,Object? tags = null,}) {
|
||||
return _then(_LandmarkDescription(
|
||||
description: null == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
||||
as String,tags: null == tags ? _self._tags : tags // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
@@ -1,20 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'landmark_description.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_LandmarkDescription _$LandmarkDescriptionFromJson(Map<String, dynamic> json) =>
|
||||
_LandmarkDescription(
|
||||
description: json['description'] as String,
|
||||
tags: (json['tags'] as List<dynamic>).map((e) => e as String).toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$LandmarkDescriptionToJson(
|
||||
_LandmarkDescription instance,
|
||||
) => <String, dynamic>{
|
||||
'description': instance.description,
|
||||
'tags': instance.tags,
|
||||
};
|
||||
@@ -1,31 +0,0 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'landmark_type.freezed.dart';
|
||||
part 'landmark_type.g.dart';
|
||||
|
||||
@freezed
|
||||
abstract class LandmarkType with _$LandmarkType {
|
||||
const factory LandmarkType({
|
||||
required LandmarkTypeEnum type,
|
||||
}) = _LandmarkType;
|
||||
|
||||
factory LandmarkType.fromJson(Map<String, Object?> json) => _$LandmarkTypeFromJson(json);
|
||||
}
|
||||
|
||||
@JsonEnum(alwaysCreate: true)
|
||||
enum LandmarkTypeEnum {
|
||||
@JsonValue('sightseeing')
|
||||
sightseeing,
|
||||
@JsonValue('nature')
|
||||
nature,
|
||||
@JsonValue('shopping')
|
||||
shopping,
|
||||
@JsonValue('start')
|
||||
start,
|
||||
@JsonValue('finish')
|
||||
finish,
|
||||
}
|
||||
|
||||
extension LandmarkTypeEnumExtension on LandmarkTypeEnum {
|
||||
String get value => _$LandmarkTypeEnumEnumMap[this]!;
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'landmark_type.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$LandmarkType {
|
||||
|
||||
LandmarkTypeEnum get type;
|
||||
/// Create a copy of LandmarkType
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$LandmarkTypeCopyWith<LandmarkType> get copyWith => _$LandmarkTypeCopyWithImpl<LandmarkType>(this as LandmarkType, _$identity);
|
||||
|
||||
/// Serializes this LandmarkType to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is LandmarkType&&(identical(other.type, type) || other.type == type));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,type);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LandmarkType(type: $type)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $LandmarkTypeCopyWith<$Res> {
|
||||
factory $LandmarkTypeCopyWith(LandmarkType value, $Res Function(LandmarkType) _then) = _$LandmarkTypeCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
LandmarkTypeEnum type
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$LandmarkTypeCopyWithImpl<$Res>
|
||||
implements $LandmarkTypeCopyWith<$Res> {
|
||||
_$LandmarkTypeCopyWithImpl(this._self, this._then);
|
||||
|
||||
final LandmarkType _self;
|
||||
final $Res Function(LandmarkType) _then;
|
||||
|
||||
/// Create a copy of LandmarkType
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? type = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||
as LandmarkTypeEnum,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [LandmarkType].
|
||||
extension LandmarkTypePatterns on LandmarkType {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _LandmarkType value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _LandmarkType() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _LandmarkType value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _LandmarkType():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _LandmarkType value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _LandmarkType() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( LandmarkTypeEnum type)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _LandmarkType() when $default != null:
|
||||
return $default(_that.type);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( LandmarkTypeEnum type) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _LandmarkType():
|
||||
return $default(_that.type);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( LandmarkTypeEnum type)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _LandmarkType() when $default != null:
|
||||
return $default(_that.type);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _LandmarkType implements LandmarkType {
|
||||
const _LandmarkType({required this.type});
|
||||
factory _LandmarkType.fromJson(Map<String, dynamic> json) => _$LandmarkTypeFromJson(json);
|
||||
|
||||
@override final LandmarkTypeEnum type;
|
||||
|
||||
/// Create a copy of LandmarkType
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$LandmarkTypeCopyWith<_LandmarkType> get copyWith => __$LandmarkTypeCopyWithImpl<_LandmarkType>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$LandmarkTypeToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _LandmarkType&&(identical(other.type, type) || other.type == type));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,type);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LandmarkType(type: $type)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$LandmarkTypeCopyWith<$Res> implements $LandmarkTypeCopyWith<$Res> {
|
||||
factory _$LandmarkTypeCopyWith(_LandmarkType value, $Res Function(_LandmarkType) _then) = __$LandmarkTypeCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
LandmarkTypeEnum type
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$LandmarkTypeCopyWithImpl<$Res>
|
||||
implements _$LandmarkTypeCopyWith<$Res> {
|
||||
__$LandmarkTypeCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _LandmarkType _self;
|
||||
final $Res Function(_LandmarkType) _then;
|
||||
|
||||
/// Create a copy of LandmarkType
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? type = null,}) {
|
||||
return _then(_LandmarkType(
|
||||
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||
as LandmarkTypeEnum,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
@@ -1,21 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'landmark_type.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_LandmarkType _$LandmarkTypeFromJson(Map<String, dynamic> json) =>
|
||||
_LandmarkType(type: $enumDecode(_$LandmarkTypeEnumEnumMap, json['type']));
|
||||
|
||||
Map<String, dynamic> _$LandmarkTypeToJson(_LandmarkType instance) =>
|
||||
<String, dynamic>{'type': _$LandmarkTypeEnumEnumMap[instance.type]!};
|
||||
|
||||
const _$LandmarkTypeEnumEnumMap = {
|
||||
LandmarkTypeEnum.sightseeing: 'sightseeing',
|
||||
LandmarkTypeEnum.nature: 'nature',
|
||||
LandmarkTypeEnum.shopping: 'shopping',
|
||||
LandmarkTypeEnum.start: 'start',
|
||||
LandmarkTypeEnum.finish: 'finish',
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
part 'preferences.freezed.dart';
|
||||
part 'preferences.g.dart';
|
||||
|
||||
@freezed
|
||||
abstract class Preferences with _$Preferences {
|
||||
const factory Preferences({
|
||||
/// Scores keyed by preference type (e.g. 'sightseeing', 'shopping', 'nature')
|
||||
required Map<String, int> scores,
|
||||
|
||||
/// Maximum trip duration in minutes
|
||||
required int maxTimeMinutes,
|
||||
|
||||
/// Required start location [lat, lon]
|
||||
required List<double> startLocation,
|
||||
|
||||
/// Optional end location
|
||||
List<double>? endLocation,
|
||||
|
||||
/// Optional detour tolerance in minutes
|
||||
int? detourToleranceMinutes,
|
||||
}) = _Preferences;
|
||||
|
||||
factory Preferences.fromJson(Map<String, Object?> json) => _$PreferencesFromJson(json);
|
||||
}
|
||||
@@ -1,322 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'preferences.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$Preferences {
|
||||
|
||||
/// Scores keyed by preference type (e.g. 'sightseeing', 'shopping', 'nature')
|
||||
Map<String, int> get scores;/// Maximum trip duration in minutes
|
||||
int get maxTimeMinutes;/// Required start location [lat, lon]
|
||||
List<double> get startLocation;/// Optional end location
|
||||
List<double>? get endLocation;/// Optional detour tolerance in minutes
|
||||
int? get detourToleranceMinutes;
|
||||
/// Create a copy of Preferences
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$PreferencesCopyWith<Preferences> get copyWith => _$PreferencesCopyWithImpl<Preferences>(this as Preferences, _$identity);
|
||||
|
||||
/// Serializes this Preferences to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is Preferences&&const DeepCollectionEquality().equals(other.scores, scores)&&(identical(other.maxTimeMinutes, maxTimeMinutes) || other.maxTimeMinutes == maxTimeMinutes)&&const DeepCollectionEquality().equals(other.startLocation, startLocation)&&const DeepCollectionEquality().equals(other.endLocation, endLocation)&&(identical(other.detourToleranceMinutes, detourToleranceMinutes) || other.detourToleranceMinutes == detourToleranceMinutes));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(scores),maxTimeMinutes,const DeepCollectionEquality().hash(startLocation),const DeepCollectionEquality().hash(endLocation),detourToleranceMinutes);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Preferences(scores: $scores, maxTimeMinutes: $maxTimeMinutes, startLocation: $startLocation, endLocation: $endLocation, detourToleranceMinutes: $detourToleranceMinutes)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $PreferencesCopyWith<$Res> {
|
||||
factory $PreferencesCopyWith(Preferences value, $Res Function(Preferences) _then) = _$PreferencesCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
Map<String, int> scores, int maxTimeMinutes, List<double> startLocation, List<double>? endLocation, int? detourToleranceMinutes
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$PreferencesCopyWithImpl<$Res>
|
||||
implements $PreferencesCopyWith<$Res> {
|
||||
_$PreferencesCopyWithImpl(this._self, this._then);
|
||||
|
||||
final Preferences _self;
|
||||
final $Res Function(Preferences) _then;
|
||||
|
||||
/// Create a copy of Preferences
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? scores = null,Object? maxTimeMinutes = null,Object? startLocation = null,Object? endLocation = freezed,Object? detourToleranceMinutes = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
scores: null == scores ? _self.scores : scores // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, int>,maxTimeMinutes: null == maxTimeMinutes ? _self.maxTimeMinutes : maxTimeMinutes // ignore: cast_nullable_to_non_nullable
|
||||
as int,startLocation: null == startLocation ? _self.startLocation : startLocation // ignore: cast_nullable_to_non_nullable
|
||||
as List<double>,endLocation: freezed == endLocation ? _self.endLocation : endLocation // ignore: cast_nullable_to_non_nullable
|
||||
as List<double>?,detourToleranceMinutes: freezed == detourToleranceMinutes ? _self.detourToleranceMinutes : detourToleranceMinutes // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [Preferences].
|
||||
extension PreferencesPatterns on Preferences {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _Preferences value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _Preferences() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _Preferences value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _Preferences():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _Preferences value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _Preferences() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( Map<String, int> scores, int maxTimeMinutes, List<double> startLocation, List<double>? endLocation, int? detourToleranceMinutes)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _Preferences() when $default != null:
|
||||
return $default(_that.scores,_that.maxTimeMinutes,_that.startLocation,_that.endLocation,_that.detourToleranceMinutes);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( Map<String, int> scores, int maxTimeMinutes, List<double> startLocation, List<double>? endLocation, int? detourToleranceMinutes) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _Preferences():
|
||||
return $default(_that.scores,_that.maxTimeMinutes,_that.startLocation,_that.endLocation,_that.detourToleranceMinutes);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( Map<String, int> scores, int maxTimeMinutes, List<double> startLocation, List<double>? endLocation, int? detourToleranceMinutes)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _Preferences() when $default != null:
|
||||
return $default(_that.scores,_that.maxTimeMinutes,_that.startLocation,_that.endLocation,_that.detourToleranceMinutes);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _Preferences implements Preferences {
|
||||
const _Preferences({required final Map<String, int> scores, required this.maxTimeMinutes, required final List<double> startLocation, final List<double>? endLocation, this.detourToleranceMinutes}): _scores = scores,_startLocation = startLocation,_endLocation = endLocation;
|
||||
factory _Preferences.fromJson(Map<String, dynamic> json) => _$PreferencesFromJson(json);
|
||||
|
||||
/// Scores keyed by preference type (e.g. 'sightseeing', 'shopping', 'nature')
|
||||
final Map<String, int> _scores;
|
||||
/// Scores keyed by preference type (e.g. 'sightseeing', 'shopping', 'nature')
|
||||
@override Map<String, int> get scores {
|
||||
if (_scores is EqualUnmodifiableMapView) return _scores;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(_scores);
|
||||
}
|
||||
|
||||
/// Maximum trip duration in minutes
|
||||
@override final int maxTimeMinutes;
|
||||
/// Required start location [lat, lon]
|
||||
final List<double> _startLocation;
|
||||
/// Required start location [lat, lon]
|
||||
@override List<double> get startLocation {
|
||||
if (_startLocation is EqualUnmodifiableListView) return _startLocation;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_startLocation);
|
||||
}
|
||||
|
||||
/// Optional end location
|
||||
final List<double>? _endLocation;
|
||||
/// Optional end location
|
||||
@override List<double>? get endLocation {
|
||||
final value = _endLocation;
|
||||
if (value == null) return null;
|
||||
if (_endLocation is EqualUnmodifiableListView) return _endLocation;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(value);
|
||||
}
|
||||
|
||||
/// Optional detour tolerance in minutes
|
||||
@override final int? detourToleranceMinutes;
|
||||
|
||||
/// Create a copy of Preferences
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$PreferencesCopyWith<_Preferences> get copyWith => __$PreferencesCopyWithImpl<_Preferences>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$PreferencesToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _Preferences&&const DeepCollectionEquality().equals(other._scores, _scores)&&(identical(other.maxTimeMinutes, maxTimeMinutes) || other.maxTimeMinutes == maxTimeMinutes)&&const DeepCollectionEquality().equals(other._startLocation, _startLocation)&&const DeepCollectionEquality().equals(other._endLocation, _endLocation)&&(identical(other.detourToleranceMinutes, detourToleranceMinutes) || other.detourToleranceMinutes == detourToleranceMinutes));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_scores),maxTimeMinutes,const DeepCollectionEquality().hash(_startLocation),const DeepCollectionEquality().hash(_endLocation),detourToleranceMinutes);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Preferences(scores: $scores, maxTimeMinutes: $maxTimeMinutes, startLocation: $startLocation, endLocation: $endLocation, detourToleranceMinutes: $detourToleranceMinutes)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$PreferencesCopyWith<$Res> implements $PreferencesCopyWith<$Res> {
|
||||
factory _$PreferencesCopyWith(_Preferences value, $Res Function(_Preferences) _then) = __$PreferencesCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
Map<String, int> scores, int maxTimeMinutes, List<double> startLocation, List<double>? endLocation, int? detourToleranceMinutes
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$PreferencesCopyWithImpl<$Res>
|
||||
implements _$PreferencesCopyWith<$Res> {
|
||||
__$PreferencesCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _Preferences _self;
|
||||
final $Res Function(_Preferences) _then;
|
||||
|
||||
/// Create a copy of Preferences
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? scores = null,Object? maxTimeMinutes = null,Object? startLocation = null,Object? endLocation = freezed,Object? detourToleranceMinutes = freezed,}) {
|
||||
return _then(_Preferences(
|
||||
scores: null == scores ? _self._scores : scores // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, int>,maxTimeMinutes: null == maxTimeMinutes ? _self.maxTimeMinutes : maxTimeMinutes // ignore: cast_nullable_to_non_nullable
|
||||
as int,startLocation: null == startLocation ? _self._startLocation : startLocation // ignore: cast_nullable_to_non_nullable
|
||||
as List<double>,endLocation: freezed == endLocation ? _self._endLocation : endLocation // ignore: cast_nullable_to_non_nullable
|
||||
as List<double>?,detourToleranceMinutes: freezed == detourToleranceMinutes ? _self.detourToleranceMinutes : detourToleranceMinutes // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
@@ -1,28 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'preferences.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_Preferences _$PreferencesFromJson(Map<String, dynamic> json) => _Preferences(
|
||||
scores: Map<String, int>.from(json['scores'] as Map),
|
||||
maxTimeMinutes: (json['maxTimeMinutes'] as num).toInt(),
|
||||
startLocation: (json['startLocation'] as List<dynamic>)
|
||||
.map((e) => (e as num).toDouble())
|
||||
.toList(),
|
||||
endLocation: (json['endLocation'] as List<dynamic>?)
|
||||
?.map((e) => (e as num).toDouble())
|
||||
.toList(),
|
||||
detourToleranceMinutes: (json['detourToleranceMinutes'] as num?)?.toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$PreferencesToJson(_Preferences instance) =>
|
||||
<String, dynamic>{
|
||||
'scores': instance.scores,
|
||||
'maxTimeMinutes': instance.maxTimeMinutes,
|
||||
'startLocation': instance.startLocation,
|
||||
'endLocation': instance.endLocation,
|
||||
'detourToleranceMinutes': instance.detourToleranceMinutes,
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
import 'package:anyway/domain/entities/landmark.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'trip.freezed.dart';
|
||||
part 'trip.g.dart';
|
||||
|
||||
|
||||
@unfreezed
|
||||
abstract class Trip with _$Trip {
|
||||
|
||||
factory Trip({
|
||||
required String uuid,
|
||||
// Duration totalTime,
|
||||
|
||||
/// total time in minutes
|
||||
int? totalTimeMinutes,
|
||||
|
||||
/// ordered list of landmarks in this trip
|
||||
required List<Landmark> landmarks,
|
||||
}) = _Trip;
|
||||
|
||||
factory Trip.fromJson(Map<String, Object?> json) => _$TripFromJson(json);
|
||||
|
||||
}
|
||||
@@ -1,278 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'trip.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$Trip {
|
||||
|
||||
String get uuid; set uuid(String value);// Duration totalTime,
|
||||
/// total time in minutes
|
||||
int? get totalTimeMinutes;// Duration totalTime,
|
||||
/// total time in minutes
|
||||
set totalTimeMinutes(int? value);/// ordered list of landmarks in this trip
|
||||
List<Landmark> get landmarks;/// ordered list of landmarks in this trip
|
||||
set landmarks(List<Landmark> value);
|
||||
/// Create a copy of Trip
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$TripCopyWith<Trip> get copyWith => _$TripCopyWithImpl<Trip>(this as Trip, _$identity);
|
||||
|
||||
/// Serializes this Trip to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Trip(uuid: $uuid, totalTimeMinutes: $totalTimeMinutes, landmarks: $landmarks)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $TripCopyWith<$Res> {
|
||||
factory $TripCopyWith(Trip value, $Res Function(Trip) _then) = _$TripCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String uuid, int? totalTimeMinutes, List<Landmark> landmarks
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$TripCopyWithImpl<$Res>
|
||||
implements $TripCopyWith<$Res> {
|
||||
_$TripCopyWithImpl(this._self, this._then);
|
||||
|
||||
final Trip _self;
|
||||
final $Res Function(Trip) _then;
|
||||
|
||||
/// Create a copy of Trip
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? uuid = null,Object? totalTimeMinutes = freezed,Object? landmarks = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
uuid: null == uuid ? _self.uuid : uuid // ignore: cast_nullable_to_non_nullable
|
||||
as String,totalTimeMinutes: freezed == totalTimeMinutes ? _self.totalTimeMinutes : totalTimeMinutes // ignore: cast_nullable_to_non_nullable
|
||||
as int?,landmarks: null == landmarks ? _self.landmarks : landmarks // ignore: cast_nullable_to_non_nullable
|
||||
as List<Landmark>,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [Trip].
|
||||
extension TripPatterns on Trip {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _Trip value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _Trip() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _Trip value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _Trip():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _Trip value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _Trip() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String uuid, int? totalTimeMinutes, List<Landmark> landmarks)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _Trip() when $default != null:
|
||||
return $default(_that.uuid,_that.totalTimeMinutes,_that.landmarks);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String uuid, int? totalTimeMinutes, List<Landmark> landmarks) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _Trip():
|
||||
return $default(_that.uuid,_that.totalTimeMinutes,_that.landmarks);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String uuid, int? totalTimeMinutes, List<Landmark> landmarks)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _Trip() when $default != null:
|
||||
return $default(_that.uuid,_that.totalTimeMinutes,_that.landmarks);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _Trip implements Trip {
|
||||
_Trip({required this.uuid, this.totalTimeMinutes, required this.landmarks});
|
||||
factory _Trip.fromJson(Map<String, dynamic> json) => _$TripFromJson(json);
|
||||
|
||||
@override String uuid;
|
||||
// Duration totalTime,
|
||||
/// total time in minutes
|
||||
@override int? totalTimeMinutes;
|
||||
/// ordered list of landmarks in this trip
|
||||
@override List<Landmark> landmarks;
|
||||
|
||||
/// Create a copy of Trip
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$TripCopyWith<_Trip> get copyWith => __$TripCopyWithImpl<_Trip>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$TripToJson(this, );
|
||||
}
|
||||
|
||||
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Trip(uuid: $uuid, totalTimeMinutes: $totalTimeMinutes, landmarks: $landmarks)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$TripCopyWith<$Res> implements $TripCopyWith<$Res> {
|
||||
factory _$TripCopyWith(_Trip value, $Res Function(_Trip) _then) = __$TripCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String uuid, int? totalTimeMinutes, List<Landmark> landmarks
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$TripCopyWithImpl<$Res>
|
||||
implements _$TripCopyWith<$Res> {
|
||||
__$TripCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _Trip _self;
|
||||
final $Res Function(_Trip) _then;
|
||||
|
||||
/// Create a copy of Trip
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? uuid = null,Object? totalTimeMinutes = freezed,Object? landmarks = null,}) {
|
||||
return _then(_Trip(
|
||||
uuid: null == uuid ? _self.uuid : uuid // ignore: cast_nullable_to_non_nullable
|
||||
as String,totalTimeMinutes: freezed == totalTimeMinutes ? _self.totalTimeMinutes : totalTimeMinutes // ignore: cast_nullable_to_non_nullable
|
||||
as int?,landmarks: null == landmarks ? _self.landmarks : landmarks // ignore: cast_nullable_to_non_nullable
|
||||
as List<Landmark>,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
@@ -1,21 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'trip.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_Trip _$TripFromJson(Map<String, dynamic> json) => _Trip(
|
||||
uuid: json['uuid'] as String,
|
||||
totalTimeMinutes: (json['totalTimeMinutes'] as num?)?.toInt(),
|
||||
landmarks: (json['landmarks'] as List<dynamic>)
|
||||
.map((e) => Landmark.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$TripToJson(_Trip instance) => <String, dynamic>{
|
||||
'uuid': instance.uuid,
|
||||
'totalTimeMinutes': instance.totalTimeMinutes,
|
||||
'landmarks': instance.landmarks,
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
abstract class OnboardingRepository {
|
||||
/// Returns true when the user accepted the onboarding agreement
|
||||
Future<bool> isOnboarded();
|
||||
|
||||
/// Sets the onboarding completion flag
|
||||
Future<void> setOnboarded(bool value);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import 'package:anyway/domain/entities/preferences.dart';
|
||||
|
||||
abstract class PreferencesRepository {
|
||||
Future<Preferences> getPreferences();
|
||||
Future<void> savePreferences(Preferences preferences);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import 'package:anyway/domain/entities/landmark.dart';
|
||||
import 'package:anyway/domain/entities/preferences.dart';
|
||||
import 'package:anyway/domain/entities/trip.dart';
|
||||
|
||||
abstract class TripRepository {
|
||||
Future<Trip> getTrip({Preferences? preferences, String? tripUUID, List<Landmark>? landmarks});
|
||||
|
||||
Future<List<Landmark>> searchLandmarks(Preferences preferences);
|
||||
|
||||
// TODO - should these be moved to a separate local repository?
|
||||
// not every TripRepository should have a concept of "all saved trips"
|
||||
Future<List<Trip>> getSavedTrips();
|
||||
|
||||
Future<Trip?> getSavedTrip(String uuid);
|
||||
|
||||
Future<void> saveTrip(Trip trip);
|
||||
|
||||
Future<void> deleteSavedTrip(String uuid);
|
||||
}
|
||||
@@ -1,39 +1,26 @@
|
||||
import 'package:anyway/presentation/pages/start.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:anyway/core/constants.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:anyway/constants.dart';
|
||||
import 'package:anyway/utils/get_first_page.dart';
|
||||
import 'package:anyway/utils/load_trips.dart';
|
||||
|
||||
|
||||
/// The app entry point.
|
||||
/// Initializes persistence, sets up dependency injection via ProviderScope,
|
||||
/// and determines which screen (login or main app) to show based on auth state.
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
void main() => runApp(const App());
|
||||
|
||||
// initialize local persistence (shared preferences)
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
// Some global variables
|
||||
final GlobalKey<ScaffoldMessengerState> rootScaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
|
||||
final SavedTrips savedTrips = SavedTrips();
|
||||
// the list of saved trips is then populated implicitly by getFirstPage()
|
||||
|
||||
// the app wrapped in ProviderScope
|
||||
runApp(const ProviderScope(child: MyApp()));
|
||||
}
|
||||
|
||||
class MyApp extends ConsumerWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
class App extends StatelessWidget {
|
||||
const App({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: APP_NAME,
|
||||
theme: APP_THEME,
|
||||
scaffoldMessengerKey: rootScaffoldMessengerKey,
|
||||
home: const StartPage()
|
||||
|
||||
// TODO - set up routing
|
||||
// onGenerateRoute: AppRouter.onGenerateRoute,
|
||||
);
|
||||
}
|
||||
Widget build(BuildContext context) => MaterialApp(
|
||||
title: APP_NAME,
|
||||
home: getFirstPage(),
|
||||
theme: APP_THEME,
|
||||
scaffoldMessengerKey: rootScaffoldMessengerKey
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ class _CurrentTripErrorMessageState extends State<CurrentTripErrorMessage> {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Text(
|
||||
Text(
|
||||
"😢",
|
||||
style: TextStyle(
|
||||
fontSize: 40,
|
||||
@@ -8,7 +8,7 @@ import 'package:anyway/structs/trip.dart';
|
||||
class CurrentTripGreeter extends StatefulWidget {
|
||||
final Trip trip;
|
||||
|
||||
const CurrentTripGreeter({
|
||||
CurrentTripGreeter({
|
||||
super.key,
|
||||
required this.trip,
|
||||
});
|
||||
@@ -47,4 +47,4 @@ class _CurrentTripGreeterState extends State<CurrentTripGreeter> {
|
||||
)
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import 'package:anyway/modules/landmark_card.dart';
|
||||
|
||||
// Returns a list of widgets that represent the landmarks matching the given selector
|
||||
List<Widget> landmarksList(Trip trip, {required bool Function(Landmark) selector}) {
|
||||
|
||||
|
||||
List<Widget> children = [];
|
||||
|
||||
if (trip.landmarks.isEmpty || trip.landmarks.length <= 1 && trip.landmarks.first.type == typeStart ) {
|
||||
@@ -30,10 +30,10 @@ List<Widget> landmarksList(Trip trip, {required bool Function(Landmark) selector
|
||||
Landmark? nextLandmark = landmark.next;
|
||||
while (nextLandmark != null && nextLandmark.visited) {
|
||||
nextLandmark = nextLandmark.next;
|
||||
}
|
||||
}
|
||||
if (nextLandmark != null) {
|
||||
children.add(
|
||||
StepBetweenLandmarks(current: landmark, next: nextLandmark)
|
||||
StepBetweenLandmarks(current: landmark, next: nextLandmark!)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,7 @@ class _CurrentTripLoadingIndicatorState extends State<CurrentTripLoadingIndicato
|
||||
|
||||
// automatically cycle through the greeter texts
|
||||
class StatusText extends StatefulWidget {
|
||||
const StatusText({super.key});
|
||||
const StatusText({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_StatusTextState createState() => _StatusTextState();
|
||||
@@ -110,10 +110,10 @@ class AnimatedDotsText extends StatefulWidget {
|
||||
final TextStyle style;
|
||||
|
||||
const AnimatedDotsText({
|
||||
super.key,
|
||||
Key? key,
|
||||
required this.baseText,
|
||||
required this.style,
|
||||
});
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_AnimatedDotsTextState createState() => _AnimatedDotsTextState();
|
||||
@@ -13,7 +13,7 @@ import 'package:anyway/modules/landmark_map_marker.dart';
|
||||
class CurrentTripMap extends StatefulWidget {
|
||||
final Trip? trip;
|
||||
|
||||
const CurrentTripMap({super.key, this.trip});
|
||||
CurrentTripMap({this.trip});
|
||||
|
||||
@override
|
||||
State<CurrentTripMap> createState() => _CurrentTripMapState();
|
||||
@@ -22,7 +22,7 @@ class CurrentTripMap extends StatefulWidget {
|
||||
class _CurrentTripMapState extends State<CurrentTripMap> {
|
||||
late GoogleMapController mapController;
|
||||
|
||||
final CameraPosition _cameraPosition = const CameraPosition(
|
||||
CameraPosition _cameraPosition = CameraPosition(
|
||||
target: LatLng(48.8566, 2.3522),
|
||||
zoom: 11.0,
|
||||
);
|
||||
@@ -41,7 +41,7 @@ class _CurrentTripMapState extends State<CurrentTripMap> {
|
||||
void dispose() {
|
||||
widget.trip?.removeListener(setMapMarkers);
|
||||
widget.trip?.removeListener(setMapRoute);
|
||||
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ class _CurrentTripPanelState extends State<CurrentTripPanel> {
|
||||
// this way the greeter will be centered when the panel is collapsed
|
||||
// note that we need to account for the padding above
|
||||
height: MediaQuery.of(context).size.height * TRIP_PANEL_MIN_HEIGHT - 10,
|
||||
child: Center(child:
|
||||
child: Center(child:
|
||||
AutoSizeText(
|
||||
maxLines: 1,
|
||||
'Error',
|
||||
@@ -81,7 +81,7 @@ class _CurrentTripPanelState extends State<CurrentTripPanel> {
|
||||
),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
padding: EdgeInsets.all(10),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
@@ -94,6 +94,9 @@ class _CurrentTripPanelState extends State<CurrentTripPanel> {
|
||||
ExpansionTile(
|
||||
leading: const Icon(Icons.location_on),
|
||||
title: const Text('Visited Landmarks (tap to expand)'),
|
||||
children: [
|
||||
...landmarksList(widget.trip, selector: (Landmark landmark) => landmark.visited),
|
||||
],
|
||||
visualDensity: VisualDensity.compact,
|
||||
collapsedShape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
@@ -101,15 +104,12 @@ class _CurrentTripPanelState extends State<CurrentTripPanel> {
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
children: [
|
||||
...landmarksList(widget.trip, selector: (Landmark landmark) => landmark.visited),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
|
||||
const Padding(padding: EdgeInsets.only(top: 10)),
|
||||
|
||||
@@ -20,14 +20,14 @@ class _saveButtonState extends State<saveButton> {
|
||||
onPressed: () async {
|
||||
savedTrips.addTrip(widget.trip);
|
||||
rootScaffoldMessengerKey.currentState!.showSnackBar(
|
||||
const SnackBar(
|
||||
SnackBar(
|
||||
content: Text('Trip saved'),
|
||||
duration: Duration(seconds: 2),
|
||||
dismissDirection: DismissDirection.horizontal
|
||||
)
|
||||
);
|
||||
},
|
||||
child: const SizedBox(
|
||||
child: SizedBox(
|
||||
width: 100,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -13,12 +13,12 @@ import 'package:anyway/structs/landmark.dart';
|
||||
class LandmarkCard extends StatefulWidget {
|
||||
final Landmark landmark;
|
||||
final Trip parentTrip;
|
||||
|
||||
const LandmarkCard(
|
||||
|
||||
LandmarkCard(
|
||||
this.landmark,
|
||||
this.parentTrip,
|
||||
);
|
||||
|
||||
|
||||
@override
|
||||
_LandmarkCardState createState() => _LandmarkCardState();
|
||||
}
|
||||
@@ -26,7 +26,7 @@ class LandmarkCard extends StatefulWidget {
|
||||
|
||||
class _LandmarkCardState extends State<LandmarkCard> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
constraints: BoxConstraints(
|
||||
// express the max height in terms text lines
|
||||
@@ -38,7 +38,7 @@ class _LandmarkCardState extends State<LandmarkCard> {
|
||||
),
|
||||
elevation: 5,
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
|
||||
|
||||
// if the image is available, display it on the left side of the card, otherwise only display the text
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -66,11 +66,11 @@ class _LandmarkCardState extends State<LandmarkCard> {
|
||||
color: PRIMARY_COLOR,
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(5),
|
||||
padding: EdgeInsets.all(5),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.timer_outlined, size: 16),
|
||||
Icon(Icons.timer_outlined, size: 16),
|
||||
Text("${widget.landmark.duration?.inMinutes} minutes"),
|
||||
],
|
||||
)
|
||||
@@ -97,7 +97,7 @@ class _LandmarkCardState extends State<LandmarkCard> {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 2,
|
||||
),
|
||||
|
||||
|
||||
if (widget.landmark.nameEN != null)
|
||||
Text(
|
||||
widget.landmark.nameEN!,
|
||||
@@ -114,7 +114,7 @@ class _LandmarkCardState extends State<LandmarkCard> {
|
||||
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.only(left: 5, right: 5, bottom: 10),
|
||||
padding: EdgeInsets.only(left: 5, right: 5, bottom: 10),
|
||||
// the scroll view should be flush once the buttons are scrolled to the left
|
||||
// but initially there should be some padding
|
||||
child: Wrap(
|
||||
@@ -124,7 +124,7 @@ class _LandmarkCardState extends State<LandmarkCard> {
|
||||
doneToggleButton(),
|
||||
if (widget.landmark.websiteURL != null)
|
||||
websiteButton(),
|
||||
|
||||
|
||||
optionsButton()
|
||||
],
|
||||
),
|
||||
@@ -181,7 +181,7 @@ class _LandmarkCardState extends State<LandmarkCard> {
|
||||
title: const Text('Favorite'),
|
||||
onTap: () async {
|
||||
rootScaffoldMessengerKey.currentState!.showSnackBar(
|
||||
const SnackBar(content: Text("Not implemented yet"))
|
||||
SnackBar(content: Text("Not implemented yet"))
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -193,7 +193,7 @@ class _LandmarkCardState extends State<LandmarkCard> {
|
||||
|
||||
|
||||
Widget imagePlaceholder (Landmark landmark) => Expanded(
|
||||
child:
|
||||
child:
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
@@ -7,7 +7,7 @@ class ThemedMarker extends StatelessWidget {
|
||||
final Landmark landmark;
|
||||
final int position;
|
||||
|
||||
const ThemedMarker({
|
||||
ThemedMarker({
|
||||
super.key,
|
||||
required this.landmark,
|
||||
required this.position
|
||||
@@ -24,12 +24,12 @@ class ThemedMarker extends StatelessWidget {
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(5),
|
||||
padding: EdgeInsets.all(5),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Text('$position', style: const TextStyle(color: Colors.black, fontSize: 25)),
|
||||
child: Text('$position', style: TextStyle(color: Colors.black, fontSize: 25)),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -40,7 +40,7 @@ class ThemedMarker extends StatelessWidget {
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: landmark.visited ? const LinearGradient(colors: [Colors.grey, Colors.white]) : APP_GRADIENT,
|
||||
gradient: landmark.visited ? LinearGradient(colors: [Colors.grey, Colors.white]) : APP_GRADIENT,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.black, width: 5),
|
||||
),
|
||||
@@ -54,4 +54,4 @@ class ThemedMarker extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ class NewTripButton extends StatefulWidget {
|
||||
final Trip trip;
|
||||
final UserPreferences preferences;
|
||||
|
||||
const NewTripButton({super.key,
|
||||
const NewTripButton({
|
||||
required this.trip,
|
||||
required this.preferences,
|
||||
});
|
||||
@@ -35,8 +35,8 @@ class _NewTripButtonState extends State<NewTripButton> {
|
||||
return FloatingActionButton.extended(
|
||||
onPressed: onPressed,
|
||||
icon: const Icon(Icons.directions),
|
||||
label: const AutoSizeText('Start planning!'),
|
||||
);
|
||||
label: AutoSizeText('Start planning!'),
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -21,7 +21,7 @@ const Map<String, List> debugLocations = {
|
||||
class NewTripLocationSearch extends StatefulWidget {
|
||||
Future<SharedPreferences> prefs = SharedPreferences.getInstance();
|
||||
Trip trip;
|
||||
|
||||
|
||||
NewTripLocationSearch(
|
||||
this.trip,
|
||||
);
|
||||
@@ -71,13 +71,13 @@ class _NewTripLocationSearchState extends State<NewTripLocationSearch> {
|
||||
hintText: 'Enter a city name or long press on the map.',
|
||||
onSubmitted: setTripLocation,
|
||||
controller: _controller,
|
||||
leading: const Icon(Icons.search),
|
||||
leading: Icon(Icons.search),
|
||||
trailing: [
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
setTripLocation(_controller.text);
|
||||
},
|
||||
child: const Text('Search'),
|
||||
child: Text('Search'),
|
||||
)
|
||||
]
|
||||
);
|
||||
@@ -97,7 +97,7 @@ class _NewTripLocationSearchState extends State<NewTripLocationSearch> {
|
||||
)
|
||||
);
|
||||
},
|
||||
child: const Text('Use current location'),
|
||||
child: Text('Use current location'),
|
||||
);
|
||||
|
||||
@override
|
||||
@@ -106,4 +106,4 @@ class _NewTripMapState extends State<NewTripMap> {
|
||||
myLocationEnabled: useLocation,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import 'package:flutter/material.dart';
|
||||
class NewTripOptionsButton extends StatefulWidget {
|
||||
final Trip trip;
|
||||
|
||||
const NewTripOptionsButton({super.key, required this.trip});
|
||||
const NewTripOptionsButton({required this.trip});
|
||||
|
||||
@override
|
||||
State<NewTripOptionsButton> createState() => _NewTripOptionsButtonState();
|
||||
@@ -33,7 +33,7 @@ class _NewTripOptionsButtonState extends State<NewTripOptionsButton> {
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
label: const AutoSizeText('Set preferences')
|
||||
);
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -13,8 +13,8 @@ class OnboardingAgreementCard extends StatefulWidget {
|
||||
final ValueChanged<bool> onAgreementChanged;
|
||||
|
||||
|
||||
const OnboardingAgreementCard({
|
||||
super.key,
|
||||
OnboardingAgreementCard({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.imagePath,
|
||||
@@ -30,12 +30,12 @@ class _OnboardingAgreementCardState extends State<OnboardingAgreementCard> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
OnboardingCard(title: widget.title, description: widget.description, imagePath: widget.imagePath),
|
||||
const Padding(padding: EdgeInsets.only(top: 20)),
|
||||
Padding(padding: EdgeInsets.only(top: 20)),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
@@ -65,7 +65,7 @@ class _OnboardingAgreementCardState extends State<OnboardingAgreementCard> {
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
|
||||
// The text of the agreement
|
||||
Text(
|
||||
"I agree to the ",
|
||||
@@ -73,7 +73,7 @@ class _OnboardingAgreementCardState extends State<OnboardingAgreementCard> {
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
// The clickable text of the agreement that shows the agreement text
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
@@ -91,9 +91,9 @@ class _OnboardingAgreementCardState extends State<OnboardingAgreementCard> {
|
||||
data: snapshot.data.toString(),
|
||||
);
|
||||
} else {
|
||||
return const CircularProgressIndicator();
|
||||
return CircularProgressIndicator();
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
)
|
||||
);
|
||||
@@ -6,7 +6,7 @@ class OnboardingCard extends StatelessWidget {
|
||||
final String description;
|
||||
final String imagePath;
|
||||
|
||||
const OnboardingCard({super.key,
|
||||
const OnboardingCard({
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.imagePath,
|
||||
@@ -14,9 +14,9 @@ class OnboardingCard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
@@ -26,12 +26,12 @@ class OnboardingCard extends StatelessWidget {
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.only(top: 20)),
|
||||
Padding(padding: EdgeInsets.only(top: 20)),
|
||||
SvgPicture.asset(
|
||||
imagePath,
|
||||
height: 200,
|
||||
),
|
||||
const Padding(padding: EdgeInsets.only(top: 20)),
|
||||
Padding(padding: EdgeInsets.only(top: 20)),
|
||||
Text(
|
||||
description,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
64
frontend/lib/modules/trips_saved_list.dart
Normal file
64
frontend/lib/modules/trips_saved_list.dart
Normal file
@@ -0,0 +1,64 @@
|
||||
import 'package:anyway/pages/current_trip.dart';
|
||||
import 'package:anyway/utils/load_trips.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
|
||||
|
||||
class TripsOverview extends StatefulWidget {
|
||||
final SavedTrips trips;
|
||||
const TripsOverview({
|
||||
super.key,
|
||||
required this.trips,
|
||||
});
|
||||
|
||||
@override
|
||||
State<TripsOverview> createState() => _TripsOverviewState();
|
||||
}
|
||||
|
||||
class _TripsOverviewState extends State<TripsOverview> {
|
||||
Widget listBuild (BuildContext context, SavedTrips trips) {
|
||||
List<Widget> children;
|
||||
List<Trip> items = trips.trips;
|
||||
children = List<Widget>.generate(items.length, (index) {
|
||||
Trip trip = items[index];
|
||||
return ListTile(
|
||||
title: FutureBuilder(
|
||||
future: trip.cityName,
|
||||
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return Text("Trip to ${snapshot.data}");
|
||||
} else if (snapshot.hasError) {
|
||||
return Text("Error: ${snapshot.error}");
|
||||
} else {
|
||||
return const Text("Trip to ...");
|
||||
}
|
||||
},
|
||||
),
|
||||
leading: Icon(Icons.pin_drop),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => TripPage(trip: trip)
|
||||
)
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
return ListView(
|
||||
children: children,
|
||||
padding: const EdgeInsets.only(top: 0),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListenableBuilder(
|
||||
listenable: widget.trips,
|
||||
builder: (BuildContext context, Widget? child) {
|
||||
return listBuild(context, widget.trips);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
|
||||
|
||||
List<Map<String, dynamic>> locationActions = [
|
||||
{'name': 'Toilet', 'action': () {}},
|
||||
{'name': 'Food', 'action': () {}},
|
||||
{'name': 'Surrounding landmarks', 'action': () {}},
|
||||
|
||||
];
|
||||
|
||||
class CurrentTripLocations extends StatefulWidget {
|
||||
final Trip? trip;
|
||||
|
||||
const CurrentTripLocations({super.key, this.trip});
|
||||
|
||||
@override
|
||||
State<CurrentTripLocations> createState() => _CurrentTripLocationsState();
|
||||
}
|
||||
|
||||
class _CurrentTripLocationsState extends State<CurrentTripLocations> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// A horizontally scrolling list of buttons with predefined actions
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 10.0),
|
||||
child: Row(
|
||||
children: [
|
||||
if (widget.trip != null)
|
||||
for (Map action in locationActions)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 3.0),
|
||||
child: ElevatedButton(
|
||||
onPressed: action['action'],
|
||||
child: AutoSizeText(
|
||||
action['name'],
|
||||
maxLines: 1,
|
||||
minFontSize: 8,
|
||||
maxFontSize: 16,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:anyway/modules/current_trip_map.dart';
|
||||
import 'package:anyway/modules/current_trip_locations.dart';
|
||||
|
||||
|
||||
class CurrentTripOverview extends StatefulWidget {
|
||||
final Trip? trip;
|
||||
|
||||
const CurrentTripOverview({super.key, this.trip});
|
||||
|
||||
|
||||
@override
|
||||
State<CurrentTripOverview> createState() => _CurrentTripOverviewState();
|
||||
}
|
||||
|
||||
class _CurrentTripOverviewState extends State<CurrentTripOverview> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// The background map has a horizontally scrolling list of rounded buttons overlaid
|
||||
return Stack(
|
||||
alignment: Alignment.topLeft,
|
||||
children: [
|
||||
CurrentTripMap(trip: widget.trip),
|
||||
CurrentTripLocations(trip: widget.trip),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
class OnboardingCard extends StatelessWidget {
|
||||
final String title;
|
||||
final String description;
|
||||
final String imagePath;
|
||||
|
||||
const OnboardingCard({super.key,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.imagePath,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.headlineLarge!.copyWith(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.only(top: 20)),
|
||||
SvgPicture.asset(
|
||||
imagePath,
|
||||
height: 200,
|
||||
),
|
||||
const Padding(padding: EdgeInsets.only(top: 20)),
|
||||
Text(
|
||||
description,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
]
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
import 'package:anyway/pages/current_trip.dart';
|
||||
import 'package:anyway/utils/load_trips.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
|
||||
|
||||
class TripsOverview extends StatefulWidget {
|
||||
final SavedTrips trips;
|
||||
const TripsOverview({
|
||||
super.key,
|
||||
required this.trips,
|
||||
});
|
||||
|
||||
@override
|
||||
State<TripsOverview> createState() => _TripsOverviewState();
|
||||
}
|
||||
|
||||
class _TripsOverviewState extends State<TripsOverview> {
|
||||
Widget tripListItemBuilder(BuildContext context, int index) {
|
||||
Trip trip = widget.trips.trips[index];
|
||||
return ListTile(
|
||||
title: FutureBuilder(
|
||||
future: trip.cityName,
|
||||
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return Text("Trip to ${snapshot.data}");
|
||||
} else if (snapshot.hasError) {
|
||||
return Text("Error: ${snapshot.error}");
|
||||
} else {
|
||||
return const Text("Trip to ...");
|
||||
}
|
||||
},
|
||||
),
|
||||
// emoji of the country flag of the trip's country
|
||||
leading: const Icon(Icons.pin_drop),
|
||||
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => TripPage(trip: trip)
|
||||
)
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Widget listBuild (BuildContext context, SavedTrips trips) {
|
||||
// List<Widget> children;
|
||||
// List<Trip> items = trips.trips;
|
||||
// children = List<Widget>.generate(items.length, (index) {
|
||||
// Trip trip = items[index];
|
||||
// return ListTile(
|
||||
// title: FutureBuilder(
|
||||
// future: trip.cityName,
|
||||
// builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
|
||||
// if (snapshot.hasData) {
|
||||
// return Text("Trip to ${snapshot.data}");
|
||||
// } else if (snapshot.hasError) {
|
||||
// return Text("Error: ${snapshot.error}");
|
||||
// } else {
|
||||
// return const Text("Trip to ...");
|
||||
// }
|
||||
// },
|
||||
// ),
|
||||
// leading: const Icon(Icons.pin_drop),
|
||||
// onTap: () {
|
||||
// Navigator.of(context).push(
|
||||
// MaterialPageRoute(
|
||||
// builder: (context) => TripPage(trip: trip)
|
||||
// )
|
||||
// );
|
||||
// },
|
||||
// );
|
||||
// });
|
||||
|
||||
// return ListView(
|
||||
// padding: const EdgeInsets.only(top: 0),
|
||||
// children: children,
|
||||
// );
|
||||
// }
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListenableBuilder(
|
||||
listenable: widget.trips,
|
||||
builder: (BuildContext context, Widget? child) => ListView.builder(
|
||||
itemCount: widget.trips.trips.length,
|
||||
itemBuilder: tripListItemBuilder,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:sliding_up_panel/sliding_up_panel.dart';
|
||||
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
import 'package:anyway/modules/current_trip_overview.dart';
|
||||
import 'package:anyway/modules/current_trip_map.dart';
|
||||
import 'package:anyway/modules/current_trip_panel.dart';
|
||||
|
||||
final Shader textGradient = APP_GRADIENT.createShader(const Rect.fromLTWH(0.0, 0.0, 200.0, 70.0));
|
||||
final Shader textGradient = APP_GRADIENT.createShader(Rect.fromLTWH(0.0, 0.0, 200.0, 70.0));
|
||||
TextStyle greeterStyle = TextStyle(
|
||||
foreground: Paint()..shader = textGradient,
|
||||
fontWeight: FontWeight.bold,
|
||||
@@ -18,7 +18,7 @@ TextStyle greeterStyle = TextStyle(
|
||||
class TripPage extends StatefulWidget {
|
||||
final Trip trip;
|
||||
|
||||
const TripPage({super.key,
|
||||
TripPage({
|
||||
required this.trip,
|
||||
});
|
||||
|
||||
@@ -39,7 +39,7 @@ class _TripPageState extends State<TripPage> with ScaffoldLayout{
|
||||
panelBuilder: (scrollcontroller) => CurrentTripPanel(controller: scrollcontroller, trip: widget.trip),
|
||||
// using collapsed and panelBuilder seems to show both at the same time, so we include the greeter in the panelBuilder
|
||||
// collapsed: Greeter(trip: widget.trip),
|
||||
body: CurrentTripOverview(trip: widget.trip),
|
||||
body: CurrentTripMap(trip: widget.trip),
|
||||
minHeight: MediaQuery.of(context).size.height * TRIP_PANEL_MIN_HEIGHT,
|
||||
maxHeight: MediaQuery.of(context).size.height * TRIP_PANEL_MAX_HEIGHT,
|
||||
// padding in this context is annoying: it offsets the notion of vertical alignment.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user