Adding features to find public toilets and shopping streets #41
| @@ -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 | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
							
								
								
									
										23
									
								
								backend/src/structs/shopping_location.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								backend/src/structs/shopping_location.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										0
									
								
								backend/src/utils/cluster_processing.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								backend/src/utils/cluster_processing.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| @@ -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. | ||||
|         """ | ||||
|     #     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] | ||||
|     #     # Half the side length in m (since it's a square bbox) | ||||
|     #     half_side_length_m = reachable_bbox_side / 2 | ||||
|  | ||||
|         # Half the side length in km (since it's a square bbox) | ||||
|         half_side_length_km = reachable_bbox_side / 2 / 1000 | ||||
|     #     return tuple((f"around:{half_side_length_m}", str(coordinates[0]), str(coordinates[1]))) | ||||
|  | ||||
|         # 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 | ||||
|  | ||||
|         # 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 : | ||||
|   | ||||
		Reference in New Issue
	
	Block a user