new endpoint for toilets
	
		
			
	
		
	
	
		
	
		
			Some checks failed
		
		
	
	
		
			
				
	
				Build and deploy the backend to staging / Build and push image (pull_request) Failing after 2m35s
				
			
		
			
				
	
				Build and deploy the backend to staging / Deploy to staging (pull_request) Has been skipped
				
			
		
			
				
	
				Run linting on the backend code / Build (pull_request) Failing after 28s
				
			
		
			
				
	
				Run testing on the backend code / Build (pull_request) Failing after 1m24s
				
			
		
		
	
	
				
					
				
			
		
			Some checks failed
		
		
	
	Build and deploy the backend to staging / Build and push image (pull_request) Failing after 2m35s
				
			Build and deploy the backend to staging / Deploy to staging (pull_request) Has been skipped
				
			Run linting on the backend code / Build (pull_request) Failing after 28s
				
			Run testing on the backend code / Build (pull_request) Failing after 1m24s
				
			This commit is contained in:
		@@ -1,13 +1,14 @@
 | 
				
			|||||||
"""Main app for backend api"""
 | 
					"""Main app for backend api"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import logging
 | 
					import logging
 | 
				
			||||||
from fastapi import FastAPI, HTTPException
 | 
					from fastapi import FastAPI, HTTPException, Query
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .structs.landmark import Landmark
 | 
					from .structs.landmark import Landmark, Toilets
 | 
				
			||||||
from .structs.preferences import Preferences
 | 
					from .structs.preferences import Preferences
 | 
				
			||||||
from .structs.linked_landmarks import LinkedLandmarks
 | 
					from .structs.linked_landmarks import LinkedLandmarks
 | 
				
			||||||
from .structs.trip import Trip
 | 
					from .structs.trip import Trip
 | 
				
			||||||
from .utils.landmarks_manager import LandmarkManager
 | 
					from .utils.landmarks_manager import LandmarkManager
 | 
				
			||||||
 | 
					from .utils.toilets_manager import ToiletsManager
 | 
				
			||||||
from .utils.optimizer import Optimizer
 | 
					from .utils.optimizer import Optimizer
 | 
				
			||||||
from .utils.refiner import Refiner
 | 
					from .utils.refiner import Refiner
 | 
				
			||||||
from .persistence import client as cache_client
 | 
					from .persistence import client as cache_client
 | 
				
			||||||
@@ -36,19 +37,15 @@ def new_trip(preferences: Preferences,
 | 
				
			|||||||
        (uuid) : The uuid of the first landmark in the optimized route
 | 
					        (uuid) : The uuid of the first landmark in the optimized route
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    if preferences is None:
 | 
					    if preferences is None:
 | 
				
			||||||
        raise HTTPException(status_code=406,
 | 
					        raise HTTPException(status_code=406, detail="Preferences not provided or incomplete.")
 | 
				
			||||||
                            detail="Preferences not provided or incomplete.")
 | 
					 | 
				
			||||||
    if (preferences.shopping.score == 0 and
 | 
					    if (preferences.shopping.score == 0 and
 | 
				
			||||||
        preferences.sightseeing.score == 0 and
 | 
					        preferences.sightseeing.score == 0 and
 | 
				
			||||||
        preferences.nature.score == 0) :
 | 
					        preferences.nature.score == 0) :
 | 
				
			||||||
        raise HTTPException(status_code=406,
 | 
					        raise HTTPException(status_code=406, detail="All preferences are 0.")
 | 
				
			||||||
                            detail="All preferences are 0.")
 | 
					 | 
				
			||||||
    if start is None:
 | 
					    if start is None:
 | 
				
			||||||
        raise HTTPException(status_code=406,
 | 
					        raise HTTPException(status_code=406, detail="Start coordinates not provided")
 | 
				
			||||||
                            detail="Start coordinates not provided")
 | 
					 | 
				
			||||||
    if not (-90 <= start[0] <= 90 or -180 <= start[1] <= 180):
 | 
					    if not (-90 <= start[0] <= 90 or -180 <= start[1] <= 180):
 | 
				
			||||||
        raise HTTPException(status_code=422,
 | 
					        raise HTTPException(status_code=422, detail="Start coordinates not in range")
 | 
				
			||||||
                            detail="Start coordinates not in range")
 | 
					 | 
				
			||||||
    if end is None:
 | 
					    if end is None:
 | 
				
			||||||
        end = start
 | 
					        end = start
 | 
				
			||||||
        logger.info("No end coordinates provided. Using start=end.")
 | 
					        logger.info("No end coordinates provided. Using start=end.")
 | 
				
			||||||
@@ -135,3 +132,31 @@ def get_landmark(landmark_uuid: str) -> Landmark:
 | 
				
			|||||||
        return landmark
 | 
					        return landmark
 | 
				
			||||||
    except KeyError as exc:
 | 
					    except KeyError as exc:
 | 
				
			||||||
        raise HTTPException(status_code=404, detail="Landmark not found") from exc
 | 
					        raise HTTPException(status_code=404, detail="Landmark not found") from exc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.post("/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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -79,8 +79,8 @@ def get_distance(p1: tuple[float, float], p2: tuple[float, float]) -> int:
 | 
				
			|||||||
    Calculate the time in minutes to travel from one location to another.
 | 
					    Calculate the time in minutes to travel from one location to another.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Args:
 | 
					    Args:
 | 
				
			||||||
        p1 (Tuple[float, float]): Coordinates of the starting location.
 | 
					        p1 (tuple[float, float]): Coordinates of the starting location.
 | 
				
			||||||
        p2 (Tuple[float, float]): Coordinates of the destination.
 | 
					        p2 (tuple[float, float]): Coordinates of the destination.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Returns:
 | 
					        Returns:
 | 
				
			||||||
        int: Time to travel from p1 to p2 in minutes.
 | 
					        int: Time to travel from p1 to p2 in minutes.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -115,3 +115,28 @@ class Landmark(BaseModel) :
 | 
				
			|||||||
        return (self.uuid == value.uuid or
 | 
					        return (self.uuid == value.uuid or
 | 
				
			||||||
                self.osm_id == value.osm_id or
 | 
					                self.osm_id == value.osm_id or
 | 
				
			||||||
                (self.name == value.name and self.distance(value) < 0.001))
 | 
					                (self.name == value.name and self.distance(value) < 0.001))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Toilets(BaseModel) :
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Model for toilets. When false/empty the information is either false either not known.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    location : tuple
 | 
				
			||||||
 | 
					    wheelchair : Optional[bool] = False
 | 
				
			||||||
 | 
					    changing_table : Optional[bool] = False
 | 
				
			||||||
 | 
					    fee : Optional[bool] = False
 | 
				
			||||||
 | 
					    opening_hours : Optional[str] = ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __str__(self) -> str:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        String representation of the Toilets object.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            str: A formatted string with the toilets location.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return f'Toilets @{self.location}'
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    class Config:
 | 
				
			||||||
 | 
					        # This allows us to easily convert the model to and from dictionaries
 | 
				
			||||||
 | 
					        orm_mode = True
 | 
				
			||||||
							
								
								
									
										103
									
								
								backend/src/tests/test_toilets.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								backend/src/tests/test_toilets.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,103 @@
 | 
				
			|||||||
 | 
					"""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.
 | 
				
			||||||
 | 
					        ([40.768502, -73.958408], 200), # New York, Upper East Side.
 | 
				
			||||||
 | 
					        ([53.482864, -2.2411116], 200), # Manchester, centre.
 | 
				
			||||||
 | 
					        ([-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
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    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,6 +1,5 @@
 | 
				
			|||||||
"""Helper methods for testing."""
 | 
					"""Helper methods for testing."""
 | 
				
			||||||
import logging
 | 
					import logging
 | 
				
			||||||
from typing import List
 | 
					 | 
				
			||||||
from fastapi import HTTPException
 | 
					from fastapi import HTTPException
 | 
				
			||||||
from pydantic import ValidationError
 | 
					from pydantic import ValidationError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -8,7 +7,7 @@ from ..structs.landmark import Landmark
 | 
				
			|||||||
from ..persistence import client as cache_client
 | 
					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.
 | 
					    Convert the list of landmarks into a list containing their osm ids for quick landmark checking.
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
@@ -95,7 +94,7 @@ def fetch_landmark_cache(landmark_uuid: str):
 | 
				
			|||||||
    
 | 
					    
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def load_trip_landmarks(client, first_uuid: str, from_cache=None) -> List[Landmark]:
 | 
					def load_trip_landmarks(client, first_uuid: str, from_cache=None) -> list[Landmark]:
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Load all landmarks for a trip using the response from the API.
 | 
					    Load all landmarks for a trip using the response from the API.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -120,7 +119,7 @@ def load_trip_landmarks(client, first_uuid: str, from_cache=None) -> List[Landma
 | 
				
			|||||||
    return landmarks
 | 
					    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.
 | 
					    Allows to show the detailed trip in the html test report.
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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.
 | 
					    Calculate the time in minutes to travel from one location to another.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Args:
 | 
					    Args:
 | 
				
			||||||
        p1 (Tuple[float, float]): Coordinates of the starting location.
 | 
					        p1 (tuple[float, float]): Coordinates of the starting location.
 | 
				
			||||||
        p2 (Tuple[float, float]): Coordinates of the destination.
 | 
					        p2 (tuple[float, float]): Coordinates of the destination.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Returns:
 | 
					        Returns:
 | 
				
			||||||
        int: Time to travel from p1 to p2 in minutes.
 | 
					        int: Time to travel from p1 to p2 in minutes.
 | 
				
			||||||
@@ -55,8 +55,8 @@ def get_distance(p1: tuple[float, float], p2: tuple[float, float]) -> int:
 | 
				
			|||||||
    Calculate the time in minutes to travel from one location to another.
 | 
					    Calculate the time in minutes to travel from one location to another.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Args:
 | 
					    Args:
 | 
				
			||||||
        p1 (Tuple[float, float]): Coordinates of the starting location.
 | 
					        p1 (tuple[float, float]): Coordinates of the starting location.
 | 
				
			||||||
        p2 (Tuple[float, float]): Coordinates of the destination.
 | 
					        p2 (tuple[float, float]): Coordinates of the destination.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Returns:
 | 
					        Returns:
 | 
				
			||||||
        int: Time to travel from p1 to p2 in minutes.
 | 
					        int: Time to travel from p1 to p2 in minutes.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -79,6 +79,7 @@ class LandmarkManager:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        # Create a bbox using the around technique
 | 
					        # Create a bbox using the around technique
 | 
				
			||||||
        bbox = tuple((f"around:{reachable_bbox_side/2}", str(center_coordinates[0]), str(center_coordinates[1])))
 | 
					        bbox = tuple((f"around:{reachable_bbox_side/2}", str(center_coordinates[0]), str(center_coordinates[1])))
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
        # list for sightseeing
 | 
					        # list for sightseeing
 | 
				
			||||||
        if preferences.sightseeing.score != 0:
 | 
					        if preferences.sightseeing.score != 0:
 | 
				
			||||||
            score_function = lambda score: score * 10 * preferences.sightseeing.score / 5
 | 
					            score_function = lambda score: score * 10 * preferences.sightseeing.score / 5
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -44,7 +44,7 @@ class Optimizer:
 | 
				
			|||||||
            resx (list[float]): List of edge weights.
 | 
					            resx (list[float]): List of edge weights.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Returns:
 | 
					        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):
 | 
					        for i, elem in enumerate(resx):
 | 
				
			||||||
@@ -79,7 +79,7 @@ class Optimizer:
 | 
				
			|||||||
            L (int): Number of landmarks.
 | 
					            L (int): Number of landmarks.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Returns:
 | 
					        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
 | 
					        l1 = [0]*L*L
 | 
				
			||||||
@@ -107,7 +107,7 @@ class Optimizer:
 | 
				
			|||||||
            resx (list): List of edge weights.
 | 
					            resx (list): List of edge weights.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Returns:
 | 
					        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
 | 
					        # first round the results to have only 0-1 values
 | 
				
			||||||
@@ -180,7 +180,7 @@ class Optimizer:
 | 
				
			|||||||
            max_time (int): Maximum time of visit allowed.
 | 
					            max_time (int): Maximum time of visit allowed.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Returns:
 | 
					        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 + ...
 | 
					        # Objective function coefficients. a*x1 + b*x2 + c*x3 + ...
 | 
				
			||||||
@@ -212,7 +212,7 @@ class Optimizer:
 | 
				
			|||||||
            L (int): Number of landmarks.
 | 
					            L (int): Number of landmarks.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Returns:
 | 
					        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
 | 
					        ones = [1]*L
 | 
				
			||||||
@@ -239,7 +239,7 @@ class Optimizer:
 | 
				
			|||||||
            L (int): Number of landmarks.
 | 
					            L (int): Number of landmarks.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Returns:
 | 
					        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)
 | 
					        upper_ind = np.triu_indices(L,0,L)
 | 
				
			||||||
@@ -270,7 +270,7 @@ class Optimizer:
 | 
				
			|||||||
            L (int): Number of landmarks.
 | 
					            L (int): Number of landmarks.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Returns:
 | 
					        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
 | 
					        l = [0]*L*L
 | 
				
			||||||
@@ -293,7 +293,7 @@ class Optimizer:
 | 
				
			|||||||
            landmarks (list[Landmark]): List of landmarks, where some are marked as 'must_do'.
 | 
					            landmarks (list[Landmark]): List of landmarks, where some are marked as 'must_do'.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Returns:
 | 
					        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)
 | 
					        L = len(landmarks)
 | 
				
			||||||
