Compare commits
7 Commits
a676af3a67
...
2288a50b60
Author | SHA1 | Date | |
---|---|---|---|
2288a50b60 | |||
02133a4abe | |||
03cf58ce43 | |||
c4fddc1a57 | |||
af5aa0097c | |||
b82f9997a4 | |||
1d5553f7f2 |
30
LICENSE.md
30
LICENSE.md
@ -1,30 +0,0 @@
|
|||||||
# License
|
|
||||||
|
|
||||||
## Proprietary License
|
|
||||||
|
|
||||||
All code and resources in this repository are the property of AnyDev. The software and related documentation are provided solely for use with services provided by AnyDev. Redistribution, modification, or use of this software outside of its intended service is strictly prohibited without explicit permission.
|
|
||||||
|
|
||||||
### Copyright © 2024 AnyDev
|
|
||||||
|
|
||||||
All rights reserved.
|
|
||||||
|
|
||||||
### Restrictions
|
|
||||||
|
|
||||||
- You may not modify, distribute, copy, or reverse engineer any part of this codebase.
|
|
||||||
- This software is licensed for use solely in conjunction with services provided by AnyDev.
|
|
||||||
- Any commercial use of this software is strictly prohibited without explicit written consent from AnyDev.
|
|
||||||
|
|
||||||
## Third-Party Dependencies
|
|
||||||
|
|
||||||
This project uses third-party dependencies, which are subject to their respective licenses.
|
|
||||||
|
|
||||||
- Python backend dependencies: fastapi, pydantic, numpy, shapely, etc. – Licensed under their respective licenses.
|
|
||||||
- Flutter frontend dependencies: Cupertino Icons, sliding_up_panel, http, etc. – Licensed under their respective licenses.
|
|
||||||
|
|
||||||
Please refer to each project's documentation for the specific terms and conditions.
|
|
||||||
|
|
||||||
## OpenStreetMap Data Usage
|
|
||||||
|
|
||||||
This project uses data derived from **OpenStreetMap**. OpenStreetMap data is available under the [Open Database License (ODbL)](https://www.openstreetmap.org/copyright). We comply with the ODbL license, and some of the data displayed in the service may be derived from OpenStreetMap sources. We do not redistribute raw OpenStreetMap data; instead, it is processed and transformed before being used in our services.
|
|
||||||
|
|
||||||
More information about OpenStreetMap data usage can be found [here](https://www.openstreetmap.org/copyright).
|
|
3
backend/.gitignore
vendored
3
backend/.gitignore
vendored
@ -1,9 +1,6 @@
|
|||||||
# osm-cache
|
# osm-cache
|
||||||
cache_XML/
|
cache_XML/
|
||||||
|
|
||||||
# secrets
|
|
||||||
*secrets.yaml
|
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
]
|
|
File diff suppressed because one or more lines are too long
@ -1,16 +1,17 @@
|
|||||||
"""Main app for backend api"""
|
"""Main app for backend api"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from fastapi import FastAPI, HTTPException, BackgroundTasks
|
from fastapi import FastAPI, HTTPException, BackgroundTasks, Query
|
||||||
|
|
||||||
from .logging_config import configure_logging
|
from .logging_config import configure_logging
|
||||||
from .structs.landmark import Landmark
|
from .structs.landmark import Landmark, Toilets
|
||||||
from .structs.preferences import Preferences
|
from .structs.preferences import Preferences
|
||||||
from .structs.linked_landmarks import LinkedLandmarks
|
from .structs.linked_landmarks import LinkedLandmarks
|
||||||
from .structs.trip import Trip
|
from .structs.trip import Trip
|
||||||
from .landmarks.landmarks_manager import LandmarkManager
|
from .utils.landmarks_manager import LandmarkManager
|
||||||
from .toilets.toilet_routes import router as toilets_router
|
from .utils.toilets_manager import ToiletsManager
|
||||||
from .optimization.optimizer import Optimizer
|
from .optimization.optimizer import Optimizer
|
||||||
from .optimization.refiner import Refiner
|
from .optimization.refiner import Refiner
|
||||||
from .overpass.overpass import fill_cache
|
from .overpass.overpass import fill_cache
|
||||||
@ -36,10 +37,6 @@ async def lifespan(app: FastAPI):
|
|||||||
app = FastAPI(lifespan=lifespan)
|
app = FastAPI(lifespan=lifespan)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
app.include_router(toilets_router)
|
|
||||||
|
|
||||||
|
|
||||||
@app.post("/trip/new")
|
@app.post("/trip/new")
|
||||||
def new_trip(preferences: Preferences,
|
def new_trip(preferences: Preferences,
|
||||||
start: tuple[float, float],
|
start: tuple[float, float],
|
||||||
@ -69,8 +66,6 @@ def new_trip(preferences: Preferences,
|
|||||||
end = start
|
end = start
|
||||||
logger.info("No end coordinates provided. Using start=end.")
|
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',
|
start_landmark = Landmark(name='start',
|
||||||
type='start',
|
type='start',
|
||||||
location=(start[0], start[1]),
|
location=(start[0], start[1]),
|
||||||
@ -92,7 +87,6 @@ def new_trip(preferences: Preferences,
|
|||||||
n_tags=0)
|
n_tags=0)
|
||||||
|
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
# Generate the landmarks from the start location
|
# Generate the landmarks from the start location
|
||||||
landmarks, landmarks_short = manager.generate_landmarks_list(
|
landmarks, landmarks_short = manager.generate_landmarks_list(
|
||||||
center_coordinates = start,
|
center_coordinates = start,
|
||||||
@ -114,7 +108,6 @@ def new_trip(preferences: Preferences,
|
|||||||
try:
|
try:
|
||||||
base_tour = optimizer.solve_optimization(preferences.max_time_minute, landmarks_short)
|
base_tour = optimizer.solve_optimization(preferences.max_time_minute, landmarks_short)
|
||||||
except Exception as exc:
|
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
|
raise HTTPException(status_code=500, detail=f"Optimization failed: {str(exc)}") from exc
|
||||||
|
|
||||||
t_first_stage = time.time() - start_time
|
t_first_stage = time.time() - start_time
|
||||||
@ -126,21 +119,22 @@ def new_trip(preferences: Preferences,
|
|||||||
refined_tour = refiner.refine_optimization(landmarks, base_tour,
|
refined_tour = refiner.refine_optimization(landmarks, base_tour,
|
||||||
preferences.max_time_minute,
|
preferences.max_time_minute,
|
||||||
preferences.detour_tolerance_minute)
|
preferences.detour_tolerance_minute)
|
||||||
except Exception as exc :
|
except TimeoutError as te :
|
||||||
logger.warning(f"Refiner failed. Proceeding with base trip {str(exc)}")
|
logger.error(f'Refiner failed : {str(te)} Using base tour.')
|
||||||
refined_tour = base_tour
|
refined_tour = base_tour
|
||||||
|
except Exception as exc :
|
||||||
|
raise HTTPException(status_code=500, detail=f"An unexpected error occurred: {str(exc)}") from exc
|
||||||
|
|
||||||
t_second_stage = time.time() - start_time
|
t_second_stage = time.time() - start_time
|
||||||
|
|
||||||
logger.debug(f'First stage optimization\t: {round(t_first_stage,3)} seconds')
|
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.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')
|
logger.info(f'Total computation time\t: {round(t_first_stage + t_second_stage,3)} seconds')
|
||||||
linked_tour = LinkedLandmarks(refined_tour)
|
|
||||||
|
|
||||||
|
linked_tour = LinkedLandmarks(refined_tour)
|
||||||
# upon creation of the trip, persistence of both the trip and its landmarks is ensured.
|
# upon creation of the trip, persistence of both the trip and its landmarks is ensured.
|
||||||
trip = Trip.from_linked_landmarks(linked_tour, cache_client)
|
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.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))
|
|
||||||
|
|
||||||
background_tasks.add_task(fill_cache)
|
background_tasks.add_task(fill_cache)
|
||||||
|
|
||||||
@ -163,7 +157,6 @@ def get_trip(trip_uuid: str) -> Trip:
|
|||||||
trip = cache_client.get(f"trip_{trip_uuid}")
|
trip = cache_client.get(f"trip_{trip_uuid}")
|
||||||
return trip
|
return trip
|
||||||
except KeyError as exc:
|
except KeyError as exc:
|
||||||
logger.error(f"Failed to fetch trip with UUID {trip_uuid}: {str(exc)}")
|
|
||||||
raise HTTPException(status_code=404, detail="Trip not found") from exc
|
raise HTTPException(status_code=404, detail="Trip not found") from exc
|
||||||
|
|
||||||
|
|
||||||
@ -182,45 +175,32 @@ def get_landmark(landmark_uuid: str) -> Landmark:
|
|||||||
landmark = cache_client.get(f"landmark_{landmark_uuid}")
|
landmark = cache_client.get(f"landmark_{landmark_uuid}")
|
||||||
return landmark
|
return landmark
|
||||||
except KeyError as exc:
|
except KeyError as exc:
|
||||||
logger.error(f"Failed to fetch landmark with UUID {landmark_uuid}: {str(exc)}")
|
|
||||||
raise HTTPException(status_code=404, detail="Landmark not found") from exc
|
raise HTTPException(status_code=404, detail="Landmark not found") from exc
|
||||||
|
|
||||||
|
|
||||||
@app.post("/trip/recompute-time/{trip_uuid}/{removed_landmark_uuid}")
|
@app.post("/toilets/new")
|
||||||
def update_trip_time(trip_uuid: str, removed_landmark_uuid: str) -> Trip:
|
def get_toilets(location: tuple[float, float] = Query(...), radius: int = 500) -> list[Toilets] :
|
||||||
"""
|
"""
|
||||||
Updates the reaching times of a given trip when removing a landmark.
|
Endpoint to find toilets within a specified radius from a given location.
|
||||||
|
|
||||||
|
This endpoint expects the `location` and `radius` as **query parameters**, not in the request body.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
landmark_uuid (str) : unique identifier for a Landmark.
|
location (tuple[float, float]): The latitude and longitude of the location to search from.
|
||||||
|
radius (int, optional): The radius (in meters) within which to search for toilets. Defaults to 500 meters.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(Landmark) : the corresponding Landmark.
|
list[Toilets]: A list of Toilets objects that meet the criteria.
|
||||||
"""
|
"""
|
||||||
# First, fetch the trip in the cache.
|
if location is None:
|
||||||
|
raise HTTPException(status_code=406, detail="Coordinates not provided or invalid")
|
||||||
|
if not (-90 <= location[0] <= 90 or -180 <= location[1] <= 180):
|
||||||
|
raise HTTPException(status_code=422, detail="Start coordinates not in range")
|
||||||
|
|
||||||
|
toilets_manager = ToiletsManager(location, radius)
|
||||||
|
|
||||||
try :
|
try :
|
||||||
trip = cache_client.get(f'trip_{trip_uuid}')
|
toilets_list = toilets_manager.generate_toilet_list()
|
||||||
|
return toilets_list
|
||||||
except KeyError as exc:
|
except KeyError as exc:
|
||||||
logger.error(f"Failed to update trip with UUID {trip_uuid} (trip not found): {str(exc)}")
|
raise HTTPException(status_code=404, detail="No toilets found") from exc
|
||||||
raise HTTPException(status_code=404, detail='Trip not found') from exc
|
|
||||||
|
|
||||||
landmarks = []
|
|
||||||
next_uuid = trip.first_landmark_uuid
|
|
||||||
|
|
||||||
# Extract landmarks
|
|
||||||
try :
|
|
||||||
while next_uuid is not None:
|
|
||||||
landmark = cache_client.get(f'landmark_{next_uuid}')
|
|
||||||
# Filter out the removed landmark.
|
|
||||||
if next_uuid != removed_landmark_uuid :
|
|
||||||
landmarks.append(landmark)
|
|
||||||
next_uuid = landmark.next_uuid # Prepare for the next iteration
|
|
||||||
except KeyError as exc:
|
|
||||||
logger.error(f"Failed to update trip with UUID {trip_uuid} : {str(exc)}")
|
|
||||||
raise HTTPException(status_code=404, detail=f'landmark {next_uuid} not found') from exc
|
|
||||||
|
|
||||||
# Re-link every thing and compute times again
|
|
||||||
linked_tour = LinkedLandmarks(landmarks)
|
|
||||||
trip = Trip.from_linked_landmarks(linked_tour, cache_client)
|
|
||||||
|
|
||||||
return trip
|
|
||||||
|
@ -257,6 +257,7 @@ class Optimizer:
|
|||||||
Returns:
|
Returns:
|
||||||
None: This function modifies the `prob` object by adding L-2 equality constraints in-place.
|
None: This function modifies the `prob` object by adding L-2 equality constraints in-place.
|
||||||
"""
|
"""
|
||||||
|
# FIXME: weird 0 artifact in the coefficients popping up
|
||||||
# Loop through rows 1 to L-2 to prevent stacked ones
|
# Loop through rows 1 to L-2 to prevent stacked ones
|
||||||
for i in range(1, L-1):
|
for i in range(1, L-1):
|
||||||
# Add the constraint that sums across each "row" or "block" in the decision variables
|
# Add the constraint that sums across each "row" or "block" in the decision variables
|
||||||
@ -589,15 +590,15 @@ class Optimizer:
|
|||||||
try :
|
try :
|
||||||
prob.solve(pl.PULP_CBC_CMD(msg=False, timeLimit=self.time_limit+1, gapRel=self.gap_rel))
|
prob.solve(pl.PULP_CBC_CMD(msg=False, timeLimit=self.time_limit+1, gapRel=self.gap_rel))
|
||||||
except Exception as exc :
|
except Exception as exc :
|
||||||
raise Exception(f"No solution found: {str(exc)}") from exc
|
raise Exception(f"No solution found: {exc}") from exc
|
||||||
status = pl.LpStatus[prob.status]
|
status = pl.LpStatus[prob.status]
|
||||||
solution = [pl.value(var) for var in x] # The values of the decision variables (will be 0 or 1)
|
solution = [pl.value(var) for var in x] # The values of the decision variables (will be 0 or 1)
|
||||||
|
|
||||||
self.logger.debug("First results are out. Looking out for circles and correcting...")
|
self.logger.debug("First results are out. Looking out for circles and correcting.")
|
||||||
|
|
||||||
# Raise error if no solution is found. FIXME: for now this throws the internal server error
|
# Raise error if no solution is found. FIXME: for now this throws the internal server error
|
||||||
if status != 'Optimal' :
|
if status != 'Optimal' :
|
||||||
self.logger.warning("The problem is overconstrained, no solution on first try.")
|
self.logger.error("The problem is overconstrained, no solution on first try.")
|
||||||
raise ArithmeticError("No solution could be found. Please try again with more time or different preferences.")
|
raise ArithmeticError("No solution could be found. Please try again with more time or different preferences.")
|
||||||
|
|
||||||
# If there is a solution, we're good to go, just check for connectiveness
|
# If there is a solution, we're good to go, just check for connectiveness
|
||||||
@ -607,7 +608,7 @@ class Optimizer:
|
|||||||
while circles is not None :
|
while circles is not None :
|
||||||
i += 1
|
i += 1
|
||||||
if i == self.max_iter :
|
if i == self.max_iter :
|
||||||
self.logger.warning(f'Timeout: No solution found after {self.max_iter} iterations.')
|
self.logger.error(f'Timeout: No solution found after {self.max_iter} iterations.')
|
||||||
raise TimeoutError(f"Optimization took too long. No solution found after {self.max_iter} iterations.")
|
raise TimeoutError(f"Optimization took too long. No solution found after {self.max_iter} iterations.")
|
||||||
|
|
||||||
for circle in circles :
|
for circle in circles :
|
||||||
@ -617,13 +618,12 @@ class Optimizer:
|
|||||||
try :
|
try :
|
||||||
prob.solve(pl.PULP_CBC_CMD(msg=False, timeLimit=self.time_limit, gapRel=self.gap_rel))
|
prob.solve(pl.PULP_CBC_CMD(msg=False, timeLimit=self.time_limit, gapRel=self.gap_rel))
|
||||||
except Exception as exc :
|
except Exception as exc :
|
||||||
self.logger.warning("No solution found: {str(exc)")
|
raise Exception(f"No solution found: {exc}") from exc
|
||||||
raise Exception(f"No solution found: {str(exc)}") from exc
|
|
||||||
|
|
||||||
solution = [pl.value(var) for var in x]
|
solution = [pl.value(var) for var in x]
|
||||||
|
|
||||||
if pl.LpStatus[prob.status] != 'Optimal' :
|
if pl.LpStatus[prob.status] != 'Optimal' :
|
||||||
self.logger.warning("The problem is overconstrained, no solution after {i} cycles.")
|
self.logger.error("The problem is overconstrained, no solution after {i} cycles.")
|
||||||
raise ArithmeticError("No solution could be found. Please try again with more time or different preferences.")
|
raise ArithmeticError("No solution could be found. Please try again with more time or different preferences.")
|
||||||
|
|
||||||
circles = self.is_connected(solution)
|
circles = self.is_connected(solution)
|
||||||
|
@ -278,7 +278,7 @@ class Refiner :
|
|||||||
better_tour_poly = concave_hull(MultiPoint(coords)) # Create concave hull with "core" of tour leaving out start and finish
|
better_tour_poly = concave_hull(MultiPoint(coords)) # Create concave hull with "core" of tour leaving out start and finish
|
||||||
xs, ys = better_tour_poly.exterior.xy
|
xs, ys = better_tour_poly.exterior.xy
|
||||||
"""
|
"""
|
||||||
FIXED : ERROR HERE :
|
ERROR HERE :
|
||||||
Exception has occurred: AttributeError
|
Exception has occurred: AttributeError
|
||||||
'LineString' object has no attribute 'exterior'
|
'LineString' object has no attribute 'exterior'
|
||||||
"""
|
"""
|
||||||
@ -356,7 +356,7 @@ class Refiner :
|
|||||||
|
|
||||||
# If unsuccessful optimization, use the base_tour.
|
# If unsuccessful optimization, use the base_tour.
|
||||||
if new_tour is None:
|
if new_tour is None:
|
||||||
self.logger.warning("Refiner failed: No solution found during second stage optimization.")
|
self.logger.warning("No solution found for the refined tour. Returning the initial tour.")
|
||||||
new_tour = base_tour
|
new_tour = base_tour
|
||||||
|
|
||||||
# If only one landmark, return it.
|
# If only one landmark, return it.
|
||||||
@ -369,7 +369,6 @@ class Refiner :
|
|||||||
# Fix the tour using Polygons if the path looks weird.
|
# Fix the tour using Polygons if the path looks weird.
|
||||||
# Conditions : circular trip and invalid polygon.
|
# Conditions : circular trip and invalid polygon.
|
||||||
if base_tour[0].location == base_tour[-1].location and not better_poly.is_valid :
|
if base_tour[0].location == base_tour[-1].location and not better_poly.is_valid :
|
||||||
self.logger.debug("Tours might be funky, attempting to correct with polygons")
|
|
||||||
better_tour = self.fix_using_polygon(better_tour)
|
better_tour = self.fix_using_polygon(better_tour)
|
||||||
|
|
||||||
return better_tour
|
return better_tour
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
"""Module defining the handling of cache data from Overpass requests."""
|
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import hashlib
|
import hashlib
|
||||||
@ -62,7 +61,7 @@ class JSONCache(CachingStrategyBase):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def set(self, key, value):
|
def set(self, key, value):
|
||||||
"""Save the JSON data in the cache."""
|
"""Save the JSON data as an ElementTree to the cache."""
|
||||||
filename = self._filename(key)
|
filename = self._filename(key)
|
||||||
try:
|
try:
|
||||||
# Write the JSON data to the cache file
|
# Write the JSON data to the cache file
|
||||||
@ -95,7 +94,7 @@ class JSONCache(CachingStrategyBase):
|
|||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""Cleanup method, if needed."""
|
"""Cleanup method, if needed."""
|
||||||
|
pass
|
||||||
|
|
||||||
class CachingStrategy:
|
class CachingStrategy:
|
||||||
"""
|
"""
|
||||||
@ -108,7 +107,6 @@ class CachingStrategy:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def use(cls, strategy_name='JSON', **kwargs):
|
def use(cls, strategy_name='JSON', **kwargs):
|
||||||
"""Define the caching strategy to use."""
|
|
||||||
if cls.__strategy:
|
if cls.__strategy:
|
||||||
cls.__strategy.close()
|
cls.__strategy.close()
|
||||||
|
|
||||||
@ -121,12 +119,10 @@ class CachingStrategy:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get(cls, key):
|
def get(cls, key):
|
||||||
"""Get the data from the cache."""
|
|
||||||
return cls.__strategy.get(key)
|
return cls.__strategy.get(key)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def set(cls, key, value):
|
def set(cls, key, value):
|
||||||
"""Save the data in the cache."""
|
|
||||||
cls.__strategy.set(key, value)
|
cls.__strategy.set(key, value)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
"""Module allowing connexion to overpass api and fectch data from OSM."""
|
"""Module allowing connexion to overpass api and fectch data from OSM."""
|
||||||
import os
|
import os
|
||||||
import time
|
|
||||||
import urllib
|
import urllib
|
||||||
import math
|
import math
|
||||||
import logging
|
import logging
|
||||||
@ -53,22 +52,22 @@ class Overpass :
|
|||||||
# Retrieve cached data and identify missing cache entries
|
# Retrieve cached data and identify missing cache entries
|
||||||
cached_responses, non_cached_cells = self._retrieve_cached_data(overlapping_cells, osm_types, selector, conditions, out)
|
cached_responses, non_cached_cells = self._retrieve_cached_data(overlapping_cells, osm_types, selector, conditions, out)
|
||||||
|
|
||||||
self.logger.debug(f'Cache hit for {len(overlapping_cells)-len(non_cached_cells)}/{len(overlapping_cells)} quadrants.')
|
self.logger.info(f'Cache hit for {len(overlapping_cells)-len(non_cached_cells)}/{len(overlapping_cells)} quadrants.')
|
||||||
|
|
||||||
# If there is no missing data, return the cached responses after filtering.
|
# If there is no missing data, return the cached responses after filtering.
|
||||||
if not non_cached_cells :
|
if not non_cached_cells :
|
||||||
return Overpass._filter_landmarks(cached_responses, bbox)
|
return Overpass._filter_landmarks(cached_responses, bbox)
|
||||||
|
|
||||||
# If there is no cached data, fetch all from Overpass.
|
# If there is no cached data, fetch all from Overpass.
|
||||||
if not cached_responses :
|
elif not cached_responses :
|
||||||
query_str = Overpass.build_query(bbox, osm_types, selector, conditions, out)
|
query_str = Overpass.build_query(bbox, osm_types, selector, conditions, out)
|
||||||
self.logger.debug(f'Query string: {query_str}')
|
|
||||||
return self.fetch_data_from_api(query_str)
|
return self.fetch_data_from_api(query_str)
|
||||||
|
|
||||||
|
# Hybrid cache: some data from Overpass, some data from cache.
|
||||||
|
else :
|
||||||
# Resize the bbox for smaller search area and build new query string.
|
# Resize the bbox for smaller search area and build new query string.
|
||||||
non_cached_bbox = Overpass._get_non_cached_bbox(non_cached_cells, bbox)
|
non_cached_bbox = Overpass._get_non_cached_bbox(non_cached_cells, bbox)
|
||||||
query_str = Overpass.build_query(non_cached_bbox, osm_types, selector, conditions, out)
|
query_str = Overpass.build_query(non_cached_bbox, osm_types, selector, conditions, out)
|
||||||
self.logger.debug(f'Query string: {query_str}')
|
|
||||||
non_cached_responses = self.fetch_data_from_api(query_str)
|
non_cached_responses = self.fetch_data_from_api(query_str)
|
||||||
return Overpass._filter_landmarks(cached_responses, bbox) + non_cached_responses
|
return Overpass._filter_landmarks(cached_responses, bbox) + non_cached_responses
|
||||||
|
|
||||||
@ -95,10 +94,9 @@ class Overpass :
|
|||||||
return elements
|
return elements
|
||||||
|
|
||||||
except urllib.error.URLError as e:
|
except urllib.error.URLError as e:
|
||||||
self.logger.error(f"Error connecting to Overpass API: {str(e)}")
|
self.logger.error(f"Error connecting to Overpass API: {e}")
|
||||||
raise ConnectionError(f"Error connecting to Overpass API: {str(e)}") from e
|
raise ConnectionError(f"Error connecting to Overpass API: {e}") from e
|
||||||
except Exception as exc :
|
except Exception as exc :
|
||||||
self.logger.error(f"unexpected error while fetching data from Overpass: {str(exc)}")
|
|
||||||
raise Exception(f'An unexpected error occured: {str(exc)}') from exc
|
raise Exception(f'An unexpected error occured: {str(exc)}') from exc
|
||||||
|
|
||||||
|
|
||||||
@ -122,7 +120,7 @@ class Overpass :
|
|||||||
self.caching_strategy.set(cache_key, elements)
|
self.caching_strategy.set(cache_key, elements)
|
||||||
self.logger.debug(f'Cache set for {cache_key}')
|
self.logger.debug(f'Cache set for {cache_key}')
|
||||||
except urllib.error.URLError as e:
|
except urllib.error.URLError as e:
|
||||||
raise ConnectionError(f"Error connecting to Overpass API: {str(e)}") from e
|
raise ConnectionError(f"Error connecting to Overpass API: {e}") from e
|
||||||
except Exception as exc :
|
except Exception as exc :
|
||||||
raise Exception(f'An unexpected error occured: {str(exc)}') from exc
|
raise Exception(f'An unexpected error occured: {str(exc)}') from exc
|
||||||
|
|
||||||
@ -153,7 +151,7 @@ class Overpass :
|
|||||||
- If no conditions are provided, the query will just use the `selector` to filter the OSM
|
- If no conditions are provided, the query will just use the `selector` to filter the OSM
|
||||||
elements without additional constraints.
|
elements without additional constraints.
|
||||||
"""
|
"""
|
||||||
query = '[out:json][timeout:20];('
|
query = '[out:json];('
|
||||||
|
|
||||||
# convert the bbox to string.
|
# convert the bbox to string.
|
||||||
bbox_str = f"({','.join(map(str, bbox))})"
|
bbox_str = f"({','.join(map(str, bbox))})"
|
||||||
@ -388,7 +386,7 @@ def get_base_info(elem: dict, osm_type: OSM_TYPES, with_name=False) :
|
|||||||
if with_name :
|
if with_name :
|
||||||
name = elem.get('tags', {}).get('name')
|
name = elem.get('tags', {}).get('name')
|
||||||
return osm_id, coords, name
|
return osm_id, coords, name
|
||||||
|
else :
|
||||||
return osm_id, coords
|
return osm_id, coords
|
||||||
|
|
||||||
|
|
||||||
@ -399,25 +397,18 @@ def fill_cache():
|
|||||||
"""
|
"""
|
||||||
overpass = Overpass()
|
overpass = Overpass()
|
||||||
|
|
||||||
n_files = 0
|
|
||||||
total = 0
|
|
||||||
|
|
||||||
with os.scandir(OSM_CACHE_DIR) as it:
|
with os.scandir(OSM_CACHE_DIR) as it:
|
||||||
for entry in it:
|
for entry in it:
|
||||||
if entry.is_file() and entry.name.startswith('hollow_'):
|
if entry.is_file() and entry.name.startswith('hollow_'):
|
||||||
total += 1
|
|
||||||
try :
|
try :
|
||||||
# Read the whole file content as a string
|
# Read the whole file content as a string
|
||||||
with open(entry.path, 'r', encoding='utf-8') as f:
|
with open(entry.path, 'r') as f:
|
||||||
# load data and fill the cache with the query and key
|
# load data and fill the cache with the query and key
|
||||||
json_data = json.load(f)
|
json_data = json.load(f)
|
||||||
overpass.fill_cache(json_data)
|
overpass.fill_cache(json_data)
|
||||||
n_files += 1
|
|
||||||
time.sleep(1)
|
|
||||||
# Now delete the file as the cache is filled
|
# Now delete the file as the cache is filled
|
||||||
os.remove(entry.path)
|
os.remove(entry.path)
|
||||||
|
|
||||||
except Exception as exc :
|
except Exception as exc :
|
||||||
overpass.logger.error(f'An error occured while parsing file {entry.path} as .json file: {str(exc)}')
|
overpass.logger.error(f'An error occured while parsing file {entry.path} as .json file')
|
||||||
|
|
||||||
overpass.logger.info(f"Successfully filled {n_files}/{total} cache files.")
|
|
||||||
|
@ -72,7 +72,6 @@ sightseeing:
|
|||||||
# - castle
|
# - castle
|
||||||
# - museum
|
# - museum
|
||||||
|
|
||||||
|
|
||||||
museums:
|
museums:
|
||||||
tourism:
|
tourism:
|
||||||
- museum
|
- museum
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
max_bbox_side: 4000 #m
|
max_bbox_side: 4000 #m
|
||||||
radius_close_to: 50
|
radius_close_to: 50
|
||||||
church_coeff: 0.75
|
church_coeff: 0.55
|
||||||
nature_coeff: 1.6
|
nature_coeff: 1.4
|
||||||
overall_coeff: 10
|
overall_coeff: 10
|
||||||
tag_exponent: 1.15
|
tag_exponent: 1.15
|
||||||
image_bonus: 1.1
|
image_bonus: 1.1
|
||||||
viewpoint_bonus: 10
|
viewpoint_bonus: 5
|
||||||
wikipedia_bonus: 1.25
|
wikipedia_bonus: 1.25
|
||||||
|
name_bonus: 3
|
||||||
N_important: 60
|
N_important: 60
|
||||||
pay_bonus: -1
|
pay_bonus: -1
|
||||||
|
@ -5,5 +5,5 @@ max_landmarks: 10
|
|||||||
max_landmarks_refiner: 20
|
max_landmarks_refiner: 20
|
||||||
overshoot: 0.0016
|
overshoot: 0.0016
|
||||||
time_limit: 1
|
time_limit: 1
|
||||||
gap_rel: 0.025
|
gap_rel: 0.05
|
||||||
max_iter: 80
|
max_iter: 40
|
@ -1,7 +1,8 @@
|
|||||||
"""Definition of the Landmark class to handle visitable objects across the world."""
|
"""Definition of the Landmark class to handle visitable objects across the world."""
|
||||||
|
|
||||||
from typing import Optional, Literal
|
from typing import Optional, Literal
|
||||||
from uuid import uuid4, UUID
|
from uuid import uuid4, UUID
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
|
|
||||||
# Output to frontend
|
# Output to frontend
|
||||||
@ -49,8 +50,7 @@ class Landmark(BaseModel) :
|
|||||||
image_url : Optional[str] = None
|
image_url : Optional[str] = None
|
||||||
website_url : Optional[str] = None
|
website_url : Optional[str] = None
|
||||||
wiki_url : Optional[str] = None
|
wiki_url : Optional[str] = None
|
||||||
keywords: Optional[dict] = {}
|
description : Optional[str] = None # TODO future
|
||||||
description : Optional[str] = None
|
|
||||||
duration : Optional[int] = 5
|
duration : Optional[int] = 5
|
||||||
name_en : Optional[str] = None
|
name_en : Optional[str] = None
|
||||||
|
|
||||||
@ -69,7 +69,6 @@ class Landmark(BaseModel) :
|
|||||||
is_viewpoint : Optional[bool] = False
|
is_viewpoint : Optional[bool] = False
|
||||||
is_place_of_worship : Optional[bool] = False
|
is_place_of_worship : Optional[bool] = False
|
||||||
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
"""
|
"""
|
||||||
String representation of the Landmark object.
|
String representation of the Landmark object.
|
||||||
@ -123,3 +122,26 @@ class Landmark(BaseModel) :
|
|||||||
return (self.uuid == value.uuid or
|
return (self.uuid == value.uuid or
|
||||||
self.osm_id == value.osm_id or
|
self.osm_id == value.osm_id or
|
||||||
(self.name == value.name and self.distance(value) < 0.001))
|
(self.name == value.name and self.distance(value) < 0.001))
|
||||||
|
|
||||||
|
|
||||||
|
class Toilets(BaseModel) :
|
||||||
|
"""
|
||||||
|
Model for toilets. When false/empty the information is either false either not known.
|
||||||
|
"""
|
||||||
|
location : tuple
|
||||||
|
wheelchair : Optional[bool] = False
|
||||||
|
changing_table : Optional[bool] = False
|
||||||
|
fee : Optional[bool] = False
|
||||||
|
opening_hours : Optional[str] = ""
|
||||||
|
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""
|
||||||
|
String representation of the Toilets object.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: A formatted string with the toilets location.
|
||||||
|
"""
|
||||||
|
return f'Toilets @{self.location}'
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
@ -1,26 +0,0 @@
|
|||||||
"""Definition of the Toilets class."""
|
|
||||||
from typing import Optional
|
|
||||||
from pydantic import BaseModel, ConfigDict
|
|
||||||
|
|
||||||
|
|
||||||
class Toilets(BaseModel) :
|
|
||||||
"""
|
|
||||||
Model for toilets. When false/empty the information is either false either not known.
|
|
||||||
"""
|
|
||||||
location : tuple
|
|
||||||
wheelchair : Optional[bool] = False
|
|
||||||
changing_table : Optional[bool] = False
|
|
||||||
fee : Optional[bool] = False
|
|
||||||
opening_hours : Optional[str] = ""
|
|
||||||
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
"""
|
|
||||||
String representation of the Toilets object.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: A formatted string with the toilets location.
|
|
||||||
"""
|
|
||||||
return f'Toilets @{self.location}'
|
|
||||||
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
|
@ -31,9 +31,9 @@ def test_turckheim(client, request): # pylint: disable=redefined-outer-name
|
|||||||
"shopping": {"type": "shopping", "score": 0},
|
"shopping": {"type": "shopping", "score": 0},
|
||||||
"max_time_minute": duration_minutes,
|
"max_time_minute": duration_minutes,
|
||||||
"detour_tolerance_minute": 0},
|
"detour_tolerance_minute": 0},
|
||||||
"start": [48.084588, 7.280405]
|
# "start": [48.084588, 7.280405]
|
||||||
# "start": [45.74445023349939, 4.8222687890538865]
|
# "start": [45.74445023349939, 4.8222687890538865]
|
||||||
# "start": [45.75156398104873, 4.827154464827647]
|
"start": [45.75156398104873, 4.827154464827647]
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
result = response.json()
|
result = response.json()
|
||||||
@ -46,6 +46,8 @@ def test_turckheim(client, request): # pylint: disable=redefined-outer-name
|
|||||||
# Add details to report
|
# Add details to report
|
||||||
log_trip_details(request, landmarks, result['total_time'], duration_minutes)
|
log_trip_details(request, landmarks, result['total_time'], duration_minutes)
|
||||||
|
|
||||||
|
# for elem in landmarks :
|
||||||
|
# print(elem)
|
||||||
|
|
||||||
# checks :
|
# checks :
|
||||||
assert response.status_code == 200 # check for successful planning
|
assert response.status_code == 200 # check for successful planning
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from ..structs.toilets import Toilets
|
from ..structs.landmark import Toilets
|
||||||
from ..main import app
|
from ..main import app
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""Helper methods for testing."""
|
"""Helper methods for testing."""
|
||||||
import logging
|
import logging
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
|
from pydantic import ValidationError
|
||||||
|
|
||||||
from ..structs.landmark import Landmark
|
from ..structs.landmark import Landmark
|
||||||
from ..cache import client as cache_client
|
from ..cache import client as cache_client
|
||||||
@ -38,7 +39,7 @@ def fetch_landmark(landmark_uuid: str):
|
|||||||
try:
|
try:
|
||||||
landmark = cache_client.get(f'landmark_{landmark_uuid}')
|
landmark = cache_client.get(f'landmark_{landmark_uuid}')
|
||||||
if not landmark :
|
if not landmark :
|
||||||
logger.error(f'Cache miss for landmark UUID: {landmark_uuid}')
|
logger.warning(f'Cache miss for landmark UUID: {landmark_uuid}')
|
||||||
raise HTTPException(status_code=404, detail=f'Landmark with UUID {landmark_uuid} not found in cache.')
|
raise HTTPException(status_code=404, detail=f'Landmark with UUID {landmark_uuid} not found in cache.')
|
||||||
|
|
||||||
# Validate that the fetched data is a dictionary
|
# Validate that the fetched data is a dictionary
|
||||||
|
@ -1,38 +0,0 @@
|
|||||||
"""Defines the endpoint for fetching toilet locations."""
|
|
||||||
from fastapi import HTTPException, APIRouter, Query
|
|
||||||
|
|
||||||
from ..structs.toilets import Toilets
|
|
||||||
from .toilets_manager import ToiletsManager
|
|
||||||
|
|
||||||
|
|
||||||
# Define the API router
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/toilets/new")
|
|
||||||
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.
|
|
||||||
|
|
||||||
This endpoint expects the `location` and `radius` as **query parameters**, not in the request body.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
location (tuple[float, float]): The latitude and longitude of the location to search from.
|
|
||||||
radius (int, optional): The radius (in meters) within which to search for toilets. Defaults to 500 meters.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list[Toilets]: A list of Toilets objects that meet the criteria.
|
|
||||||
"""
|
|
||||||
if location is None:
|
|
||||||
raise HTTPException(status_code=406, detail="Coordinates not provided or invalid")
|
|
||||||
if not (-90 <= location[0] <= 90 or -180 <= location[1] <= 180):
|
|
||||||
raise HTTPException(status_code=422, detail="Start coordinates not in range")
|
|
||||||
|
|
||||||
toilets_manager = ToiletsManager(location, radius)
|
|
||||||
|
|
||||||
try :
|
|
||||||
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
|
|
@ -1,6 +1,6 @@
|
|||||||
"""Find clusters of interest to add more general areas of visit to the tour."""
|
"""Find clusters of interest to add more general areas of visit to the tour."""
|
||||||
import logging
|
import logging
|
||||||
from typing import Literal, Tuple
|
from typing import Literal
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from sklearn.cluster import DBSCAN
|
from sklearn.cluster import DBSCAN
|
||||||
@ -8,8 +8,8 @@ from pydantic import BaseModel
|
|||||||
|
|
||||||
from ..overpass.overpass import Overpass, get_base_info
|
from ..overpass.overpass import Overpass, get_base_info
|
||||||
from ..structs.landmark import Landmark
|
from ..structs.landmark import Landmark
|
||||||
from ..utils.get_time_distance import get_distance
|
from .get_time_distance import get_distance
|
||||||
from ..utils.bbox import create_bbox
|
from .utils import create_bbox
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ class Cluster(BaseModel):
|
|||||||
"""
|
"""
|
||||||
type: Literal['street', 'area']
|
type: Literal['street', 'area']
|
||||||
importance: int
|
importance: int
|
||||||
centroid: Tuple[float, float]
|
centroid: tuple
|
||||||
# start: Optional[list] = None # for later use if we want to have streets as well
|
# start: Optional[list] = None # for later use if we want to have streets as well
|
||||||
# end: Optional[list] = None
|
# end: Optional[list] = None
|
||||||
|
|
||||||
@ -103,7 +103,7 @@ class ClusterManager:
|
|||||||
out = out
|
out = out
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning(f"Error fetching clusters: {e}")
|
self.logger.error(f"Error fetching clusters: {e}")
|
||||||
|
|
||||||
if result is None :
|
if result is None :
|
||||||
self.logger.debug(f"Found no {cluster_type} clusters, overpass query returned no datapoints.")
|
self.logger.debug(f"Found no {cluster_type} clusters, overpass query returned no datapoints.")
|
||||||
@ -178,12 +178,11 @@ class ClusterManager:
|
|||||||
|
|
||||||
# Calculate the centroid as the mean of the points
|
# Calculate the centroid as the mean of the points
|
||||||
centroid = np.mean(current_cluster, axis=0)
|
centroid = np.mean(current_cluster, axis=0)
|
||||||
centroid = tuple((round(centroid[0], 7), round(centroid[1], 7)))
|
|
||||||
|
|
||||||
if self.cluster_type == 'shopping' :
|
if self.cluster_type == 'shopping' :
|
||||||
score = len(current_cluster)*3
|
score = len(current_cluster)*2
|
||||||
else :
|
else :
|
||||||
score = len(current_cluster)*15
|
score = len(current_cluster)*8
|
||||||
locations.append(Cluster(
|
locations.append(Cluster(
|
||||||
type='area',
|
type='area',
|
||||||
centroid=centroid,
|
centroid=centroid,
|
||||||
@ -216,7 +215,7 @@ class ClusterManager:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Define the bounding box for a given radius around the coordinates
|
# Define the bounding box for a given radius around the coordinates
|
||||||
bbox = create_bbox(cluster.centroid, 300)
|
bbox = create_bbox(cluster.centroid, 1000)
|
||||||
|
|
||||||
# Query neighborhoods and shopping malls
|
# Query neighborhoods and shopping malls
|
||||||
selectors = ['"place"~"^(suburb|neighborhood|neighbourhood|quarter|city_block)$"']
|
selectors = ['"place"~"^(suburb|neighborhood|neighbourhood|quarter|city_block)$"']
|
||||||
@ -224,10 +223,10 @@ class ClusterManager:
|
|||||||
if self.cluster_type == 'shopping' :
|
if self.cluster_type == 'shopping' :
|
||||||
selectors.append('"shop"="mall"')
|
selectors.append('"shop"="mall"')
|
||||||
new_name = 'Shopping Area'
|
new_name = 'Shopping Area'
|
||||||
t = 30
|
t = 40
|
||||||
else :
|
else :
|
||||||
new_name = 'Neighborhood'
|
new_name = 'Neighborhood'
|
||||||
t = 20
|
t = 15
|
||||||
|
|
||||||
min_dist = float('inf')
|
min_dist = float('inf')
|
||||||
osm_id = 0
|
osm_id = 0
|
||||||
@ -239,28 +238,30 @@ class ClusterManager:
|
|||||||
result = self.overpass.send_query(bbox = bbox,
|
result = self.overpass.send_query(bbox = bbox,
|
||||||
osm_types = osm_types,
|
osm_types = osm_types,
|
||||||
selector = sel,
|
selector = sel,
|
||||||
out = 'ids center tags'
|
out = 'ids center'
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning(f"Error fetching clusters: {e}")
|
self.logger.error(f"Error fetching clusters: {e}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if result is None :
|
if result is None :
|
||||||
self.logger.warning(f"Error fetching clusters: query result is None")
|
self.logger.error(f"Error fetching clusters: {e}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for elem in result:
|
for elem in result:
|
||||||
# Get basic info
|
osm_type = elem.get('type')
|
||||||
id, coords, name = get_base_info(elem, elem.get('type'), with_name=True)
|
|
||||||
|
id, coords, name = get_base_info(elem, osm_type, with_name=True)
|
||||||
|
|
||||||
if name is None or coords is None :
|
if name is None or coords is None :
|
||||||
continue
|
continue
|
||||||
|
|
||||||
d = get_distance(cluster.centroid, coords)
|
d = get_distance(cluster.centroid, coords)
|
||||||
if d < min_dist :
|
if d < min_dist :
|
||||||
min_dist = d
|
min_dist = d
|
||||||
new_name = name # add name
|
new_name = name
|
||||||
osm_type = elem.get('type') # add type: 'way' or 'relation'
|
osm_type = osm_type # Add type: 'way' or 'relation'
|
||||||
osm_id = id # add OSM id
|
osm_id = id # Add OSM id
|
||||||
|
|
||||||
return Landmark(
|
return Landmark(
|
||||||
name=new_name,
|
name=new_name,
|
@ -4,10 +4,10 @@ import yaml
|
|||||||
|
|
||||||
from ..structs.preferences import Preferences
|
from ..structs.preferences import Preferences
|
||||||
from ..structs.landmark import Landmark
|
from ..structs.landmark import Landmark
|
||||||
from ..utils.take_most_important import take_most_important
|
from .take_most_important import take_most_important
|
||||||
from .cluster_manager import ClusterManager
|
from .cluster_manager import ClusterManager
|
||||||
from ..overpass.overpass import Overpass, get_base_info
|
from ..overpass.overpass import Overpass, get_base_info
|
||||||
from ..utils.bbox import create_bbox
|
from .utils import create_bbox
|
||||||
|
|
||||||
from ..constants import AMENITY_SELECTORS_PATH, LANDMARK_PARAMETERS_PATH, OPTIMIZER_PARAMETERS_PATH
|
from ..constants import AMENITY_SELECTORS_PATH, LANDMARK_PARAMETERS_PATH, OPTIMIZER_PARAMETERS_PATH
|
||||||
|
|
||||||
@ -39,6 +39,7 @@ class LandmarkManager:
|
|||||||
self.overall_coeff = parameters['overall_coeff']
|
self.overall_coeff = parameters['overall_coeff']
|
||||||
self.tag_exponent = parameters['tag_exponent']
|
self.tag_exponent = parameters['tag_exponent']
|
||||||
self.image_bonus = parameters['image_bonus']
|
self.image_bonus = parameters['image_bonus']
|
||||||
|
self.name_bonus = parameters['name_bonus']
|
||||||
self.wikipedia_bonus = parameters['wikipedia_bonus']
|
self.wikipedia_bonus = parameters['wikipedia_bonus']
|
||||||
self.viewpoint_bonus = parameters['viewpoint_bonus']
|
self.viewpoint_bonus = parameters['viewpoint_bonus']
|
||||||
self.pay_bonus = parameters['pay_bonus']
|
self.pay_bonus = parameters['pay_bonus']
|
||||||
@ -146,8 +147,6 @@ class LandmarkManager:
|
|||||||
score *= self.wikipedia_bonus
|
score *= self.wikipedia_bonus
|
||||||
if landmark.is_place_of_worship :
|
if landmark.is_place_of_worship :
|
||||||
score *= self.church_coeff
|
score *= self.church_coeff
|
||||||
if landmark.is_viewpoint :
|
|
||||||
score *= self.viewpoint_bonus
|
|
||||||
if landmarktype == 'nature' :
|
if landmarktype == 'nature' :
|
||||||
score *= self.nature_coeff
|
score *= self.nature_coeff
|
||||||
|
|
||||||
@ -197,12 +196,12 @@ class LandmarkManager:
|
|||||||
out = 'ids center tags'
|
out = 'ids center tags'
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.debug(f"Failed to fetch landmarks, proceeding without: {str(e)}")
|
self.logger.error(f"Error fetching landmarks: {e}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
return_list += self._to_landmarks(result, landmarktype, preference_level)
|
return_list += self._to_landmarks(result, landmarktype, preference_level)
|
||||||
|
|
||||||
# self.logger.debug(f"Fetched {len(return_list)} landmarks of type {landmarktype} in {bbox}")
|
self.logger.debug(f"Fetched {len(return_list)} landmarks of type {landmarktype} in {bbox}")
|
||||||
|
|
||||||
return return_list
|
return return_list
|
||||||
|
|
||||||
@ -246,6 +245,8 @@ class LandmarkManager:
|
|||||||
attractiveness=0,
|
attractiveness=0,
|
||||||
n_tags=len(tags))
|
n_tags=len(tags))
|
||||||
|
|
||||||
|
# self.logger.debug('added landmark.')
|
||||||
|
|
||||||
# Browse through tags to add information to landmark.
|
# Browse through tags to add information to landmark.
|
||||||
for key, value in tags.items():
|
for key, value in tags.items():
|
||||||
|
|
||||||
@ -266,7 +267,7 @@ class LandmarkManager:
|
|||||||
landmark.image_url = value
|
landmark.image_url = value
|
||||||
if key == 'website' :
|
if key == 'website' :
|
||||||
landmark.website_url = value
|
landmark.website_url = value
|
||||||
if value == 'place_of_worship' :
|
if key == 'place_of_worship' :
|
||||||
landmark.is_place_of_worship = True
|
landmark.is_place_of_worship = True
|
||||||
if key == 'wikipedia' :
|
if key == 'wikipedia' :
|
||||||
landmark.wiki_url = value
|
landmark.wiki_url = value
|
||||||
@ -275,7 +276,6 @@ class LandmarkManager:
|
|||||||
if 'building:' in key or 'pay' in key :
|
if 'building:' in key or 'pay' in key :
|
||||||
landmark.n_tags -= 1
|
landmark.n_tags -= 1
|
||||||
|
|
||||||
|
|
||||||
# Set the duration.
|
# Set the duration.
|
||||||
if value in ['museum', 'aquarium', 'planetarium'] :
|
if value in ['museum', 'aquarium', 'planetarium'] :
|
||||||
landmark.duration = 60
|
landmark.duration = 60
|
||||||
@ -286,138 +286,14 @@ class LandmarkManager:
|
|||||||
landmark.is_place_of_worship = False
|
landmark.is_place_of_worship = False
|
||||||
landmark.duration = 10
|
landmark.duration = 10
|
||||||
|
|
||||||
landmark.description, landmark.keywords = self.description_and_keywords(tags)
|
else:
|
||||||
self.set_landmark_score(landmark, landmarktype, preference_level)
|
self.set_landmark_score(landmark, landmarktype, preference_level)
|
||||||
landmarks.append(landmark)
|
landmarks.append(landmark)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
||||||
return landmarks
|
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:
|
def dict_to_selector_list(d: dict) -> list:
|
||||||
"""
|
"""
|
||||||
Convert a dictionary of key-value pairs to a list of Overpass query strings.
|
Convert a dictionary of key-value pairs to a list of Overpass query strings.
|
@ -2,8 +2,8 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from ..overpass.overpass import Overpass, get_base_info
|
from ..overpass.overpass import Overpass, get_base_info
|
||||||
from ..structs.toilets import Toilets
|
from ..structs.landmark import Toilets
|
||||||
from ..utils.bbox import create_bbox
|
from .utils import create_bbox
|
||||||
|
|
||||||
|
|
||||||
# silence the overpass logger
|
# silence the overpass logger
|
||||||
@ -65,7 +65,7 @@ class ToiletsManager:
|
|||||||
try:
|
try:
|
||||||
result = self.overpass.fetch_data_from_api(query_str=query)
|
result = self.overpass.fetch_data_from_api(query_str=query)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Error fetching toilets: {e}")
|
self.logger.error(f"Error fetching landmarks: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
toilets_list = self.to_toilets(result)
|
toilets_list = self.to_toilets(result)
|
48
status
48
status
@ -1,48 +0,0 @@
|
|||||||
error: wrong number of arguments, should be from 1 to 2
|
|
||||||
usage: git config [<options>]
|
|
||||||
|
|
||||||
Config file location
|
|
||||||
--[no-]global use global config file
|
|
||||||
--[no-]system use system config file
|
|
||||||
--[no-]local use repository config file
|
|
||||||
--[no-]worktree use per-worktree config file
|
|
||||||
-f, --[no-]file <file>
|
|
||||||
use given config file
|
|
||||||
--[no-]blob <blob-id> read config from given blob object
|
|
||||||
|
|
||||||
Action
|
|
||||||
--[no-]get get value: name [value-pattern]
|
|
||||||
--[no-]get-all get all values: key [value-pattern]
|
|
||||||
--[no-]get-regexp get values for regexp: name-regex [value-pattern]
|
|
||||||
--[no-]get-urlmatch get value specific for the URL: section[.var] URL
|
|
||||||
--[no-]replace-all replace all matching variables: name value [value-pattern]
|
|
||||||
--[no-]add add a new variable: name value
|
|
||||||
--[no-]unset remove a variable: name [value-pattern]
|
|
||||||
--[no-]unset-all remove all matches: name [value-pattern]
|
|
||||||
--[no-]rename-section rename section: old-name new-name
|
|
||||||
--[no-]remove-section remove a section: name
|
|
||||||
-l, --[no-]list list all
|
|
||||||
--[no-]fixed-value use string equality when comparing values to 'value-pattern'
|
|
||||||
-e, --[no-]edit open an editor
|
|
||||||
--[no-]get-color find the color configured: slot [default]
|
|
||||||
--[no-]get-colorbool find the color setting: slot [stdout-is-tty]
|
|
||||||
|
|
||||||
Type
|
|
||||||
-t, --[no-]type <type>
|
|
||||||
value is given this type
|
|
||||||
--bool value is "true" or "false"
|
|
||||||
--int value is decimal number
|
|
||||||
--bool-or-int value is --bool or --int
|
|
||||||
--bool-or-str value is --bool or string
|
|
||||||
--path value is a path (file or directory name)
|
|
||||||
--expiry-date value is an expiry date
|
|
||||||
|
|
||||||
Other
|
|
||||||
-z, --[no-]null terminate values with NUL byte
|
|
||||||
--[no-]name-only show variable names only
|
|
||||||
--[no-]includes respect include directives on lookup
|
|
||||||
--[no-]show-origin show origin of config (file, standard input, blob, command line)
|
|
||||||
--[no-]show-scope show scope of config (worktree, local, global, system, command)
|
|
||||||
--[no-]default <value>
|
|
||||||
with --get, use default value when missing entry
|
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user