simplified the bbox creation
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Failing after 2m8s
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 29s
Run testing on the backend code / Build (pull_request) Failing after 1m53s

This commit is contained in:
Helldragon67 2024-12-03 15:05:27 +01:00
parent 06d2f4c8aa
commit ef26b882b1
6 changed files with 213 additions and 54 deletions

View File

@ -47,7 +47,7 @@ def new_trip(preferences: Preferences,
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=423, 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

View File

@ -6,8 +6,11 @@ from sklearn.cluster import DBSCAN
from sklearn.decomposition import PCA from sklearn.decomposition import PCA
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
from pydantic import BaseModel 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): class ShoppingLocation(BaseModel):
type: Literal['street', 'area'] type: Literal['street', 'area']
@ -15,10 +18,34 @@ class ShoppingLocation(BaseModel):
centroid: tuple centroid: tuple
start: Optional[list] = None start: Optional[list] = None
end: 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) : def extract_points(filestr: str) :
""" """
Extract points from geojson file. Extract points from geojson file.
@ -44,6 +71,37 @@ def extract_points(filestr: str) :
return np.array(points) 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): def filter_clusters(cluster_points, cluster_labels):
""" """
Remove clusters of less importance. Remove clusters of less importance.
@ -113,16 +171,16 @@ def fit_lines(points, labels):
if np.linalg.norm(t) <= 0.0045 : if np.linalg.norm(t) <= 0.0045 :
loc = ShoppingLocation( loc = ShoppingLocation(
type='area', type='area',
centroid=tuple(centroid), centroid=tuple((centroid[1], centroid[0])),
importance = len(cluster_points) importance = len(cluster_points),
) )
else : else :
loc = ShoppingLocation( loc = ShoppingLocation(
type='street', type='street',
centroid=tuple(centroid), centroid=tuple(centroid),
importance = len(cluster_points),
start=start_point, start=start_point,
end=end_point, end=end_point
importance = len(cluster_points)
) )
locations.append(loc) 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 # 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)) fig, axes = plt.subplots(1, 3, figsize=(15, 5))
# Plot Raw data points
# Plot 0: Raw data points
axes[0].set_title('Raw Data') axes[0].set_title('Raw Data')
axes[0].scatter(points[:, 0], points[:, 1], color='blue', s=20) axes[0].scatter(points[:, 0], points[:, 1], color='blue', s=20)
# Apply DBSCAN to find clusters. Choose different settings for different cities. # Apply DBSCAN to find clusters. Choose different settings for different cities.
if len(points) > 400 : if len(points) > 400 :
dbscan = DBSCAN(eps=0.00118, min_samples=15, algorithm='kd_tree') # for large cities 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] clustered_labels = labels[labels != -1]
noise_points = points[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].set_title('DBSCAN Clusters')
axes[1].scatter(clustered_points[:, 0], clustered_points[:, 1], c=clustered_labels, cmap='rainbow', s=20) 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') 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) clustered_points, clustered_labels = filter_clusters(clustered_points, clustered_labels)
# Fit lines # Fit lines
corners, locations = fit_lines(clustered_points, clustered_labels) corners, locations = fit_lines(clustered_points, clustered_labels)
(xmin, xmax, ymin, ymax) = corners (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(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 : 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' : if loc.type == 'street' :
line_x = loc.start line_x = loc.start
line_y = loc.end line_y = loc.end
axes[2].plot(line_x, line_y, color='lime', linewidth=3) axes[2].plot(line_x, line_y, color='lime', linewidth=3)
else : else :
axes[2].scatter(loc.centroid[0], loc.centroid[1], color='None', edgecolors='lime', s=200, linewidth=3) 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') 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_xlim(xmin-0.01, xmax+0.01)
axes[0].set_ylim(ymin-0.01, ymax+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_xlim(xmin-0.01, xmax+0.01)
axes[2].set_ylim(ymin-0.01, ymax+0.01) axes[2].set_ylim(ymin-0.01, ymax+0.01)
# Adjust layout for better spacing # plt.tight_layout()
plt.tight_layout() # plt.show()
# Show the plots
plt.show()

View 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

View File

View 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 walk_time = walk_distance / AVERAGE_WALKING_SPEED * 60
return round(walk_time) 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

View File

@ -79,7 +79,8 @@ class LandmarkManager:
# use set to avoid duplicates, this requires some __methods__ to be set in Landmark # use set to avoid duplicates, this requires some __methods__ to be set in Landmark
all_landmarks = set() 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 # 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
@ -97,7 +98,7 @@ class LandmarkManager:
score_function = lambda score: score * 10 * preferences.shopping.score / 5 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) current_landmarks = self.fetch_landmarks(bbox, self.amenity_selectors['shopping'], preferences.shopping.type, score_function)
# set time for all shopping activites : # 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) all_landmarks.update(current_landmarks)
@ -151,36 +152,24 @@ class LandmarkManager:
return 0 return 0
def create_bbox(self, coordinates: tuple[float, float], reachable_bbox_side: int) -> tuple[float, float, float, float]: # 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. # Create a bounding box around the given coordinates.
Args: # Args:
coordinates (tuple[float, float]): The latitude and longitude of the center of the bounding box. # 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. # reachable_bbox_side (int): The side length of the bounding box in meters.
Returns: # Returns:
tuple[float, float, float, float]: The minimum latitude, minimum longitude, maximum latitude, and maximum longitude # tuple[float, float, float, float]: The minimum latitude, minimum longitude, maximum latitude, and maximum longitude
defining the bounding box. # defining the bounding box.
""" # """
lat = coordinates[0] # # Half the side length in m (since it's a square bbox)
lon = coordinates[1] # half_side_length_m = reachable_bbox_side / 2
# Half the side length in km (since it's a square bbox) # return tuple((f"around:{half_side_length_m}", str(coordinates[0]), str(coordinates[1])))
half_side_length_km = reachable_bbox_side / 2 / 1000
# 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]: 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): for sel in dict_to_selector_list(amenity_selector):
self.logger.debug(f"Current selector: {sel}") 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'] element_types = ['way', 'relation']
if 'viewpoint' in sel : if 'viewpoint' in sel :