Compare commits
No commits in common. "ddd2e91328123cbbbe9ee3981fe61d0c7ac1495b" and "ced4d63382eeab54a0b75c654e1cc068093c6aac" have entirely different histories.
ddd2e91328
...
ced4d63382
@ -30,5 +30,5 @@ jobs:
|
||||
working-directory: backend
|
||||
|
||||
- name: Run linter
|
||||
run: pipenv run pylint src --fail-under=9
|
||||
run: pipenv run pylint src
|
||||
working-directory: backend
|
||||
|
@ -439,8 +439,7 @@ disable=raw-checker-failed,
|
||||
use-symbolic-message-instead,
|
||||
use-implicit-booleaness-not-comparison-to-string,
|
||||
use-implicit-booleaness-not-comparison-to-zero,
|
||||
import-error,
|
||||
line-too-long
|
||||
import-error
|
||||
|
||||
# Enable the message, report, category or checker with the given id(s). You can
|
||||
# either give multiple identifier separated by comma (,) or put this option
|
||||
|
@ -23,5 +23,3 @@ osmpythontools = "*"
|
||||
pywikibot = "*"
|
||||
pymemcache = "*"
|
||||
fastapi-cli = "*"
|
||||
scikit-learn = "*"
|
||||
pyqt6 = "*"
|
||||
|
3975
backend/Pipfile.lock
generated
3975
backend/Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@ -1,14 +1,13 @@
|
||||
"""Main app for backend api"""
|
||||
|
||||
import logging
|
||||
from fastapi import FastAPI, HTTPException, Query
|
||||
from fastapi import FastAPI, HTTPException
|
||||
|
||||
from .structs.landmark import Landmark, Toilets
|
||||
from .structs.landmark import Landmark
|
||||
from .structs.preferences import Preferences
|
||||
from .structs.linked_landmarks import LinkedLandmarks
|
||||
from .structs.trip import Trip
|
||||
from .utils.landmarks_manager import LandmarkManager
|
||||
from .utils.toilets_manager import ToiletsManager
|
||||
from .utils.optimizer import Optimizer
|
||||
from .utils.refiner import Refiner
|
||||
from .persistence import client as cache_client
|
||||
@ -37,15 +36,19 @@ def new_trip(preferences: Preferences,
|
||||
(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.")
|
||||
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.")
|
||||
raise HTTPException(status_code=406,
|
||||
detail="All preferences are 0.")
|
||||
if start is None:
|
||||
raise HTTPException(status_code=406, detail="Start coordinates not provided")
|
||||
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")
|
||||
raise HTTPException(status_code=423,
|
||||
detail="Start coordinates not in range")
|
||||
if end is None:
|
||||
end = start
|
||||
logger.info("No end coordinates provided. Using start=end.")
|
||||
@ -58,7 +61,7 @@ def new_trip(preferences: Preferences,
|
||||
attractiveness=0,
|
||||
must_do=True,
|
||||
n_tags = 0)
|
||||
|
||||
|
||||
end_landmark = Landmark(name='finish',
|
||||
type='finish',
|
||||
location=(end[0], end[1]),
|
||||
@ -66,7 +69,7 @@ def new_trip(preferences: Preferences,
|
||||
osm_id=0,
|
||||
attractiveness=0,
|
||||
must_do=True,
|
||||
n_tags=0)
|
||||
n_tags = 0)
|
||||
|
||||
# Generate the landmarks from the start location
|
||||
landmarks, landmarks_short = manager.generate_landmarks_list(
|
||||
@ -131,32 +134,5 @@ def get_landmark(landmark_uuid: str) -> Landmark:
|
||||
landmark = cache_client.get(f"landmark_{landmark_uuid}")
|
||||
return landmark
|
||||
except KeyError as exc:
|
||||
raise HTTPException(status_code=404, detail="Landmark not found") from exc
|
||||
|
||||
|
||||
@app.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()
|
||||
return toilets_list
|
||||
except KeyError as exc:
|
||||
raise HTTPException(status_code=404, detail="No toilets found") from exc
|
||||
raise HTTPException(status_code=404,
|
||||
detail="Landmark not found") from exc
|
||||
|
@ -1,5 +1,5 @@
|
||||
"""Module used for handling cache"""
|
||||
from pymemcache import serde
|
||||
|
||||
from pymemcache.client.base import Client
|
||||
|
||||
from .constants import MEMCACHED_HOST_PATH
|
||||
@ -70,6 +70,5 @@ else:
|
||||
MEMCACHED_HOST_PATH,
|
||||
timeout=1,
|
||||
allow_unicode_keys=True,
|
||||
encoding='utf-8',
|
||||
serde=serde.pickle_serde
|
||||
encoding='utf-8'
|
||||
)
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,698 +0,0 @@
|
||||
{
|
||||
"type": "FeatureCollection",
|
||||
"generator": "overpass-turbo",
|
||||
"copyright": "The data included in this document is from www.openstreetmap.org. The data is made available under ODbL.",
|
||||
"timestamp": "2024-12-02T21:14:59Z",
|
||||
"features": [
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"@id": "node/1345741798",
|
||||
"name": "Cordonnerie Saint-Joseph",
|
||||
"shop": "shoes"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
7.3481705,
|
||||
48.0816462
|
||||
]
|
||||
},
|
||||
"id": "node/1345741798"
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"@id": "node/2659184738",
|
||||
"brand": "Armand Thiery",
|
||||
"brand:wikidata": "Q2861975",
|
||||
"brand:wikipedia": "fr:Armand Thiery",
|
||||
"name": "Armand Thiery",
|
||||
"opening_hours": "Mo-Sa 09:30-19:00",
|
||||
"shop": "clothes",
|
||||
"wheelchair": "limited"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
7.3594454,
|
||||
48.0785574
|
||||
]
|
||||
},
|
||||
"id": "node/2659184738"
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"@id": "node/3618136290",
|
||||
"name": "Chez Dominique",
|
||||
"shop": "clothes"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
7.3362362,
|
||||
48.0712174
|
||||
]
|
||||
},
|
||||
"id": "node/3618136290"
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"@id": "node/3618136605",
|
||||
"name": "Divamod",
|
||||
"shop": "clothes"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
7.3304253,
|
||||
48.0782989
|
||||
]
|
||||
},
|
||||
"id": "node/3618136605"
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"@id": "node/3618284507",
|
||||
"name": "Star tendances et voyages",
|
||||
"shop": "clothes"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
7.3474029,
|
||||
48.0830993
|
||||
]
|
||||
},
|
||||
"id": "node/3618284507"
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"@id": "node/3619696125",
|
||||
"brand": "Zeeman",
|
||||
"brand:wikidata": "Q184399",
|
||||
"name": "Zeeman",
|
||||
"shop": "clothes"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
7.3413834,
|
||||
48.0638444
|
||||
]
|
||||
},
|
||||
"id": "node/3619696125"
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"@id": "node/4594398129",
|
||||
"name": "Miss et Mister",
|
||||
"shop": "clothes"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
7.3308309,
|
||||
48.0779118
|
||||
]
|
||||
},
|
||||
"id": "node/4594398129"
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"@id": "node/4907320441",
|
||||
"brand": "Sergent Major",
|
||||
"brand:wikidata": "Q62521738",
|
||||
"clothes": "babies;children",
|
||||
"name": "Sergent Major",
|
||||
"opening_hours": "Mo-Sa 09:30-19:00",
|
||||
"shop": "clothes",
|
||||
"wheelchair": "no"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
7.359116,
|
||||
48.0787229
|
||||
]
|
||||
},
|
||||
"id": "node/4907320441"
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"@id": "node/4907364791",
|
||||
"brand": "Armand Thiery",
|
||||
"brand:wikidata": "Q2861975",
|
||||
"brand:wikipedia": "fr:Armand Thiery",
|
||||
"clothes": "women",
|
||||
"name": "Armand Thiery",
|
||||
"shop": "clothes"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
7.3601857,
|
||||
48.0783373
|
||||
]
|
||||
},
|
||||
"id": "node/4907364791"
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"@id": "node/4907385675",
|
||||
"check_date": "2024-05-19",
|
||||
"clothes": "children",
|
||||
"name": "Du Pareil...au même",
|
||||
"shop": "clothes"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
7.3604521,
|
||||
48.0779726
|
||||
]
|
||||
},
|
||||
"id": "node/4907385675"
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"@id": "node/4922191645",
|
||||
"name": "Abilos",
|
||||
"shop": "clothes"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
7.3566167,
|
||||
48.0794136
|
||||
]
|
||||
},
|
||||
"id": "node/4922191645"
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"@id": "node/4922191648",
|
||||
"brand": "Esprit",
|
||||
"brand:wikidata": "Q532746",
|
||||
"brand:wikipedia": "en:Esprit Holdings",
|
||||
"name": "Esprit",
|
||||
"shop": "clothes"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
7.3554004,
|
||||
48.0787549
|
||||
]
|
||||
},
|
||||
"id": "node/4922191648"
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"@id": "node/4922191972",
|
||||
"brand": "Guess",
|
||||
"brand:wikidata": "Q2470307",
|
||||
"brand:wikipedia": "en:Guess (clothing)",
|
||||
"name": "Guess",
|
||||
"shop": "clothes"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
7.355273,
|
||||
48.0788003
|
||||
]
|
||||
},
|
||||
"id": "node/4922191972"
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"@id": "node/4922192001",
|
||||
"name": "Lingerie",
|
||||
"shop": "clothes"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
7.3575453,
|
||||
48.0779317
|
||||
]
|
||||
},
|
||||
"id": "node/4922192001"
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"@id": "node/5359915869",
|
||||
"name": "Al Assil",
|
||||
"shop": "clothes"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
7.3305665,
|
||||
48.0780902
|
||||
]
|
||||
},
|
||||
"id": "node/5359915869"
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"@id": "node/9089360040",
|
||||
"brand": "Grain de Malice",
|
||||
"brand:wikidata": "Q66757157",
|
||||
"clothes": "women",
|
||||
"name": "Grain de Malice",
|
||||
"shop": "clothes",
|
||||
"short_name": "GDM"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
7.3593125,
|
||||
48.0786234
|
||||
]
|
||||
},
|
||||
"id": "node/9089360040"
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"@id": "node/9095193153",
|
||||
"brand": "Undiz",
|
||||
"brand:wikidata": "Q105306275",
|
||||
"clothes": "underwear",
|
||||
"name": "Undiz",
|
||||
"shop": "clothes"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
7.3599579,
|
||||
48.0782846
|
||||
]
|
||||
},
|
||||
"id": "node/9095193153"
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"@id": "node/9095193154",
|
||||
"branch": "Lingerie",
|
||||
"brand": "RougeGorge",
|
||||
"brand:wikidata": "Q104600739",
|
||||
"clothes": "underwear",
|
||||
"name": "RougeGorge",
|
||||
"shop": "clothes"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
7.3604883,
|
||||
48.0781607
|
||||
]
|
||||
},
|
||||
"id": "node/9095193154"
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"@id": "node/9095212690",
|
||||
"alt_name": "North Face",
|
||||
"brand": "The North Face",
|
||||
"brand:wikidata": "Q152784",
|
||||
"brand:wikipedia": "en:The North Face",
|
||||
"check_date": "2024-05-19",
|
||||
"name": "The North Face",
|
||||
"shop": "clothes"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
7.3603923,
|
||||
48.0773727
|
||||
]
|
||||
},
|
||||
"id": "node/9095212690"
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"@id": "node/9095270059",
|
||||
"air_conditioning": "no",
|
||||
"clothes": "men",
|
||||
"level": "0",
|
||||
"name": "Maison Aume",
|
||||
"second_hand": "no",
|
||||
"shop": "clothes",
|
||||
"wheelchair": "no"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
7.361364,
|
||||
48.0799999
|
||||
]
|
||||
},
|
||||
"id": "node/9095270059"
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"@id": "node/9098624272",
|
||||
"name": "Destock Place",
|
||||
"shop": "clothes"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
7.3575161,
|
||||
48.0793009
|
||||
]
|
||||
},
|
||||
"id": "node/9098624272"
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"@id": "node/9123861652",
|
||||
"name": "Weackers",
|
||||
"shop": "shoes"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
7.361329,
|
||||
48.0785972
|
||||
]
|
||||
},
|
||||
"id": "node/9123861652"
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"@id": "node/9162179887",
|
||||
"brand": "Calzedonia",
|
||||
"brand:wikidata": "Q1027874",
|
||||
"brand:wikipedia": "en:Calzedonia",
|
||||
"name": "Calzedonia",
|
||||
"shop": "clothes"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
7.3606374,
|
||||
48.0780809
|
||||
]
|
||||
},
|
||||
"id": "node/9162179887"
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"@id": "node/9162206449",
|
||||
"clothes": "women",
|
||||
"name": "Cop. Copine",
|
||||
"shop": "clothes"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
7.3600947,
|
||||
48.078399
|
||||
]
|
||||
},
|
||||
"id": "node/9162206449"
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"@id": "node/9162226360",
|
||||
"brand": "Okaïdi",
|
||||
"brand:wikidata": "Q3350027",
|
||||
"brand:wikipedia": "fr:Okaïdi",
|
||||
"name": "Okaïdi",
|
||||
"shop": "clothes"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
7.3596986,
|
||||
48.078428
|
||||
]
|
||||
},
|
||||
"id": "node/9162226360"
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"@id": "node/9162227010",
|
||||
"brand": "Jules",
|
||||
"brand:wikidata": "Q3188386",
|
||||
"brand:wikipedia": "fr:Jules (enseigne)",
|
||||
"clothes": "men",
|
||||
"name": "Jules",
|
||||
"opening_hours": "Mo-Sa 09:30-19:00",
|
||||
"phone": "+33 3 89 41 03 62",
|
||||
"shop": "clothes",
|
||||
"website": "https://www.jules.com/fr-fr/magasins/1600133/"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
7.3600323,
|
||||
48.0782229
|
||||
]
|
||||
},
|
||||
"id": "node/9162227010"
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"@id": "node/10151865029",
|
||||
"name": "Atelier Cinq",
|
||||
"shop": "clothes"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
7.3571756,
|
||||
48.0772657
|
||||
]
|
||||
},
|
||||
"id": "node/10151865029"
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"@id": "node/10862176110",
|
||||
"name": "L'hexagone",
|
||||
"shop": "bag"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
7.3808571,
|
||||
48.0814138
|
||||
]
|
||||
},
|
||||
"id": "node/10862176110"
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"@id": "node/11150877331",
|
||||
"brand": "Punt Roma",
|
||||
"brand:wikidata": "Q101423290",
|
||||
"clothes": "women",
|
||||
"name": "Punt Roma",
|
||||
"shop": "clothes"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
7.3571859,
|
||||
48.0779406
|
||||
]
|
||||
},
|
||||
"id": "node/11150877331"
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"@id": "node/11150959880",
|
||||
"name": "Caroll",
|
||||
"shop": "clothes"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
7.3579354,
|
||||
48.0779291
|
||||
]
|
||||
},
|
||||
"id": "node/11150959880"
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"@id": "node/11302242094",
|
||||
"branch": "Wintzenheim",
|
||||
"name": "Label Fripe",
|
||||
"opening_hours": "Mo-Sa 09:00-18:45",
|
||||
"phone": "+33 3 89 27 39 25",
|
||||
"second_hand": "only",
|
||||
"shop": "clothes",
|
||||
"website": "https://labelfripe.fr/label-fripe-wintzenheim/"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
7.3109899,
|
||||
48.0850362
|
||||
]
|
||||
},
|
||||
"id": "node/11302242094"
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"@id": "node/11392247003",
|
||||
"name": "Lingerie Sipp",
|
||||
"shop": "clothes"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
7.3111507,
|
||||
48.0841835
|
||||
]
|
||||
},
|
||||
"id": "node/11392247003"
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"@id": "node/11778819781",
|
||||
"addr:city": "Colmar",
|
||||
"addr:housenumber": "10",
|
||||
"addr:postcode": "68000",
|
||||
"addr:street": "Rue des Têtes",
|
||||
"clothes": "suits;hats;men",
|
||||
"name": "Phillipe",
|
||||
"phone": "0389411983",
|
||||
"shop": "clothes"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
7.3559389,
|
||||
48.0789064
|
||||
]
|
||||
},
|
||||
"id": "node/11778819781"
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"@id": "node/11799215969",
|
||||
"brand": "Petit Bateau",
|
||||
"brand:wikidata": "Q3377090",
|
||||
"name": "Petit Bateau",
|
||||
"opening_hours": "Mo-Sa 10:00-19:00; Su 10:00-18:00",
|
||||
"phone": "+33 3 89 24 97 85",
|
||||
"shop": "clothes",
|
||||
"website": "https://stores.petit-bateau.com/france/colmar/9-rue-des-boulangers"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
7.355149,
|
||||
48.0780213
|
||||
]
|
||||
},
|
||||
"id": "node/11799215969"
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"@id": "node/11816704669",
|
||||
"addr:housenumber": "10",
|
||||
"addr:street": "Rue des Boulangers",
|
||||
"name": "des petits hauts",
|
||||
"shop": "clothes"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
7.3555001,
|
||||
48.0780768
|
||||
]
|
||||
},
|
||||
"id": "node/11816704669"
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"@id": "node/12320343534",
|
||||
"addr:city": "Colmar",
|
||||
"addr:housenumber": "44",
|
||||
"addr:postcode": "68000",
|
||||
"addr:street": "Rue des Clefs",
|
||||
"brand": "Un Jour Ailleurs",
|
||||
"brand:wikidata": "Q105106211",
|
||||
"clothes": "women",
|
||||
"name": "Un Jour Ailleurs",
|
||||
"opening_hours": "Mo-Fr 10:00-19:00; Sa 10:00-18:30",
|
||||
"phone": "+33368318572",
|
||||
"shop": "clothes",
|
||||
"website": "https://boutique.unjourailleurs.com/fr/mode-femme/boutique-colmar-76"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
7.35897,
|
||||
48.0789807
|
||||
]
|
||||
},
|
||||
"id": "node/12320343534"
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"@id": "node/12320343536",
|
||||
"addr:city": "Colmar",
|
||||
"addr:housenumber": "38",
|
||||
"addr:postcode": "68000",
|
||||
"addr:street": "Rue des Clefs",
|
||||
"brand": "Timberland",
|
||||
"brand:wikidata": "Q1539185",
|
||||
"name": "Timberland",
|
||||
"opening_hours": "Mo-Sa 10:00-19:00",
|
||||
"phone": "+33389298650",
|
||||
"shop": "clothes"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
7.3592409,
|
||||
48.0788785
|
||||
]
|
||||
},
|
||||
"id": "node/12320343536"
|
||||
}
|
||||
]
|
||||
}
|
@ -1,350 +0,0 @@
|
||||
# pylint: skip-file
|
||||
|
||||
import numpy as np
|
||||
import json
|
||||
import os
|
||||
from typing import Optional, Literal
|
||||
from sklearn.cluster import DBSCAN
|
||||
from sklearn.decomposition import PCA
|
||||
import matplotlib.pyplot as plt
|
||||
from pydantic import BaseModel
|
||||
from OSMPythonTools.overpass import Overpass, overpassQueryBuilder
|
||||
from OSMPythonTools.cachingStrategy import CachingStrategy, JSON
|
||||
from math import sin, cos, sqrt, atan2, radians
|
||||
|
||||
|
||||
EARTH_RADIUS_KM = 6373
|
||||
|
||||
|
||||
class ShoppingLocation(BaseModel):
|
||||
type: Literal['street', 'area']
|
||||
importance: int
|
||||
centroid: tuple
|
||||
start: Optional[list] = None
|
||||
end: Optional[list] = None
|
||||
|
||||
|
||||
# Output to frontend
|
||||
class Landmark(BaseModel) :
|
||||
# Properties of the landmark
|
||||
name : str
|
||||
type: Literal['sightseeing', 'nature', 'shopping', 'start', 'finish']
|
||||
location : tuple
|
||||
osm_type : str
|
||||
osm_id : int
|
||||
attractiveness : int
|
||||
n_tags : int
|
||||
image_url : Optional[str] = None
|
||||
website_url : Optional[str] = None
|
||||
description : Optional[str] = None # TODO future
|
||||
duration : Optional[int] = 0
|
||||
name_en : Optional[str] = None
|
||||
|
||||
# Additional properties depending on specific tour
|
||||
must_do : Optional[bool] = False
|
||||
must_avoid : Optional[bool] = False
|
||||
is_secondary : Optional[bool] = False
|
||||
|
||||
time_to_reach_next : Optional[int] = 0
|
||||
next_uuid : Optional[str] = None
|
||||
|
||||
|
||||
def extract_points(filestr: str) :
|
||||
"""
|
||||
Extract points from geojson file.
|
||||
|
||||
Returns :
|
||||
np.array containing the points
|
||||
"""
|
||||
points = []
|
||||
|
||||
with open(os.path.dirname(__file__) + '/' + filestr, 'r') as f:
|
||||
geojson = json.load(f)
|
||||
|
||||
for feature in geojson['features']:
|
||||
if feature['geometry']['type'] == 'Point':
|
||||
centroid = feature['geometry']['coordinates']
|
||||
points.append(centroid)
|
||||
|
||||
elif feature['geometry']['type'] == 'Polygon':
|
||||
centroid = np.array(feature['geometry']['coordinates'][0][0])
|
||||
points.append(centroid)
|
||||
|
||||
# Convert the list of points to a NumPy array
|
||||
return np.array(points)
|
||||
|
||||
|
||||
def get_distance(p1: tuple[float, float], p2: tuple[float, float]) -> int:
|
||||
"""
|
||||
Calculate the time in minutes to travel from one location to another.
|
||||
|
||||
Args:
|
||||
p1 (tuple[float, float]): Coordinates of the starting location.
|
||||
p2 (tuple[float, float]): Coordinates of the destination.
|
||||
|
||||
Returns:
|
||||
int: Time to travel from p1 to p2 in minutes.
|
||||
"""
|
||||
|
||||
|
||||
if p1 == p2:
|
||||
return 0
|
||||
else:
|
||||
# Compute the distance in km along the surface of the Earth
|
||||
# (assume spherical Earth)
|
||||
# this is the haversine formula, stolen from stackoverflow
|
||||
# in order to not use any external libraries
|
||||
lat1, lon1 = radians(p1[0]), radians(p1[1])
|
||||
lat2, lon2 = radians(p2[0]), radians(p2[1])
|
||||
|
||||
dlon = lon2 - lon1
|
||||
dlat = lat2 - lat1
|
||||
|
||||
a = sin(dlat / 2)**2 + cos(lat1) * cos(lat2) * sin(dlon / 2)**2
|
||||
c = 2 * atan2(sqrt(a), sqrt(1 - a))
|
||||
|
||||
return EARTH_RADIUS_KM * c
|
||||
|
||||
def filter_clusters(cluster_points, cluster_labels):
|
||||
"""
|
||||
Remove clusters of less importance.
|
||||
"""
|
||||
label_counts = np.bincount(cluster_labels)
|
||||
|
||||
# Step 3: Get the indices (labels) of the 5 largest clusters
|
||||
top_5_labels = np.argsort(label_counts)[-5:] # Get the largest 5 clusters
|
||||
|
||||
# Step 4: Filter points to keep only the points in the top 5 clusters
|
||||
filtered_cluster_points = []
|
||||
filtered_cluster_labels = []
|
||||
|
||||
for label in top_5_labels:
|
||||
filtered_cluster_points.append(cluster_points[cluster_labels == label])
|
||||
filtered_cluster_labels.append(np.full((label_counts[label],), label)) # Replicate the label
|
||||
|
||||
# Concatenate filtered clusters into a single array
|
||||
return np.vstack(filtered_cluster_points), np.concatenate(filtered_cluster_labels)
|
||||
|
||||
|
||||
def fit_lines(points, labels):
|
||||
"""
|
||||
Fit lines to identified clusters.
|
||||
"""
|
||||
all_x = []
|
||||
all_y = []
|
||||
lines = []
|
||||
locations = []
|
||||
|
||||
for label in set(labels):
|
||||
cluster_points = points[labels == label]
|
||||
|
||||
# If there's not enough points, skip
|
||||
if len(cluster_points) < 2:
|
||||
continue
|
||||
|
||||
# Apply PCA to find the principal component (i.e., the line of best fit)
|
||||
pca = PCA(n_components=1)
|
||||
pca.fit(cluster_points)
|
||||
|
||||
direction = pca.components_[0]
|
||||
centroid = pca.mean_
|
||||
|
||||
# Project the cluster points onto the principal direction (line direction)
|
||||
projections = np.dot(cluster_points - centroid, direction)
|
||||
|
||||
# Get the range of the projections to find the approximate length of the cluster
|
||||
cluster_length = projections.max() - projections.min()
|
||||
|
||||
# Now adjust `t` so that it scales with the cluster length
|
||||
t = np.linspace(-cluster_length / 2.75, cluster_length / 2.75, 10)
|
||||
|
||||
# Calculate the start and end of the line based on min/max projections
|
||||
start_point = centroid[0] + t*direction[0]
|
||||
end_point = centroid[1] + t*direction[1]
|
||||
|
||||
# Store the line
|
||||
lines.append((start_point, end_point))
|
||||
|
||||
# For visualization, store the points
|
||||
all_x.append(min(start_point))
|
||||
all_x.append(max(start_point))
|
||||
all_y.append(min(end_point))
|
||||
all_y.append(max(end_point))
|
||||
|
||||
if np.linalg.norm(t) <= 0.0045 :
|
||||
loc = ShoppingLocation(
|
||||
type='area',
|
||||
centroid=tuple((centroid[1], centroid[0])),
|
||||
importance = len(cluster_points),
|
||||
)
|
||||
else :
|
||||
loc = ShoppingLocation(
|
||||
type='street',
|
||||
centroid=tuple((centroid[1], centroid[0])),
|
||||
importance = len(cluster_points),
|
||||
start=start_point,
|
||||
end=end_point
|
||||
)
|
||||
|
||||
locations.append(loc)
|
||||
|
||||
xmin = min(all_x)
|
||||
xmax = max(all_x)
|
||||
ymin = min(all_y)
|
||||
ymax = max(all_y)
|
||||
corners = (xmin, xmax, ymin, ymax)
|
||||
|
||||
return corners, locations
|
||||
|
||||
|
||||
|
||||
def create_landmark(shopping_location: ShoppingLocation):
|
||||
|
||||
# Define the bounding box for a given radius around the coordinates
|
||||
lat, lon = shopping_location.centroid
|
||||
bbox = ("around:1000", str(lat), str(lon))
|
||||
|
||||
overpass = Overpass()
|
||||
# CachingStrategy.use(JSON, cacheDir=OSM_CACHE_DIR)
|
||||
|
||||
# Query neighborhoods and shopping malls
|
||||
selectors = ['"place"~"^(suburb|neighborhood|neighbourhood|quarter|city_block)$"', '"shop"="mall"']
|
||||
|
||||
min_dist = float('inf')
|
||||
new_name = 'Shopping Area'
|
||||
new_name_en = None
|
||||
osm_id = 0
|
||||
osm_type = 'node'
|
||||
|
||||
for sel in selectors :
|
||||
query = overpassQueryBuilder(
|
||||
bbox = bbox,
|
||||
elementType = ['node', 'way', 'relation'],
|
||||
selector = sel,
|
||||
includeCenter = True,
|
||||
out = 'center'
|
||||
)
|
||||
|
||||
try:
|
||||
result = overpass.query(query)
|
||||
except Exception as e:
|
||||
raise Exception("query unsuccessful")
|
||||
|
||||
for elem in result.elements():
|
||||
|
||||
location = (elem.centerLat(), elem.centerLon())
|
||||
|
||||
if location[0] is None :
|
||||
location = (elem.lat(), elem.lon())
|
||||
if location[0] is None :
|
||||
# print(f"Fetching coordinates failed with {elem.type()}/{elem.id()}")
|
||||
continue
|
||||
|
||||
# print(f"Distance : {get_distance(shopping_location.centroid, location)}")
|
||||
d = get_distance(shopping_location.centroid, location)
|
||||
if d < min_dist :
|
||||
min_dist = d
|
||||
new_name = elem.tag('name')
|
||||
osm_type = elem.type() # Add type: 'way' or 'relation'
|
||||
osm_id = elem.id() # Add OSM id
|
||||
|
||||
# add english name if it exists
|
||||
try :
|
||||
new_name_en = elem.tag('name:en')
|
||||
except:
|
||||
pass
|
||||
|
||||
return Landmark(
|
||||
name=new_name,
|
||||
type='shopping',
|
||||
location=shopping_location.centroid, # TODO: use the fact the we can also recognize streets.
|
||||
attractiveness=shopping_location.importance,
|
||||
n_tags=0,
|
||||
osm_id=osm_id,
|
||||
osm_type=osm_type,
|
||||
name_en=new_name_en
|
||||
)
|
||||
|
||||
|
||||
# Extract points
|
||||
points = extract_points('vienna_data.json')
|
||||
|
||||
# print(len(points))
|
||||
|
||||
######## Create a figure with 1 row and 3 columns for side-by-side plots
|
||||
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
|
||||
# Plot Raw data points
|
||||
axes[0].set_title('Raw Data')
|
||||
axes[0].scatter(points[:, 0], points[:, 1], color='blue', s=20)
|
||||
|
||||
|
||||
# Apply DBSCAN to find clusters. Choose different settings for different cities.
|
||||
if len(points) > 400 :
|
||||
dbscan = DBSCAN(eps=0.00118, min_samples=15, algorithm='kd_tree') # for large cities
|
||||
else :
|
||||
dbscan = DBSCAN(eps=0.00075, min_samples=10, algorithm='kd_tree') # for small cities
|
||||
|
||||
labels = dbscan.fit_predict(points)
|
||||
|
||||
# Separate clustered points and noise points
|
||||
clustered_points = points[labels != -1]
|
||||
clustered_labels = labels[labels != -1]
|
||||
noise_points = points[labels == -1]
|
||||
|
||||
######## Plot n°1: DBSCAN Clustering Results
|
||||
axes[1].set_title('DBSCAN Clusters')
|
||||
axes[1].scatter(clustered_points[:, 0], clustered_points[:, 1], c=clustered_labels, cmap='rainbow', s=20)
|
||||
axes[1].scatter(noise_points[:, 0], noise_points[:, 1], c='blue', s=7, label='Noise')
|
||||
|
||||
# Keep the 5 biggest clusters
|
||||
clustered_points, clustered_labels = filter_clusters(clustered_points, clustered_labels)
|
||||
|
||||
# Fit lines
|
||||
corners, locations = fit_lines(clustered_points, clustered_labels)
|
||||
(xmin, xmax, ymin, ymax) = corners
|
||||
|
||||
|
||||
######## Plot clustered points in normal size and noise points separately
|
||||
axes[2].scatter(clustered_points[:, 0], clustered_points[:, 1], c=clustered_labels, cmap='rainbow', s=30)
|
||||
axes[2].set_title('PCA Fitted Lines on Clusters')
|
||||
|
||||
# Create a list of Landmarks for the shopping things
|
||||
shopping_landmarks = []
|
||||
for loc in locations :
|
||||
axes[2].scatter(loc.centroid[1], loc.centroid[0], color='red', marker='x', s=200, linewidth=3)
|
||||
landmark = create_landmark(loc)
|
||||
shopping_landmarks.append(landmark)
|
||||
axes[2].text(loc.centroid[1], loc.centroid[0], landmark.name,
|
||||
ha='center', va='top', fontsize=6,
|
||||
bbox=dict(facecolor='white', edgecolor='black', boxstyle='round,pad=0.2'),
|
||||
zorder=3)
|
||||
|
||||
|
||||
|
||||
####### Plot the detected lines in the final plot #######
|
||||
# for loc in locations:
|
||||
# if loc.type == 'street' :
|
||||
# line_x = loc.start
|
||||
# line_y = loc.end
|
||||
# axes[2].plot(line_x, line_y, color='lime', linewidth=3)
|
||||
# else :
|
||||
|
||||
|
||||
|
||||
axes[0].set_xlim(xmin-0.01, xmax+0.01)
|
||||
axes[0].set_ylim(ymin-0.01, ymax+0.01)
|
||||
|
||||
axes[1].set_xlim(xmin-0.01, xmax+0.01)
|
||||
axes[1].set_ylim(ymin-0.01, ymax+0.01)
|
||||
|
||||
axes[2].set_xlim(xmin-0.01, xmax+0.01)
|
||||
axes[2].set_ylim(ymin-0.01, ymax+0.01)
|
||||
|
||||
|
||||
print("\n\n\n")
|
||||
for landmark in shopping_landmarks :
|
||||
print(f"{landmark.name} is a shopping area with a score of {landmark.attractiveness}")
|
||||
|
||||
|
||||
plt.tight_layout()
|
||||
plt.show()
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -73,6 +73,8 @@ class Landmark(BaseModel) :
|
||||
t_to_next_str = f", time_to_next={self.time_to_reach_next}" if self.time_to_reach_next else ""
|
||||
is_secondary_str = ", secondary" if self.is_secondary else ""
|
||||
type_str = '(' + self.type + ')'
|
||||
if self.type in ["start", "finish", "nature", "shopping"] :
|
||||
type_str += '\t '
|
||||
|
||||
return (f'Landmark{type_str}: [{self.name} @{self.location}, '
|
||||
f'score={self.attractiveness}{t_to_next_str}{is_secondary_str}]')
|
||||
@ -115,28 +117,3 @@ class Landmark(BaseModel) :
|
||||
return (self.uuid == value.uuid or
|
||||
self.osm_id == value.osm_id or
|
||||
(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}'
|
||||
|
||||
class Config:
|
||||
# This allows us to easily convert the model to and from dictionaries
|
||||
orm_mode = True
|
@ -1,42 +0,0 @@
|
||||
"""Collection of tests to ensure correct handling of invalid input."""
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
import pytest
|
||||
|
||||
from .test_utils import load_trip_landmarks
|
||||
from ..main import app
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def client():
|
||||
"""Client used to call the app."""
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
def test_cache(client, request): # pylint: disable=redefined-outer-name
|
||||
"""
|
||||
Test n°1 : Custom test in Turckheim to ensure small villages are also supported.
|
||||
|
||||
Args:
|
||||
client:
|
||||
request:
|
||||
"""
|
||||
duration_minutes = 15
|
||||
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.084588, 7.280405]
|
||||
}
|
||||
)
|
||||
result = response.json()
|
||||
landmarks = load_trip_landmarks(client, result['first_landmark_uuid'])
|
||||
landmarks_cached = load_trip_landmarks(client, result['first_landmark_uuid'], True)
|
||||
|
||||
# checks :
|
||||
assert response.status_code == 200 # check for successful planning
|
||||
assert landmarks_cached == landmarks
|
@ -33,19 +33,19 @@ def invalid_client():
|
||||
([91, 181], {"sightseeing": {"type": "nature", "score": 5},
|
||||
"nature": {"type": "nature", "score": 5},
|
||||
"shopping": {"type": "shopping", "score": 5},
|
||||
}, 422),
|
||||
}, 423),
|
||||
([-91, 181], {"sightseeing": {"type": "nature", "score": 5},
|
||||
"nature": {"type": "nature", "score": 5},
|
||||
"shopping": {"type": "shopping", "score": 5},
|
||||
}, 422),
|
||||
}, 423),
|
||||
([91, -181], {"sightseeing": {"type": "nature", "score": 5},
|
||||
"nature": {"type": "nature", "score": 5},
|
||||
"shopping": {"type": "shopping", "score": 5},
|
||||
}, 422),
|
||||
}, 423),
|
||||
([-91, -181], {"sightseeing": {"type": "nature", "score": 5},
|
||||
"nature": {"type": "nature", "score": 5},
|
||||
"shopping": {"type": "shopping", "score": 5},
|
||||
}, 422),
|
||||
}, 423),
|
||||
]
|
||||
)
|
||||
def test_input(invalid_client, start, preferences, status_code): # pylint: disable=redefined-outer-name
|
||||
|
@ -78,36 +78,6 @@ def test_bellecour(client, request) : # pylint: disable=redefined-outer-name
|
||||
assert 136200148 in osm_ids # check for Cathédrale St. Jean in trip
|
||||
|
||||
|
||||
def test_shopping(client, request) : # pylint: disable=redefined-outer-name
|
||||
"""
|
||||
Test n°3 : Custom test in Lyon centre to ensure shopping clusters are found.
|
||||
|
||||
Args:
|
||||
client:
|
||||
request:
|
||||
"""
|
||||
duration_minutes = 600
|
||||
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'])
|
||||
# osm_ids = landmarks_to_osmid(landmarks)
|
||||
|
||||
# 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 duration_minutes*0.8 < int(result['total_time']) < duration_minutes*1.2
|
||||
|
||||
# def test_new_trip_single_prefs(client):
|
||||
# response = client.post(
|
||||
|
@ -1,102 +0,0 @@
|
||||
"""Collection of tests to ensure correct implementation and track progress. """
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
import pytest
|
||||
|
||||
from ..structs.landmark import Toilets
|
||||
from ..main import app
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def client():
|
||||
"""Client used to call the app."""
|
||||
return TestClient(app)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"location,radius,status_code",
|
||||
[
|
||||
({}, None, 422), # Invalid case: no location at all.
|
||||
([443], 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
|
||||
"""
|
||||
Test n°1 : Verify handling of invalid input.
|
||||
|
||||
Args:
|
||||
client:
|
||||
request:
|
||||
"""
|
||||
response = client.post(
|
||||
"/toilets/new",
|
||||
params={
|
||||
"location": location,
|
||||
"radius": radius
|
||||
}
|
||||
)
|
||||
|
||||
# checks :
|
||||
assert response.status_code == status_code
|
||||
|
||||
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"location,status_code",
|
||||
[
|
||||
([48.2270, 7.4370], 200), # Orschwiller.
|
||||
([10.2012, 10.123], 200), # Nigerian desert.
|
||||
([63.989, -19.677], 200), # Hekla volcano, Iceland
|
||||
]
|
||||
)
|
||||
def test_no_toilets(client, location, status_code): # pylint: disable=redefined-outer-name
|
||||
"""
|
||||
Test n°3 : Verify the code finds some toilets in big cities.
|
||||
|
||||
Args:
|
||||
client:
|
||||
request:
|
||||
"""
|
||||
response = client.post(
|
||||
"/toilets/new",
|
||||
params={
|
||||
"location": location
|
||||
}
|
||||
)
|
||||
toilets_list = [Toilets.model_validate(toilet) for toilet in response.json()]
|
||||
|
||||
# checks :
|
||||
assert response.status_code == 200 # check for successful planning
|
||||
assert isinstance(toilets_list, list) # check that the return type is a list
|
||||
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"location,status_code",
|
||||
[
|
||||
([45.7576485, 4.8330241], 200), # Lyon, Bellecour.
|
||||
([-6.913795, 107.60278], 200), # Bandung, train station
|
||||
([-22.970140, -43.18181], 200), # Rio de Janeiro, Copacabana
|
||||
]
|
||||
)
|
||||
def test_toilets(client, location, status_code): # pylint: disable=redefined-outer-name
|
||||
"""
|
||||
Test n°3 : Verify the code finds some toilets in big cities.
|
||||
|
||||
Args:
|
||||
client:
|
||||
request:
|
||||
"""
|
||||
response = client.post(
|
||||
"/toilets/new",
|
||||
params={
|
||||
"location": location,
|
||||
"radius" : 600
|
||||
}
|
||||
)
|
||||
toilets_list = [Toilets.model_validate(toilet) for toilet in response.json()]
|
||||
|
||||
# checks :
|
||||
assert response.status_code == 200 # check for successful planning
|
||||
assert isinstance(toilets_list, list) # check that the return type is a list
|
||||
assert len(toilets_list) > 0
|
@ -1,13 +1,11 @@
|
||||
"""Helper methods for testing."""
|
||||
import logging
|
||||
from typing import List
|
||||
from fastapi import HTTPException
|
||||
from pydantic import ValidationError
|
||||
|
||||
from ..structs.landmark import Landmark
|
||||
from ..persistence import client as cache_client
|
||||
|
||||
|
||||
def landmarks_to_osmid(landmarks: list[Landmark]) -> list[int] :
|
||||
def landmarks_to_osmid(landmarks: List[Landmark]) -> List[int] :
|
||||
"""
|
||||
Convert the list of landmarks into a list containing their osm ids for quick landmark checking.
|
||||
|
||||
@ -33,68 +31,22 @@ def fetch_landmark(client, landmark_uuid: str):
|
||||
Returns:
|
||||
dict: Landmark data fetched from the API.
|
||||
"""
|
||||
logger = logging.getLogger(__name__)
|
||||
response = client.get(f"/landmark/{landmark_uuid}")
|
||||
|
||||
if response.status_code != 200:
|
||||
raise HTTPException(status_code=500,
|
||||
raise HTTPException(status_code=999,
|
||||
detail=f"Failed to fetch landmark with UUID {landmark_uuid}: {response.status_code}")
|
||||
|
||||
try:
|
||||
json_data = response.json()
|
||||
logger.info(f"API Response: {json_data}")
|
||||
except ValueError as e:
|
||||
logger.error(f"Failed to parse response as JSON: {response.text}")
|
||||
raise HTTPException(status_code=500, detail="Invalid response format from API")
|
||||
|
||||
# Try validating against the Landmark model here to ensure consistency
|
||||
try:
|
||||
landmark = Landmark(**json_data)
|
||||
except ValidationError as ve:
|
||||
logging.error(f"Validation error: {ve}")
|
||||
raise HTTPException(status_code=500, detail="Invalid data format received from API")
|
||||
|
||||
json_data = response.json()
|
||||
|
||||
if "detail" in json_data:
|
||||
raise HTTPException(status_code=500, detail=json_data["detail"])
|
||||
|
||||
return Landmark(**json_data)
|
||||
raise HTTPException(status_code=999, detail=json_data["detail"])
|
||||
|
||||
|
||||
def fetch_landmark_cache(landmark_uuid: str):
|
||||
"""
|
||||
Fetch landmark data from the cache based on the landmark UUID.
|
||||
|
||||
Args:
|
||||
landmark_uuid (str): The UUID of the landmark.
|
||||
|
||||
Returns:
|
||||
dict: Landmark data fetched from the cache or raises an HTTP exception.
|
||||
"""
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Try to fetch the landmark data from the cache
|
||||
try:
|
||||
landmark = cache_client.get(f"landmark_{landmark_uuid}")
|
||||
if not landmark :
|
||||
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.")
|
||||
|
||||
# Validate that the fetched data is a dictionary
|
||||
if not isinstance(landmark, Landmark):
|
||||
logger.error(f"Invalid cache data format for landmark UUID: {landmark_uuid}. Expected dict, got {type(landmark).__name__}.")
|
||||
raise HTTPException(status_code=500, detail="Invalid cache data format.")
|
||||
|
||||
return landmark
|
||||
|
||||
except Exception as exc:
|
||||
logger.error(f"Unexpected error occurred while fetching landmark UUID {landmark_uuid}: {exc}")
|
||||
raise HTTPException(status_code=500, detail="An unexpected error occurred while fetching the landmark from the cache") from exc
|
||||
|
||||
|
||||
return json_data
|
||||
|
||||
|
||||
def load_trip_landmarks(client, first_uuid: str, from_cache=None) -> list[Landmark]:
|
||||
def load_trip_landmarks(client, first_uuid: str) -> List[Landmark]:
|
||||
"""
|
||||
Load all landmarks for a trip using the response from the API.
|
||||
|
||||
@ -108,18 +60,19 @@ def load_trip_landmarks(client, first_uuid: str, from_cache=None) -> list[Landma
|
||||
next_uuid = first_uuid
|
||||
|
||||
while next_uuid is not None:
|
||||
if from_cache :
|
||||
landmark = fetch_landmark_cache(next_uuid)
|
||||
else :
|
||||
landmark = fetch_landmark(client, next_uuid)
|
||||
|
||||
landmarks.append(landmark)
|
||||
next_uuid = landmark.next_uuid # Prepare for the next iteration
|
||||
landmark_data = fetch_landmark(client, next_uuid)
|
||||
# # Convert UUIDs to strings explicitly
|
||||
# landmark_data = {
|
||||
# key: str(value) if isinstance(value, UUID) else value
|
||||
# for key, value in landmark_data.items()
|
||||
# }
|
||||
landmarks.append(Landmark(**landmark_data)) # Create Landmark objects
|
||||
next_uuid = landmark_data.get('next_uuid') # Prepare for the next iteration
|
||||
|
||||
return landmarks
|
||||
|
||||
|
||||
def log_trip_details(request, landmarks: list[Landmark], duration: int, target_duration: int) :
|
||||
def log_trip_details(request, landmarks: List[Landmark], duration: int, target_duration: int) :
|
||||
"""
|
||||
Allows to show the detailed trip in the html test report.
|
||||
|
||||
|
@ -1,283 +0,0 @@
|
||||
import logging
|
||||
from typing import Literal
|
||||
|
||||
import numpy as np
|
||||
from sklearn.cluster import DBSCAN
|
||||
from pydantic import BaseModel
|
||||
from OSMPythonTools.overpass import Overpass, overpassQueryBuilder
|
||||
from OSMPythonTools.cachingStrategy import CachingStrategy, JSON
|
||||
|
||||
from ..structs.landmark import Landmark
|
||||
from ..utils.get_time_separation import get_distance
|
||||
from ..constants import AMENITY_SELECTORS_PATH, LANDMARK_PARAMETERS_PATH, OPTIMIZER_PARAMETERS_PATH, OSM_CACHE_DIR
|
||||
|
||||
|
||||
class ShoppingLocation(BaseModel):
|
||||
""""
|
||||
A classe representing an interesting area for shopping.
|
||||
|
||||
It can represent either a general area or a specifc route with start and end point.
|
||||
The importance represents the number of shops found in this cluster.
|
||||
|
||||
Attributes:
|
||||
type : either a 'street' or 'area' (representing a denser field of shops).
|
||||
importance : size of the cluster (number of points).
|
||||
centroid : center of the cluster.
|
||||
start : if the type is a street it goes from here...
|
||||
end : ...to here
|
||||
"""
|
||||
type: Literal['street', 'area']
|
||||
importance: int
|
||||
centroid: tuple
|
||||
# start: Optional[list] = None # for later use if we want to have streets as well
|
||||
# end: Optional[list] = None
|
||||
|
||||
|
||||
class ShoppingManager:
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# NOTE: all points are in (lat, lon) format
|
||||
valid: bool # Ensure the manager is valid (ie there are some clusters to be found)
|
||||
all_points: list
|
||||
cluster_points: list
|
||||
cluster_labels: list
|
||||
shopping_locations: list[ShoppingLocation]
|
||||
|
||||
def __init__(self, bbox: tuple) -> None:
|
||||
"""
|
||||
Upon intialization, generate the point cloud used for cluster detection.
|
||||
The points represent bag/clothes shops and general boutiques.
|
||||
|
||||
Args:
|
||||
bbox: The bounding box coordinates (around:radius, center_lat, center_lon).
|
||||
"""
|
||||
|
||||
# Initialize overpass and cache
|
||||
self.overpass = Overpass()
|
||||
CachingStrategy.use(JSON, cacheDir=OSM_CACHE_DIR)
|
||||
|
||||
# Initialize the points for cluster detection
|
||||
query = overpassQueryBuilder(
|
||||
bbox = bbox,
|
||||
elementType = ['node'],
|
||||
selector = ['"shop"~"^(bag|boutique|clothes)$"'],
|
||||
includeCenter = True,
|
||||
out = 'skel'
|
||||
)
|
||||
|
||||
try:
|
||||
result = self.overpass.query(query)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error fetching landmarks: {e}")
|
||||
|
||||
if len(result.elements()) == 0 :
|
||||
self.valid = False
|
||||
|
||||
else :
|
||||
points = []
|
||||
for elem in result.elements() :
|
||||
points.append(tuple((elem.lat(), elem.lon())))
|
||||
|
||||
self.all_points = np.array(points)
|
||||
self.valid = True
|
||||
|
||||
|
||||
def generate_shopping_landmarks(self) -> list[Landmark]:
|
||||
"""
|
||||
Generate shopping landmarks based on clustered locations.
|
||||
|
||||
This method first generates clusters of locations and then extracts shopping-related
|
||||
locations from these clusters. It transforms each shopping location into a `Landmark` object.
|
||||
|
||||
Returns:
|
||||
list[Landmark]: A list of `Landmark` objects representing shopping locations.
|
||||
Returns an empty list if no clusters are found.
|
||||
"""
|
||||
|
||||
self.generate_clusters()
|
||||
|
||||
if len(set(self.cluster_labels)) == 0 :
|
||||
return [] # Return empty list if no clusters were found
|
||||
|
||||
# Then generate the shopping locations
|
||||
self.generate_shopping_locations()
|
||||
|
||||
# Transform the locations in landmarks and return the list
|
||||
shopping_landmarks = []
|
||||
for location in self.shopping_locations :
|
||||
shopping_landmarks.append(self.create_landmark(location))
|
||||
|
||||
return shopping_landmarks
|
||||
|
||||
|
||||
|
||||
def generate_clusters(self) :
|
||||
"""
|
||||
Generate clusters of points using DBSCAN.
|
||||
|
||||
This method applies the DBSCAN clustering algorithm with different
|
||||
parameters depending on the size of the city (number of points).
|
||||
It filters out noise points and keeps only the largest clusters.
|
||||
|
||||
The method updates:
|
||||
- `self.cluster_points`: The points belonging to clusters.
|
||||
- `self.cluster_labels`: The labels for the points in clusters.
|
||||
|
||||
The method also calls `filter_clusters()` to retain only the largest clusters.
|
||||
"""
|
||||
|
||||
# Apply DBSCAN to find clusters. Choose different settings for different cities.
|
||||
if len(self.all_points) > 200 :
|
||||
dbscan = DBSCAN(eps=0.00118, min_samples=15, algorithm='kd_tree') # for large cities
|
||||
else :
|
||||
dbscan = DBSCAN(eps=0.00075, min_samples=10, algorithm='kd_tree') # for small cities
|
||||
|
||||
labels = dbscan.fit_predict(self.all_points)
|
||||
|
||||
# Separate clustered points and noise points
|
||||
self.cluster_points = self.all_points[labels != -1]
|
||||
self.cluster_labels = labels[labels != -1]
|
||||
|
||||
# filter the clusters to keep only the largest ones
|
||||
self.filter_clusters()
|
||||
|
||||
|
||||
def generate_shopping_locations(self) :
|
||||
"""
|
||||
Generate shopping locations based on clustered points.
|
||||
|
||||
This method iterates over the different clusters, calculates the centroid
|
||||
(as the mean of the points within each cluster), and assigns an importance
|
||||
based on the size of the cluster.
|
||||
|
||||
The generated shopping locations are stored in `self.shopping_locations`
|
||||
as a list of `ShoppingLocation` objects, each with:
|
||||
- `type`: Set to 'area'.
|
||||
- `centroid`: The calculated centroid of the cluster.
|
||||
- `importance`: The number of points in the cluster.
|
||||
"""
|
||||
|
||||
locations = []
|
||||
|
||||
# loop through the different clusters
|
||||
for label in set(self.cluster_labels):
|
||||
|
||||
# Extract points belonging to the current cluster
|
||||
current_cluster = self.cluster_points[self.cluster_labels == label]
|
||||
|
||||
# Calculate the centroid as the mean of the points
|
||||
centroid = np.mean(current_cluster, axis=0)
|
||||
|
||||
locations.append(ShoppingLocation(
|
||||
type='area',
|
||||
centroid=centroid,
|
||||
importance = len(current_cluster)
|
||||
))
|
||||
|
||||
self.shopping_locations = locations
|
||||
|
||||
|
||||
def create_landmark(self, shopping_location: ShoppingLocation) -> Landmark:
|
||||
"""
|
||||
Create a Landmark object based on the given shopping location.
|
||||
|
||||
This method queries the Overpass API for nearby neighborhoods and shopping malls
|
||||
within a 1000m radius around the shopping location centroid. It selects the closest
|
||||
result and creates a landmark with the associated details such as name, type, and OSM ID.
|
||||
|
||||
Parameters:
|
||||
shopping_location (ShoppingLocation): A ShoppingLocation object containing
|
||||
the centroid and importance of the area.
|
||||
|
||||
Returns:
|
||||
Landmark: A Landmark object containing details such as the name, type,
|
||||
location, attractiveness, and OSM details.
|
||||
"""
|
||||
|
||||
# Define the bounding box for a given radius around the coordinates
|
||||
lat, lon = shopping_location.centroid
|
||||
bbox = ("around:1000", str(lat), str(lon))
|
||||
|
||||
# Query neighborhoods and shopping malls
|
||||
selectors = ['"place"~"^(suburb|neighborhood|neighbourhood|quarter|city_block)$"', '"shop"="mall"']
|
||||
|
||||
min_dist = float('inf')
|
||||
new_name = 'Shopping Area'
|
||||
new_name_en = None
|
||||
osm_id = 0
|
||||
osm_type = 'node'
|
||||
|
||||
for sel in selectors :
|
||||
query = overpassQueryBuilder(
|
||||
bbox = bbox,
|
||||
elementType = ['node', 'way', 'relation'],
|
||||
selector = sel,
|
||||
includeCenter = True,
|
||||
out = 'center'
|
||||
)
|
||||
|
||||
try:
|
||||
result = self.overpass.query(query)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error fetching landmarks: {e}")
|
||||
continue
|
||||
|
||||
for elem in result.elements():
|
||||
location = (elem.centerLat(), elem.centerLon())
|
||||
|
||||
if location[0] is None :
|
||||
location = (elem.lat(), elem.lon())
|
||||
if location[0] is None :
|
||||
continue
|
||||
|
||||
d = get_distance(shopping_location.centroid, location)
|
||||
if d < min_dist :
|
||||
min_dist = d
|
||||
new_name = elem.tag('name')
|
||||
osm_type = elem.type() # Add type: 'way' or 'relation'
|
||||
osm_id = elem.id() # Add OSM id
|
||||
|
||||
# Add english name if it exists
|
||||
try :
|
||||
new_name_en = elem.tag('name:en')
|
||||
except:
|
||||
pass
|
||||
|
||||
return Landmark(
|
||||
name=new_name,
|
||||
type='shopping',
|
||||
location=shopping_location.centroid, # TODO: use the fact the we can also recognize streets.
|
||||
attractiveness=shopping_location.importance,
|
||||
n_tags=0,
|
||||
osm_id=osm_id,
|
||||
osm_type=osm_type,
|
||||
name_en=new_name_en
|
||||
)
|
||||
|
||||
|
||||
def filter_clusters(self):
|
||||
"""
|
||||
Filter clusters to retain only the 5 largest clusters by point count.
|
||||
|
||||
This method calculates the size of each cluster and filters out all but the
|
||||
5 largest clusters. It then updates the cluster points and labels to reflect
|
||||
only those from the top 5 clusters.
|
||||
"""
|
||||
label_counts = np.bincount(self.cluster_labels)
|
||||
|
||||
# Step 3: Get the indices (labels) of the 5 largest clusters
|
||||
top_5_labels = np.argsort(label_counts)[-5:] # Get the largest 5 clusters
|
||||
|
||||
# Step 4: Filter points to keep only the points in the top 5 clusters
|
||||
filtered_cluster_points = []
|
||||
filtered_cluster_labels = []
|
||||
|
||||
for label in top_5_labels:
|
||||
filtered_cluster_points.append(self.cluster_points[self.cluster_labels == label])
|
||||
filtered_cluster_labels.append(np.full((label_counts[label],), label)) # Replicate the label
|
||||
|
||||
# update the cluster points and labels with the filtered data
|
||||
self.cluster_points = np.vstack(filtered_cluster_points)
|
||||
self.cluster_labels = np.concatenate(filtered_cluster_labels)
|
||||
|
@ -15,8 +15,8 @@ def get_time(p1: tuple[float, float], p2: tuple[float, float]) -> int:
|
||||
Calculate the time in minutes to travel from one location to another.
|
||||
|
||||
Args:
|
||||
p1 (tuple[float, float]): Coordinates of the starting location.
|
||||
p2 (tuple[float, float]): Coordinates of the destination.
|
||||
p1 (Tuple[float, float]): Coordinates of the starting location.
|
||||
p2 (Tuple[float, float]): Coordinates of the destination.
|
||||
|
||||
Returns:
|
||||
int: Time to travel from p1 to p2 in minutes.
|
||||
@ -48,35 +48,3 @@ def get_time(p1: tuple[float, float], p2: tuple[float, float]) -> int:
|
||||
walk_time = walk_distance / AVERAGE_WALKING_SPEED * 60
|
||||
|
||||
return round(walk_time)
|
||||
|
||||
|
||||
def get_distance(p1: tuple[float, float], p2: tuple[float, float]) -> int:
|
||||
"""
|
||||
Calculate the time in minutes to travel from one location to another.
|
||||
|
||||
Args:
|
||||
p1 (tuple[float, float]): Coordinates of the starting location.
|
||||
p2 (tuple[float, float]): Coordinates of the destination.
|
||||
|
||||
Returns:
|
||||
int: Time to travel from p1 to p2 in minutes.
|
||||
"""
|
||||
|
||||
|
||||
if p1 == p2:
|
||||
return 0
|
||||
else:
|
||||
# Compute the distance in km along the surface of the Earth
|
||||
# (assume spherical Earth)
|
||||
# this is the haversine formula, stolen from stackoverflow
|
||||
# in order to not use any external libraries
|
||||
lat1, lon1 = radians(p1[0]), radians(p1[1])
|
||||
lat2, lon2 = radians(p2[0]), radians(p2[1])
|
||||
|
||||
dlon = lon2 - lon1
|
||||
dlat = lat2 - lat1
|
||||
|
||||
a = sin(dlat / 2)**2 + cos(lat1) * cos(lat2) * sin(dlon / 2)**2
|
||||
c = 2 * atan2(sqrt(a), sqrt(1 - a))
|
||||
|
||||
return EARTH_RADIUS_KM * c
|
@ -1,11 +1,13 @@
|
||||
import math, yaml, logging
|
||||
import math
|
||||
import yaml
|
||||
import logging
|
||||
|
||||
from OSMPythonTools.overpass import Overpass, overpassQueryBuilder
|
||||
from OSMPythonTools.cachingStrategy import CachingStrategy, JSON
|
||||
|
||||
from ..structs.preferences import Preferences
|
||||
from ..structs.landmark import Landmark
|
||||
from .take_most_important import take_most_important
|
||||
from .cluster_processing import ShoppingManager
|
||||
|
||||
from ..constants import AMENITY_SELECTORS_PATH, LANDMARK_PARAMETERS_PATH, OPTIMIZER_PARAMETERS_PATH, OSM_CACHE_DIR
|
||||
|
||||
@ -77,9 +79,7 @@ class LandmarkManager:
|
||||
# use set to avoid duplicates, this requires some __methods__ to be set in Landmark
|
||||
all_landmarks = set()
|
||||
|
||||
# Create a bbox using the around technique
|
||||
bbox = tuple((f"around:{reachable_bbox_side/2}", str(center_coordinates[0]), str(center_coordinates[1])))
|
||||
|
||||
bbox = self.create_bbox(center_coordinates, reachable_bbox_side)
|
||||
# list for sightseeing
|
||||
if preferences.sightseeing.score != 0:
|
||||
score_function = lambda score: score * 10 * preferences.sightseeing.score / 5
|
||||
@ -96,19 +96,10 @@ class LandmarkManager:
|
||||
if preferences.shopping.score != 0:
|
||||
score_function = lambda score: score * 10 * preferences.shopping.score / 5
|
||||
current_landmarks = self.fetch_landmarks(bbox, self.amenity_selectors['shopping'], preferences.shopping.type, score_function)
|
||||
|
||||
# set time for all shopping activites :
|
||||
for landmark in current_landmarks : landmark.duration = 30
|
||||
for landmark in current_landmarks : landmark.duration = 45
|
||||
all_landmarks.update(current_landmarks)
|
||||
|
||||
# special pipeline for shopping malls
|
||||
shopping_manager = ShoppingManager(bbox)
|
||||
if shopping_manager.valid :
|
||||
shopping_clusters = shopping_manager.generate_shopping_landmarks()
|
||||
for landmark in shopping_clusters : landmark.duration = 45
|
||||
all_landmarks.update(shopping_clusters)
|
||||
|
||||
|
||||
|
||||
landmarks_constrained = take_most_important(all_landmarks, self.N_important)
|
||||
self.logger.info(f'Generated {len(all_landmarks)} landmarks around {center_coordinates}, and constrained to {len(landmarks_constrained)} most important ones.')
|
||||
@ -160,24 +151,36 @@ class LandmarkManager:
|
||||
return 0
|
||||
|
||||
|
||||
# def create_bbox(self, coordinates: tuple[float, float], reachable_bbox_side: int) -> tuple[float, float, float, float]:
|
||||
# """
|
||||
# Create a bounding box around the given coordinates.
|
||||
def create_bbox(self, coordinates: tuple[float, float], reachable_bbox_side: int) -> tuple[float, float, float, float]:
|
||||
"""
|
||||
Create a bounding box around the given coordinates.
|
||||
|
||||
# Args:
|
||||
# coordinates (tuple[float, float]): The latitude and longitude of the center of the bounding box.
|
||||
# reachable_bbox_side (int): The side length of the bounding box in meters.
|
||||
Args:
|
||||
coordinates (tuple[float, float]): The latitude and longitude of the center of the bounding box.
|
||||
reachable_bbox_side (int): The side length of the bounding box in meters.
|
||||
|
||||
# Returns:
|
||||
# tuple[float, float, float, float]: The minimum latitude, minimum longitude, maximum latitude, and maximum longitude
|
||||
# defining the bounding box.
|
||||
# """
|
||||
Returns:
|
||||
tuple[float, float, float, float]: The minimum latitude, minimum longitude, maximum latitude, and maximum longitude
|
||||
defining the bounding box.
|
||||
"""
|
||||
|
||||
lat = coordinates[0]
|
||||
lon = coordinates[1]
|
||||
|
||||
# # Half the side length in m (since it's a square bbox)
|
||||
# half_side_length_m = reachable_bbox_side / 2
|
||||
# Half the side length in km (since it's a square bbox)
|
||||
half_side_length_km = reachable_bbox_side / 2 / 1000
|
||||
|
||||
# return tuple((f"around:{half_side_length_m}", str(coordinates[0]), str(coordinates[1])))
|
||||
# Convert distance to degrees
|
||||
lat_diff = half_side_length_km / 111 # 1 degree latitude is approximately 111 km
|
||||
lon_diff = half_side_length_km / (111 * math.cos(math.radians(lat))) # Adjust for longitude based on latitude
|
||||
|
||||
# Calculate bbox
|
||||
min_lat = lat - lat_diff
|
||||
max_lat = lat + lat_diff
|
||||
min_lon = lon - lon_diff
|
||||
max_lon = lon + lon_diff
|
||||
|
||||
return min_lat, min_lon, max_lat, max_lon
|
||||
|
||||
|
||||
def fetch_landmarks(self, bbox: tuple, amenity_selector: dict, landmarktype: str, score_function: callable) -> list[Landmark]:
|
||||
@ -185,7 +188,7 @@ class LandmarkManager:
|
||||
Fetches landmarks of a specified type from OpenStreetMap (OSM) within a bounding box centered on given coordinates.
|
||||
|
||||
Args:
|
||||
bbox (tuple[float, float, float, float]): The bounding box coordinates (around:radius, center_lat, center_lon).
|
||||
bbox (tuple[float, float, float, float]): The bounding box coordinates (min_lat, min_lon, max_lat, max_lon).
|
||||
amenity_selector (dict): The Overpass API query selector for the desired landmark type.
|
||||
landmarktype (str): The type of the landmark (e.g., 'sightseeing', 'nature', 'shopping').
|
||||
score_function (callable): The function to compute the score of the landmark based on its attributes.
|
||||
@ -209,9 +212,7 @@ class LandmarkManager:
|
||||
for sel in dict_to_selector_list(amenity_selector):
|
||||
self.logger.debug(f"Current selector: {sel}")
|
||||
|
||||
# query_conditions = ['count_tags()>5']
|
||||
# if landmarktype == 'shopping' : # use this later for shopping clusters
|
||||
# element_types = ['node']
|
||||
query_conditions = ['count_tags()>5']
|
||||
element_types = ['way', 'relation']
|
||||
|
||||
if 'viewpoint' in sel :
|
||||
@ -227,7 +228,7 @@ class LandmarkManager:
|
||||
selector = sel,
|
||||
conditions = query_conditions, # except for nature....
|
||||
includeCenter = True,
|
||||
out = 'center'
|
||||
out = 'body'
|
||||
)
|
||||
self.logger.debug(f"Query: {query}")
|
||||
|
||||
@ -364,6 +365,7 @@ class LandmarkManager:
|
||||
return return_list
|
||||
|
||||
|
||||
|
||||
def dict_to_selector_list(d: dict) -> list:
|
||||
"""
|
||||
Convert a dictionary of key-value pairs to a list of Overpass query strings.
|
||||
|
@ -44,7 +44,7 @@ class Optimizer:
|
||||
resx (list[float]): List of edge weights.
|
||||
|
||||
Returns:
|
||||
tuple[list[int], list[int]]: A tuple containing a new row for constraint matrix and new value for upper bound vector.
|
||||
Tuple[list[int], list[int]]: A tuple containing a new row for constraint matrix and new value for upper bound vector.
|
||||
"""
|
||||
|
||||
for i, elem in enumerate(resx):
|
||||
@ -79,7 +79,7 @@ class Optimizer:
|
||||
L (int): Number of landmarks.
|
||||
|
||||
Returns:
|
||||
tuple[np.ndarray, list[int]]: A tuple containing a new row for constraint matrix and new value for upper bound vector.
|
||||
Tuple[np.ndarray, list[int]]: A tuple containing a new row for constraint matrix and new value for upper bound vector.
|
||||
"""
|
||||
|
||||
l1 = [0]*L*L
|
||||
@ -107,7 +107,7 @@ class Optimizer:
|
||||
resx (list): List of edge weights.
|
||||
|
||||
Returns:
|
||||
tuple[list[int], Optional[list[list[int]]]]: A tuple containing the visit order and a list of any detected circles.
|
||||
Tuple[list[int], Optional[list[list[int]]]]: A tuple containing the visit order and a list of any detected circles.
|
||||
"""
|
||||
|
||||
# first round the results to have only 0-1 values
|
||||
@ -180,7 +180,7 @@ class Optimizer:
|
||||
max_time (int): Maximum time of visit allowed.
|
||||
|
||||
Returns:
|
||||
tuple[list[float], list[float], list[int]]: Objective function coefficients, inequality constraint coefficients, and the right-hand side of the inequality constraint.
|
||||
Tuple[list[float], list[float], list[int]]: Objective function coefficients, inequality constraint coefficients, and the right-hand side of the inequality constraint.
|
||||
"""
|
||||
|
||||
# Objective function coefficients. a*x1 + b*x2 + c*x3 + ...
|
||||
@ -212,7 +212,7 @@ class Optimizer:
|
||||
L (int): Number of landmarks.
|
||||
|
||||
Returns:
|
||||
tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
|
||||
Tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
|
||||
"""
|
||||
|
||||
ones = [1]*L
|
||||
@ -239,7 +239,7 @@ class Optimizer:
|
||||
L (int): Number of landmarks.
|
||||
|
||||
Returns:
|
||||
tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
|
||||
Tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
|
||||
"""
|
||||
|
||||
upper_ind = np.triu_indices(L,0,L)
|
||||
@ -270,7 +270,7 @@ class Optimizer:
|
||||
L (int): Number of landmarks.
|
||||
|
||||
Returns:
|
||||
tuple[list[np.ndarray], list[int]]: Equality constraint coefficients and the right-hand side of the equality constraints.
|
||||
Tuple[list[np.ndarray], list[int]]: Equality constraint coefficients and the right-hand side of the equality constraints.
|
||||
"""
|
||||
|
||||
l = [0]*L*L
|
||||
@ -293,7 +293,7 @@ class Optimizer:
|
||||
landmarks (list[Landmark]): List of landmarks, where some are marked as 'must_do'.
|
||||
|
||||
Returns:
|
||||
tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
|
||||
Tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
|
||||
"""
|
||||
|
||||
L = len(landmarks)
|
||||
@ -319,7 +319,7 @@ class Optimizer:
|
||||
landmarks (list[Landmark]): List of landmarks, where some are marked as 'must_avoid'.
|
||||
|
||||
Returns:
|
||||
tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
|
||||
Tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
|
||||
"""
|
||||
|
||||
L = len(landmarks)
|
||||
@ -346,7 +346,7 @@ class Optimizer:
|
||||
L (int): Number of landmarks.
|
||||
|
||||
Returns:
|
||||
tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
|
||||
Tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
|
||||
"""
|
||||
|
||||
l_start = [1]*L + [0]*L*(L-1) # sets departures only for start (horizontal ones)
|
||||
@ -374,7 +374,7 @@ class Optimizer:
|
||||
L (int): Number of landmarks.
|
||||
|
||||
Returns:
|
||||
tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
|
||||
Tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
|
||||
"""
|
||||
|
||||
A = [0]*L*L
|
||||
|
@ -2,6 +2,7 @@ import yaml, logging
|
||||
|
||||
from shapely import buffer, LineString, Point, Polygon, MultiPoint, concave_hull
|
||||
from math import pi
|
||||
from typing import List
|
||||
|
||||
from ..structs.landmark import Landmark
|
||||
from . import take_most_important, get_time_separation
|
||||
@ -134,7 +135,7 @@ class Refiner :
|
||||
|
||||
return tour
|
||||
|
||||
def integrate_landmarks(self, sub_list: list[Landmark], main_list: list[Landmark]) :
|
||||
def integrate_landmarks(self, sub_list: List[Landmark], main_list: List[Landmark]) :
|
||||
"""
|
||||
Inserts 'sub_list' of Landmarks inside the 'main_list' by leaving the ends untouched.
|
||||
|
||||
|
@ -1,78 +0,0 @@
|
||||
import logging, yaml
|
||||
from OSMPythonTools.overpass import Overpass, overpassQueryBuilder
|
||||
from OSMPythonTools.cachingStrategy import CachingStrategy, JSON
|
||||
|
||||
from ..structs.landmark import Toilets
|
||||
from ..constants import LANDMARK_PARAMETERS_PATH, OSM_CACHE_DIR
|
||||
|
||||
|
||||
# silence the overpass logger
|
||||
logging.getLogger('OSMPythonTools').setLevel(level=logging.CRITICAL)
|
||||
|
||||
class ToiletsManager:
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
location: tuple[float, float]
|
||||
radius: int # radius in meters
|
||||
|
||||
|
||||
def __init__(self, location: tuple[float, float], radius : int) -> None:
|
||||
|
||||
self.radius = radius
|
||||
self.location = location
|
||||
self.overpass = Overpass()
|
||||
CachingStrategy.use(JSON, cacheDir=OSM_CACHE_DIR)
|
||||
|
||||
|
||||
def generate_toilet_list(self) -> list[Toilets] :
|
||||
|
||||
|
||||
# Create a bbox using the around technique
|
||||
bbox = tuple((f"around:{self.radius}", str(self.location[0]), str(self.location[1])))
|
||||
toilets_list = []
|
||||
|
||||
query = overpassQueryBuilder(
|
||||
bbox = bbox,
|
||||
elementType = ['node', 'way', 'relation'],
|
||||
# selector can in principle be a list already,
|
||||
# but it generates the intersection of the queries
|
||||
# we want the union
|
||||
selector = ['"amenity"="toilets"'],
|
||||
includeCenter = True,
|
||||
out = 'center'
|
||||
)
|
||||
self.logger.debug(f"Query: {query}")
|
||||
|
||||
try:
|
||||
result = self.overpass.query(query)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error fetching landmarks: {e}")
|
||||
return None
|
||||
|
||||
for elem in result.elements():
|
||||
location = (elem.centerLat(), elem.centerLon())
|
||||
|
||||
# handle unprecise and no-name locations
|
||||
if location[0] is None:
|
||||
location = (elem.lat(), elem.lon())
|
||||
else :
|
||||
continue
|
||||
|
||||
toilets = Toilets(location=location)
|
||||
|
||||
if 'wheelchair' in elem.tags().keys() and elem.tag('wheelchair') == 'yes':
|
||||
toilets.wheelchair = True
|
||||
|
||||
if 'changing_table' in elem.tags().keys() and elem.tag('changing_table') == 'yes':
|
||||
toilets.changing_table = True
|
||||
|
||||
if 'fee' in elem.tags().keys() and elem.tag('fee') == 'yes':
|
||||
toilets.fee = True
|
||||
|
||||
if 'opening_hours' in elem.tags().keys() :
|
||||
toilets.opening_hours = elem.tag('opening_hours')
|
||||
|
||||
toilets_list.append(toilets)
|
||||
|
||||
return toilets_list
|
Loading…
x
Reference in New Issue
Block a user