Implement backend API for landmarks, trip optimization, and toilet locations
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 1m49s
Run linting on the backend code / Build (pull_request) Successful in 30s
Run testing on the backend code / Build (pull_request) Failing after 45s
Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 32s

- Added landmarks_router.py to handle landmark retrieval based on user preferences and location.
- Implemented optimization_router.py for trip optimization, including handling preferences and landmarks.
- Created toilets_router.py to fetch toilet locations within a specified radius from a given location.
- Enhanced error handling and logging across all new endpoints.
- Generated a comprehensive report.html for test results and environment details.
This commit is contained in:
kscheidecker 2025-07-13 17:43:24 +02:00
parent 54bc9028ad
commit b0f9d31ee2
15 changed files with 1235 additions and 217 deletions

File diff suppressed because one or more lines are too long

View File

@ -4,7 +4,6 @@ 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 .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.bbox import create_bbox
@ -23,7 +22,7 @@ class LandmarkManager:
church_coeff: float # coeff to adjsut score of churches church_coeff: float # coeff to adjsut score of churches
nature_coeff: float # coeff to adjust score of parks nature_coeff: float # coeff to adjust score of parks
overall_coeff: float # coeff to adjust weight of tags overall_coeff: float # coeff to adjust weight of tags
n_important: int # number of important landmarks to consider # n_important: int # number of important landmarks to consider
def __init__(self) -> None: def __init__(self) -> None:
@ -42,7 +41,7 @@ class LandmarkManager:
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']
self.n_important = parameters['N_important'] # self.n_important = parameters['N_important']
with OPTIMIZER_PARAMETERS_PATH.open('r') as f: with OPTIMIZER_PARAMETERS_PATH.open('r') as f:
parameters = yaml.safe_load(f) parameters = yaml.safe_load(f)
@ -60,7 +59,7 @@ class LandmarkManager:
center_coordinates: tuple[float, float], center_coordinates: tuple[float, float],
preferences: Preferences, preferences: Preferences,
allow_clusters: bool = True allow_clusters: bool = True
) -> tuple[list[Landmark], list[Landmark]]: ) -> list[Landmark] :
""" """
Generate and prioritize a list of landmarks based on user preferences. Generate and prioritize a list of landmarks based on user preferences.
@ -127,13 +126,12 @@ class LandmarkManager:
all_landmarks.update(shopping_clusters) all_landmarks.update(shopping_clusters)
landmarks_constrained = take_most_important(all_landmarks, self.n_important)
# DETAILS HERE # DETAILS HERE
# self.logger.info(f'All landmarks generated : {len(all_landmarks)} landmarks around {center_coordinates}, and constrained to {len(landmarks_constrained)} most important ones.') # self.logger.info(f'All landmarks generated : {len(all_landmarks)} landmarks around {center_coordinates}, and constrained to {len(landmarks_constrained)} most important ones.')
self.logger.info(f'Found {len(all_landmarks)} landmarks in total.') self.logger.info(f'Found {len(all_landmarks)} landmarks in total.')
return all_landmarks, landmarks_constrained return sorted(all_landmarks, key=lambda x: x.attractiveness, reverse=True)
def set_landmark_score(self, landmark: Landmark, landmarktype: str, preference_level: int) : def set_landmark_score(self, landmark: Landmark, landmarktype: str, preference_level: int) :
""" """

View File

