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
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:
parent
06d2f4c8aa
commit
ef26b882b1
@ -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
|
||||||
|
@ -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 :
|
||||||
|
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:
|
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()
|
|
||||||
|
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
|
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
|
@ -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 :
|
||||||
|
Loading…
x
Reference in New Issue
Block a user