|  |  |  | @@ -11,13 +11,28 @@ from ..structs.landmark import Landmark | 
		
	
		
			
				|  |  |  |  | from ..utils.get_time_separation import get_distance | 
		
	
		
			
				|  |  |  |  | from ..constants import AMENITY_SELECTORS_PATH, LANDMARK_PARAMETERS_PATH, OPTIMIZER_PARAMETERS_PATH, OSM_CACHE_DIR | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  | 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      # for later use if we want to have streets as well | 
		
	
		
			
				|  |  |  |  |     # end: Optional[list] = None | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  | class ShoppingManager: | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |     logger = logging.getLogger(__name__) | 
		
	
	
		
			
				
					
					|  |  |  | @@ -31,7 +46,11 @@ class ShoppingManager: | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |     def __init__(self, bbox: tuple) -> None: | 
		
	
		
			
				|  |  |  |  |         """ | 
		
	
		
			
				|  |  |  |  |         Upon intialization, generate the list of shops used for cluster points. | 
		
	
		
			
				|  |  |  |  |         Upon intialization, generate the point cloud used for cluster detection. | 
		
	
		
			
				|  |  |  |  |         The points represent bag/clothes shops and general boutiques. | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |         Args:  | 
		
	
		
			
				|  |  |  |  |             bbox: The bounding box coordinates (around:radius, center_lat, center_lon). | 
		
	
		
			
				|  |  |  |  |         """ | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |         # Initialize overpass and cache | 
		
	
	
		
			
				
					
					|  |  |  | @@ -52,8 +71,10 @@ class ShoppingManager: | 
		
	
		
			
				|  |  |  |  |         except Exception as e: | 
		
	
		
			
				|  |  |  |  |             self.logger.error(f"Error fetching landmarks: {e}") | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |         if len(result.elements()) > 0 : | 
		
	
		
			
				|  |  |  |  |         if len(result.elements()) == 0 : | 
		
	
		
			
				|  |  |  |  |             self.valid = False | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         else : | 
		
	
		
			
				|  |  |  |  |             points = [] | 
		
	
		
			
				|  |  |  |  |             for elem in result.elements() : | 
		
	
		
			
				|  |  |  |  |                 points.append(tuple((elem.lat(), elem.lon()))) | 
		
	
	
		
			
				
					
					|  |  |  | @@ -61,18 +82,23 @@ class ShoppingManager: | 
		
	
		
			
				|  |  |  |  |             self.all_points = np.array(points) | 
		
	
		
			
				|  |  |  |  |             self.valid = True             | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |         else :  | 
		
	
		
			
				|  |  |  |  |             self.valid = False | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |     def generate_shopping_landmarks(self) -> list[Landmark]: | 
		
	
		
			
				|  |  |  |  |         """ | 
		
	
		
			
				|  |  |  |  |         Generate shopping landmarks based on clustered locations. | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |         This method first generates clusters of locations and then  extracts shopping-related  | 
		
	
		
			
				|  |  |  |  |         locations from these clusters. It transforms each shopping location into a `Landmark` object. | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |         Returns: | 
		
	
		
			
				|  |  |  |  |             list[Landmark]: A list of `Landmark` objects representing shopping locations. | 
		
	
		
			
				|  |  |  |  |                             Returns an empty list if no clusters are found. | 
		
	
		
			
				|  |  |  |  |         """ | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |         # First generate the clusters | 
		
	
		
			
				|  |  |  |  |         self.generate_clusters() | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |         # Return empty list if no clusters were found | 
		
	
		
			
				|  |  |  |  |         if len(set(self.cluster_labels)) == 0 : | 
		
	
		
			
				|  |  |  |  |             return [] | 
		
	
		
			
				|  |  |  |  |             return []       # Return empty list if no clusters were found | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |         # Then generate the shopping locations | 
		
	
		
			
				|  |  |  |  |         self.generate_shopping_locations() | 
		
	
	
		
			
				
					
					|  |  |  | @@ -87,6 +113,19 @@ class ShoppingManager: | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |     def generate_clusters(self) : | 
		
	
		
			
				|  |  |  |  |         """ | 
		
	
		
			
				|  |  |  |  |         Generate clusters of points using DBSCAN. | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |         This method applies the DBSCAN clustering algorithm with different | 
		
	
		
			
				|  |  |  |  |         parameters depending on the size of the city (number of points).  | 
		
	
		
			
				|  |  |  |  |         It filters out noise points and keeps only the largest clusters. | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |         The method updates: | 
		
	
		
			
				|  |  |  |  |             - `self.cluster_points`: The points belonging to clusters. | 
		
	
		
			
				|  |  |  |  |             - `self.cluster_labels`: The labels for the points in clusters. | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         The method also calls `filter_clusters()` to retain only the largest clusters. | 
		
	
		
			
				|  |  |  |  |         """ | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |         # Apply DBSCAN to find clusters. Choose different settings for different cities. | 
		
	
		
			
				|  |  |  |  |         if len(self.all_points) > 200 : | 
		
	
	
		
			
				
					
					|  |  |  | @@ -105,6 +144,19 @@ class ShoppingManager: | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |     def generate_shopping_locations(self) : | 
		
	
		
			
				|  |  |  |  |         """ | 
		
	
		
			
				|  |  |  |  |         Generate shopping locations based on clustered points. | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |         This method iterates over the different clusters, calculates the centroid  | 
		
	
		
			
				|  |  |  |  |         (as the mean of the points within each cluster), and assigns an importance  | 
		
	
		
			
				|  |  |  |  |         based on the size of the cluster. | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |         The generated shopping locations are stored in `self.shopping_locations`  | 
		
	
		
			
				|  |  |  |  |         as a list of `ShoppingLocation` objects, each with: | 
		
	
		
			
				|  |  |  |  |             - `type`: Set to 'area'. | 
		
	
		
			
				|  |  |  |  |             - `centroid`: The calculated centroid of the cluster. | 
		
	
		
			
				|  |  |  |  |             - `importance`: The number of points in the cluster. | 
		
	
		
			
				|  |  |  |  |         """ | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |         locations = [] | 
		
	
		
			
				|  |  |  |  |  | 
		
	
	
		
			
				
					
					|  |  |  | @@ -127,6 +179,21 @@ class ShoppingManager: | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |     def create_landmark(self, shopping_location: ShoppingLocation) -> Landmark: | 
		
	
		
			
				|  |  |  |  |         """ | 
		
	
		
			
				|  |  |  |  |         Create a Landmark object based on the given shopping location. | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |         This method queries the Overpass API for nearby neighborhoods and shopping malls  | 
		
	
		
			
				|  |  |  |  |         within a 1000m radius around the shopping location centroid. It selects the closest  | 
		
	
		
			
				|  |  |  |  |         result and creates a landmark with the associated details such as name, type, and OSM ID. | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |         Parameters: | 
		
	
		
			
				|  |  |  |  |             shopping_location (ShoppingLocation): A ShoppingLocation object containing  | 
		
	
		
			
				|  |  |  |  |             the centroid and importance of the area. | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |         Returns: | 
		
	
		
			
				|  |  |  |  |             Landmark: A Landmark object containing details such as the name, type,  | 
		
	
		
			
				|  |  |  |  |             location, attractiveness, and OSM details. | 
		
	
		
			
				|  |  |  |  |         """ | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |         # Define the bounding box for a given radius around the coordinates | 
		
	
		
			
				|  |  |  |  |         lat, lon = shopping_location.centroid | 
		
	
	
		
			
				
					
					|  |  |  | @@ -153,10 +220,10 @@ class ShoppingManager: | 
		
	
		
			
				|  |  |  |  |             try: | 
		
	
		
			
				|  |  |  |  |                 result = self.overpass.query(query) | 
		
	
		
			
				|  |  |  |  |             except Exception as e: | 
		
	
		
			
				|  |  |  |  |                 raise Exception("query unsuccessful") | 
		
	
		
			
				|  |  |  |  |                 self.logger.error(f"Error fetching landmarks: {e}") | 
		
	
		
			
				|  |  |  |  |                 continue | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |             for elem in result.elements(): | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |                 location = (elem.centerLat(), elem.centerLon()) | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |                 if location[0] is None :  | 
		
	
	
		
			
				
					
					|  |  |  | @@ -171,7 +238,7 @@ class ShoppingManager: | 
		
	
		
			
				|  |  |  |  |                     osm_type = elem.type()      # Add type: 'way' or 'relation' | 
		
	
		
			
				|  |  |  |  |                     osm_id = elem.id()          # Add OSM id  | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |                     # add english name if it exists | 
		
	
		
			
				|  |  |  |  |                     # Add english name if it exists | 
		
	
		
			
				|  |  |  |  |                     try : | 
		
	
		
			
				|  |  |  |  |                         new_name_en = elem.tag('name:en') | 
		
	
		
			
				|  |  |  |  |                     except: | 
		
	
	
		
			
				
					
					|  |  |  | @@ -191,7 +258,11 @@ class ShoppingManager: | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |     def filter_clusters(self): | 
		
	
		
			
				|  |  |  |  |         """ | 
		
	
		
			
				|  |  |  |  |         Remove clusters of lesser importance. | 
		
	
		
			
				|  |  |  |  |         Filter clusters to retain only the 5 largest clusters by point count. | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |         This method calculates the size of each cluster and filters out all but the  | 
		
	
		
			
				|  |  |  |  |         5 largest clusters. It then updates the cluster points and labels to reflect  | 
		
	
		
			
				|  |  |  |  |         only those from the top 5 clusters. | 
		
	
		
			
				|  |  |  |  |         """ | 
		
	
		
			
				|  |  |  |  |         label_counts = np.bincount(self.cluster_labels) | 
		
	
		
			
				|  |  |  |  |  | 
		
	
	
		
			
				
					
					|  |  |  |   |