@ -6,7 +6,7 @@ from fastapi import HTTPException, APIRouter
from ..structs.landmark import Landmark from ..structs.landmark import Landmark
from ..structs.preferences import Preferences, Preference from ..structs.preferences import Preferences, Preference
from ..landmarks.landmarks_manager import LandmarkManager from .landmarks_manager import LandmarkManager
# Setup the logger and the Landmarks Manager # Setup the logger and the Landmarks Manager
@ -14,7 +14,7 @@ logger = logging.getLogger(__name__)
manager = LandmarkManager() manager = LandmarkManager()
# Start the router # Initialize the API router
router = APIRouter() router = APIRouter()
@ -48,7 +48,7 @@ def get_landmarks(
start_time = time.time() start_time = time.time()
# Generate the landmarks from the start location # Generate the landmarks from the start location
landmarks, _ = manager.generate_landmarks_list( landmarks = manager.generate_landmarks_list(
center_coordinates = start, center_coordinates = start,
preferences = preferences preferences = preferences
) )
@ -62,7 +62,7 @@ def get_landmarks(
return landmarks return landmarks
@router.post("/landmarks/get-nearby/{lat}/{lon}") @router.post("/get-nearby/landmarks/{lat}/{lon}")
def get_landmarks_nearby( def get_landmarks_nearby(
lat: float, lat: float,
lon: float lon: float
@ -81,6 +81,7 @@ def get_landmarks_nearby(
list[Landmark]: A list of selected nearby landmarks. list[Landmark]: A list of selected nearby landmarks.
""" """
logger.info(f'Fetching landmarks nearby ({lat}, {lon}).') logger.info(f'Fetching landmarks nearby ({lat}, {lon}).')
# Define fixed preferences: # Define fixed preferences:
prefs = Preferences( prefs = Preferences(
sightseeing = Preference( sightseeing = Preference(
@ -100,18 +101,23 @@ def get_landmarks_nearby(
) )
# Find the landmarks around the location # Find the landmarks around the location
_, landmarks_around = manager.generate_landmarks_list( landmarks_around = manager.generate_landmarks_list(
center_coordinates = (lat, lon), center_coordinates = (lat, lon),
preferences = prefs, preferences = prefs,
allow_clusters=False, allow_clusters=False,
) )
if len(landmarks_around) == 0 : if len(landmarks_around) == 0 :
raise HTTPException(status_code=500, detail="No landmarks were found.") raise HTTPException(status_code=500, detail="No landmarks were found.")
# select 5 landmarks from there # select 8 - 12 landmarks from there
if len(landmarks_around) > 6 : if len(landmarks_around) > 8 :
landmarks_around = landmarks_around[:2] + random.sample(landmarks_around[3:], 2) n_imp = random.randint(2,5)
rest = random.randint(8 - n_imp, min(12, len(landmarks_around))-n_imp)
logger.info(f'Found {len(landmarks_around)} landmarks nearby ({lat}, {lon}).') print(f'len = {len(landmarks_around)}\nn_imp = {n_imp}\nrest = {rest}')
logger.debug('Suggested landmarks :\n\t' + '\n\t'.join(f'{landmark}' for landmark in landmarks_around)) landmarks_around = landmarks_around[:n_imp] + random.sample(landmarks_around[n_imp:], rest)
logger.info(f'Found {len(landmarks_around)} landmarks to suggest nearby ({lat}, {lon}).')
# logger.debug('Suggested landmarks :\n\t' + '\n\t'.join(f'{landmark}' for landmark in landmarks_around))
return landmarks_around return landmarks_around

View File

@ -1,22 +1,18 @@
"""Main app for backend api""" """Main app for backend api"""
import logging import logging
import time
import random
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException, BackgroundTasks from fastapi import FastAPI, HTTPException
from .logging_config import configure_logging from .logging_config import configure_logging
from .structs.landmark import Landmark from .structs.landmark import Landmark
from .structs.preferences import Preferences, Preference
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 .landmarks.landmarks_manager import LandmarkManager
from .toilets.toilets_route import router as toilets_router from .toilets.toilets_router import router as toilets_router
from .optimization.optimization_routes import router as optimization_router from .optimization.optimization_router import router as optimization_router
from .landmarks.landmarks_routes import router as landmarks_router from .landmarks.landmarks_router import router as landmarks_router, get_landmarks_nearby
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 .cache import client as cache_client from .cache import client as cache_client
@ -40,118 +36,21 @@ app = FastAPI(lifespan=lifespan)
app.include_router(toilets_router) # Fetches the global list of landmarks given preferences and start/end coordinates. Two routes
app.include_router(optimization_router) # Call with "/get/landmarks/" for main entry point of the trip generation pipeline.
# Call with "/get-nearby/landmarks/" for the NEARBY feature.
app.include_router(landmarks_router) app.include_router(landmarks_router)
@app.post("/trip/new") # Optimizes the trip given preferences. Second step in the main trip generation pipeline
def new_trip( # Call with "/optimize/trip"
preferences: Preferences, app.include_router(optimization_router)
start: tuple[float, float],
end: tuple[float, float] | None = None,
background_tasks: BackgroundTasks = None
) -> Trip:
"""
Main function to call the optimizer.
Args:
preferences : the preferences specified by the user as the post body
start : the coordinates of the starting point
end : the coordinates of the finishing point
Returns:
(uuid) : The uuid of the first landmark in the optimized route
"""
if preferences is None:
raise HTTPException(status_code=406, detail="Preferences not provided or incomplete.")
if (preferences.shopping.score == 0 and
preferences.sightseeing.score == 0 and
preferences.nature.score == 0) :
raise HTTPException(status_code=406, detail="All preferences are 0.")
if start is None:
raise HTTPException(status_code=406, detail="Start coordinates not provided")
if not (-90 <= start[0] <= 90 or -180 <= start[1] <= 180):
raise HTTPException(status_code=422, detail="Start coordinates not in range")
if end is None:
end = start
logger.info("No end coordinates provided. Using start=end.")
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}") # Fetches toilets near given coordinates.
# Call with "/get/toilets" for fetching toilets around coordinates
app.include_router(toilets_router)
start_landmark = Landmark(name='start',
type='start',
location=(start[0], start[1]),
osm_type='start',
osm_id=0,
attractiveness=0,
duration=0,
must_do=True,
n_tags = 0)
end_landmark = Landmark(name='finish',
type='finish',
location=(end[0], end[1]),
osm_type='end',
osm_id=0,
attractiveness=0,
duration=0,
must_do=True,
n_tags=0)
start_time = time.time()
# Generate the landmarks from the start location
landmarks, landmarks_short = manager.generate_landmarks_list(
center_coordinates = start,
preferences = preferences
)
if len(landmarks) == 0 :
raise HTTPException(status_code=500, detail="No landmarks were found.")
# insert start and finish to the landmarks list
landmarks_short.insert(0, start_landmark)
landmarks_short.append(end_landmark)
t_generate_landmarks = time.time() - start_time
logger.info(f'Fetched {len(landmarks)} landmarks in \t: {round(t_generate_landmarks,3)} seconds')
start_time = time.time()
# First stage optimization
try:
base_tour = optimizer.solve_optimization(preferences.max_time_minute, landmarks_short)
except Exception as exc:
logger.error(f"Trip generation failed: {str(exc)}")
raise HTTPException(status_code=500, detail=f"Optimization failed: {str(exc)}") from exc
t_first_stage = time.time() - start_time
start_time = time.time()
# Second stage optimization
# TODO : only if necessary (not enough landmarks for ex.)
try :
refined_tour = refiner.refine_optimization(landmarks, base_tour,
preferences.max_time_minute,
preferences.detour_tolerance_minute)
except Exception as exc :
logger.warning(f"Refiner failed. Proceeding with base trip {str(exc)}")
refined_tour = base_tour
t_second_stage = time.time() - start_time
logger.debug(f'First stage optimization\t: {round(t_first_stage,3)} seconds')
logger.debug(f'Second stage optimization\t: {round(t_second_stage,3)} seconds')
logger.info(f'Total computation time\t: {round(t_first_stage + t_second_stage,3)} seconds')
linked_tour = LinkedLandmarks(refined_tour)
# upon creation of the trip, persistence of both the trip and its landmarks is ensured.
trip = Trip.from_linked_landmarks(linked_tour, cache_client)
logger.info(f'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('Detailed trip :\n\t' + '\n\t'.join(f'{landmark}' for landmark in refined_tour))
background_tasks.add_task(fill_cache)
return trip
#### For already existing trips/landmarks #### For already existing trips/landmarks

View File

@ -1,19 +1,20 @@
"""Main app for backend api""" """API entry point for the trip optimization."""
import logging import logging
import time import time
import yaml import yaml
from fastapi import HTTPException, APIRouter, BackgroundTasks from fastapi import HTTPException, APIRouter, BackgroundTasks
from .optimizer import Optimizer
from .refiner import Refiner
from ..structs.landmark import Landmark from ..structs.landmark import Landmark
from ..structs.preferences import Preferences from ..structs.preferences import Preferences
from ..structs.linked_landmarks import LinkedLandmarks from ..structs.linked_landmarks import LinkedLandmarks
from ..utils.take_most_important import take_most_important from ..utils.take_most_important import take_most_important
from ..structs.trip import Trip from ..structs.trip import Trip
from ..optimization.optimizer import Optimizer
from ..optimization.refiner import Refiner
from ..overpass.overpass import fill_cache from ..overpass.overpass import fill_cache
from ..cache import client as cache_client from ..cache import client as cache_client
from ..constants import LANDMARK_PARAMETERS_PATH from ..constants import OPTIMIZER_PARAMETERS_PATH
# Setup the Logger, Optimizer and Refiner # Setup the Logger, Optimizer and Refiner
@ -22,7 +23,7 @@ optimizer = Optimizer()
refiner = Refiner(optimizer=optimizer) refiner = Refiner(optimizer=optimizer)
# Start the router # Initialize the API router
router = APIRouter() router = APIRouter()
@ -66,7 +67,8 @@ def optimize_trip(
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}") 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]),
osm_type='start', osm_type='start',
@ -74,9 +76,11 @@ def optimize_trip(
attractiveness=0, attractiveness=0,
duration=0, duration=0,
must_do=True, must_do=True,
n_tags = 0) n_tags = 0
)
end_landmark = Landmark(name='finish', end_landmark = Landmark(
name='finish',
type='finish', type='finish',
location=(end[0], end[1]), location=(end[0], end[1]),
osm_type='end', osm_type='end',
@ -84,10 +88,11 @@ def optimize_trip(
attractiveness=0, attractiveness=0,
duration=0, duration=0,
must_do=True, must_do=True,
n_tags=0) n_tags=0
)
# From the parameters load the length at which to truncate the landmarks list. # From the parameters load the length at which to truncate the landmarks list.
with LANDMARK_PARAMETERS_PATH.open('r') as f: with OPTIMIZER_PARAMETERS_PATH.open('r') as f:
parameters = yaml.safe_load(f) parameters = yaml.safe_load(f)
n_important = parameters['N_important'] n_important = parameters['N_important']

View File

@ -7,5 +7,4 @@ tag_exponent: 1.15
image_bonus: 1.1 image_bonus: 1.1
viewpoint_bonus: 10 viewpoint_bonus: 10
wikipedia_bonus: 1.25 wikipedia_bonus: 1.25
N_important: 60
pay_bonus: -1 pay_bonus: -1

View File

@ -7,3 +7,4 @@ overshoot: 0.0016
time_limit: 1 time_limit: 1
gap_rel: 0.025 gap_rel: 0.025
max_iter: 80 max_iter: 80
N_important: 60

View File

@ -1,7 +1,7 @@
"""Defines the Preferences used as input for trip generation.""" """Defines the Preferences used as input for trip generation."""
from typing import Optional, Literal from typing import Optional, Literal
from pydantic import BaseModel from pydantic import BaseModel, field_validator
class Preference(BaseModel) : class Preference(BaseModel) :
@ -15,6 +15,13 @@ class Preference(BaseModel) :
type: Literal['sightseeing', 'nature', 'shopping', 'start', 'finish'] type: Literal['sightseeing', 'nature', 'shopping', 'start', 'finish']
score: int # score could be from 1 to 5 score: int # score could be from 1 to 5
@field_validator("type")
@classmethod
def validate_type(cls, v):
if v not in {'sightseeing', 'nature', 'shopping', 'start', 'finish'}:
raise ValueError(f"Invalid type: {v}")
return v
# Input for optimization # Input for optimization
class Preferences(BaseModel) : class Preferences(BaseModel) :

View File

@ -19,15 +19,35 @@ def invalid_client():
([48.8566, 2.3522], {}, 422), ([48.8566, 2.3522], {}, 422),
# Invalid cases: incomplete preferences. # Invalid cases: incomplete preferences.
([48.084588, 7.280405], {"sightseeing": {"type": "sightseeing", "score": 5}, # no shopping ([48.084588, 7.280405], {"sightseeing": {"type": "sightseeing", "score": 5}, # no shopping pref
"nature": {"type": "nature", "score": 5}, "nature": {"type": "nature", "score": 5},
}, 422), }, 422),
([48.084588, 7.280405], {"sightseeing": {"type": "sightseeing", "score": 5}, # no nature ([48.084588, 7.280405], {"sightseeing": {"type": "sightseeing", "score": 5}, # no nature pref
"shopping": {"type": "shopping", "score": 5}, "shopping": {"type": "shopping", "score": 5},
}, 422), }, 422),
([48.084588, 7.280405], {"nature": {"type": "nature", "score": 5}, # no sightseeing ([48.084588, 7.280405], {"nature": {"type": "nature", "score": 5}, # no sightseeing pref
"shopping": {"type": "shopping", "score": 5}, "shopping": {"type": "shopping", "score": 5},
}, 422), }, 422),
([48.084588, 7.280405], {"sightseeing": {"type": "nature", "score": 1}, # mixed up preferences types. TODO: i suggest reducing the complexity by remove the Preference object.
"nature": {"type": "shopping", "score": 1},
"shopping": {"type": "shopping", "score": 1},
}, 422),
([48.084588, 7.280405], {"doesnotexist": {"type": "sightseeing", "score": 2}, # non-existing preferences types
"nature": {"type": "nature", "score": 2},
"shopping": {"type": "shopping", "score": 2},
}, 422),
([48.084588, 7.280405], {"sightseeing": {"type": "sightseeing", "score": 3}, # non-existing preferences types
"nature": {"type": "doesntexisteither", "score": 3},
"shopping": {"type": "shopping", "score": 3},
}, 422),
([48.084588, 7.280405], {"sightseeing": {"type": "sightseeing", "score": -1}, # negative preference value
"nature": {"type": "doesntexisteither", "score": 4},
"shopping": {"type": "shopping", "score": 4},
}, 422),
([48.084588, 7.280405], {"sightseeing": {"type": "sightseeing", "score": 10}, # too high preference value
"nature": {"type": "doesntexisteither", "score": 4},
"shopping": {"type": "shopping", "score": 4},
}, 422),
# Invalid cases: unexisting coords # Invalid cases: unexisting coords
([91, 181], {"sightseeing": {"type": "sightseeing", "score": 5}, ([91, 181], {"sightseeing": {"type": "sightseeing", "score": 5},
@ -53,7 +73,7 @@ def test_input(invalid_client, start, preferences, status_code): # pylint: dis
Test new trip creation with different sets of preferences and locations. Test new trip creation with different sets of preferences and locations.
""" """
response = invalid_client.post( response = invalid_client.post(
"/trip/new", "/get/landmarks",
json ={ json ={
"preferences": preferences, "preferences": preferences,
"start": start "start": start

View File

@ -1,22 +1,32 @@
"""Collection of tests to ensure correct implementation and track progress of the get_landmarks_nearby feature. """ """Collection of tests to ensure correct implementation and track progress of the get_landmarks_nearby feature. """
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
import pytest import pytest
from ..main import app from ..main import app
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def client(): def client():
"""Client used to call the app.""" """Client used to call the app."""
return TestClient(app) return TestClient(app)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"location,status_code", "location,status_code",
[ [
([45.7576485, 4.8330241], 2000), # Lyon, Bellecour. ([45.7576485, 4.8330241], 200), # Lyon, France
([41.4020572, 2.1818985], 2000), # Barcelona ([41.4020572, 2.1818985], 200), # Barcelona, Spain
([59.3293, 18.0686], 200), # Stockholm, Sweden
([43.6532, -79.3832], 200), # Toronto, Canada
([38.7223, -9.1393], 200), # Lisbon, Portugal
([6.5244, 3.3792], 200), # Lagos, Nigeria
([17.3850, 78.4867], 200), # Hyderabad, India
([30.0444, 31.2357], 200), # Cairo, Egypt
([50.8503, 4.3517], 200), # Brussels, Belgium
([35.2271, -80.8431], 200), # Charlotte, USA
([10.4806, -66.9036], 200), # Caracas, Venezuela
([9.51074, -13.71118], 200), # Conakry, Guinea
] ]
) )
def test_nearby(client, location, status_code): # pylint: disable=redefined-outer-name def test_nearby(client, location, status_code): # pylint: disable=redefined-outer-name
@ -27,9 +37,7 @@ def test_nearby(client, location, status_code): # pylint: disable=redefined-o
client: client:
request: request:
""" """
response = client.post(f"/landmarks/get-nearby/{location[0]}/{location[1]}") response = client.post(f"/get-nearby/landmarks/{location[0]}/{location[1]}")
print(response)
print(response.json())
suggestions = response.json() suggestions = response.json()
# checks : # checks :

View File

@ -30,12 +30,13 @@ def test_invalid_input(client, location, radius, status_code): # pylint: disa
request: request:
""" """
response = client.post( response = client.post(
"/toilets/new", "/get/toilets",
params={ params={
"location": location, "location": location,
"radius": radius "radius": radius
} }
) )
print(response.json())
# checks : # checks :
assert response.status_code == status_code assert response.status_code == status_code
@ -58,11 +59,12 @@ def test_no_toilets(client, location, status_code): # pylint: disable=redefin
request: request:
""" """
response = client.post( response = client.post(
"/toilets/new", "/get/toilets",
params={ params={
"location": location "location": location
} }
) )
print(response.json())
toilets_list = [Toilets.model_validate(toilet) for toilet in response.json()] toilets_list = [Toilets.model_validate(toilet) for toilet in response.json()]
# checks : # checks :
@ -87,12 +89,14 @@ def test_toilets(client, location, status_code): # pylint: disable=redefined-
request: request:
""" """
response = client.post( response = client.post(
"/toilets/new", "/get/toilets",
params={ params={
"location": location, "location": location,
"radius" : 600 "radius" : 600
} }
) )
print(response.json())
toilets_list = [Toilets.model_validate(toilet) for toilet in response.json()] toilets_list = [Toilets.model_validate(toilet) for toilet in response.json()]
# checks : # checks :

View File

@ -23,9 +23,9 @@ def client():
# Realistic # Realistic
(5, 0, 0, 20, [48.0845881, 7.2804050], None), # Turckheim (5, 0, 0, 20, [48.0845881, 7.2804050], None), # Turckheim
(5, 5, 5, 120, [45.7576485, 4.8330241], None), # Lyon, Bellecour (5, 5, 5, 120, [45.7576485, 4.8330241], None), # Lyon, Bellecour
(5, 5, 5, 240, [50.9423526, 6.9577780], None), # Cologne, centre (5, 2, 5, 240, [50.9423526, 6.9577780], None), # Cologne, centre
(5, 5, 5, 180, [48.5846589226, 7.74078715721], None), # Strasbourg, centre (3, 5, 0, 180, [48.5846589226, 7.74078715721], None), # Strasbourg, centre
(5, 5, 5, 180, [47.377884227, 8.5395114066], None), # Zurich, centre (2, 4, 5, 180, [47.377884227, 8.5395114066], None), # Zurich, centre
(5, 0, 5, 200, [48.85468881798671, 2.3423925755998374], None), # Paris, centre (5, 0, 5, 200, [48.85468881798671, 2.3423925755998374], None), # Paris, centre
(5, 5, 5, 600, [40.72592726802, -73.9920434795], None), # New York, Lower Manhattan (5, 5, 5, 600, [40.72592726802, -73.9920434795], None), # New York, Lower Manhattan
] ]

View File

@ -1,16 +1,20 @@
"""Defines the endpoint for fetching toilet locations.""" """API entry point for fetching toilet locations."""
from fastapi import HTTPException, APIRouter, Query from fastapi import HTTPException, APIRouter, Query
from ..structs.toilets import Toilets
from .toilets_manager import ToiletsManager from .toilets_manager import ToiletsManager
from ..structs.toilets import Toilets
# Define the API router # Initialize the API router
router = APIRouter() router = APIRouter()
@router.post("/toilets/new") @router.post("/get/toilets")
def get_toilets(location: tuple[float, float] = Query(...), radius: int = 500) -> list[Toilets] : def get_toilets(
location: tuple[float, float] = Query(...),
radius: int = 500
) -> list[Toilets] :
""" """
Endpoint to find toilets within a specified radius from a given location. Endpoint to find toilets within a specified radius from a given location.

View File

@ -1,24 +0,0 @@
"""Helper function to return only the major landmarks from a large list."""
from ..structs.landmark import Landmark
def take_most_important(
landmarks: list[Landmark],
n_important: int
) -> list[Landmark]:
"""
Given a list of landmarks, return the most important landmarks based on their attractiveness.
Args:
landmarks (list[Landmark]): List of landmarks that needs to be truncated
n_important (int): Number of most important landmarks to return
Returns:
list[Landmark]: List of the n_important most important landmarks
"""
if n_important == 0 :
raise ValueError('Number of landmarks to keep cannot be zero.')
# Sort landmarks by attractiveness (descending)
sorted_landmarks = sorted(landmarks, key=lambda x: x.attractiveness, reverse=True)
return sorted_landmarks[:n_important]

1091
report.html Normal file

File diff suppressed because it is too large Load Diff