27 Commits

Author SHA1 Message Date
c3e72001be hot fix to reintroduce the /trip/new endpoint
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Failing after 1m39s
Build and deploy the backend to staging / Add a comment to the PR to notify about the deployment (pull_request) Successful in 4s
Run linting on the backend code / Build (pull_request) Successful in 2m32s
Run testing on the backend code / Build (pull_request) Failing after 5m25s
2025-12-07 18:00:06 +01:00
f09836c470 Merge pull request 'fix/cluster-manager-crash-due-to-overpass-exception' (#72) from fix/cluster-manager-crash-due-to-overpass-exception into main
Some checks failed
Build and deploy the backend to production / Build and push image (push) Successful in 1m50s
Build and release apps to production track / Get version (push) Has been cancelled
Build and release apps to production track / Build and upload android app (push) Has been cancelled
Build and release apps to production track / Build and upload ios app (push) Has been cancelled
Reviewed-on: #72
2025-11-24 19:22:20 +00:00
9e90aed957 better logs in cluster manager
Some checks failed
Build and deploy the backend to staging / Deploy to staging (pull_request) Has been cancelled
Build and deploy the backend to staging / Build and push image (pull_request) Has been cancelled
Run linting on the backend code / Build (pull_request) Successful in 2m34s
Run testing on the backend code / Build (pull_request) Failing after 55m28s
2025-11-24 15:56:34 +01:00
d176416b15 changed pylint install
Some checks failed
Build and deploy the backend to staging / Deploy to staging (pull_request) Has been cancelled
Build and deploy the backend to staging / Build and push image (pull_request) Has been cancelled
Run linting on the backend code / Build (pull_request) Successful in 2m32s
Run testing on the backend code / Build (pull_request) Has been cancelled
2025-11-24 15:43:23 +01:00
5063bc2b69 switched back to manual install of uv for testing 2025-11-24 15:42:20 +01:00
e95088ad25 simpler lint job
Some checks failed
Build and deploy the backend to staging / Deploy to staging (pull_request) Has been cancelled
Build and deploy the backend to staging / Build and push image (pull_request) Has been cancelled
Run linting on the backend code / Build (pull_request) Failing after 2m24s
Run testing on the backend code / Build (pull_request) Failing after 22s
2025-11-24 15:35:25 +01:00
313bdede03 Merge pull request 'backend - towards a better gitops deploy strategy' (#70) from feature/staging-deployment-on-pr into main
Reviewed-on: #70
2025-11-24 14:30:56 +00:00
8ceaf549e4 other test
Some checks failed
Build and deploy the backend to staging / Deploy to staging (pull_request) Has been cancelled
Build and deploy the backend to staging / Build and push image (pull_request) Has been cancelled
Run linting on the backend code / Build (pull_request) Has been cancelled
Run testing on the backend code / Build (pull_request) Failing after 22s
2025-11-24 15:27:58 +01:00
63d3ff63e7 changed to manual uv install
Some checks failed
Build and deploy the backend to staging / Deploy to staging (pull_request) Has been cancelled
Build and deploy the backend to staging / Build and push image (pull_request) Has been cancelled
Run linting on the backend code / Build (pull_request) Failing after 5m16s
Run testing on the backend code / Build (pull_request) Has been cancelled
2025-11-24 15:22:03 +01:00
e1dbbbb274 fixed ci for lint and test
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 2m48s
Run linting on the backend code / Build (pull_request) Failing after 41s
Run testing on the backend code / Build (pull_request) Failing after 25s
Build and deploy the backend to staging / Deploy to staging (pull_request) Failing after 1m0s
2025-11-24 15:14:46 +01:00
139555dc8e Merge remote-tracking branch 'origin/upgrade/move-backend-to-uv' into fix/cluster-manager-crash-due-to-overpass-exception
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 2m23s
Run linting on the backend code / Build (pull_request) Failing after 2m22s
Run testing on the backend code / Build (pull_request) Failing after 2m26s
Build and deploy the backend to staging / Deploy to staging (pull_request) Failing after 47s
2025-11-24 14:57:02 +01:00
1785e125c7 detected -> found in cluster manager
Some checks failed
Run testing on the backend code / Build (pull_request) Has been cancelled
Run linting on the backend code / Build (pull_request) Has been cancelled
Build and deploy the backend to staging / Deploy to staging (pull_request) Has been cancelled
Build and deploy the backend to staging / Build and push image (pull_request) Has been cancelled
2025-11-24 14:53:31 +01:00
82d37288d6 stop tracking env 2025-11-24 14:53:13 +01:00
9b0821926c fix for cluster manager
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 2m11s
Run linting on the backend code / Build (pull_request) Failing after 2m22s
Run testing on the backend code / Build (pull_request) Failing after 2m26s
Build and deploy the backend to staging / Deploy to staging (pull_request) Failing after 3h3m27s
2025-11-20 18:48:26 +01:00
0514fa063f suggested fix to avoid UnboundLocalError 2025-11-20 18:47:34 +01:00
9ccf68d983 fixed the toilets and works with uv now
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 1m58s
Run linting on the backend code / Build (pull_request) Failing after 20s
Run testing on the backend code / Build (pull_request) Failing after 22m6s
Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 1m8s
2025-07-27 17:13:11 +02:00
132aa5a19b changed to no dev when building the docker image
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 2m24s
Run linting on the backend code / Build (pull_request) Failing after 21s
Run testing on the backend code / Build (pull_request) Failing after 22m41s
Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 36s
2025-07-26 12:44:41 +02:00
19b0c37a97 fixed the missing dependency in the refiner and changed the test run to using uv 2025-07-26 12:44:12 +02:00
ecdef605a7 cleanup and removed pipenv files 2025-07-26 12:41:58 +02:00
e2a918112b changed to uv fo managing dependencies 2025-07-26 12:41:15 +02:00
96b0718081 removed unused landmark attributes
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 1m39s
Run linting on the backend code / Build (pull_request) Successful in 31s
Run testing on the backend code / Build (pull_request) Failing after 49s
Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 38s
2025-07-13 17:47:12 +02:00
d9e5d9dac6 fixed dependcu
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 2m8s
Run linting on the backend code / Build (pull_request) Successful in 29s
Run testing on the backend code / Build (pull_request) Failing after 46s
Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 28s
2025-07-13 17:45:13 +02:00
b0f9d31ee2 Implement backend API for landmarks, trip optimization, and toilet locations
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 1m49s
Run linting on the backend code / Build (pull_request) Successful in 30s
Run testing on the backend code / Build (pull_request) Failing after 45s
Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 32s
- Added landmarks_router.py to handle landmark retrieval based on user preferences and location.
- Implemented optimization_router.py for trip optimization, including handling preferences and landmarks.
- Created toilets_router.py to fetch toilet locations within a specified radius from a given location.
- Enhanced error handling and logging across all new endpoints.
- Generated a comprehensive report.html for test results and environment details.
2025-07-13 17:43:24 +02:00
54bc9028ad simplified test pipeline
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 1m38s
Run linting on the backend code / Build (pull_request) Successful in 27s
Run testing on the backend code / Build (pull_request) Failing after 17m36s
Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 35s
2025-07-02 21:59:07 +02:00
37926e68ec fixed typo in invalid inputs
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 2m24s
Run linting on the backend code / Build (pull_request) Successful in 27s
Run testing on the backend code / Build (pull_request) Failing after 20m39s
Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 32s
2025-07-02 21:58:47 +02:00
e2d3d29956 working split
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 1m46s
Run linting on the backend code / Build (pull_request) Successful in 2m31s
Run testing on the backend code / Build (pull_request) Failing after 12m37s
Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 29s
2025-06-22 14:24:00 +02:00
6921ab57f8 added more structure
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 3m29s
Run linting on the backend code / Build (pull_request) Successful in 27s
Run testing on the backend code / Build (pull_request) Failing after 12m29s
Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 34s
2025-06-21 18:54:42 +02:00
141 changed files with 3759 additions and 8694 deletions

View File

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

View File

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

View File

@@ -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
View File

@@ -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
View File

@@ -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
View File

@@ -0,0 +1 @@
3.12.9

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -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
View File

@@ -0,0 +1,6 @@
def main():
print("Hello from backend!")
if __name__ == "__main__":
main()

55
backend/pyproject.toml Normal file
View 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

View File

@@ -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]:

View File

@@ -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.

View 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

View File

@@ -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'

View File

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

View 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

View File

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

View File

@@ -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_'):

View File

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

View File

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

View File

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

View File

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

View File

@@ -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) :

