From c3e72001befe0cf6404e2b2cb9bbc3a50e97fb53 Mon Sep 17 00:00:00 2001 From: kilian Date: Sun, 7 Dec 2025 18:00:06 +0100 Subject: [PATCH] hot fix to reintroduce the /trip/new endpoint --- backend/src/main.py | 128 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 127 insertions(+), 1 deletion(-) diff --git a/backend/src/main.py b/backend/src/main.py index 4cde3f1..ca43d50 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -1,12 +1,16 @@ """Main app for backend api""" import logging +import yaml +import time from contextlib import asynccontextmanager -from fastapi import FastAPI, HTTPException +from fastapi import FastAPI, HTTPException, BackgroundTasks from .logging_config import configure_logging from .structs.landmark import Landmark +from .structs.preferences import Preferences from .structs.linked_landmarks import LinkedLandmarks from .structs.trip import Trip +from .overpass.overpass import fill_cache from .landmarks.landmarks_manager import LandmarkManager from .toilets.toilets_router import router as toilets_router from .optimization.optimization_router import router as optimization_router @@ -14,6 +18,7 @@ from .landmarks.landmarks_router import router as landmarks_router, get_landmark from .optimization.optimizer import Optimizer from .optimization.refiner import Refiner from .cache import client as cache_client +from .constants import OPTIMIZER_PARAMETERS_PATH logger = logging.getLogger(__name__) @@ -53,6 +58,127 @@ app.include_router(toilets_router) +###### TO REMOVE ONCE THE FRONTEND IS UP TO DATE ###### +@app.post("/trip/new") +def new_trip(preferences: Preferences, + start: tuple[float, float], + 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") + + logger.info(f"Requested new trip generation. Details:\n\tCoordinates: {start}\n\tTime: {preferences.max_time_minute}\n\tSightseeing: {preferences.sightseeing.score}\n\tNature: {preferences.nature.score}\n\tShopping: {preferences.shopping.score}") + + start_time = time.time() + + # Generate the landmarks from the start location + landmarks = manager.generate_landmarks_list( + center_coordinates = start, + preferences = preferences + ) + + t_generate_landmarks = time.time() - start_time + logger.info(f'Fetched {len(landmarks)} landmarks in \t: {round(t_generate_landmarks,3)} seconds') + + + start_time = time.time() + + logger.info(f"Requested new trip generation. Details:\n\tCoordinates: {start}\n\tTime: {preferences.max_time_minute}\n\tSightseeing: {preferences.sightseeing.score}\n\tNature: {preferences.nature.score}\n\tShopping: {preferences.shopping.score}") + + start_landmark = Landmark( + name='start', + type='start', + location=(start[0], start[1]), + osm_type='start', + osm_id=0, + attractiveness=0, + duration=0, + must_do=True, + n_tags = 0 + ) + + end_landmark = Landmark( + name='finish', + type='finish', + location=(end[0], end[1]), + osm_type='end', + osm_id=0, + attractiveness=0, + duration=0, + must_do=True, + n_tags=0 + ) + + # From the parameters load the length at which to truncate the landmarks list. + with OPTIMIZER_PARAMETERS_PATH.open('r') as f: + parameters = yaml.safe_load(f) + n_important = parameters['N_important'] + + # Truncate to the most important landmarks for a shorter list + landmarks_short = landmarks[:n_important] + + # insert start and finish to the shorter landmarks list + landmarks_short.insert(0, start_landmark) + landmarks_short.append(end_landmark) + + # First stage optimization + try: + base_tour = optimizer.solve_optimization(preferences.max_time_minute, landmarks_short) + except Exception as exc: + logger.error(f"Trip generation failed: {str(exc)}") + raise HTTPException(status_code=500, detail=f"Optimization failed: {str(exc)}") from exc + + t_first_stage = time.time() - start_time + start_time = time.time() + + # Second stage optimization + try : + refined_tour = refiner.refine_optimization( + landmarks, base_tour, + preferences.max_time_minute, + preferences.detour_tolerance_minute + ) + except Exception as exc : + logger.warning(f"Refiner failed. Proceeding with base trip {str(exc)}") + refined_tour = base_tour + + t_second_stage = time.time() - start_time + + logger.debug(f'First stage optimization\t: {round(t_first_stage,3)} seconds') + logger.debug(f'Second stage optimization\t: {round(t_second_stage,3)} seconds') + logger.info(f'Total computation time\t: {round(t_first_stage + t_second_stage,3)} seconds') + linked_tour = LinkedLandmarks(refined_tour) + + # upon creation of the trip, persistence of both the trip and its landmarks is ensured. + trip = Trip.from_linked_landmarks(linked_tour, cache_client) + logger.info(f'Optimized a trip of {trip.total_time} minutes with {len(refined_tour)} landmarks in {round(t_first_stage + t_second_stage,3)} seconds.') + logger.info('Detailed trip :\n\t' + '\n\t'.join(f'{landmark}' for landmark in refined_tour)) + + background_tasks.add_task(fill_cache) + + return trip + + + + #### For already existing trips/landmarks @app.get("/trip/{trip_uuid}") def get_trip(trip_uuid: str) -> Trip: