From c58c10b0572a3be083061f6bbfb595a3819ed81f Mon Sep 17 00:00:00 2001 From: Kilian Scheidecker Date: Tue, 4 Jun 2024 00:20:54 +0200 Subject: [PATCH] fixed input as coordinates --- .gitignore | 1 + backend/src/landmarks_manager.py | 219 ++++++++++++++++++++++------- backend/src/main.py | 35 ++++- backend/src/optimizer.py | 4 +- backend/src/structs/landmarks.py | 20 +-- backend/src/structs/preferences.py | 8 +- backend/src/tester.py | 67 +++++++++ 7 files changed, 274 insertions(+), 80 deletions(-) create mode 100644 .gitignore create mode 100644 backend/src/tester.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e934adf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +cache/ diff --git a/backend/src/landmarks_manager.py b/backend/src/landmarks_manager.py index 67141c7..3f8a62f 100644 --- a/backend/src/landmarks_manager.py +++ b/backend/src/landmarks_manager.py @@ -2,77 +2,190 @@ from OSMPythonTools.api import Api from OSMPythonTools.overpass import Overpass, overpassQueryBuilder, Nominatim from dataclasses import dataclass from pydantic import BaseModel +import math as m +from structs.landmarks import Landmark, LandmarkType +from structs.preferences import Preferences, Preference +from typing import List +from typing import Tuple + +RADIUS = 0.0005 # size of the bbox in degrees. 0.0005 ~ 50m +BBOX_SIDE = 10 # size of bbox in km for general area, 10km +RADIUS_CLOSE_TO = 50 # size of area in m for close features, 5àm radius +MIN_SCORE = 100 # discard elements with score < 100 +MIN_TAGS = 5 # discard elements withs less than 5 tags -# Defines the landmark class (aka some place there is to visit) -@dataclass -class Landmarkkkk : - name : str - attractiveness : int - id : int +# Include th json here +# Create a list of all things to visit given some preferences and a city. Ready for the optimizer +def generate_landmarks(coordinates: Tuple[float, float], preferences: Preferences) : + + l_sights = ["'tourism'='museum'", "'tourism'='attraction'", "'tourism'='gallery'", 'historic', "'amenity'='arts_centre'", "'amenity'='planetarium'", "'amenity'='place_of_worship'", "'amenity'='fountain'", '"water"="reflecting_pool"'] + l_nature = ["'leisure'='park'", 'geological', "'natural'='geyser'", "'natural'='hot_spring'", '"natural"="arch"', '"natural"="cave_entrance"', '"natural"="volcano"', '"natural"="stone"', '"tourism"="alpine_hut"', '"tourism"="picnic_site"', '"tourism"="viewpoint"', '"tourism"="zoo"', '"waterway"="waterfall"'] + l_shop = ["'shop'='department_store'", "'shop'='mall'"] #, '"shop"="collector"', '"shop"="antiques"'] + + # List for sightseeing + L1 = get_landmarks(coordinates, l_sights, LandmarkType(landmark_type='sightseeing')) + correct_score(L1, preferences.sightseeing) -class Landmark(BaseModel) : - name : str - attractiveness : int - loc : tuple + # List for nature + L2 = get_landmarks(coordinates, l_nature, LandmarkType(landmark_type='nature')) + correct_score(L2, preferences.nature) + + # List for shopping + L3 = get_landmarks(coordinates, l_shop, LandmarkType(landmark_type='shopping')) + correct_score(L3, preferences.shopping) -# Converts a OSM id to a landmark -def add_from_id(id: int, score: int) : + L = L1 + L2 + L3 - try : - s = 'way/' + str(id) # prepare string for query - obj = api.query(s) # object to add + return cleanup_list(L) + +# Determines if two locations are close to each other +def is_close_to(loc1: Tuple[float, float], loc2: Tuple[float, float]) : + + alpha = (180*RADIUS_CLOSE_TO)/(6371000*m.pi) + if abs(loc1[0] - loc2[0]) + abs(loc1[1] - loc2[1]) < alpha*2 : + return True + else : + return False + +# Remove duplicate elements and elements with low score +def cleanup_list(L: List[Landmark]) : + L_clean = [] + names = [] + + for landmark in L : + + if landmark.name in names : # Remove duplicates + continue + + elif landmark.attractiveness < MIN_SCORE : # Remove uninteresting + continue + + elif landmark.n_tags < MIN_TAGS : # Remove uninteresting 2.0 + continue + + else : + names.append(landmark.name) + L_clean.append(landmark) + + return L_clean + + +# Correct the score of a list of landmarks by taking into account preferences and the number of tags +def correct_score(L: List[Landmark], preference: Preference) : + + if len(L) == 0 : + return + + if L[0].type != preference.type : + raise TypeError(f"LandmarkType {preference.type} does not match the type of Landmark {L[0].name}") + + for elem in L : + elem.attractiveness = int(elem.attractiveness/100) + elem.n_tags # arbitrary correction of the balance score vs number of tags + elem.attractiveness = elem.attractiveness*preference.score # arbitrary computation + +# Correct the score of a list of landmarks by taking into account preferences and the number of tags +def correct_score_test(L: List[Landmark], preference: Preference) : + + if len(L) == 0 : + return + + if L[0].type != preference.type : + raise TypeError(f"LandmarkType {preference.type} does not match the type of Landmark {L[0].name}") + + for elem in L : + elem.attractiveness = int(elem.attractiveness/100) + elem.n_tags # arbitrary correction of the balance score vs number of tags + elem.attractiveness = elem.attractiveness*preference.score # arbitrary computation + +# Function to count elements within a 25m radius of a location +def count_elements_within_radius(coordinates: Tuple[float, float]) -> int: + + lat = coordinates[0] + lon = coordinates[1] + + bbox = {'latLower':lat-RADIUS,'lonLower':lon-RADIUS,'latHigher':lat+RADIUS,'lonHigher': lon+RADIUS} + overpass = Overpass() + + # Build the query to find elements within the radius + radius_query = overpassQueryBuilder(bbox=[bbox['latLower'],bbox['lonLower'],bbox['latHigher'],bbox['lonHigher']], + elementType=['node', 'way', 'relation']) + + try : + radius_result = overpass.query(radius_query) + + # The count is the number of elements found + return radius_result.countElements() + except : - s = 'relation/' + str(id) # prepare string for query - obj = api.query(s) # object to add + return None - return Landmarkkkk(obj.tag('name:fr'), score, id) # create Landmark out of it +# Creates a bounding box around precise coordinates +def create_bbox(coordinates: Tuple[float, float], side_length: int) -> Tuple[float, float, float, float]: + """ + Create a simple bounding box around given coordinates. + :param coordinates: tuple (lat, lon) + -> lat: Latitude of the center point. + -> lon: Longitude of the center point. + :param side_length: int - side length of the bbox in km + :return: Bounding box as (min_lat, min_lon, max_lat, max_lon). + """ + lat = coordinates[0] + lon = coordinates[1] + # Half the side length in km (since it's a square bbox) + half_side_length_km = side_length / 2.0 + + # Convert distance to degrees + lat_diff = half_side_length_km / 111 # 1 degree latitude is approximately 111 km + lon_diff = half_side_length_km / (111 * m.cos(m.radians(lat))) # Adjust for longitude based on latitude + + # Calculate bbox + min_lat = lat - lat_diff + max_lat = lat + lat_diff + min_lon = lon - lon_diff + max_lon = lon + lon_diff + + return min_lat, min_lon, max_lat, max_lon + +# Generates the list of landmarks for a given Landmarktype. Needs coordinates, a list of amenities and the corresponding LandmarkType +def get_landmarks(coordinates: Tuple[float, float], l: List[Landmark], landmarktype: LandmarkType): -def get_sights(city_country: str): - nominatim = Nominatim() - areaId = nominatim.query(city_country).areaId() overpass = Overpass() - # list of stuff we want to define as sights - l = ["'tourism'='museum'", "'tourism'='attraction'", "'tourism'='gallery'", 'historic', "'amenity'='arts_centre'", "'amenity'='planetarium'", '"amenity"="place_of_worship"'] - score = 0 + # Generate a bbox around currunt coordinates + bbox = create_bbox(coordinates, BBOX_SIDE) + + # Initialize some variables + N = 0 + L = [] for amenity in l : - query = overpassQueryBuilder(area=areaId, elementType=['way', 'relation'], selector=amenity, includeGeometry=True) + query = overpassQueryBuilder(bbox=bbox, elementType=['way', 'relation'], selector=amenity, includeCenter=True, out='body') result = overpass.query(query) - score += result.countElements() + N += result.countElements() - return score + for elem in result.elements(): + name = elem.tag('name') # Add name + location = (elem.centerLat(), elem.centerLon()) # Add coordinates (lat, lon) -# take a lsit of tuples (id, score) to generate a list of landmarks -def generate_landmarks(ids_and_scores: list) : + # skip if unprecise location + if name is None or location[0] is None: + continue + else : + + osm_type = elem.type() # Add type : 'way' or 'relation' + osm_id = elem.id() # Add OSM id + elem_type = landmarktype # Add the landmark type as 'sightseeing + n_tags = len(elem.tags().keys()) # Add number of tags + + # Add score of given landmark based on the number of surrounding elements + score = count_elements_within_radius(location) - L = [] - for tup in ids_and_scores : - L.append(add_from_id(tup[0], tup[1])) + if score is not None : + # Generate the landmark and append it to the list + landmark = Landmark(name=name, type=elem_type, location=location, osm_type=osm_type, osm_id=osm_id, attractiveness=score, must_do=False, n_tags=n_tags) + L.append(landmark) return L -""" -api = Api() - -l = (7515426, 70) -t = (5013364, 100) -n = (201611261, 99) -a = (226413508, 50) -m = (23762981, 30) - - -ids_and_scores = [t, l, n, a, m] - -landmarks = generate_landmarks(ids_and_scores) - - -for obj in landmarks : - print(obj)""" - - - -print(get_sights('Paris, France')) \ No newline at end of file diff --git a/backend/src/main.py b/backend/src/main.py index d24346f..3118f96 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -1,7 +1,9 @@ from optimizer import solve_optimization +from landmarks_manager import generate_landmarks from structs.landmarks import LandmarkTest from structs.landmarks import Landmark -from structs.preferences import Preferences +from structs.landmarktype import LandmarkType +from structs.preferences import Preferences, Preference from fastapi import FastAPI, Query, Body from typing import List @@ -12,13 +14,22 @@ app = FastAPI() #"http://127.0.0.1:8000/process?param1={param1}¶m2={param2}" # This should become main at some point @app.post("/optimizer/{longitude}/{latitude}") -def main(longitude: float, latitude: float, prefrences: Preferences = Body(...)) -> List[Landmark]: +def main(longitude: float, latitude: float, preferences: Preferences = Body(...)) -> List[Landmark]: # From frontend get longitude, latitude and prefence list + + # Generate the landmark list + landmarks = generate_landmarks(tuple((longitude, latitude)), preferences) + + # Set the max distance + max_steps = 90 + + # Compute the visiting order + visiting_order = solve_optimization(landmarks, max_steps, True) + + return visiting_order - landmarks = [] - return landmarks @app.get("test") def test(): @@ -46,6 +57,16 @@ def test(): #return("max steps :", max_steps, "\n", visiting_order) -"""# keep this for debug -if __name__ == "__main__": - main()""" \ No newline at end of file +# input city, country in the form of 'Paris, France' +@app.post("/test2/{city_country}") +def test2(city_country: str, preferences: Preferences = Body(...)) -> List[Landmark]: + + landmarks = generate_landmarks(city_country, preferences) + + max_steps = 9000000 + + visiting_order = solve_optimization(landmarks, max_steps, True) + + + + diff --git a/backend/src/optimizer.py b/backend/src/optimizer.py index 78f34a0..907647a 100644 --- a/backend/src/optimizer.py +++ b/backend/src/optimizer.py @@ -212,7 +212,7 @@ def init_ub_dist(landmarks: list, max_steps: int): dist_table = [0]*len(landmarks) c.append(-spot1.attractiveness) for j, spot2 in enumerate(landmarks) : - dist_table[j] = manhattan_distance(spot1.loc, spot2.loc) + dist_table[j] = manhattan_distance(spot1.location, spot2.location) A.append(dist_table) c = c*len(landmarks) A_ub = [] @@ -238,7 +238,7 @@ def respect_user_mustsee(landmarks: list, A_eq: list, b_eq: list) : for k in range(L-1) : l[k*L+L-1] = 1 - H += manhattan_distance(elem.loc, elem_prev.loc) + H += manhattan_distance(elem.location, elem_prev.location) elem_prev = elem """for i in range(7): diff --git a/backend/src/structs/landmarks.py b/backend/src/structs/landmarks.py index 48e6729..61715bf 100644 --- a/backend/src/structs/landmarks.py +++ b/backend/src/structs/landmarks.py @@ -1,4 +1,5 @@ from pydantic import BaseModel +from OSMPythonTools.api import Api from .landmarktype import LandmarkType from .preferences import Preferences @@ -12,19 +13,10 @@ class Landmark(BaseModel) : name : str type: LandmarkType # De facto mapping depending on how the query was executed with overpass. Should still EXACTLY correspond to the preferences location : tuple - - # loop through the preferences and assign a score to the landmark - def score(self, preferences: Preferences): - - for preference_name, preference in preferences.__dict__.items(): - - if (preference_name == self.type.landmark_type) : - score = preference.score - - if (not score) : - raise Exception(f"Could not determine score for landmark {self.name}") - - else : - return score + osm_type : str + osm_id : int + attractiveness : int + must_do : bool + n_tags : int diff --git a/backend/src/structs/preferences.py b/backend/src/structs/preferences.py index 94e0836..500ebec 100644 --- a/backend/src/structs/preferences.py +++ b/backend/src/structs/preferences.py @@ -3,8 +3,8 @@ from .landmarktype import LandmarkType class Preference(BaseModel) : name: str - type: LandmarkType - score: int + type: LandmarkType # should match the attributes of the Preferences class + score: int # score could be from 1 to 5 # Input for optimization class Preferences(BaseModel) : @@ -17,11 +17,11 @@ class Preferences(BaseModel) : # Shopping (diriger plutôt vers des zones / rues commerçantes) shopping : Preference - # Food (price low or high. Combien on veut dépenser pour manger à midi/soir) +""" # Food (price low or high. Combien on veut dépenser pour manger à midi/soir) food_budget : Preference # Tolérance au détour (ce qui détermine (+ ou -) le chemin emprunté) - detour_tol : Preference + detour_tol : Preference""" diff --git a/backend/src/tester.py b/backend/src/tester.py new file mode 100644 index 0000000..e531783 --- /dev/null +++ b/backend/src/tester.py @@ -0,0 +1,67 @@ +from optimizer import solve_optimization +from landmarks_manager import generate_landmarks +from structs.landmarks import LandmarkTest +from structs.landmarks import Landmark +from structs.landmarktype import LandmarkType +from structs.preferences import Preferences, Preference +from fastapi import FastAPI, Query, Body +from typing import List + + +def test3(city_country: str) -> List[Landmark]: + + + preferences = Preferences( + sightseeing=Preference( + name='sightseeing', + type=LandmarkType(landmark_type='sightseeing'), + score = 5), + nature=Preference( + name='nature', + type=LandmarkType(landmark_type='nature'), + score = 0), + shopping=Preference( + name='shopping', + type=LandmarkType(landmark_type='shopping'), + score = 5)) + + landmarks = generate_landmarks(city_country, preferences) + + max_steps = 9 + + visiting_order = solve_optimization(landmarks, max_steps, True) + + print(len(visiting_order)) + + return len(visiting_order) + + +def test4(coordinates: tuple[float, float]) -> List[Landmark]: + + + preferences = Preferences( + sightseeing=Preference( + name='sightseeing', + type=LandmarkType(landmark_type='sightseeing'), + score = 5), + nature=Preference( + name='nature', + type=LandmarkType(landmark_type='nature'), + score = 0), + shopping=Preference( + name='shopping', + type=LandmarkType(landmark_type='shopping'), + score = 5)) + + landmarks = generate_landmarks(coordinates, preferences) + + max_steps = 90 + + visiting_order = solve_optimization(landmarks, max_steps, True) + + print(len(visiting_order)) + + return len(visiting_order) + + +test3(tuple((48.834378, 2.322113))) \ No newline at end of file