View File

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

View File

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

View 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

View File

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

View 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}"

View File

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

View File

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

View File

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

View 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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -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."
'';
}

View File

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

View File

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

View File

@@ -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.

View File

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

View File

@@ -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],
);
);

View File

@@ -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>();

View File

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

View File

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

View File

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

View File

@@ -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: []),
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
```

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +0,0 @@
import 'package:anyway/domain/entities/preferences.dart';
abstract class PreferencesRepository {
Future<Preferences> getPreferences();
Future<void> savePreferences(Preferences preferences);
}

View File

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

View File

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

View File

@@ -20,7 +20,7 @@ class _CurrentTripErrorMessageState extends State<CurrentTripErrorMessage> {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Text(
Text(
"😢",
style: TextStyle(
fontSize: 40,

View File

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

View File

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

View File

@@ -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();

View File

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

View File

@@ -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)),

View File

@@ -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,

View File

@@ -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(

View File

@@ -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 {
),
);
}
}
}

View File

@@ -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!'),
);
}
);
}

View File

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

View File

@@ -106,4 +106,4 @@ class _NewTripMapState extends State<NewTripMap> {
myLocationEnabled: useLocation,
);
}
}
}

View File

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

View File

@@ -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();
}
},
)
);

View File

@@ -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(

View 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);
}
);
}
}

View File

@@ -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,
),
),
),
],
),
);
}
}

View File

@@ -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),
],
);
}
}

View File

@@ -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,
),
),
]
),
);
}
}

View File

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

View File

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