From ef26b882b163dbf2774419091d05692243d4be3c Mon Sep 17 00:00:00 2001 From: Helldragon67 Date: Tue, 3 Dec 2024 15:05:27 +0100 Subject: [PATCH] simplified the bbox creation --- backend/src/main.py | 2 +- backend/src/sandbox/get_streets.py | 163 +++++++++++++++++++---- backend/src/structs/shopping_location.py | 23 ++++ backend/src/utils/cluster_processing.py | 0 backend/src/utils/get_time_separation.py | 32 +++++ backend/src/utils/landmarks_manager.py | 47 +++---- 6 files changed, 213 insertions(+), 54 deletions(-) create mode 100644 backend/src/structs/shopping_location.py create mode 100644 backend/src/utils/cluster_processing.py diff --git a/backend/src/main.py b/backend/src/main.py index c6117cb..11e3039 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -47,7 +47,7 @@ def new_trip(preferences: Preferences, 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=423, + raise HTTPException(status_code=422, detail="Start coordinates not in range") if end is None: end = start diff --git a/backend/src/sandbox/get_streets.py b/backend/src/sandbox/get_streets.py index c10efe7..50e61ce 100644 --- a/backend/src/sandbox/get_streets.py +++ b/backend/src/sandbox/get_streets.py @@ -6,8 +6,11 @@ from sklearn.cluster import DBSCAN from sklearn.decomposition import PCA import matplotlib.pyplot as plt from pydantic import BaseModel +from OSMPythonTools.overpass import Overpass, overpassQueryBuilder +from math import sin, cos, sqrt, atan2, radians +EARTH_RADIUS_KM = 6373 class ShoppingLocation(BaseModel): type: Literal['street', 'area'] @@ -15,10 +18,34 @@ class ShoppingLocation(BaseModel): centroid: tuple start: Optional[list] = None end: Optional[list] = None - end: Optional[tuple] = None +# Output to frontend +class Landmark(BaseModel) : + # Properties of the landmark + name : str + type: Literal['sightseeing', 'nature', 'shopping', 'start', 'finish'] + location : tuple + osm_type : str + osm_id : int + attractiveness : int + n_tags : int + image_url : Optional[str] = None + website_url : Optional[str] = None + description : Optional[str] = None # TODO future + duration : Optional[int] = 0 + name_en : Optional[str] = None + + # Additional properties depending on specific tour + must_do : Optional[bool] = False + must_avoid : Optional[bool] = False + is_secondary : Optional[bool] = False + + time_to_reach_next : Optional[int] = 0 + next_uuid : Optional[str] = None + + def extract_points(filestr: str) : """ Extract points from geojson file. @@ -44,6 +71,37 @@ def extract_points(filestr: str) : return np.array(points) +def get_distance(p1: tuple[float, float], p2: tuple[float, float]) -> int: + """ + Calculate the time in minutes to travel from one location to another. + + Args: + p1 (Tuple[float, float]): Coordinates of the starting location. + p2 (Tuple[float, float]): Coordinates of the destination. + + Returns: + int: Time to travel from p1 to p2 in minutes. + """ + + + if p1 == p2: + return 0 + else: + # Compute the distance in km along the surface of the Earth + # (assume spherical Earth) + # this is the haversine formula, stolen from stackoverflow + # in order to not use any external libraries + lat1, lon1 = radians(p1[0]), radians(p1[1]) + lat2, lon2 = radians(p2[0]), radians(p2[1]) + + dlon = lon2 - lon1 + dlat = lat2 - lat1 + + a = sin(dlat / 2)**2 + cos(lat1) * cos(lat2) * sin(dlon / 2)**2 + c = 2 * atan2(sqrt(a), sqrt(1 - a)) + + return EARTH_RADIUS_KM * c + def filter_clusters(cluster_points, cluster_labels): """ Remove clusters of less importance. @@ -113,16 +171,16 @@ def fit_lines(points, labels): if np.linalg.norm(t) <= 0.0045 : loc = ShoppingLocation( type='area', - centroid=tuple(centroid), - importance = len(cluster_points) + centroid=tuple((centroid[1], centroid[0])), + importance = len(cluster_points), ) else : loc = ShoppingLocation( type='street', centroid=tuple(centroid), + importance = len(cluster_points), start=start_point, - end=end_point, - importance = len(cluster_points) + end=end_point ) locations.append(loc) @@ -137,16 +195,73 @@ def fit_lines(points, labels): +def create_landmark(shopping_location: ShoppingLocation): + + # Define the bounding box for a given radius around the coordinates + lat, lon = shopping_location.centroid + bbox = ("around:500", str(lat), str(lon)) + + overpass = Overpass() + + # Query neighborhoods and shopping malls + query = overpassQueryBuilder( + bbox = bbox, + elementType = ['node'], + selector = ['"place"="suburb"'], + includeCenter = True, + out = 'body' + ) + + try: + result = overpass.query(query) + print(f'query OK with {len(result.elements())} elements') + except Exception as e: + raise Exception("query unsuccessful") + + min_dist = float('inf') + new_name = 'Shopping Area' + new_name_en = None + osm_id = 0 + osm_type = 'node' + + for elem in result.elements(): + location = (elem.centerLat(), elem.centerLon()) + print(f"Distance : {get_distance(shopping_location.centroid, location)}") + if get_distance(shopping_location.centroid, location) < min_dist : + new_name = elem.tag('name') + osm_type = elem.type() # Add type: 'way' or 'relation' + osm_id = elem.id() # Add OSM id + + print("closer thing found") + + # add english name if it exists + try : + new_name_en = elem.tag('name:en') + except: + pass + + return Landmark( + name=new_name, + type='shopping', + location=shopping_location.centroid, # TODO: use the fact the we can also recognize streets. + attractiveness=shopping_location.importance, + n_tags=0, + osm_id=osm_id, + osm_type=osm_type, + name_en=new_name_en + ) + + # Extract points -points = extract_points('strasbourg_data.json') +points = extract_points('lyon_data.json') -# Create a figure with 1 row and 3 columns for side-by-side plots +######## Create a figure with 1 row and 3 columns for side-by-side plots fig, axes = plt.subplots(1, 3, figsize=(15, 5)) - -# Plot 0: Raw data points +# Plot Raw data points axes[0].set_title('Raw Data') axes[0].scatter(points[:, 0], points[:, 1], color='blue', s=20) + # Apply DBSCAN to find clusters. Choose different settings for different cities. if len(points) > 400 : dbscan = DBSCAN(eps=0.00118, min_samples=15, algorithm='kd_tree') # for large cities @@ -160,40 +275,41 @@ clustered_points = points[labels != -1] clustered_labels = labels[labels != -1] noise_points = points[labels == -1] -# Plot n°1: DBSCAN Clustering Results +######## Plot n°1: DBSCAN Clustering Results axes[1].set_title('DBSCAN Clusters') axes[1].scatter(clustered_points[:, 0], clustered_points[:, 1], c=clustered_labels, cmap='rainbow', s=20) axes[1].scatter(noise_points[:, 0], noise_points[:, 1], c='blue', s=7, label='Noise') clustered_points, clustered_labels = filter_clusters(clustered_points, clustered_labels) - - # Fit lines corners, locations = fit_lines(clustered_points, clustered_labels) (xmin, xmax, ymin, ymax) = corners -# Plot clustered points in normal size and noise points separately +######## Plot clustered points in normal size and noise points separately axes[2].scatter(clustered_points[:, 0], clustered_points[:, 1], c=clustered_labels, cmap='rainbow', s=30) -# axes[2].scatter(noise_points[:, 0], noise_points[:, 1], c='blue', s=10, label='Noise') -# Step 4: Plot the detected lines in the final plot + +# Create a list of Landmarks for the shopping things +shopping_landmarks = [] +for loc in locations : + landmark = create_landmark(loc) + shopping_landmarks.append(landmark) + print(f"{landmark.name} is a shopping area with a score of {landmark.attractiveness}") + + +####### Plot the detected lines in the final plot ####### for loc in locations: - if loc.type == 'street' : line_x = loc.start line_y = loc.end axes[2].plot(line_x, line_y, color='lime', linewidth=3) else : axes[2].scatter(loc.centroid[0], loc.centroid[1], color='None', edgecolors='lime', s=200, linewidth=3) - # print(8) axes[2].set_title('PCA Fitted Lines on Clusters') - -# print(all_x) -# Adjust the axis limit for previous plots axes[0].set_xlim(xmin-0.01, xmax+0.01) axes[0].set_ylim(ymin-0.01, ymax+0.01) @@ -203,8 +319,5 @@ axes[1].set_ylim(ymin-0.01, ymax+0.01) axes[2].set_xlim(xmin-0.01, xmax+0.01) axes[2].set_ylim(ymin-0.01, ymax+0.01) -# Adjust layout for better spacing -plt.tight_layout() - -# Show the plots -plt.show() +# plt.tight_layout() +# plt.show() diff --git a/backend/src/structs/shopping_location.py b/backend/src/structs/shopping_location.py new file mode 100644 index 0000000..d784ca4 --- /dev/null +++ b/backend/src/structs/shopping_location.py @@ -0,0 +1,23 @@ +from typing import Literal, Optional +from pydantic import BaseModel + + +class ShoppingLocation(BaseModel): + """" + A classe representing an interesting area for shopping. + + It can represent either a general area or a specifc route with start and end point. + The importance represents the number of shops found in this cluster. + + Attributes: + type : either a 'street' or 'area' (representing a denser field of shops). + importance : size of the cluster (number of points). + centroid : center of the cluster. + start : if the type is a street it goes from here... + end : ...to here + """ + type: Literal['street', 'area'] + importance: int + centroid: tuple + start: Optional[list] = None + end: Optional[list] = None \ No newline at end of file diff --git a/backend/src/utils/cluster_processing.py b/backend/src/utils/cluster_processing.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/utils/get_time_separation.py b/backend/src/utils/get_time_separation.py index aae848c..210c28d 100644 --- a/backend/src/utils/get_time_separation.py +++ b/backend/src/utils/get_time_separation.py @@ -48,3 +48,35 @@ def get_time(p1: tuple[float, float], p2: tuple[float, float]) -> int: walk_time = walk_distance / AVERAGE_WALKING_SPEED * 60 return round(walk_time) + + +def get_distance(p1: tuple[float, float], p2: tuple[float, float]) -> int: + """ + Calculate the time in minutes to travel from one location to another. + + Args: + p1 (Tuple[float, float]): Coordinates of the starting location. + p2 (Tuple[float, float]): Coordinates of the destination. + + Returns: + int: Time to travel from p1 to p2 in minutes. + """ + + + if p1 == p2: + return 0 + else: + # Compute the distance in km along the surface of the Earth + # (assume spherical Earth) + # this is the haversine formula, stolen from stackoverflow + # in order to not use any external libraries + lat1, lon1 = radians(p1[0]), radians(p1[1]) + lat2, lon2 = radians(p2[0]), radians(p2[1]) + + dlon = lon2 - lon1 + dlat = lat2 - lat1 + + a = sin(dlat / 2)**2 + cos(lat1) * cos(lat2) * sin(dlon / 2)**2 + c = 2 * atan2(sqrt(a), sqrt(1 - a)) + + return EARTH_RADIUS_KM * c \ No newline at end of file diff --git a/backend/src/utils/landmarks_manager.py b/backend/src/utils/landmarks_manager.py index e6ccf57..4e2872e 100644 --- a/backend/src/utils/landmarks_manager.py +++ b/backend/src/utils/landmarks_manager.py @@ -79,7 +79,8 @@ class LandmarkManager: # use set to avoid duplicates, this requires some __methods__ to be set in Landmark all_landmarks = set() - bbox = self.create_bbox(center_coordinates, reachable_bbox_side) + # Create a bbox using the around + bbox = tuple((f"around:{reachable_bbox_side/2}", str(center_coordinates[0]), str(center_coordinates[1]))) # list for sightseeing if preferences.sightseeing.score != 0: score_function = lambda score: score * 10 * preferences.sightseeing.score / 5 @@ -97,7 +98,7 @@ class LandmarkManager: score_function = lambda score: score * 10 * preferences.shopping.score / 5 current_landmarks = self.fetch_landmarks(bbox, self.amenity_selectors['shopping'], preferences.shopping.type, score_function) # set time for all shopping activites : - for landmark in current_landmarks : landmark.duration = 45 + for landmark in current_landmarks : landmark.duration = 30 all_landmarks.update(current_landmarks) @@ -151,36 +152,24 @@ class LandmarkManager: return 0 - def create_bbox(self, coordinates: tuple[float, float], reachable_bbox_side: int) -> tuple[float, float, float, float]: - """ - Create a bounding box around the given coordinates. + # def create_bbox(self, coordinates: tuple[float, float], reachable_bbox_side: int) -> tuple[float, float, float, float]: + # """ + # Create a bounding box around the given coordinates. - Args: - coordinates (tuple[float, float]): The latitude and longitude of the center of the bounding box. - reachable_bbox_side (int): The side length of the bounding box in meters. + # Args: + # coordinates (tuple[float, float]): The latitude and longitude of the center of the bounding box. + # reachable_bbox_side (int): The side length of the bounding box in meters. - Returns: - tuple[float, float, float, float]: The minimum latitude, minimum longitude, maximum latitude, and maximum longitude - defining the bounding box. - """ - - lat = coordinates[0] - lon = coordinates[1] + # Returns: + # tuple[float, float, float, float]: The minimum latitude, minimum longitude, maximum latitude, and maximum longitude + # defining the bounding box. + # """ - # Half the side length in km (since it's a square bbox) - half_side_length_km = reachable_bbox_side / 2 / 1000 + # # Half the side length in m (since it's a square bbox) + # half_side_length_m = reachable_bbox_side / 2 - # 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 * math.cos(math.radians(lat))) # Adjust for longitude based on latitude + # return tuple((f"around:{half_side_length_m}", str(coordinates[0]), str(coordinates[1]))) - # 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 def fetch_landmarks(self, bbox: tuple, amenity_selector: dict, landmarktype: str, score_function: callable) -> list[Landmark]: @@ -212,7 +201,9 @@ class LandmarkManager: for sel in dict_to_selector_list(amenity_selector): self.logger.debug(f"Current selector: {sel}") - query_conditions = ['count_tags()>5'] + # query_conditions = ['count_tags()>5'] + # if landmarktype == 'shopping' : # use this later for shopping clusters + # element_types = ['node'] element_types = ['way', 'relation'] if 'viewpoint' in sel :