refactored landmark manager and clean up
This commit is contained in:
@@ -14,6 +14,6 @@ OSM_CACHE_DIR = Path(cache_dir_string)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.basicConfig(
|
||||
level = logging.INFO,
|
||||
level = logging.DEBUG,
|
||||
format = '%(asctime)s - %(name)s\t- %(levelname)s\t- %(message)s'
|
||||
)
|
||||
|
@@ -1,151 +0,0 @@
|
||||
import yaml
|
||||
import logging
|
||||
from OSMPythonTools import cachingStrategy, overpass
|
||||
from structs.landmarks import Landmark, LandmarkType
|
||||
from structs.preferences import Preferences, Preference
|
||||
import constants
|
||||
|
||||
SIGHTSEEING = LandmarkType(landmark_type='sightseeing')
|
||||
NATURE = LandmarkType(landmark_type='nature')
|
||||
SHOPPING = LandmarkType(landmark_type='shopping')
|
||||
|
||||
|
||||
class LandmarkManager:
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def __init__(self) -> None:
|
||||
strategy = cachingStrategy.JSON(cacheDir=constants.OSM_CACHE_DIR)
|
||||
self.query_builder = overpass.Overpass()
|
||||
with constants.AMENITY_SELECTORS_PATH.open('r') as f:
|
||||
self.amenity_selectors = yaml.safe_load(f)
|
||||
with constants.LANDMARK_PARAMETERS_PATH.open('r') as f:
|
||||
self.parameters = yaml.safe_load(f)
|
||||
# max_distance = parameters['city_bbox_side']
|
||||
|
||||
|
||||
def get_landmark_lists(self, preferences: Preferences, center_coordinates: tuple[float, float]) -> tuple[list[Landmark], list[Landmark]]:
|
||||
'''
|
||||
Generate a list of landmarks based on the preferences of the user and the center (ie. start) coordinates.
|
||||
The list is then used by the pathfinding algorithm to generate a path that goes through the most interesting landmarks.
|
||||
:param preferences: the preferences specified by the user
|
||||
:param center_coordinates: the coordinates of the starting point
|
||||
'''
|
||||
|
||||
L = []
|
||||
|
||||
# List for sightseeing
|
||||
if preferences.sightseeing.score != 0:
|
||||
score_func = lambda loc, n_tags: int((10 + n_tags * self.parameters['tag_coeff']) * self.parameters['church_coeff'])
|
||||
L1 = self.fetch_landmarks(self.amenity_selectors['sightseeing'], SIGHTSEEING, center_coordinates, self.parameters['city_bbox_side'], score_func)
|
||||
self.correct_score(L1, preferences.sightseeing)
|
||||
L += L1
|
||||
|
||||
# List for nature
|
||||
if preferences.nature.score != 0:
|
||||
score_func = lambda loc, n_tags: int((10 + n_tags * self.parameters['tag_coeff']) * self.parameters['park_coeff'])
|
||||
L2 = self.fetch_landmarks(self.amenity_selectors['nature'], NATURE, center_coordinates, self.parameters['city_bbox_side'], score_func)
|
||||
self.correct_score(L2, preferences.nature)
|
||||
L += L2
|
||||
|
||||
# List for shopping
|
||||
if preferences.shopping.score != 0:
|
||||
score_func = lambda loc, n_tags: int((10 + n_tags * self.parameters['tag_coeff']))
|
||||
L3 = self.fetch_landmarks(self.amenity_selectors['shopping'], SHOPPING, center_coordinates, self.parameters['city_bbox_side'], score_func)
|
||||
self.correct_score(L3, preferences.shopping)
|
||||
L += L3
|
||||
|
||||
# remove duplicates
|
||||
L = list(set(L))
|
||||
L_constrained = self.take_most_important(L, self.parameters['N_important'])
|
||||
self.logger.info(f'Generated {len(L)} landmarks around {center_coordinates}, and constrained to {len(L_constrained)} most important ones.')
|
||||
return L, L_constrained
|
||||
|
||||
|
||||
|
||||
# Take the most important landmarks from the list
|
||||
def take_most_important(self, landmarks: list[Landmark], n_max: int) -> list[Landmark]:
|
||||
|
||||
landmarks_sorted = sorted(landmarks, key=lambda x: x.attractiveness, reverse=True)
|
||||
return landmarks_sorted[:n_max]
|
||||
|
||||
|
||||
|
||||
# Correct the score of a list of landmarks by taking into account preference settings
|
||||
def correct_score(self, L: list[Landmark], preference: Preference):
|
||||
|
||||
if len(L) == 0 :
|
||||
return
|
||||
|
||||
if L[0].type != preference.type :
|
||||
raise TypeError(f"LandmarkType {preference.type} does not match the type of Landmark {L[0].name}")
|
||||
|
||||
for elem in L :
|
||||
elem.attractiveness = int(elem.attractiveness*preference.score/500) # arbitrary computation
|
||||
|
||||
|
||||
# Function to count elements within a certain radius of a location
|
||||
def count_elements_within_radius(self, point: Point, radius: int) -> int:
|
||||
|
||||
center_coordinates = (point.x, point.y)
|
||||
try:
|
||||
landmarks = ox.features_from_point(
|
||||
center_point = center_coordinates,
|
||||
dist = radius,
|
||||
tags = {'building': True} # this is a common tag to give an estimation of the number of elements in the area
|
||||
)
|
||||
return len(landmarks)
|
||||
except ox._errors.InsufficientResponseError:
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
|
||||
def fetch_landmarks(
|
||||
self,
|
||||
amenity_selectors: list[dict],
|
||||
landmarktype: LandmarkType,
|
||||
center_coordinates: tuple[float, float],
|
||||
distance: int,
|
||||
score_function: callable
|
||||
) -> list[Landmark]:
|
||||
|
||||
landmarks = ox.features_from_point(
|
||||
center_point = center_coordinates,
|
||||
dist = distance,
|
||||
tags = amenity_selectors
|
||||
)
|
||||
self.logger.info(f'Fetched {len(landmarks)} landmarks around {center_coordinates}.')
|
||||
|
||||
# cleanup the list
|
||||
# remove rows where name is None
|
||||
landmarks = landmarks[landmarks['name'].notna()]
|
||||
# TODO: remove rows that are part of another building
|
||||
|
||||
ret_landmarks = []
|
||||
for element, description in landmarks.iterrows():
|
||||
osm_type = element[0]
|
||||
osm_id = element[1]
|
||||
location = description['geometry']
|
||||
n_tags = len(description['nodes']) if type(description['nodes']) == list else 1
|
||||
|
||||
if type(location) == Point:
|
||||
location = location
|
||||
elif type(location) == Polygon or type(location) == MultiPolygon:
|
||||
location = location.centroid
|
||||
elif type(location) == LineString:
|
||||
location = location.interpolate(location.length/2)
|
||||
|
||||
score = score_function(location, n_tags)
|
||||
landmark = Landmark(
|
||||
name = description['name'],
|
||||
type = landmarktype,
|
||||
location = (location.x, location.y),
|
||||
osm_type = osm_type,
|
||||
osm_id = osm_id,
|
||||
attractiveness = score,
|
||||
must_do = False,
|
||||
n_tags = n_tags
|
||||
)
|
||||
ret_landmarks.append(landmark)
|
||||
|
||||
return ret_landmarks
|
@@ -1,419 +0,0 @@
|
||||
import numpy as np
|
||||
import yaml
|
||||
|
||||
from typing import List, Tuple
|
||||
from scipy.optimize import linprog
|
||||
from math import radians, sin, cos, acos
|
||||
|
||||
from structs.landmarks import Landmark
|
||||
import constants
|
||||
|
||||
# Function to print the result
|
||||
def print_res(L: List[Landmark], L_tot):
|
||||
|
||||
if len(L) == L_tot:
|
||||
print('\nAll landmarks can be visited within max_steps, the following order is suggested : ')
|
||||
else :
|
||||
print('Could not visit all the landmarks, the following order is suggested : ')
|
||||
|
||||
dist = 0
|
||||
for elem in L :
|
||||
if elem.time_to_reach_next is not None :
|
||||
print('- ' + elem.name + ', time to reach next = ' + str(elem.time_to_reach_next))
|
||||
dist += elem.time_to_reach_next
|
||||
else :
|
||||
print('- ' + elem.name)
|
||||
|
||||
print("\nMinutes walked : " + str(dist))
|
||||
print(f"Visited {len(L)-2} out of {L_tot-2} landmarks")
|
||||
|
||||
|
||||
# Prevent the use of a particular solution
|
||||
def prevent_config(resx, A_ub, b_ub) -> bool:
|
||||
|
||||
for i, elem in enumerate(resx):
|
||||
resx[i] = round(elem)
|
||||
|
||||
N = len(resx) # Number of edges
|
||||
L = int(np.sqrt(N)) # Number of landmarks
|
||||
|
||||
nonzeroind = np.nonzero(resx)[0] # the return is a little funky so I use the [0]
|
||||
nonzero_tup = np.unravel_index(nonzeroind, (L,L))
|
||||
|
||||
ind_a = nonzero_tup[0].tolist()
|
||||
vertices_visited = ind_a
|
||||
vertices_visited.remove(0)
|
||||
|
||||
ones = [1]*L
|
||||
h = [0]*N
|
||||
for i in range(L) :
|
||||
if i in vertices_visited :
|
||||
h[i*L:i*L+L] = ones
|
||||
|
||||
A_ub = np.vstack((A_ub, h))
|
||||
b_ub.append(len(vertices_visited)-1)
|
||||
|
||||
return A_ub, b_ub
|
||||
|
||||
|
||||
# Prevent the possibility of a given solution bit
|
||||
def break_cricle(circle_vertices: list, L: int, A_ub: list, b_ub: list) -> bool:
|
||||
|
||||
if L-1 in circle_vertices :
|
||||
circle_vertices.remove(L-1)
|
||||
|
||||
h = [0]*L*L
|
||||
for i in range(L) :
|
||||
if i in circle_vertices :
|
||||
h[i*L:i*L+L] = [1]*L
|
||||
|
||||
A_ub = np.vstack((A_ub, h))
|
||||
b_ub.append(len(circle_vertices)-1)
|
||||
|
||||
return A_ub, b_ub
|
||||
|
||||
|
||||
# Checks if the path is connected, returns a circle if it finds one and the RESULT
|
||||
def is_connected(resx) -> bool:
|
||||
|
||||
# first round the results to have only 0-1 values
|
||||
for i, elem in enumerate(resx):
|
||||
resx[i] = round(elem)
|
||||
|
||||
N = len(resx) # length of res
|
||||
L = int(np.sqrt(N)) # number of landmarks. CAST INTO INT but should not be a problem because N = L**2 by def.
|
||||
n_edges = resx.sum() # number of edges
|
||||
|
||||
nonzeroind = np.nonzero(resx)[0] # the return is a little funny so I use the [0]
|
||||
|
||||
nonzero_tup = np.unravel_index(nonzeroind, (L,L))
|
||||
|
||||
ind_a = nonzero_tup[0].tolist()
|
||||
ind_b = nonzero_tup[1].tolist()
|
||||
|
||||
edges = []
|
||||
edges_visited = []
|
||||
vertices_visited = []
|
||||
|
||||
edge1 = (ind_a[0], ind_b[0])
|
||||
edges_visited.append(edge1)
|
||||
vertices_visited.append(edge1[0])
|
||||
|
||||
for i, a in enumerate(ind_a) :
|
||||
edges.append((a, ind_b[i])) # Create the list of edges
|
||||
|
||||
remaining = edges
|
||||
remaining.remove(edge1)
|
||||
|
||||
break_flag = False
|
||||
while len(remaining) > 0 and not break_flag:
|
||||
for edge2 in remaining :
|
||||
if edge2[0] == edge1[1] :
|
||||
if edge1[1] in vertices_visited :
|
||||
edges_visited.append(edge2)
|
||||
break_flag = True
|
||||
break
|
||||
else :
|
||||
vertices_visited.append(edge1[1])
|
||||
edges_visited.append(edge2)
|
||||
remaining.remove(edge2)
|
||||
edge1 = edge2
|
||||
|
||||
elif edge1[1] == L-1 or edge1[1] in vertices_visited:
|
||||
break_flag = True
|
||||
break
|
||||
|
||||
vertices_visited.append(edge1[1])
|
||||
|
||||
|
||||
if len(vertices_visited) == n_edges +1 :
|
||||
return vertices_visited, []
|
||||
else:
|
||||
return vertices_visited, edges_visited
|
||||
|
||||
|
||||
# Function that returns the distance in meters from one location to another
|
||||
def get_distance(p1: Tuple[float, float], p2: Tuple[float, float], detour: float, speed: float) :
|
||||
|
||||
# Compute the straight-line distance in km
|
||||
if p1 == p2 :
|
||||
return 0, 0
|
||||
else:
|
||||
dist = 6371.01 * acos(sin(radians(p1[0]))*sin(radians(p2[0])) + cos(radians(p1[0]))*cos(radians(p2[0]))*cos(radians(p1[1]) - radians(p2[1])))
|
||||
|
||||
# Consider the detour factor for average cityto deterline walking distance (in km)
|
||||
walk_dist = dist*detour
|
||||
|
||||
# Time to walk this distance (in minutes)
|
||||
walk_time = walk_dist/speed*60
|
||||
|
||||
if walk_time > 15 :
|
||||
walk_time = 5*round(walk_time/5)
|
||||
else :
|
||||
walk_time = round(walk_time)
|
||||
|
||||
|
||||
return round(walk_dist, 1), walk_time
|
||||
|
||||
|
||||
# Initialize A and c. Compute the distances from all landmarks to each other and store attractiveness
|
||||
# We want to maximize the sightseeing : max(c) st. A*x < b and A_eq*x = b_eq
|
||||
def init_ub_dist(landmarks: List[Landmark], max_steps: int):
|
||||
|
||||
# Read the parameters from the file
|
||||
with constants.OPTIMIZER_PARAMETERS_PATH.open('r') as f:
|
||||
parameters = yaml.safe_load(f)
|
||||
detour = parameters['detour_factor']
|
||||
speed = parameters['average_walking_speed']
|
||||
|
||||
# Objective function coefficients. a*x1 + b*x2 + c*x3 + ...
|
||||
c = []
|
||||
# Coefficients of inequality constraints (left-hand side)
|
||||
A_ub = []
|
||||
|
||||
for spot1 in landmarks :
|
||||
dist_table = [0]*len(landmarks)
|
||||
c.append(-spot1.attractiveness)
|
||||
for j, spot2 in enumerate(landmarks) :
|
||||
t = get_distance(spot1.location, spot2.location, detour, speed)[1]
|
||||
dist_table[j] = t
|
||||
A_ub += dist_table
|
||||
c = c*len(landmarks)
|
||||
|
||||
return c, A_ub, [max_steps]
|
||||
|
||||
|
||||
# Constraint to respect only one travel per landmark. Also caps the total number of visited landmarks
|
||||
def respect_number(L:int, A_ub, b_ub):
|
||||
|
||||
ones = [1]*L
|
||||
zeros = [0]*L
|
||||
for i in range(L) :
|
||||
h = zeros*i + ones + zeros*(L-1-i)
|
||||
A_ub = np.vstack((A_ub, h))
|
||||
b_ub.append(1)
|
||||
|
||||
# Read the parameters from the file
|
||||
with constants.OPTIMIZER_PARAMETERS_PATH.open('r') as f:
|
||||
parameters = yaml.safe_load(f)
|
||||
max_landmarks = parameters['max_landmarks']
|
||||
|
||||
A_ub = np.vstack((A_ub, ones*L))
|
||||
b_ub.append(max_landmarks+1)
|
||||
|
||||
return A_ub, b_ub
|
||||
|
||||
|
||||
# Constraint to not have d14 and d41 simultaneously. Does not prevent circular symmetry with more elements
|
||||
def break_sym(L, A_ub, b_ub):
|
||||
upper_ind = np.triu_indices(L,0,L)
|
||||
|
||||
up_ind_x = upper_ind[0]
|
||||
up_ind_y = upper_ind[1]
|
||||
|
||||
for i, _ in enumerate(up_ind_x) :
|
||||
l = [0]*L*L
|
||||
if up_ind_x[i] != up_ind_y[i] :
|
||||
l[up_ind_x[i]*L + up_ind_y[i]] = 1
|
||||
l[up_ind_y[i]*L + up_ind_x[i]] = 1
|
||||
|
||||
A_ub = np.vstack((A_ub,l))
|
||||
b_ub.append(1)
|
||||
|
||||
return A_ub, b_ub
|
||||
|
||||
|
||||
# Constraint to not stay in position. Removes d11, d22, d33, etc.
|
||||
def init_eq_not_stay(L: int):
|
||||
l = [0]*L*L
|
||||
|
||||
for i in range(L) :
|
||||
for j in range(L) :
|
||||
if j == i :
|
||||
l[j + i*L] = 1
|
||||
|
||||
l = np.array(np.array(l))
|
||||
|
||||
return [l], [0]
|
||||
|
||||
|
||||
# Go through the landmarks and force the optimizer to use landmarks where attractiveness is set to -1
|
||||
def respect_user_mustsee(landmarks: List[Landmark], A_eq: list, b_eq: list) :
|
||||
L = len(landmarks)
|
||||
|
||||
for i, elem in enumerate(landmarks) :
|
||||
if elem.must_do is True and elem.name not in ['finish', 'start']:
|
||||
l = [0]*L*L
|
||||
for j in range(L) : # sets the horizontal ones (go from)
|
||||
l[j +i*L] = 1 # sets the vertical ones (go to) double check if good
|
||||
|
||||
for k in range(L-1) :
|
||||
l[k*L+L-1] = 1
|
||||
|
||||
A_eq = np.vstack((A_eq,l))
|
||||
b_eq.append(2)
|
||||
|
||||
return A_eq, b_eq
|
||||
|
||||
|
||||
# Constraint to ensure start at start and finish at goal
|
||||
def respect_start_finish(L: int, A_eq: list, b_eq: list):
|
||||
ls = [1]*L + [0]*L*(L-1) # sets only horizontal ones for start (go from)
|
||||
ljump = [0]*L*L
|
||||
ljump[L-1] = 1 # Prevent start finish jump
|
||||
lg = [0]*L*L
|
||||
ll = [0]*L*(L-1) + [1]*L
|
||||
for k in range(L-1) : # sets only vertical ones for goal (go to)
|
||||
ll[k*L] = 1
|
||||
if k != 0 : # Prevent the shortcut start -> finish
|
||||
lg[k*L+L-1] = 1
|
||||
|
||||
|
||||
A_eq = np.vstack((A_eq,ls))
|
||||
A_eq = np.vstack((A_eq,ljump))
|
||||
A_eq = np.vstack((A_eq,lg))
|
||||
A_eq = np.vstack((A_eq,ll))
|
||||
b_eq.append(1)
|
||||
b_eq.append(0)
|
||||
b_eq.append(1)
|
||||
b_eq.append(0)
|
||||
|
||||
return A_eq, b_eq
|
||||
|
||||
|
||||
# Constraint to tie the problem together. Necessary but not sufficient to avoid circles
|
||||
def respect_order(N: int, A_eq, b_eq):
|
||||
for i in range(N-1) : # Prevent stacked ones
|
||||
if i == 0 or i == N-1: # Don't touch start or finish
|
||||
continue
|
||||
else :
|
||||
l = [0]*N
|
||||
l[i] = -1
|
||||
l = l*N
|
||||
for j in range(N) :
|
||||
l[i*N + j] = 1
|
||||
|
||||
A_eq = np.vstack((A_eq,l))
|
||||
b_eq.append(0)
|
||||
|
||||
return A_eq, b_eq
|
||||
|
||||
|
||||
# Computes the time to reach from each landmark to the next
|
||||
def link_list(order: List[int], landmarks: List[Landmark]) -> List[Landmark]:
|
||||
|
||||
# Read the parameters from the file
|
||||
with constants.OPTIMIZER_PARAMETERS_PATH.open('r') as f:
|
||||
parameters = yaml.safe_load(f)
|
||||
|
||||
detour_factor = parameters['detour_factor']
|
||||
speed = parameters['average_walking_speed']
|
||||
|
||||
L = []
|
||||
j = 0
|
||||
total_dist = 0
|
||||
while j < len(order)-1 :
|
||||
elem = landmarks[order[j]]
|
||||
next = landmarks[order[j+1]]
|
||||
|
||||
d = get_distance(elem.location, next.location, detour_factor, speed)[1]
|
||||
elem.time_to_reach_next = d
|
||||
L.append(elem)
|
||||
j += 1
|
||||
total_dist += d
|
||||
|
||||
L.append(next)
|
||||
|
||||
return L, total_dist
|
||||
|
||||
|
||||
def link_list_simple(ordered_visit: List[Landmark])-> List[Landmark] :
|
||||
|
||||
# Read the parameters from the file
|
||||
with constants.OPTIMIZER_PARAMETERS_PATH.open('r') as f:
|
||||
parameters = yaml.safe_load(f)
|
||||
|
||||
detour_factor = parameters['detour_factor']
|
||||
speed = parameters['average_walking_speed']
|
||||
|
||||
L = []
|
||||
j = 0
|
||||
total_dist = 0
|
||||
while j < len(ordered_visit)-1 :
|
||||
elem = ordered_visit[j]
|
||||
next = ordered_visit[j+1]
|
||||
|
||||
elem.next_uuid = next.uuid
|
||||
d = get_distance(elem.location, next.location, detour_factor, speed)[1]
|
||||
elem.time_to_reach_next = d
|
||||
if elem.name not in ['start', 'finish'] :
|
||||
elem.must_do = True
|
||||
L.append(elem)
|
||||
j += 1
|
||||
total_dist += d
|
||||
|
||||
L.append(next)
|
||||
|
||||
return L, total_dist
|
||||
|
||||
|
||||
# Main optimization pipeline
|
||||
def solve_optimization (landmarks :List[Landmark], max_steps: int, printing_details: bool) :
|
||||
|
||||
L = len(landmarks)
|
||||
|
||||
# SET CONSTRAINTS FOR INEQUALITY
|
||||
c, A_ub, b_ub = init_ub_dist(landmarks, max_steps) # Add the distances from each landmark to the other
|
||||
A_ub, b_ub = respect_number(L, A_ub, b_ub) # Respect max number of visits (no more possible stops than landmarks).
|
||||
A_ub, b_ub = break_sym(L, A_ub, b_ub) # break the 'zig-zag' symmetry
|
||||
|
||||
# SET CONSTRAINTS FOR EQUALITY
|
||||
A_eq, b_eq = init_eq_not_stay(L) # Force solution not to stay in same place
|
||||
A_eq, b_eq = respect_user_mustsee(landmarks, A_eq, b_eq) # Check if there are user_defined must_see. Also takes care of start/goal
|
||||
A_eq, b_eq = respect_start_finish(L, A_eq, b_eq) # Force start and finish positions
|
||||
A_eq, b_eq = respect_order(L, A_eq, b_eq) # Respect order of visit (only works when max_steps is limiting factor)
|
||||
|
||||
# SET BOUNDS FOR DECISION VARIABLE (x can only be 0 or 1)
|
||||
x_bounds = [(0, 1)]*L*L
|
||||
|
||||
# Solve linear programming problem
|
||||
res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq = b_eq, bounds=x_bounds, method='highs', integrality=3)
|
||||
|
||||
# Raise error if no solution is found
|
||||
if not res.success :
|
||||
raise ArithmeticError("No solution could be found, the problem is overconstrained. Please adapt your must_dos")
|
||||
|
||||
# If there is a solution, we're good to go, just check for connectiveness
|
||||
else :
|
||||
order, circle = is_connected(res.x)
|
||||
i = 0
|
||||
timeout = 80
|
||||
while len(circle) != 0 and i < timeout:
|
||||
A_ub, b_ub = prevent_config(res.x, A_ub, b_ub)
|
||||
A_ub, b_ub = break_cricle(order, len(landmarks), A_ub, b_ub)
|
||||
res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq = b_eq, bounds=x_bounds, method='highs', integrality=3)
|
||||
order, circle = is_connected(res.x)
|
||||
if len(circle) == 0 :
|
||||
break
|
||||
print(i)
|
||||
i += 1
|
||||
|
||||
if i == timeout :
|
||||
raise TimeoutError(f"Optimization took too long. No solution found after {timeout} iterations.")
|
||||
|
||||
# Add the times to reach and stop optimizing
|
||||
L, total_dist = link_list(order, landmarks)
|
||||
|
||||
if printing_details is True :
|
||||
if i != 0 :
|
||||
print(f"Neded to recompute paths {i} times because of unconnected loops...")
|
||||
print_res(L, len(landmarks))
|
||||
print("\nTotal score : " + str(int(-res.fun)))
|
||||
|
||||
return L
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@@ -1,303 +0,0 @@
|
||||
from collections import defaultdict
|
||||
from heapq import heappop, heappush
|
||||
from itertools import permutations
|
||||
import os
|
||||
import yaml
|
||||
|
||||
from shapely import buffer, LineString, Point, Polygon, MultiPoint, concave_hull
|
||||
from typing import List, Tuple
|
||||
from math import pi
|
||||
|
||||
from structs.landmarks import Landmark
|
||||
from landmarks_manager import take_most_important
|
||||
from backend.src.example_optimizer import solve_optimization, link_list_simple, print_res, get_distance
|
||||
import constants
|
||||
|
||||
|
||||
# Create corridor from tour
|
||||
def create_corridor(landmarks: List[Landmark], width: float) :
|
||||
|
||||
corrected_width = (180*width)/(6371000*pi)
|
||||
|
||||
path = create_linestring(landmarks)
|
||||
obj = buffer(path, corrected_width, join_style="mitre", cap_style="square", mitre_limit=2)
|
||||
|
||||
return obj
|
||||
|
||||
|
||||
# Create linestring from tour
|
||||
def create_linestring(landmarks: List[Landmark])->List[Point] :
|
||||
|
||||
points = []
|
||||
|
||||
for landmark in landmarks :
|
||||
points.append(Point(landmark.location))
|
||||
|
||||
return LineString(points)
|
||||
|
||||
|
||||
# Check if some coordinates are in area. Used for the corridor
|
||||
def is_in_area(area: Polygon, coordinates) -> bool :
|
||||
point = Point(coordinates)
|
||||
return point.within(area)
|
||||
|
||||
|
||||
# Function to determine if two landmarks are close to each other
|
||||
def is_close_to(location1: Tuple[float], location2: Tuple[float]):
|
||||
"""Determine if two locations are close by rounding their coordinates to 3 decimals."""
|
||||
absx = abs(location1[0] - location2[0])
|
||||
absy = abs(location1[1] - location2[1])
|
||||
|
||||
return absx < 0.001 and absy < 0.001
|
||||
#return (round(location1[0], 3), round(location1[1], 3)) == (round(location2[0], 3), round(location2[1], 3))
|
||||
|
||||
|
||||
# Rearrange some landmarks in the order of visit to group visit
|
||||
def rearrange(landmarks: List[Landmark]) -> List[Landmark]:
|
||||
|
||||
i = 1
|
||||
while i < len(landmarks):
|
||||
j = i+1
|
||||
while j < len(landmarks):
|
||||
if is_close_to(landmarks[i].location, landmarks[j].location) and landmarks[i].name not in ['start', 'finish'] and landmarks[j].name not in ['start', 'finish']:
|
||||
# If they are not adjacent, move the j-th element to be adjacent to the i-th element
|
||||
if j != i + 1:
|
||||
landmarks.insert(i + 1, landmarks.pop(j))
|
||||
break # Move to the next i-th element after rearrangement
|
||||
j += 1
|
||||
i += 1
|
||||
|
||||
return landmarks
|
||||
|
||||
|
||||
# Step 1: Build the graph
|
||||
graph = defaultdict(list)
|
||||
for i in range(len(landmarks)):
|
||||
for j in range(len(landmarks)):
|
||||
if i != j:
|
||||
distance = get_distance(landmarks[i].location, landmarks[j].location, detour, speed)[1]
|
||||
graph[i].append((distance, j))
|
||||
|
||||
# Step 2: Dijkstra's algorithm to find the shortest path from start to finish
|
||||
start_idx = next(i for i, lm in enumerate(landmarks) if lm.name == 'start')
|
||||
finish_idx = next(i for i, lm in enumerate(landmarks) if lm.name == 'finish')
|
||||
|
||||
distances = {i: float('inf') for i in range(len(landmarks))}
|
||||
previous_nodes = {i: None for i in range(len(landmarks))}
|
||||
distances[start_idx] = 0
|
||||
priority_queue = [(0, start_idx)]
|
||||
|
||||
while priority_queue:
|
||||
current_distance, current_index = heappop(priority_queue)
|
||||
|
||||
if current_distance > distances[current_index]:
|
||||
continue
|
||||
|
||||
for neighbor_distance, neighbor_index in graph[current_index]:
|
||||
distance = current_distance + neighbor_distance
|
||||
|
||||
if distance < distances[neighbor_index]:
|
||||
distances[neighbor_index] = distance
|
||||
previous_nodes[neighbor_index] = current_index
|
||||
heappush(priority_queue, (distance, neighbor_index))
|
||||
|
||||
# Step 3: Backtrack from finish to start to find the path
|
||||
path = []
|
||||
current_index = finish_idx
|
||||
while current_index is not None:
|
||||
path.append(landmarks[current_index])
|
||||
current_index = previous_nodes[current_index]
|
||||
path.reverse()
|
||||
|
||||
return path
|
||||
"""
|
||||
def total_path_distance(path: List[Landmark], detour, speed) -> float:
|
||||
total_distance = 0
|
||||
for i in range(len(path) - 1):
|
||||
total_distance += get_distance(path[i].location, path[i + 1].location, detour, speed)[1]
|
||||
return total_distance
|
||||
"""
|
||||
|
||||
|
||||
def find_shortest_path_through_all_landmarks(landmarks: List[Landmark]) -> List[Landmark]:
|
||||
# Read the parameters from the file
|
||||
with constants.OPTIMIZER_PARAMETERS_PATH.open('r') as f:
|
||||
parameters = yaml.safe_load(f)
|
||||
|
||||
detour = parameters['detour_factor']
|
||||
speed = parameters['average_walking_speed']
|
||||
|
||||
# Step 1: Find 'start' and 'finish' landmarks
|
||||
start_idx = next(i for i, lm in enumerate(landmarks) if lm.name == 'start')
|
||||
finish_idx = next(i for i, lm in enumerate(landmarks) if lm.name == 'finish')
|
||||
|
||||
start_landmark = landmarks[start_idx]
|
||||
finish_landmark = landmarks[finish_idx]
|
||||
|
||||
|
||||
# Step 2: Create a list of unvisited landmarks excluding 'start' and 'finish'
|
||||
unvisited_landmarks = [lm for i, lm in enumerate(landmarks) if i not in [start_idx, finish_idx]]
|
||||
|
||||
# Step 3: Initialize the path with the 'start' landmark
|
||||
path = [start_landmark]
|
||||
coordinates = [landmarks[start_idx].location]
|
||||
|
||||
current_landmark = start_landmark
|
||||
|
||||
# Step 4: Use nearest neighbor heuristic to visit all landmarks
|
||||
while unvisited_landmarks:
|
||||
nearest_landmark = min(unvisited_landmarks, key=lambda lm: get_time(current_landmark.location, lm.location, detour, speed))
|
||||
path.append(nearest_landmark)
|
||||
coordinates.append(nearest_landmark.location)
|
||||
current_landmark = nearest_landmark
|
||||
unvisited_landmarks.remove(nearest_landmark)
|
||||
|
||||
# Step 5: Finally add the 'finish' landmark to the path
|
||||
path.append(finish_landmark)
|
||||
coordinates.append(landmarks[finish_idx].location)
|
||||
|
||||
path_poly = Polygon(coordinates)
|
||||
|
||||
return path, path_poly
|
||||
|
||||
|
||||
# Returns a list of minor landmarks around the planned path to enhance experience
|
||||
def get_minor_landmarks(all_landmarks: List[Landmark], visited_landmarks: List[Landmark], width: float) -> List[Landmark] :
|
||||
|
||||
second_order_landmarks = []
|
||||
visited_names = []
|
||||
area = create_corridor(visited_landmarks, width)
|
||||
|
||||
for visited in visited_landmarks :
|
||||
visited_names.append(visited.name)
|
||||
|
||||
for landmark in all_landmarks :
|
||||
if is_in_area(area, landmark.location) and landmark.name not in visited_names:
|
||||
second_order_landmarks.append(landmark)
|
||||
|
||||
with constants.LANDMARK_PARAMETERS_PATH.open('r') as f:
|
||||
parameters = yaml.safe_load(f)
|
||||
return take_most_important(second_order_landmarks, parameters, len(visited_landmarks))
|
||||
|
||||
|
||||
# Try fix the shortest path using shapely
|
||||
def fix_using_polygon(tour: List[Landmark])-> List[Landmark] :
|
||||
|
||||
coords = []
|
||||
coords_dict = {}
|
||||
for landmark in tour :
|
||||
coords.append(landmark.location)
|
||||
if landmark.name != 'finish' :
|
||||
coords_dict[landmark.location] = landmark
|
||||
|
||||
tour_poly = Polygon(coords)
|
||||
|
||||
better_tour_poly = tour_poly.buffer(0)
|
||||
try :
|
||||
xs, ys = better_tour_poly.exterior.xy
|
||||
|
||||
if len(xs) != len(tour) :
|
||||
better_tour_poly = concave_hull(MultiPoint(coords)) # Create concave hull with "core" of tour leaving out start and finish
|
||||
xs, ys = better_tour_poly.exterior.xy
|
||||
|
||||
except :
|
||||
better_tour_poly = concave_hull(MultiPoint(coords)) # Create concave hull with "core" of tour leaving out start and finish
|
||||
xs, ys = better_tour_poly.exterior.xy
|
||||
|
||||
|
||||
# reverse the xs and ys
|
||||
xs.reverse()
|
||||
ys.reverse()
|
||||
|
||||
better_tour = [] # List of ordered visit
|
||||
name_index = {} # Maps the name of a landmark to its index in the concave polygon
|
||||
|
||||
# Loop through the polygon and generate the better (ordered) tour
|
||||
for i,x in enumerate(xs[:-1]) :
|
||||
y = ys[i]
|
||||
better_tour.append(coords_dict[tuple((x,y))])
|
||||
name_index[coords_dict[tuple((x,y))].name] = i
|
||||
|
||||
|
||||
# Scroll the list to have start in front again
|
||||
start_index = name_index['start']
|
||||
better_tour = better_tour[start_index:] + better_tour[:start_index]
|
||||
|
||||
# Append the finish back and correct the time to reach
|
||||
better_tour.append(tour[-1])
|
||||
|
||||
# Rearrange only if polygon still not simple
|
||||
if not better_tour_poly.is_simple :
|
||||
better_tour = rearrange(better_tour)
|
||||
|
||||
return better_tour
|
||||
|
||||
|
||||
# Second stage of the optimization. Use linear programming again to refine the path
|
||||
def refine_optimization(landmarks: List[Landmark], base_tour: List[Landmark], max_time: int, detour: int, print_infos: bool) -> List[Landmark] :
|
||||
|
||||
# Read the parameters from the file
|
||||
with constants.OPTIMIZER_PARAMETERS_PATH.open('r') as f:
|
||||
parameters = yaml.safe_load(f)
|
||||
max_landmarks = parameters['max_landmarks']
|
||||
|
||||
if len(base_tour)-2 >= max_landmarks :
|
||||
return base_tour
|
||||
|
||||
|
||||
# No need to refine if no detour is taken
|
||||
# if detour == 0 :
|
||||
if False :
|
||||
new_tour = base_tour
|
||||
|
||||
else :
|
||||
minor_landmarks = get_minor_landmarks(landmarks, base_tour, 200)
|
||||
|
||||
if print_infos : print("Using " + str(len(minor_landmarks)) + " minor landmarks around the predicted path")
|
||||
|
||||
# full set of visitable landmarks
|
||||
full_set = base_tour[:-1] + minor_landmarks # create full set of possible landmarks (without finish)
|
||||
full_set.append(base_tour[-1]) # add finish back
|
||||
|
||||
# get a new tour
|
||||
new_tour = solve_optimization(full_set, max_time+detour, False, max_landmarks)
|
||||
if new_tour is None :
|
||||
new_tour = base_tour
|
||||
|
||||
# Link the new tour
|
||||
new_tour, new_dist = link_list_simple(new_tour)
|
||||
|
||||
# If the tour contains only one landmark, return
|
||||
if len(new_tour) < 4 :
|
||||
return new_tour
|
||||
|
||||
# Find shortest path using the nearest neighbor heuristic
|
||||
better_tour, better_poly = find_shortest_path_through_all_landmarks(new_tour)
|
||||
|
||||
# Fix the tour using Polygons if the path looks weird
|
||||
if base_tour[0].location == base_tour[-1].location and not better_poly.is_valid :
|
||||
better_tour = fix_using_polygon(better_tour)
|
||||
|
||||
# Link the tour again
|
||||
better_tour, better_dist = link_list_simple(better_tour)
|
||||
|
||||
# Choose the better tour depending on walked distance
|
||||
if new_dist < better_dist :
|
||||
final_tour = new_tour
|
||||
else :
|
||||
final_tour = better_tour
|
||||
|
||||
if print_infos :
|
||||
print("\n\n\nRefined tour (result of second stage optimization): ")
|
||||
print_res(final_tour)
|
||||
total_score = 0
|
||||
for elem in final_tour :
|
||||
total_score += elem.attractiveness
|
||||
|
||||
print("\nTotal score : " + str(total_score))
|
||||
|
||||
|
||||
|
||||
return final_tour
|
||||
|
||||
|
@@ -1,80 +0,0 @@
|
||||
import pandas as pd
|
||||
|
||||
from typing import List
|
||||
from landmarks_manager import LandmarkManager
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
|
||||
from backend.src.example_optimizer import solve_optimization
|
||||
# from refiner import refine_optimization
|
||||
from structs.landmarks import Landmark
|
||||
from structs.landmarktype import LandmarkType
|
||||
from structs.preferences import Preferences, Preference
|
||||
|
||||
|
||||
# Helper function to create a .txt file with results
|
||||
def write_data(L: List[Landmark], file_name: str):
|
||||
|
||||
data = pd.DataFrame()
|
||||
i = 0
|
||||
|
||||
for landmark in L :
|
||||
data[i] = jsonable_encoder(landmark)
|
||||
i += 1
|
||||
|
||||
data.to_json(file_name, indent = 2, force_ascii=False)
|
||||
|
||||
def main(coordinates: tuple[float, float]) -> List[Landmark]:
|
||||
|
||||
manager = LandmarkManager()
|
||||
|
||||
preferences = Preferences(
|
||||
sightseeing=Preference(
|
||||
name='sightseeing',
|
||||
type=LandmarkType(landmark_type='sightseeing'),
|
||||
score = 5
|
||||
),
|
||||
nature=Preference(
|
||||
name='nature',
|
||||
type=LandmarkType(landmark_type='nature'),
|
||||
score = 5
|
||||
),
|
||||
shopping=Preference(
|
||||
name='shopping',
|
||||
type=LandmarkType(landmark_type='shopping'),
|
||||
score = 5
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Create start and finish
|
||||
start = Landmark(name='start', type=LandmarkType(landmark_type='start'), location=coordinates, osm_type='start', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
|
||||
finish = Landmark(name='finish', type=LandmarkType(landmark_type='finish'), location=coordinates, osm_type='finish', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
|
||||
|
||||
# Generate the landmarks from the start location
|
||||
landmarks, landmarks_short = manager.get_landmark_lists(preferences=preferences, center_coordinates=start.location)
|
||||
print([l.name for l in landmarks_short])
|
||||
|
||||
#write_data(landmarks, "landmarks.txt")
|
||||
|
||||
# Insert start and finish to the landmarks list
|
||||
landmarks_short.insert(0, start)
|
||||
landmarks_short.append(finish)
|
||||
|
||||
# TODO use these parameters in another way
|
||||
max_walking_time = 3 # hours
|
||||
detour = 30 # minutes
|
||||
|
||||
# First stage optimization
|
||||
base_tour = solve_optimization(landmarks_short, max_walking_time*60, True)
|
||||
|
||||
# Second stage optimization
|
||||
# refined_tour = refine_optimization(landmarks, base_tour, max_walking_time*60+detour, True)
|
||||
|
||||
return base_tour
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
start = (48.847132, 2.312359) # Café Chez César
|
||||
# start = (47.377859, 8.540585) # Zurich HB
|
||||
main(start)
|
@@ -2,9 +2,8 @@ import math as m
|
||||
import yaml
|
||||
import logging
|
||||
|
||||
from typing import List, Tuple
|
||||
from OSMPythonTools.overpass import Overpass, overpassQueryBuilder
|
||||
from OSMPythonTools import cachingStrategy
|
||||
from OSMPythonTools.cachingStrategy import CachingStrategy, JSON
|
||||
from pywikibot import ItemPage, Site
|
||||
from pywikibot import config
|
||||
config.put_throttle = 0
|
||||
@@ -33,10 +32,8 @@ class LandmarkManager:
|
||||
tag_coeff: float # coeff to adjust weight of tags
|
||||
N_important: int # number of important landmarks to consider
|
||||
|
||||
preferences: Preferences # preferences of visit
|
||||
location: Tuple[float] # coordinates around which to find a path
|
||||
|
||||
def __init__(self, preferences: Preferences, coordinates: Tuple[float, float]) -> None:
|
||||
def __init__(self) -> None:
|
||||
|
||||
with constants.AMENITY_SELECTORS_PATH.open('r') as f:
|
||||
self.amenity_selectors = yaml.safe_load(f)
|
||||
@@ -49,12 +46,12 @@ class LandmarkManager:
|
||||
self.park_coeff = parameters['park_coeff']
|
||||
self.tag_coeff = parameters['tag_coeff']
|
||||
self.N_important = parameters['N_important']
|
||||
|
||||
self.preferences = preferences
|
||||
self.location = coordinates
|
||||
|
||||
self.overpass = Overpass()
|
||||
CachingStrategy.use(JSON, cacheDir=constants.OSM_CACHE_DIR)
|
||||
|
||||
|
||||
def generate_landmarks_list(self) -> Tuple[List[Landmark], List[Landmark]] :
|
||||
def generate_landmarks_list(self, center_coordinates: tuple[float, float], preferences: Preferences) -> tuple[list[Landmark], list[Landmark]]:
|
||||
"""
|
||||
Generate and prioritize a list of landmarks based on user preferences.
|
||||
|
||||
@@ -62,62 +59,71 @@ class LandmarkManager:
|
||||
and current location. It scores and corrects these landmarks, removes duplicates, and then selects the most important
|
||||
landmarks based on a predefined criterion.
|
||||
|
||||
Parameters:
|
||||
center_coordinates (tuple[float, float]): The latitude and longitude of the center location around which to search.
|
||||
preferences (Preferences): The user's preference settings that influence the landmark selection.
|
||||
|
||||
Returns:
|
||||
Tuple[List[Landmark], List[Landmark]]:
|
||||
tuple[list[Landmark], list[Landmark]]:
|
||||
- A list of all existing landmarks.
|
||||
- A list of the most important landmarks based on the user's preferences.
|
||||
"""
|
||||
|
||||
L = []
|
||||
|
||||
# List for sightseeing
|
||||
if self.preferences.sightseeing.score != 0 :
|
||||
L1 = self.fetch_landmarks(self.amenity_selectors['sightseeing'], SIGHTSEEING, coordinates=self.location)
|
||||
self.correct_score(L1, self.preferences.sightseeing)
|
||||
bbox = self.create_bbox(center_coordinates)
|
||||
# list for sightseeing
|
||||
if preferences.sightseeing.score != 0:
|
||||
score_function = lambda loc, n_tags: int((self.count_elements_close_to(loc) + ((n_tags**1.2)*self.tag_coeff) )*self.church_coeff)
|
||||
L1 = self.fetch_landmarks(bbox, self.amenity_selectors['sightseeing'], SIGHTSEEING, score_function)
|
||||
self.correct_score(L1, preferences.sightseeing)
|
||||
L += L1
|
||||
|
||||
# List for nature
|
||||
if self.preferences.nature.score != 0 :
|
||||
L2 = self.fetch_landmarks(self.amenity_selectors['nature'], NATURE, coordinates=self.location)
|
||||
self.correct_score(L2, self.preferences.nature)
|
||||
|
||||
# list for nature
|
||||
if preferences.nature.score != 0:
|
||||
score_function = lambda loc, n_tags: int((self.count_elements_close_to(loc) + ((n_tags**1.2)*self.tag_coeff) )*self.park_coeff)
|
||||
L2 = self.fetch_landmarks(bbox, self.amenity_selectors['nature'], NATURE, score_function)
|
||||
self.correct_score(L2, preferences.nature)
|
||||
L += L2
|
||||
|
||||
# List for shopping
|
||||
if self.preferences.shopping.score != 0 :
|
||||
L3 = self.fetch_landmarks(self.amenity_selectors['shopping'], SHOPPING, coordinates=self.location)
|
||||
self.correct_score(L3, self.preferences.shopping)
|
||||
|
||||
# list for shopping
|
||||
if preferences.shopping.score != 0:
|
||||
score_function = lambda loc, n_tags: int(self.count_elements_close_to(loc) + ((n_tags**1.2)*self.tag_coeff))
|
||||
L3 = self.fetch_landmarks(bbox, self.amenity_selectors['shopping'], SHOPPING, score_function)
|
||||
self.correct_score(L3, preferences.shopping)
|
||||
L += L3
|
||||
|
||||
L = self.remove_duplicates(L)
|
||||
L_constrained = take_most_important(L, self.N_important)
|
||||
self.logger.info(f'Generated {len(L)} landmarks around {center_coordinates}, and constrained to {len(L_constrained)} most important ones.')
|
||||
|
||||
return L, take_most_important(L, self.N_important)
|
||||
return L, L_constrained
|
||||
|
||||
|
||||
def remove_duplicates(self, landmarks: List[Landmark]) -> List[Landmark] :
|
||||
def remove_duplicates(self, landmarks: list[Landmark]) -> list[Landmark]:
|
||||
"""
|
||||
Removes duplicate landmarks based on their names from the given list. Only retains the landmark with highest score
|
||||
|
||||
Parameters:
|
||||
landmarks (List[Landmark]): A list of Landmark objects.
|
||||
landmarks (list[Landmark]): A list of Landmark objects.
|
||||
|
||||
Returns:
|
||||
List[Landmark]: A list of unique Landmark objects based on their names.
|
||||
list[Landmark]: A list of unique Landmark objects based on their names.
|
||||
"""
|
||||
|
||||
L_clean = []
|
||||
names = []
|
||||
|
||||
for landmark in landmarks :
|
||||
for landmark in landmarks:
|
||||
if landmark.name in names:
|
||||
continue
|
||||
else :
|
||||
else:
|
||||
names.append(landmark.name)
|
||||
L_clean.append(landmark)
|
||||
|
||||
return L_clean
|
||||
|
||||
|
||||
def correct_score(self, landmarks: List[Landmark], preference: Preference) :
|
||||
def correct_score(self, landmarks: list[Landmark], preference: Preference):
|
||||
"""
|
||||
Adjust the attractiveness score of each landmark in the list based on user preferences.
|
||||
|
||||
@@ -125,24 +131,24 @@ class LandmarkManager:
|
||||
The score adjustment is computed using a simple linear transformation based on the preference score.
|
||||
|
||||
Args:
|
||||
landmarks (List[Landmark]): A list of landmarks whose scores need to be corrected.
|
||||
landmarks (list[Landmark]): A list of landmarks whose scores need to be corrected.
|
||||
preference (Preference): The user's preference settings that influence the attractiveness score adjustment.
|
||||
|
||||
Raises:
|
||||
TypeError: If the type of any landmark in the list does not match the expected type in the preference.
|
||||
"""
|
||||
|
||||
if len(landmarks) == 0 :
|
||||
if len(landmarks) == 0:
|
||||
return
|
||||
|
||||
if landmarks[0].type != preference.type :
|
||||
if landmarks[0].type != preference.type:
|
||||
raise TypeError(f"LandmarkType {preference.type} does not match the type of Landmark {landmarks[0].name}")
|
||||
|
||||
for elem in landmarks :
|
||||
for elem in landmarks:
|
||||
elem.attractiveness = int(elem.attractiveness*preference.score/5) # arbitrary computation
|
||||
|
||||
|
||||
def count_elements_close_to(self, coordinates: Tuple[float, float]) -> int:
|
||||
def count_elements_close_to(self, coordinates: tuple[float, float]) -> int:
|
||||
"""
|
||||
Count the number of OpenStreetMap elements (nodes, ways, relations) within a specified radius of the given location.
|
||||
|
||||
@@ -150,7 +156,7 @@ class LandmarkManager:
|
||||
OpenStreetMap data to count the number of elements within that bounding box.
|
||||
|
||||
Args:
|
||||
coordinates (Tuple[float, float]): The latitude and longitude of the location to search around.
|
||||
coordinates (tuple[float, float]): The latitude and longitude of the location to search around.
|
||||
|
||||
Returns:
|
||||
int: The number of elements (nodes, ways, relations) within the specified radius. Returns 0 if no elements
|
||||
@@ -164,32 +170,36 @@ class LandmarkManager:
|
||||
|
||||
alpha = (180*radius) / (6371000*m.pi)
|
||||
bbox = {'latLower':lat-alpha,'lonLower':lon-alpha,'latHigher':lat+alpha,'lonHigher': lon+alpha}
|
||||
|
||||
# Build the query to find elements within the radius
|
||||
radius_query = overpassQueryBuilder(bbox=[bbox['latLower'],bbox['lonLower'],bbox['latHigher'],bbox['lonHigher']],
|
||||
elementType=['node', 'way', 'relation'])
|
||||
|
||||
try :
|
||||
overpass = Overpass()
|
||||
radius_result = overpass.query(radius_query)
|
||||
# Build the query to find elements within the radius
|
||||
radius_query = overpassQueryBuilder(
|
||||
bbox=[bbox['latLower'],
|
||||
bbox['lonLower'],
|
||||
bbox['latHigher'],
|
||||
bbox['lonHigher']],
|
||||
elementType=['node', 'way', 'relation']
|
||||
)
|
||||
|
||||
try:
|
||||
radius_result = self.overpass.query(radius_query)
|
||||
N_elem = radius_result.countWays() + radius_result.countRelations()
|
||||
#print(f"There are {N_elem} ways/relations within 50m")
|
||||
if N_elem is None :
|
||||
self.logger.debug(f"There are {N_elem} ways/relations within 50m")
|
||||
if N_elem is None:
|
||||
return 0
|
||||
return N_elem
|
||||
except :
|
||||
except:
|
||||
return 0
|
||||
|
||||
|
||||
def create_bbox(self, coordinates: Tuple[float, float]) -> Tuple[float, float, float, float]:
|
||||
def create_bbox(self, coordinates: tuple[float, float]) -> 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.
|
||||
coordinates (tuple[float, float]): The latitude and longitude of the center of the bounding box.
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
@@ -212,44 +222,52 @@ class LandmarkManager:
|
||||
return min_lat, min_lon, max_lat, max_lon
|
||||
|
||||
|
||||
def fetch_landmarks(self, list_amenity: list, landmarktype: str, coordinates: Tuple[float, float]) -> List[Landmark] :
|
||||
def fetch_landmarks(self, bbox: tuple, amenity_selector: dict, landmarktype: str, score_function: callable) -> list[Landmark]:
|
||||
"""
|
||||
Fetches landmarks of a specified type from OpenStreetMap (OSM) within a bounding box centered on given coordinates.
|
||||
|
||||
Args:
|
||||
list_amenity (list): A list of OSM amenity queries to be used for fetching landmarks.
|
||||
These queries are typically used to filter results (e.g., [''amenity'='place_of_worship']).
|
||||
bbox (tuple[float, float, float, float]): The bounding box coordinates (min_lat, min_lon, max_lat, max_lon).
|
||||
amenity_selector (dict): The Overpass API query selector for the desired landmark type.
|
||||
landmarktype (str): The type of the landmark (e.g., 'sightseeing', 'nature', 'shopping').
|
||||
coordinates (Tuple[float, float]): The central coordinates (latitude, longitude) for the bounding box.
|
||||
score_function (callable): The function to compute the score of the landmark based on its attributes.
|
||||
|
||||
Returns:
|
||||
List[Landmark]: A list of Landmark objects that were fetched and filtered based on the provided criteria.
|
||||
list[Landmark]: A list of Landmark objects that were fetched and filtered based on the provided criteria.
|
||||
|
||||
Notes:
|
||||
- The bounding box is created around the given coordinates with a side length defined by `self.city_bbox_side`.
|
||||
- Landmarks are fetched using Overpass API queries.
|
||||
- Selectors are translated from the dictionary to the Overpass query format. (e.g., 'amenity'='place_of_worship')
|
||||
- Landmarks are filtered based on various conditions including tags and type.
|
||||
- Scores are assigned to landmarks based on their attributes and surrounding elements.
|
||||
"""
|
||||
|
||||
# Create bbox around start location
|
||||
bbox = self.create_bbox(coordinates)
|
||||
return_list = []
|
||||
|
||||
# Initialize some variables
|
||||
N = 0
|
||||
L = []
|
||||
overpass = Overpass()
|
||||
|
||||
for amenity in list_amenity :
|
||||
query = overpassQueryBuilder(bbox=bbox, elementType=['way', 'relation'], selector=amenity, includeCenter=True, out='body')
|
||||
result = overpass.query(query)
|
||||
N += result.countElements()
|
||||
# caution, when applying a list of selectors, overpass will search for elements that match ALL selectors simultaneously
|
||||
# we need to split the selectors into separate queries and merge the results
|
||||
for sel in dict_to_selector_list(amenity_selector):
|
||||
self.logger.debug(f"Current selector: {sel}")
|
||||
query = overpassQueryBuilder(
|
||||
bbox = bbox,
|
||||
elementType = ['way', 'relation'],
|
||||
selector = sel,
|
||||
# conditions = [],
|
||||
includeCenter = True,
|
||||
out = 'body'
|
||||
)
|
||||
|
||||
try:
|
||||
result = self.overpass.query(query)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error fetching landmarks: {e}")
|
||||
return
|
||||
|
||||
for elem in result.elements():
|
||||
|
||||
name = elem.tag('name') # Add name
|
||||
location = (elem.centerLat(), elem.centerLon()) # Add coordinates (lat, lon)
|
||||
|
||||
# TODO: exclude these from the get go
|
||||
# skip if unprecise location
|
||||
if name is None or location[0] is None:
|
||||
continue
|
||||
@@ -262,65 +280,86 @@ class LandmarkManager:
|
||||
if 'building:part' in elem.tags().keys() and elem.tag('building:part') == 'yes':
|
||||
continue
|
||||
|
||||
else :
|
||||
osm_type = elem.type() # Add type : 'way' or 'relation'
|
||||
osm_id = elem.id() # Add OSM id
|
||||
elem_type = landmarktype # Add the landmark type as 'sightseeing,
|
||||
n_tags = len(elem.tags().keys()) # Add number of tags
|
||||
osm_type = elem.type() # Add type: 'way' or 'relation'
|
||||
osm_id = elem.id() # Add OSM id
|
||||
elem_type = landmarktype # Add the landmark type as 'sightseeing,
|
||||
n_tags = len(elem.tags().keys()) # Add number of tags
|
||||
|
||||
# remove specific tags
|
||||
skip = False
|
||||
for tag in elem.tags().keys() :
|
||||
if "pay" in tag :
|
||||
n_tags -= 1 # discard payment options for tags
|
||||
# remove specific tags
|
||||
skip = False
|
||||
for tag in elem.tags().keys():
|
||||
if "pay" in tag:
|
||||
n_tags -= 1 # discard payment options for tags
|
||||
|
||||
if "disused" in tag :
|
||||
skip = True # skip disused amenities
|
||||
if "disused" in tag:
|
||||
skip = True # skip disused amenities
|
||||
break
|
||||
|
||||
if "wikipedia" in tag:
|
||||
n_tags += 3 # wikipedia entries count more
|
||||
|
||||
if tag == "wikidata":
|
||||
Q = elem.tag('wikidata')
|
||||
site = Site("wikidata", "wikidata")
|
||||
item = ItemPage(site, Q)
|
||||
item.get()
|
||||
n_languages = len(item.labels)
|
||||
n_tags += n_languages/10
|
||||
|
||||
if elem_type != "nature":
|
||||
if "leisure" in tag and elem.tag('leisure') == "park":
|
||||
elem_type = "nature"
|
||||
|
||||
if landmarktype != SHOPPING:
|
||||
if "shop" in tag:
|
||||
skip = True
|
||||
break
|
||||
|
||||
if "wikipedia" in tag :
|
||||
n_tags += 3 # wikipedia entries count more
|
||||
if tag == "building" and elem.tag('building') in ['retail', 'supermarket', 'parking']:
|
||||
skip = True
|
||||
break
|
||||
|
||||
if tag == "wikidata" :
|
||||
Q = elem.tag('wikidata')
|
||||
site = Site("wikidata", "wikidata")
|
||||
item = ItemPage(site, Q)
|
||||
item.get()
|
||||
n_languages = len(item.labels)
|
||||
n_tags += n_languages/10
|
||||
if skip:
|
||||
continue
|
||||
|
||||
if elem_type != "nature" :
|
||||
if "leisure" in tag and elem.tag('leisure') == "park":
|
||||
elem_type = "nature"
|
||||
score = score_function(location, n_tags)
|
||||
if score != 0:
|
||||
# Generate the landmark and append it to the list
|
||||
landmark = Landmark(
|
||||
name=name,
|
||||
type=elem_type,
|
||||
location=location,
|
||||
osm_type=osm_type,
|
||||
osm_id=osm_id,
|
||||
attractiveness=score,
|
||||
must_do=False,
|
||||
n_tags=int(n_tags)
|
||||
)
|
||||
return_list.append(landmark)
|
||||
|
||||
self.logger.debug(f"Fetched {len(return_list)} landmarks of type {landmarktype} in {bbox}")
|
||||
|
||||
if amenity not in ["'shop'='department_store'", "'shop'='mall'"] :
|
||||
if "shop" in tag :
|
||||
skip = True
|
||||
break
|
||||
|
||||
if tag == "building" and elem.tag('building') in ['retail', 'supermarket', 'parking']:
|
||||
skip = True
|
||||
break
|
||||
|
||||
if skip:
|
||||
continue
|
||||
|
||||
# Add score of given landmark based on the number of surrounding elements. Penalty for churches as there are A LOT
|
||||
if amenity == "'amenity'='place_of_worship'" :
|
||||
#score = int((count_elements_close_to(location, radius) + (n_tags*tag_coeff) )*church_coeff)
|
||||
score = int((self.count_elements_close_to(location) + ((n_tags**1.2)*self.tag_coeff) )*self.church_coeff)
|
||||
elif amenity == "'leisure'='park'" :
|
||||
score = int((self.count_elements_close_to(location) + ((n_tags**1.2)*self.tag_coeff) )*self.park_coeff)
|
||||
else :
|
||||
score = int(self.count_elements_close_to(location) + ((n_tags**1.2)*self.tag_coeff))
|
||||
|
||||
if score is not None :
|
||||
|
||||
# Generate the landmark and append it to the list
|
||||
#print(f"There are {n_tags} tags on this Landmark. Total score : {score}\n")
|
||||
landmark = Landmark(name=name, type=elem_type, location=location, osm_type=osm_type, osm_id=osm_id, attractiveness=score, must_do=False, n_tags=int(n_tags))
|
||||
L.append(landmark)
|
||||
|
||||
return L
|
||||
return return_list
|
||||
|
||||
|
||||
|
||||
def dict_to_selector_list(d: dict) -> list:
|
||||
"""
|
||||
Convert a dictionary of key-value pairs to a list of Overpass query strings.
|
||||
|
||||
Args:
|
||||
d (dict): A dictionary of key-value pairs representing the selector.
|
||||
|
||||
Returns:
|
||||
list: A list of strings representing the Overpass query selectors.
|
||||
"""
|
||||
return_list = []
|
||||
for key, value in d.items():
|
||||
if type(value) == list:
|
||||
val = '|'.join(value)
|
||||
return_list.append(f'{key}~"{val}"')
|
||||
elif type(value) == str and len(value) == 0:
|
||||
return_list.append(f'{key}')
|
||||
else:
|
||||
return_list.append(f'{key}={value}')
|
||||
return return_list
|
||||
|
@@ -24,9 +24,9 @@ def write_data(L: List[Landmark], file_name: str):
|
||||
|
||||
|
||||
def test(start_coords: tuple[float, float], finish_coords: tuple[float, float] = None) -> List[Landmark]:
|
||||
manager = LandmarkManager()
|
||||
|
||||
|
||||
|
||||
preferences = Preferences(
|
||||
sightseeing=Preference(
|
||||
name='sightseeing',
|
||||
@@ -56,10 +56,12 @@ def test(start_coords: tuple[float, float], finish_coords: tuple[float, float] =
|
||||
#finish = Landmark(name='finish', type=LandmarkType(landmark_type='finish'), location=(48.847132, 2.312359), osm_type='finish', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
|
||||
|
||||
|
||||
manager = LandmarkManager(preferences=preferences, coordinates=start_coords)
|
||||
|
||||
# Generate the landmarks from the start location
|
||||
landmarks, landmarks_short = manager.generate_landmarks_list()
|
||||
landmarks, landmarks_short = manager.generate_landmarks_list(
|
||||
center_coordinates = start_coords,
|
||||
preferences = preferences
|
||||
)
|
||||
|
||||
# Store data to file for debug purposes
|
||||
# write_data(landmarks, "landmarks_Strasbourg.txt")
|
||||
|
Reference in New Issue
Block a user