@@ -319,7 +319,7 @@ class Optimizer:
 | 
				
			|||||||
            landmarks (list[Landmark]): List of landmarks, where some are marked as 'must_avoid'.
 | 
					            landmarks (list[Landmark]): List of landmarks, where some are marked as 'must_avoid'.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Returns:
 | 
					        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)
 | 
					        L = len(landmarks)
 | 
				
			||||||
@@ -346,7 +346,7 @@ class Optimizer:
 | 
				
			|||||||
            L (int): Number of landmarks.
 | 
					            L (int): Number of landmarks.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Returns:
 | 
					        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)
 | 
					        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.
 | 
					            L (int): Number of landmarks.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Returns:
 | 
					        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
 | 
					        A = [0]*L*L
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,6 @@ import yaml, logging
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from shapely import buffer, LineString, Point, Polygon, MultiPoint, concave_hull
 | 
					from shapely import buffer, LineString, Point, Polygon, MultiPoint, concave_hull
 | 
				
			||||||
from math import pi
 | 
					from math import pi
 | 
				
			||||||
from typing import List
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
from ..structs.landmark import Landmark
 | 
					from ..structs.landmark import Landmark
 | 
				
			||||||
from . import take_most_important, get_time_separation
 | 
					from . import take_most_important, get_time_separation
 | 
				
			||||||
@@ -135,7 +134,7 @@ class Refiner :
 | 
				
			|||||||
    
 | 
					    
 | 
				
			||||||
        return tour
 | 
					        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.
 | 
					        Inserts 'sub_list' of Landmarks inside the 'main_list' by leaving the ends untouched.
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										78
									
								
								backend/src/utils/toilets_manager.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								backend/src/utils/toilets_manager.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,78 @@
 | 
				
			|||||||
 | 
					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
 | 
				
			||||||
		Reference in New Issue
	
	Block a user