Merge modifications for more separate backend functions #69
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -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) :
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,
 | 
				
			||||||
| 
						
							
	
	
	
	
	
	
	
	 
					
					kscheidecker marked this conversation as resolved
					
				 
				 | 
				|||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    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
 | 
				
			||||||
@@ -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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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) :
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 :
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 :
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -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
									
								
							
							
						
						
									
										1091
									
								
								report.html
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Reference in New Issue
	
	Block a user
	
should this be a landmark-parameter? No need to let the user choose that option. But it might be intersting to allow this to be set at runtime instead of hardcoding it.
Also a good point! Implemented in commit bfc0c9adae