From e71c92da40fda26d59660b94e2f78484a2cf9e43 Mon Sep 17 00:00:00 2001 From: Helldragon67 Date: Sun, 7 Jul 2024 10:17:50 +0200 Subject: [PATCH] added some ideas --- backend/src/amenities/sightseeing.am | 4 +- backend/src/landmarks_manager.py | 148 +- backend/src/main_example.py | 23 - backend/src/optimizer_v2.py | 262 +- backend/src/optimizer_v3.py | 326 ++ backend/src/optimizer_v4.py | 422 +++ .../src/parameters/landmarks_manager.params | 10 +- backend/src/parameters/optimizer.params | 2 +- backend/src/refiner.py | 182 +- backend/src/structs/landmarks.py | 5 +- backend/src/tester.py | 26 +- landmarks_Lyon.txt | 3192 +++++++++++++++++ 12 files changed, 4247 insertions(+), 355 deletions(-) delete mode 100644 backend/src/main_example.py create mode 100644 backend/src/optimizer_v3.py create mode 100644 backend/src/optimizer_v4.py create mode 100644 landmarks_Lyon.txt diff --git a/backend/src/amenities/sightseeing.am b/backend/src/amenities/sightseeing.am index 33bcb1a..f9a4eed 100644 --- a/backend/src/amenities/sightseeing.am +++ b/backend/src/amenities/sightseeing.am @@ -5,4 +5,6 @@ historic 'amenity'='planetarium' 'amenity'='place_of_worship' 'amenity'='fountain' -'water'='reflecting_pool' \ No newline at end of file +'water'='reflecting_pool' +# 'tourism'='attraction' might be a bit too broad +# historic as well \ No newline at end of file diff --git a/backend/src/landmarks_manager.py b/backend/src/landmarks_manager.py index 618e169..13c02d0 100644 --- a/backend/src/landmarks_manager.py +++ b/backend/src/landmarks_manager.py @@ -43,31 +43,6 @@ def generate_landmarks(preferences: Preferences, coordinates: Tuple[float, float return L, take_most_important(L) -"""def generate_landmarks(preferences: Preferences, city_country: str = None, coordinates: Tuple[float, float] = None) -> Tuple[List[Landmark], List[Landmark]] : - - l_sights, l_nature, l_shop = get_amenities() - L = [] - - # List for sightseeing - if preferences.sightseeing.score != 0 : - L1 = get_landmarks(l_sights, SIGHTSEEING, city_country=city_country, coordinates=coordinates) - correct_score(L1, preferences.sightseeing) - L += L1 - - # List for nature - if preferences.nature.score != 0 : - L2 = get_landmarks(l_nature, NATURE, city_country=city_country, coordinates=coordinates) - correct_score(L2, preferences.nature) - L += L2 - - # List for shopping - if preferences.shopping.score != 0 : - L3 = get_landmarks(l_shop, SHOPPING, city_country=city_country, coordinates=coordinates) - correct_score(L3, preferences.shopping) - L += L3 - - return remove_duplicates(L), take_most_important(L) -""" # Helper function to gather the amenities list def get_amenities() -> List[List[str]] : @@ -87,7 +62,8 @@ def get_list(path: str) -> List[str] : amenities = [] for line in content : - amenities.append(line.strip('\n')) + if not line.startswith('#') : + amenities.append(line.strip('\n')) return amenities @@ -173,7 +149,7 @@ def correct_score(L: List[Landmark], preference: Preference) : 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 + elem.attractiveness = int(elem.attractiveness*preference.score/5) # arbitrary computation # Function to count elements within a certain radius of a location @@ -192,10 +168,14 @@ def count_elements_within_radius(coordinates: Tuple[float, float], radius: int) try : overpass = Overpass() radius_result = overpass.query(radius_query) - return radius_result.countElements() + N_elem = radius_result.countWays() + radius_result.countRelations() + #print(f"There are {N_elem} ways/relations within 50m") + if N_elem is None : + return 0 + return N_elem except : - return None + return 0 # Creates a bounding box around given coordinates @@ -260,106 +240,46 @@ def get_landmarks(list_amenity: list, landmarktype: LandmarkType, coordinates: T # skip if part of another building 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 - + + # 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 + break + 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_within_radius(location, radius) + n_tags*tag_coeff )*church_coeff) + score = int((count_elements_within_radius(location, radius) + (n_tags*tag_coeff) )*church_coeff) elif amenity == "'leisure'='park'" : - score = int((count_elements_within_radius(location, radius) + n_tags*tag_coeff )*park_coeff) + score = int((count_elements_within_radius(location, radius) + (n_tags*tag_coeff) )*park_coeff) else : - score = count_elements_within_radius(location, radius) + n_tags*tag_coeff + score = count_elements_within_radius(location, radius) + (n_tags*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=n_tags) L.append(landmark) return L - -"""def get_landmarks(list_amenity: list, landmarktype: LandmarkType, city_country: str = None, coordinates: Tuple[float, float] = None) -> List[Landmark] : - - if city_country is None and coordinates is None : - raise ValueError("Either one of 'city_country' and 'coordinates' arguments must be specified") - - if city_country is not None and coordinates is not None : - raise ValueError("Cannot specify both 'city_country' and 'coordinates' at the same time, please choose either one") - - # Read the parameters from the file - with open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/landmarks_manager.params', "r") as f : - parameters = json.loads(f.read()) - tag_coeff = parameters['tag coeff'] - park_coeff = parameters['park coeff'] - church_coeff = parameters['church coeff'] - radius = parameters['radius close to'] - bbox_side = parameters['city bbox side'] - - # If city_country is specified : - if city_country is not None : - nominatim = Nominatim() - areaId = nominatim.query(city_country).areaId() - bbox = None - - # If coordinates are specified : - elif coordinates is not None : - bbox = create_bbox(coordinates, bbox_side) - areaId = None - - else : - raise ValueError("Argument number is not corresponding.") - - # Initialize some variables - N = 0 - L = [] - overpass = Overpass() - - for amenity in list_amenity : - query = overpassQueryBuilder(area=areaId, bbox=bbox, elementType=['way', 'relation'], selector=amenity, includeCenter=True, out='body') - result = overpass.query(query) - N += result.countElements() - - for elem in result.elements(): - - name = elem.tag('name') # Add name - location = (elem.centerLat(), elem.centerLon()) # Add coordinates (lat, lon) - - # skip if unprecise location - if name is None or location[0] is None: - continue - - # skip if unused - if 'disused:leisure' in elem.tags().keys(): - continue - - # skip if part of another building - 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 - - # 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_within_radius(location, radius) + n_tags*tag_coeff )*church_coeff) - elif amenity == "'leisure'='park'" : - score = int((count_elements_within_radius(location, radius) + n_tags*tag_coeff )*park_coeff) - else : - score = count_elements_within_radius(location, radius) + n_tags*tag_coeff - - if score is not None : - # 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=n_tags) - L.append(landmark) - - return L -""" \ No newline at end of file diff --git a/backend/src/main_example.py b/backend/src/main_example.py deleted file mode 100644 index 1a49305..0000000 --- a/backend/src/main_example.py +++ /dev/null @@ -1,23 +0,0 @@ -import fastapi -from dataclasses import dataclass - - -@dataclass -class Destination: - name: str - location: tuple - attractiveness: int - - - -d = Destination() - - - -def get_route() -> list[Destination]: - return {"route": "Hello World"} - -endpoint = ("/get_route", get_route) -end -if __name__ == "__main__": - fastapi.run() diff --git a/backend/src/optimizer_v2.py b/backend/src/optimizer_v2.py index 9be3c0e..1b89e29 100644 --- a/backend/src/optimizer_v2.py +++ b/backend/src/optimizer_v2.py @@ -5,24 +5,19 @@ from scipy.spatial import KDTree import numpy as np from itertools import combinations from structs.landmarks import Landmark -from optimizer import print_res, link_list_simple +from optimizer_v4 import print_res, link_list_simple, get_time import os import json import heapq - -# Define the get_distance function -def get_distance(loc1: Tuple[float, float], loc2: Tuple[float, float], detour: float, speed: float) -> Tuple[float, float]: - # Placeholder implementation, should be replaced with the actual logic - distance = geodesic(loc1, loc2).meters - return distance, distance * detour / speed - # Heuristic function: distance to the goal -def heuristic(loc1: Tuple[float, float], loc2: Tuple[float, float]) -> float: +def heuristic(loc1: Tuple[float, float], loc2: Tuple[float, float], score2: int) -> float: return geodesic(loc1, loc2).meters -def a_star(G, start_id, end_id, max_walking_time, must_do_nodes, max_landmarks, detour, speed): + +# A* planner to search through the graph +def a_star2(G, start_id, end_id, max_walking_time, must_do_nodes, max_landmarks, detour, speed): open_set = [] heapq.heappush(open_set, (0, start_id, 0, [start_id], set([start_id]))) best_path = None @@ -49,11 +44,12 @@ def a_star(G, start_id, end_id, max_walking_time, must_do_nodes, max_landmarks, for neighbor in G.neighbors(current_node): if neighbor not in visited: - distance = int(geodesic(G.nodes[current_node]['pos'], G.nodes[neighbor]['pos']).meters * detour / (speed * 16.6666)) + #distance = int(geodesic(G.nodes[current_node]['pos'], G.nodes[neighbor]['pos']).meters * detour / (speed * 16.6666)) + distance = get_time(G.nodes[current_node]['pos'], G.nodes[neighbor]['pos'], detour, speed) if current_length + distance <= max_walking_time: new_path = path + [neighbor] new_visited = visited | {neighbor} - estimated_cost = current_length + distance + heuristic(G.nodes[neighbor]['pos'], G.nodes[end_id]['pos']) + estimated_cost = current_length + distance + get_time(G.nodes[neighbor]['pos'], G.nodes[end_id]['pos'], detour, speed) heapq.heappush(open_set, (estimated_cost, neighbor, current_length + distance, new_path, new_visited)) # Check if all must_do_nodes have been visited @@ -63,31 +59,47 @@ def a_star(G, start_id, end_id, max_walking_time, must_do_nodes, max_landmarks, return None, 0 +def a_star(G, start_id, end_id, max_walking_time, must_do_nodes, max_landmarks, detour, speed): + open_set = [] + heapq.heappush(open_set, (0, start_id, 0, [start_id], set([start_id]))) + best_path = None + max_attractiveness = 0 + visited_must_do = set() -def dfs(G, current_node, end_id, current_length, path, visited, max_walking_time, must_do_nodes, max_landmarks, detour, speed): - # If the path includes all must_do nodes and reaches the end - if current_node == end_id and all(node in path for node in must_do_nodes): - return path, sum(G.nodes[node]['weight'] for node in path) + while open_set: + _, current_node, current_length, path, visited = heapq.heappop(open_set) - # If the number of landmarks exceeds the maximum allowed, return None - if len(path) > max_landmarks+1: - return None, 0 + # If current node is a must_do node and hasn't been visited yet, mark it as visited + if current_node in must_do_nodes and current_node not in visited_must_do: + visited_must_do.add(current_node) - best_path = None - max_attractiveness = 0 + # Check if path includes all must_do nodes and reaches the end + if current_node == end_id and all(node in visited for node in must_do_nodes): + attractiveness = sum(G.nodes[node]['weight'] for node in path) + if attractiveness > max_attractiveness: + best_path = path + max_attractiveness = attractiveness + continue + + if len(path) > max_landmarks + 1: + continue + + for neighbor in G.neighbors(current_node): + if neighbor not in visited: + distance = get_time(G.nodes[current_node]['pos'], G.nodes[neighbor]['pos'], detour, speed) + if current_length + distance <= max_walking_time: + new_path = path + [neighbor] + new_visited = visited | {neighbor} + estimated_cost = current_length + distance + heuristic(G.nodes[neighbor]['pos'], G.nodes[end_id]['pos'], G.nodes[neighbor]['weight']) + heapq.heappush(open_set, (estimated_cost, neighbor, current_length + distance, new_path, new_visited)) + + # Check if all must_do_nodes have been visited + if all(node in visited_must_do for node in must_do_nodes): + return best_path, max_attractiveness + else: + return None, 0 + - for neighbor in G.neighbors(current_node): - if neighbor not in visited: - distance = int(geodesic(G.nodes[current_node]['pos'], G.nodes[neighbor]['pos']).meters * detour / (speed*16.6666)) - if current_length + distance <= max_walking_time: - new_path = path + [neighbor] - new_visited = visited | {neighbor} - result_path, attractiveness = dfs(G, neighbor, end_id, current_length + distance, new_path, new_visited, max_walking_time, must_do_nodes, max_landmarks, detour, speed) - if attractiveness > max_attractiveness: - best_path = result_path - max_attractiveness = attractiveness - - return best_path, max_attractiveness def find_path(G, start_id, finish_id, max_walking_time, must_do_nodes, max_landmarks) -> List[str]: @@ -97,15 +109,12 @@ def find_path(G, start_id, finish_id, max_walking_time, must_do_nodes, max_landm detour = parameters['detour factor'] speed = parameters['average walking speed'] - - """if G[start_id]['pos'] == G[finish_id]['pos'] : - best_path, _ = dfs(G, start_id, finish_id, 0, [start_id], {start_id}, max_walking_time, must_do_nodes, max_landmarks, detour, speed) - else :""" best_path, _ = a_star(G, start_id, finish_id, max_walking_time, must_do_nodes, max_landmarks, detour, speed) return best_path if best_path else [] + # Function to dynamically adjust theta def adjust_theta(num_nodes, theta_opt, target_ratio=2.0): # Start with an initial guess @@ -114,62 +123,8 @@ def adjust_theta(num_nodes, theta_opt, target_ratio=2.0): return initial_theta / (num_nodes ** (1 / target_ratio)) -# Create a graph using NetworkX and generate the path -def generate_path(landmarks: List[Landmark], max_walking_time: float, max_landmarks: int, theta_opt = 0.0008) -> List[List[Landmark]]: - - landmap = {} - pos_dict = {} - weight_dict = {} - # Add nodes to the graph with attractiveness - for i, landmark in enumerate(landmarks): - #G.nodes[i]['attractiveness'] = landmark.attractiveness - pos_dict[i] = landmark.location - weight_dict[i] = landmark.attractiveness - #G.nodes[i]['pos'] = landmark.location - landmap[i] = landmark - if landmark.name == 'start' : - start_id = i - elif landmark.name == 'finish' : - end_id = i - - # Lambda version of get_distance - get_dist = lambda loc1, loc2: geodesic(loc1, loc2).meters + 0.001 #.meters*detour/speed +0.0000001 - - theta = adjust_theta(len(landmarks), theta_opt) - G = nx.geographical_threshold_graph(n=len(landmarks), theta=theta, pos=pos_dict, weight=weight_dict, metric=get_dist) - - # good theta : 0.000125 - # Define must_do nodes - must_do_nodes = [i for i in G.nodes() if landmap[i].must_do] - - for node1, node2 in combinations(must_do_nodes, 2): - if not G.has_edge(node1, node2): - distance = geodesic(G.nodes[node1]['pos'], G.nodes[node2]['pos']).meters + 0.001 - G.add_edge(node1, node2, weight=distance) - - print(f"Graph with {G.number_of_nodes()} nodes") - print(f"Graph with {G.number_of_edges()} edges") - print("Computing path...") - - # Find the valid path using the greedy algorithm - valid_path = find_path(G, start_id, end_id, max_walking_time, must_do_nodes, max_landmarks) - - if not valid_path: - return [] # No valid path found - - lis = [landmap[id] for id in valid_path] - - lis, tot_dist = link_list_simple(lis) - - print_res(lis, len(landmarks)) - - - return lis - - -# Create a graph using NetworkX and generate the path -def generate_path2(landmarks: List[Landmark], max_walking_time: float, max_landmarks: int) -> List[List[Landmark]]: - +# Create a graph using NetworkX and generate the path without must_do +def generate_path(landmarks: List[Landmark], max_walking_time: float, max_landmarks: int) -> List[List[Landmark]]: # Read the parameters from the file with open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/optimizer.params', "r") as f : @@ -177,11 +132,11 @@ def generate_path2(landmarks: List[Landmark], max_walking_time: float, max_landm detour = parameters['detour factor'] speed = parameters['average walking speed'] - landmap = {} pos_dict = {} weight_dict = {} G = nx.Graph() + # Add nodes to the graph with attractiveness for i, landmark in enumerate(landmarks): pos_dict[i] = landmark.location @@ -193,40 +148,89 @@ def generate_path2(landmarks: List[Landmark], max_walking_time: float, max_landm elif landmark.name == 'finish' : finish_id = i - """# If start and finish are the same no need to add another node - if pos_dict[finish_id] == pos_dict[start_id] : - end_id = start_id - else : - G.add_node(finish_id, pos=pos_dict[finish_id], weight=weight_dict[finish_id]) - end_id = finish_id""" + coords = np.array(list(pos_dict.values())) + kdtree = KDTree(coords) - # Lambda version of get_distance - #get_dist = lambda loc1, loc2: geodesic(loc1, loc2).meters + 0.001 #.meters*detour/speed +0.0000001 + k = 5 + if len(landmarks) <= k : + k = len(landmarks)-1 + + for node, coord in pos_dict.items(): + indices = kdtree.query(coord, k + 1)[1] # k+1 because the closest neighbor is the node itself + for idx in indices[1:]: # skip the first one (itself) + neighbor = list(pos_dict.keys())[idx] + distance = get_time(coord, pos_dict[neighbor], detour, speed) + G.add_edge(node, neighbor, weight=distance) + + print(f"Graph with {G.number_of_nodes()} nodes and {G.number_of_edges()} edges") + print("Start computing path...") + + # Find the valid path using the greedy algorithm + valid_path = find_path(G, start_id, finish_id, max_walking_time, [], max_landmarks) + + if not valid_path: + return [] # No valid path found + + lis = [landmap[id] for id in valid_path] + + lis, _ = link_list_simple(lis) + + print_res(lis, len(landmarks)) + + + return lis + + + +# Create a graph using NetworkX and generate the path +def generate_path2(landmarks: List[Landmark], max_walking_time: float, max_landmarks: int) -> List[List[Landmark]]: + + # Read the parameters from the file + with open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/optimizer.params', "r") as f : + parameters = json.loads(f.read()) + detour = parameters['detour factor'] + speed = parameters['average walking speed'] + + + landmap = {} + pos_dict = {} + weight_dict = {} + must_do_nodes = [] + G = nx.Graph() + # Add nodes to the graph with attractiveness + for i, landmark in enumerate(landmarks): + pos_dict[i] = landmark.location + weight_dict[i] = landmark.attractiveness + landmap[i] = landmark + if landmark.must_do : + must_do_nodes.append(i) + G.add_node(i, pos=landmark.location, weight=landmark.attractiveness) + if landmark.name == 'start' : + start_id = i + elif landmark.name == 'finish' : + finish_id = i coords = np.array(list(pos_dict.values())) kdtree = KDTree(coords) - k = 4 + k = 3 for node, coord in pos_dict.items(): - indices = kdtree.query(coord, k + 1)[1] # k+1 because the closest neighbor is the node itself - for idx in indices[1:]: # skip the first one (itself) + indices = kdtree.query(coord, k + 1)[1] # k+1 because the closest neighbor is the node itself + for idx in indices[1:]: # skip the first one (itself) neighbor = list(pos_dict.keys())[idx] - distance = get_distance(coord, pos_dict[neighbor], detour, speed) + distance = get_time(coord, pos_dict[neighbor], detour, speed) G.add_edge(node, neighbor, weight=distance) - # Define must_do nodes - must_do_nodes = [i for i in G.nodes() if landmap[i].must_do] # Add special edges between must_do nodes if len(must_do_nodes) > 0 : for node1, node2 in combinations(must_do_nodes, 2): if not G.has_edge(node1, node2): - distance = get_distance(G.nodes[node1]['pos'], G.nodes[node2]['pos'], detour, speed) + distance = get_time(G.nodes[node1]['pos'], G.nodes[node2]['pos'], detour, speed) G.add_edge(node1, node2, weight=distance) - print(f"Graph with {G.number_of_nodes()} nodes") - print(f"Graph with {G.number_of_edges()} edges") + print(f"Graph with {G.number_of_nodes()} nodes and {G.number_of_edges()} edges") print("Computing path...") # Find the valid path using the greedy algorithm @@ -249,12 +253,38 @@ def generate_path2(landmarks: List[Landmark], max_walking_time: float, max_landm def correct_path(tour: List[Landmark]) -> List[Landmark] : + # Read the parameters from the file + with open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/optimizer.params', "r") as f : + parameters = json.loads(f.read()) + detour = parameters['detour factor'] + speed = parameters['average walking speed'] + + G = nx.Graph() + coords = [] - for landmark in tour : + landmap = {} + for i, landmark in enumerate(tour) : coords.append(landmark.location) - - G = nx.circulant_graph(n=len(tour), create_using=coords) + landmap[i] = landmark + G.add_node(i, pos=landmark.location, weight=landmark.attractiveness) - path = nx.shortest_path(G=G, source=tour[0].location, target=tour[-1].location) + kdtree = KDTree(coords) + + k = 3 + for node, coord in coords: + indices = kdtree.query(coord, k + 1)[1] # k+1 because the closest neighbor is the node itself + for idx in indices[1:]: # skip the first one (itself) + neighbor = list(coords)[idx] + distance = get_time(coord, coords[neighbor], detour, speed) + G.add_edge(node, neighbor, weight=distance) + + path = nx.approximation.traveling_salesman_problem(G, weight='weight', cycle=True) + + lis = [landmap[id] for id in path] + + lis, tot_dist = link_list_simple(lis) + + print_res(lis, len(tour)) + + return path - return path \ No newline at end of file diff --git a/backend/src/optimizer_v3.py b/backend/src/optimizer_v3.py new file mode 100644 index 0000000..a4545d3 --- /dev/null +++ b/backend/src/optimizer_v3.py @@ -0,0 +1,326 @@ +import numpy as np +import json, os + +from typing import List, Tuple +from itertools import combinations +from scipy.optimize import linprog +from math import radians, sin, cos, acos +from shapely import Polygon +from geopy.distance import geodesic + + +from structs.landmarks import Landmark + + +# 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") + + +# Function that returns the distance in meters from one location to another +def get_time(p1: Tuple[float, float], p2: Tuple[float, float], detour: float, speed: float) : + + # Compute the straight-line distance in m + if p1 == p2 : + return 0 + else: + #dist = 1000 * 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]))) + dist = geodesic(p1, p2).meters + + # Consider the detour factor for average city to determine walking distance (in m) + walk_dist = dist*detour + + # Time to walk this distance (in minutes) + walk_time = walk_dist/speed*(60/1000) + + """if walk_time > 15 : + walk_time = 5*round(walk_time/5) + else : + walk_time = round(walk_time)""" + + + return round(walk_time) + + +# Checks if the path is connected, returns a circle if it finds one and the RESULT +def is_connected(resx) -> bool: + + + 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. + resx = resx[:L*L] + + # first round the results to have only 0-1 values + for i, elem in enumerate(resx): + resx[i] = round(elem) + + 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 + + +# 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 open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/optimizer.params', "r") as f : + parameters = json.loads(f.read()) + 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_time(elem.location, next.location, detour_factor, speed) + 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 + + + +# 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) + [0]*(L-1) + A_ub.append(h) + b_ub.append(1) + + # Read the parameters from the file + with open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/optimizer.params', "r") as f : + parameters = json.loads(f.read()) + max_landmarks = parameters['max landmarks'] + + A_ub.append(ones*L + [0]*(L-1)) + b_ub.append(max_landmarks+1) + + return A_ub, b_ub + + + +def solve_optimizationv3(landmarks, max_walking_time): + L = len(landmarks) + + # Read the parameters from the file + with open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/optimizer.params', "r") as f : + parameters = json.loads(f.read()) + detour = parameters['detour factor'] + speed = parameters['average walking speed'] + + # Create distance matrix + A = np.zeros((L, L)) + for i in range(L): + for j in range(L): + if i != j: + A[i, j] = get_time(landmarks[i].location, landmarks[j].location, detour, speed) + + # Define the linear program + c = np.hstack((A.flatten(), [0]*(L-1))) + bounds = [(0, 1) for _ in range(L*L+L-1)] + + # Flow conservation constraints + A_eq = [] + b_eq = [] + + # Each node (except start and end) has one incoming and one outgoing edge + for i in range(L): + if i == 0 or i == L-1: + continue + A_eq.append([1 if j // L == i else 0 for j in range(L*L)] + [0]*(L-1)) + b_eq.append(1) + A_eq.append([1 if j % L == i else 0 for j in range(L*L)] + [0]*(L-1)) + b_eq.append(1) + + # Start node constraint + A_eq.append([1 if j // L == 0 else 0 for j in range(L*L)] + [0]*(L-1)) + b_eq.append(1) + + # End node constraint + A_eq.append([1 if j % L == L-1 else 0 for j in range(L*L)] + [0]*(L-1)) + b_eq.append(1) + + # Subtour elimination constraints + A_ub = [] + b_ub = [] + + # u_i - u_j + L*x_ij <= L-1 for all i != j + for i in range(1, L): + for j in range(1, L): + if i != j: + constraint = [0] * (L * L + L - 1) + constraint[i * L + j] = L + constraint[j * L + i] = -L + A_ub.append(constraint) + b_ub.append(L - 1) + + + A_ub, b_ub = respect_number(L, A_ub, b_ub) # Respect max number of visits (no more possible stops than landmarks). + + # Convert constraints to numpy arrays + A_eq = np.array(A_eq) + A_ub = np.array(A_ub) + b_ub = np.array(b_ub) + b_eq = np.array(b_eq) + + # Solve the linear program + result = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq=b_eq, bounds=bounds, method='highs') + + if result.success: + x = result.x[:L*L].reshape((L, L)) + path = [] + for i in range(L): + for j in range(L): + if x[i, j] > 0.5: + path.append((i, j)) + print(f"({i}, {j})") + + order, _ = is_connected(result.x) + L, _ = link_list(order, landmarks) + + print_res(L, len(landmarks)) + print("\nTotal score : " + str(int(-result.fun))) + + return L + else: + print("no results") + return [] + + + +# 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 +# #A_ub, b_ub = prevent_subtours(L, A_ub, b_ub) + +# # 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 + 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) +# if not res.success : +# raise ArithmeticError(f"No solution found after {timeout} iterations.") + +# 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 + + + + + + diff --git a/backend/src/optimizer_v4.py b/backend/src/optimizer_v4.py new file mode 100644 index 0000000..acb2b87 --- /dev/null +++ b/backend/src/optimizer_v4.py @@ -0,0 +1,422 @@ +import numpy as np +import json, os + +from typing import List, Tuple +from scipy.optimize import linprog +from math import radians, sin, cos, acos +from geopy.distance import geodesic +from shapely import Polygon + +from structs.landmarks import Landmark + + +# 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_time(p1: Tuple[float, float], p2: Tuple[float, float], detour: float, speed: float) : + + # Compute the straight-line distance in km + if p1 == p2 : + return 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]))) + dist = geodesic(p1, p2).kilometers + + # 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_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): + + with open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/optimizer.params', "r") as f : + parameters = json.loads(f.read()) + 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_time(spot1.location, spot2.location, detour, speed) + 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 open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/optimizer.params', "r") as f : + parameters = json.loads(f.read()) + 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 open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/optimizer.params', "r") as f : + parameters = json.loads(f.read()) + 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_time(elem.location, next.location, detour_factor, speed) + elem.time_to_reach_next = d + elem.must_do = True + elem.location = (round(elem.location[0], 5), round(elem.location[1], 5)) + L.append(elem) + j += 1 + total_dist += d + + next.location = (round(next.location[0], 5), round(next.location[1], 5)) + L.append(next) + + return L, total_dist + + +def link_list_simple(ordered_visit: List[Landmark])-> List[Landmark] : + + # Read the parameters from the file + with open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/optimizer.params', "r") as f : + parameters = json.loads(f.read()) + 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_time(elem.location, next.location, detour_factor, speed) + 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 + + + + + + diff --git a/backend/src/parameters/landmarks_manager.params b/backend/src/parameters/landmarks_manager.params index b4c6ea5..17afc2b 100644 --- a/backend/src/parameters/landmarks_manager.params +++ b/backend/src/parameters/landmarks_manager.params @@ -1,8 +1,8 @@ { "city bbox side" : 3, - "radius close to" : 27.5, - "church coeff" : 0.7, - "park coeff" : 1.5, - "tag coeff" : 100, - "N important" : 40 + "radius close to" : 50, + "church coeff" : 0.9, + "park coeff" : 1.2, + "tag coeff" : 10, + "N important" : 30 } \ No newline at end of file diff --git a/backend/src/parameters/optimizer.params b/backend/src/parameters/optimizer.params index 847fea2..b971821 100644 --- a/backend/src/parameters/optimizer.params +++ b/backend/src/parameters/optimizer.params @@ -1,5 +1,5 @@ { "detour factor" : 1.4, "average walking speed" : 4.8, - "max landmarks" : 8 + "max landmarks" : 7 } \ No newline at end of file diff --git a/backend/src/refiner.py b/backend/src/refiner.py index a99e55b..465de3e 100644 --- a/backend/src/refiner.py +++ b/backend/src/refiner.py @@ -5,11 +5,13 @@ import os, json from shapely import buffer, LineString, Point, Polygon, MultiPoint, convex_hull, concave_hull, LinearRing from typing import List, Tuple +from scipy.spatial import KDTree from math import pi +import networkx as nx from structs.landmarks import Landmark from landmarks_manager import take_most_important -from optimizer import solve_optimization, link_list_simple, print_res, get_distance +from optimizer_v4 import solve_optimization, link_list_simple, print_res, get_time from optimizer_v2 import generate_path, generate_path2 @@ -63,7 +65,7 @@ def rearrange(landmarks: List[Landmark]) -> List[Landmark]: return landmarks -def find_shortest_path_through_all_landmarks(landmarks: List[Landmark]) -> List[Landmark]: +def find_shortest_path_through_all_landmarks(landmarks: List[Landmark]) -> Tuple[List[Landmark], Polygon]: # Read from data with open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/optimizer.params', "r") as f : @@ -90,7 +92,7 @@ def find_shortest_path_through_all_landmarks(landmarks: List[Landmark]) -> List[ # Step 4: Use nearest neighbor heuristic to visit all landmarks while unvisited_landmarks: - nearest_landmark = min(unvisited_landmarks, key=lambda lm: get_distance(current_landmark.location, lm.location, detour, speed)[1]) + 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 @@ -120,23 +122,6 @@ def get_minor_landmarks(all_landmarks: List[Landmark], visited_landmarks: List[L return take_most_important(second_order_landmarks, len(visited_landmarks)) -def get_minor_landmarks2(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) - - return take_most_important(second_order_landmarks, len(visited_landmarks)) - - - """def refine_optimization(landmarks: List[Landmark], base_tour: List[Landmark], max_time: int, print_infos: bool) -> List[Landmark] : @@ -152,16 +137,58 @@ def get_minor_landmarks2(all_landmarks: List[Landmark], visited_landmarks: List[ return new_tour""" + +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) + 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 + + # 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 + better_tour = rearrange(better_tour) + + return better_tour + + def refine_optimization(landmarks: List[Landmark], base_tour: List[Landmark], max_time: int, print_infos: bool) -> List[Landmark] : # Read from the file with open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/optimizer.params', "r") as f : parameters = json.loads(f.read()) max_landmarks = parameters['max landmarks'] + 4 - - if len(base_tour)-2 >= max_landmarks : - return base_tour - minor_landmarks = get_minor_landmarks(landmarks, base_tour, 200) @@ -175,66 +202,12 @@ def refine_optimization(landmarks: List[Landmark], base_tour: List[Landmark], ma new_tour = solve_optimization(full_set, max_time, False) new_tour, new_dist = link_list_simple(new_tour) - """#if base_tour[0].location == base_tour[-1].location : - if False : - coords = [] # Coordinates of the new tour - coords_dict = {} # maps the location of an element to the element itself. Used to access the elements back once we get the geometry + better_tour, better_poly = find_shortest_path_through_all_landmarks(new_tour) - # Iterate through the new tour without finish - for elem in new_tour[:-1] : - coords.append(Point(elem.location)) - coords_dict[elem.location] = elem # if start = goal, only finish remains - - # Create a concave polygon using the coordinates - 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]) : - better_tour.append(coords_dict[tuple((x,ys[i]))]) - name_index[coords_dict[tuple((x,ys[i]))].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(new_tour[-1]) - - # Rearrange only if polygon - better_tour = rearrange(better_tour) - - # Add the time to reach - better_tour = add_time_to_reach_simple(better_tour) - """ + if base_tour[0].location == base_tour[-1].location and not better_poly.is_valid : + better_tour = fix_using_polygon(better_tour) - """ - if not better_poly.is_simple : - - coords_dict = {} - better_tour2 = [] - for elem in better_tour : - coords_dict[elem.location] = elem - - better_poly2 = better_poly.buffer(0) - new_coords = better_poly2.exterior.coords[:] - start_coords = base_tour[0].location - start_index = new_coords. - - #for point in new_coords : - """ - - - better_tour, better_poly = find_shortest_path_through_all_landmarks(new_tour) better_tour, better_dist = link_list_simple(better_tour) if new_dist < better_dist : @@ -258,12 +231,12 @@ def refine_path(landmarks: List[Landmark], base_tour: List[Landmark], max_time: # Read from the file with open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/optimizer.params', "r") as f : parameters = json.loads(f.read()) - max_landmarks = parameters['max landmarks'] + 4 + max_landmarks = parameters['max landmarks'] + 3 """if len(base_tour)-2 >= max_landmarks : return base_tour""" - minor_landmarks = get_minor_landmarks2(landmarks, base_tour, 200) + minor_landmarks = get_minor_landmarks(landmarks, base_tour, 200) if print_infos : print("Using " + str(len(minor_landmarks)) + " minor landmarks around the predicted path") @@ -277,3 +250,46 @@ def refine_path(landmarks: List[Landmark], base_tour: List[Landmark], max_time: + +# If a tour is not connected +def correct_path(tour: List[Landmark]) -> List[Landmark] : + + # Read the parameters from the file + with open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/optimizer.params', "r") as f : + parameters = json.loads(f.read()) + detour = parameters['detour factor'] + speed = parameters['average walking speed'] + + G = nx.Graph() + + coords = [] + landmap = {} + for i, landmark in enumerate(tour) : + coords.append(landmark.location) + landmap[i] = landmark + G.add_node(i, pos=landmark.location, weight=landmark.attractiveness) + + kdtree = KDTree(coords) + + k = 3 + for node, coord in coords: + indices = kdtree.query(coord, k + 1)[1] # k+1 because the closest neighbor is the node itself + for idx in indices[1:]: # skip the first one (itself) + neighbor = list(coords)[idx] + distance = get_time(coord, coords[neighbor], detour, speed) + G.add_edge(node, neighbor, weight=distance) + + path = nx.approximation.traveling_salesman_problem(G, weight='weight', cycle=True) + + if len(path) != len(tour) : + print("nope") + + lis = [landmap[id] for id in path] + + lis, tot_dist = link_list_simple(lis) + + print_res(lis, len(tour)) + + return path + + diff --git a/backend/src/structs/landmarks.py b/backend/src/structs/landmarks.py index d02dc5f..2448236 100644 --- a/backend/src/structs/landmarks.py +++ b/backend/src/structs/landmarks.py @@ -7,8 +7,6 @@ from uuid import uuid4 # Output to frontend class Landmark(BaseModel) : - # Unique ID of a given landmark - uuid: str = Field(default_factory=uuid4) # TODO implement this ASAP # Properties of the landmark name : str @@ -21,6 +19,9 @@ class Landmark(BaseModel) : image_url : Optional[str] = None # TODO future description : Optional[str] = None # TODO future duration : Optional[int] = 0 # TODO future + + # Unique ID of a given landmark + uuid: str = Field(default_factory=uuid4) # TODO implement this ASAP # Additional properties depending on specific tour must_do : bool diff --git a/backend/src/tester.py b/backend/src/tester.py index 71b537b..3215ffd 100644 --- a/backend/src/tester.py +++ b/backend/src/tester.py @@ -6,8 +6,8 @@ from typing import List from landmarks_manager import generate_landmarks from fastapi.encoders import jsonable_encoder -from optimizer import solve_optimization -from optimizer_v2 import generate_path, generate_path2 +from optimizer_v4 import solve_optimization +from optimizer_v2 import generate_path from refiner import refine_optimization, refine_path from structs.landmarks import Landmark from structs.landmarktype import LandmarkType @@ -94,9 +94,10 @@ def test4(coordinates: tuple[float, float]) -> List[Landmark]: # Generate the landmarks from the start location landmarks, landmarks_short = generate_landmarks(preferences=preferences, coordinates=start.location) - #write_data(landmarks, "landmarks.txt") + #write_data(landmarks, "landmarks_Lyon.txt") # Insert start and finish to the landmarks list + #landmarks_short = landmarks_short[:4] landmarks_short.insert(0, start) landmarks_short.append(finish) @@ -104,24 +105,29 @@ def test4(coordinates: tuple[float, float]) -> List[Landmark]: with open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/optimizer.params', "r") as f : parameters = json.loads(f.read()) max_landmarks = parameters['max landmarks'] - max_walking_time = 45 # minutes - detour = 10 # minutes + max_walking_time = 120 # minutes + detour = 30 # minutes # First stage optimization - #base_tour = solve_optimization(landmarks_short, max_walking_time*60, True) + #base_tour = solve_optimization(landmarks_short, max_walking_time, True) + + #base_tour = solve_optimization(landmarks_short, max_walking_time, True) # First stage using NetworkX - base_tour = generate_path2(landmarks_short, max_walking_time, max_landmarks) + base_tour = generate_path(landmarks_short, max_walking_time, max_landmarks) # Second stage using linear optimization - #refined_tour = refine_optimization(landmarks, base_tour, max_walking_time+detour, True) + refined_tour = refine_optimization(landmarks, base_tour, max_walking_time+detour, True) + + # Second stage using NetworkX + #refined_tour = refine_path(landmarks, base_tour, max_walking_time+detour, True) # Use NetworkX again to correct to shortest path - refined_tour = refine_path(landmarks, base_tour, max_walking_time+detour, True) + #refined_tour = refine_path(landmarks, base_tour, max_walking_time+detour, True) - return base_tour + return refined_tour #test4(tuple((48.8344400, 2.3220540))) # Café Chez César diff --git a/landmarks_Lyon.txt b/landmarks_Lyon.txt new file mode 100644 index 0000000..e896bc2 --- /dev/null +++ b/landmarks_Lyon.txt @@ -0,0 +1,3192 @@ +{ + "0":{ + "uuid":"7454fde3-0298-4ae5-be3e-22df8f1802df", + "name":"Musée de l'Imprimerie et de la communication graphique", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7642819, + 4.8347672 + ], + "osm_type":"way", + "osm_id":62271798, + "attractiveness":232, + "n_tags":19, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "1":{ + "uuid":"bafba7eb-8264-4668-ab7d-0c40e2975031", + "name":"Le Rectangle", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7576246, + 4.8308406 + ], + "osm_type":"way", + "osm_id":82255756, + "attractiveness":117, + "n_tags":7, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "2":{ + "uuid":"0d909bfe-c7f3-4a33-866f-8e0b312a8cee", + "name":"Lugdunum Musée et Théâtres Romains", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.760294, + 4.8204842 + ], + "osm_type":"way", + "osm_id":88526510, + "attractiveness":141, + "n_tags":11, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "3":{ + "uuid":"755cee4a-1e3c-450e-aebe-35c852ce39be", + "name":"Musée des Tissus et des Arts décoratifs", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7533775, + 4.8313403 + ], + "osm_type":"way", + "osm_id":188157107, + "attractiveness":136, + "n_tags":10, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "4":{ + "uuid":"3b73bd52-2221-4b91-9709-15254bf40630", + "name":"Musée des Beaux-Arts de Lyon", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7667066, + 4.8336192 + ], + "osm_type":"way", + "osm_id":493540665, + "attractiveness":184, + "n_tags":10, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "5":{ + "uuid":"3239f459-530d-47ee-b126-b3935d6931f5", + "name":"Musées Gadagne", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7640744, + 4.827301 + ], + "osm_type":"way", + "osm_id":700244364, + "attractiveness":268, + "n_tags":21, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "6":{ + "uuid":"086096b2-2efa-464a-878a-02449c19bf8d", + "name":"Centre d'histoire de la résistance et de la déportation", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7463685, + 4.8360993 + ], + "osm_type":"way", + "osm_id":700244374, + "attractiveness":100, + "n_tags":7, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "7":{ + "uuid":"02820afa-dc5d-49a8-9792-1d237fb2bca7", + "name":"Maison de Pauline Jaricot", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7615727, + 4.8239098 + ], + "osm_type":"way", + "osm_id":1228133941, + "attractiveness":122, + "n_tags":9, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "8":{ + "uuid":"b2800371-4c33-4d8a-9485-481339ecb89f", + "name":"Théâtre Gallo-Romain de Fourvière", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7597508, + 4.8197509 + ], + "osm_type":"way", + "osm_id":88526502, + "attractiveness":207, + "n_tags":9, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "9":{ + "uuid":"832bd769-d6b8-45f5-a12e-40b0531bd235", + "name":"Odéon", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7586995, + 4.8196811 + ], + "osm_type":"way", + "osm_id":88526506, + "attractiveness":143, + "n_tags":8, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "10":{ + "uuid":"976d54a2-2e37-461b-9ad8-5bc9f3c85a8b", + "name":"Jardin archéologique Girard Desargues", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7610597, + 4.8277478 + ], + "osm_type":"way", + "osm_id":88643315, + "attractiveness":120, + "n_tags":7, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "11":{ + "uuid":"142b75ce-b132-440a-8286-d595e78849ce", + "name":"Prétendu Sanctuaire de Cybèle", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7596018, + 4.8186245 + ], + "osm_type":"way", + "osm_id":268251012, + "attractiveness":163, + "n_tags":11, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "12":{ + "uuid":"90630b36-49ef-4f12-99bf-79468aff5469", + "name":"Basilique funéraire Saint-Laurent de Choulans", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7520124, + 4.8213215 + ], + "osm_type":"way", + "osm_id":459025768, + "attractiveness":225, + "n_tags":10, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "13":{ + "uuid":"e0d79f7f-57a0-40fb-b1fe-4328aaab63ba", + "name":"Traboule 27 Quai Saint Antoine - 58 rue Mercière", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7625096, + 4.8326187 + ], + "osm_type":"way", + "osm_id":1032384037, + "attractiveness":129, + "n_tags":7, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "14":{ + "uuid":"d6412b57-8059-4f80-8dbd-79dba699f5fb", + "name":"Longue Traboule", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7621601, + 4.8269716 + ], + "osm_type":"relation", + "osm_id":7850835, + "attractiveness":160, + "n_tags":8, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "15":{ + "uuid":"4b9ed8ac-d982-400f-ba8f-96ad58a26147", + "name":"Traboule du Gouverneur", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7638034, + 4.8286625 + ], + "osm_type":"relation", + "osm_id":13807547, + "attractiveness":130, + "n_tags":6, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "16":{ + "uuid":"9c7c04d5-527f-4392-88dc-1a8fe7f525a4", + "name":"Traboule 27 rue Saint Jean - 6 rue des Trois-Maries", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7627118, + 4.8280621 + ], + "osm_type":"relation", + "osm_id":13808273, + "attractiveness":132, + "n_tags":5, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "17":{ + "uuid":"ce21f65b-32b3-4acd-a68a-8f1c852cceff", + "name":"Château de Choulans", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7538042, + 4.819353 + ], + "osm_type":"way", + "osm_id":40557440, + "attractiveness":127, + "n_tags":3, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "18":{ + "uuid":"6ffab584-f372-4be9-932d-93896a789cb1", + "name":"Site archéologique de Saint-Just Les Minimes", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7555635, + 4.8174406 + ], + "osm_type":"way", + "osm_id":40557461, + "attractiveness":184, + "n_tags":13, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "19":{ + "uuid":"ae866632-245e-446f-90d6-19155873f8bc", + "name":"Château de La Motte", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7445966, + 4.8525807 + ], + "osm_type":"way", + "osm_id":64930077, + "attractiveness":136, + "n_tags":11, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "20":{ + "uuid":"27340432-8656-4e6a-83fc-d5cb5bfb3d6c", + "name":"Basilique Saint-Martin d'Ainay", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7536291, + 4.8273993 + ], + "osm_type":"way", + "osm_id":82254733, + "attractiveness":228, + "n_tags":17, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "21":{ + "uuid":"8893c479-0dd0-47a5-a74f-2e96b7e8178f", + "name":"Hôtel de Cuzieu", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7540396, + 4.8313596 + ], + "osm_type":"way", + "osm_id":82256947, + "attractiveness":146, + "n_tags":11, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "22":{ + "uuid":"fb7ba513-c126-4683-9697-a860a2e37efc", + "name":"Thou bout de chant", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7697155, + 4.8366984 + ], + "osm_type":"way", + "osm_id":84630357, + "attractiveness":218, + "n_tags":12, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "23":{ + "uuid":"76122f96-ea9e-488f-adc0-87f501016bc5", + "name":"DRAC Rhône-Alpes", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7695583, + 4.814804 + ], + "osm_type":"way", + "osm_id":85562889, + "attractiveness":212, + "n_tags":18, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "24":{ + "uuid":"92cf61d1-6056-4cb1-8256-175b99d6c361", + "name":"Thermes Gallo-romains", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.757, + 4.8198977 + ], + "osm_type":"way", + "osm_id":113126963, + "attractiveness":64, + "n_tags":4, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "25":{ + "uuid":"06473eab-ee69-4f0c-bc8b-6499aa6c5aed", + "name":"Amphithéâtre des Trois Gaules", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7706722, + 4.830327 + ], + "osm_type":"way", + "osm_id":113289145, + "attractiveness":157, + "n_tags":10, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "26":{ + "uuid":"96640c81-0b1b-48e8-ba22-0aa6d7626794", + "name":"Église Saint-Étienne", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7609634, + 4.8276164 + ], + "osm_type":"way", + "osm_id":113301270, + "attractiveness":105, + "n_tags":6, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "27":{ + "uuid":"84511829-b6e3-4478-aaa9-d12ecc67f19b", + "name":"Église Sainte-Croix de Lyon", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7611143, + 4.8278041 + ], + "osm_type":"way", + "osm_id":113301273, + "attractiveness":116, + "n_tags":7, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "28":{ + "uuid":"e6d11bd3-7888-4303-a194-7816e3b5c13b", + "name":"Catastrophe de Fourvière", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7610546, + 4.8256139 + ], + "osm_type":"way", + "osm_id":113302187, + "attractiveness":101, + "n_tags":7, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "29":{ + "uuid":"1bfc8967-7463-430c-b2b0-0b0e43c6aeab", + "name":"Venise A Lyon", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7642715, + 4.828111 + ], + "osm_type":"way", + "osm_id":125782680, + "attractiveness":165, + "n_tags":8, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "30":{ + "uuid":"7189b6ef-1648-42e0-b8a0-741e4d7b2a20", + "name":"Hôtel Bullioud", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.76544, + 4.827045 + ], + "osm_type":"way", + "osm_id":125783464, + "attractiveness":162, + "n_tags":9, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "31":{ + "uuid":"71f1f866-533f-4ae2-9ce6-be54ba23909e", + "name":"Palais archiépiscopal", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7601908, + 4.8278098 + ], + "osm_type":"way", + "osm_id":125802969, + "attractiveness":189, + "n_tags":12, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "32":{ + "uuid":"2f0b12b9-d0a3-4ddb-b658-1e1bab569b97", + "name":"Maison du Chamarier", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.76174, + 4.8271995 + ], + "osm_type":"way", + "osm_id":125803069, + "attractiveness":188, + "n_tags":12, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "33":{ + "uuid":"208defa1-5522-46a2-a9d2-5d7117198ac8", + "name":"Mausolée romain de Satrius", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7551097, + 4.8162429 + ], + "osm_type":"way", + "osm_id":190125626, + "attractiveness":186, + "n_tags":11, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "34":{ + "uuid":"5ddab589-5681-42e0-9682-a616c44ad0d2", + "name":"Mausolée romain du Sévir Turpio", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7552332, + 4.8160853 + ], + "osm_type":"way", + "osm_id":190125627, + "attractiveness":193, + "n_tags":11, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "35":{ + "uuid":"30dd9631-b85e-40b0-a7fb-6099d2ac0c75", + "name":"Mausolées romains de Julius Severianus, de Quintus Valerius et de Julia", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7553437, + 4.8159442 + ], + "osm_type":"way", + "osm_id":190125628, + "attractiveness":200, + "n_tags":11, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "36":{ + "uuid":"98ebb3ee-9707-41e2-959b-c865fc4c6ad3", + "name":"Site historique de Lyon", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7637183, + 4.8252905 + ], + "osm_type":"way", + "osm_id":236823541, + "attractiveness":226, + "n_tags":21, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "37":{ + "uuid":"6d0423e1-02e0-46fa-b815-c6f4636690c7", + "name":"Mémorial Lyonnais du Génocide Arménien", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7564328, + 4.8340045 + ], + "osm_type":"way", + "osm_id":265922306, + "attractiveness":187, + "n_tags":9, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "38":{ + "uuid":"99a11f4e-4a32-4c48-937a-f121a2361a01", + "name":"À la gloire de la République", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7506743, + 4.8281689 + ], + "osm_type":"way", + "osm_id":339555349, + "attractiveness":143, + "n_tags":10, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "39":{ + "uuid":"36094865-b6fe-4b51-b581-96fbece8653e", + "name":"Fort Saint-Jean", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7706087, + 4.8151585 + ], + "osm_type":"way", + "osm_id":512708363, + "attractiveness":82, + "n_tags":6, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "40":{ + "uuid":"2229dca1-13df-40b4-a8b0-62d8ea69c16c", + "name":"Hôtel de l'Europe", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7591956, + 4.8304294 + ], + "osm_type":"relation", + "osm_id":1237573, + "attractiveness":214, + "n_tags":12, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "41":{ + "uuid":"38ce22e3-6168-446a-8bb4-2bf8aa1dfe12", + "name":"Aqueduc du Gier", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.6046572, + 4.6533462 + ], + "osm_type":"relation", + "osm_id":1854628, + "attractiveness":65, + "n_tags":6, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "42":{ + "uuid":"90da3b73-f769-4b13-b18c-c7afb9513bd2", + "name":"Via Agrippa", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 47.3039138, + 5.6475217 + ], + "osm_type":"relation", + "osm_id":5612334, + "attractiveness":110, + "n_tags":11, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "43":{ + "uuid":"08dcdf57-8966-4a22-86a5-04b7096f210e", + "name":"Mausolées romains", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.755224, + 4.816097 + ], + "osm_type":"relation", + "osm_id":13996300, + "attractiveness":183, + "n_tags":10, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "44":{ + "uuid":"8b38b1b1-9ab7-4291-b8d6-673b08c8941d", + "name":"Basilique de Fourvière", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7622856, + 4.8225916 + ], + "osm_type":"way", + "osm_id":29179429, + "attractiveness":188, + "n_tags":13, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "45":{ + "uuid":"44c51c43-c5da-4237-bf64-55b2c73e6597", + "name":"Église Saint-Just", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7560083, + 4.8204446 + ], + "osm_type":"way", + "osm_id":40550808, + "attractiveness":251, + "n_tags":23, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "46":{ + "uuid":"ab503e7a-9e0b-47c4-971d-57b1ba52d99d", + "name":"Église Saint-Irénée", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7551213, + 4.8136558 + ], + "osm_type":"way", + "osm_id":40553093, + "attractiveness":166, + "n_tags":14, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "47":{ + "uuid":"628deaa6-f612-4ad6-9783-3224f6cdd53a", + "name":"Mosquée Koba", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7712638, + 4.8328767 + ], + "osm_type":"way", + "osm_id":42105115, + "attractiveness":149, + "n_tags":9, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "48":{ + "uuid":"71c5fe0d-3b2e-4b17-a33f-337a0f9f0c02", + "name":"Église Sainte-Blandine", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7449134, + 4.8236918 + ], + "osm_type":"way", + "osm_id":45173893, + "attractiveness":138, + "n_tags":10, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "49":{ + "uuid":"0027282b-7e18-4e3b-a21b-a37735f2cb0e", + "name":"Église Réformée", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7460575, + 4.8377789 + ], + "osm_type":"way", + "osm_id":47246832, + "attractiveness":140, + "n_tags":7, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "50":{ + "uuid":"b132c514-6812-4c88-b978-43f333adfa69", + "name":"Église Saint-Michel", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7464463, + 4.839245 + ], + "osm_type":"way", + "osm_id":48217383, + "attractiveness":161, + "n_tags":9, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "51":{ + "uuid":"b0cbc892-d910-4b77-b301-78f90c3297c2", + "name":"Église Saint-André", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7527396, + 4.8414988 + ], + "osm_type":"way", + "osm_id":52067460, + "attractiveness":154, + "n_tags":9, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "52":{ + "uuid":"270661dd-9d15-47fd-9789-4b46bd024c6f", + "name":"Église Protestante Chinoise de Lyon", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7525663, + 4.8405518 + ], + "osm_type":"way", + "osm_id":52438459, + "attractiveness":122, + "n_tags":6, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "53":{ + "uuid":"3ff2e971-0162-4a01-a9b9-66f57d721125", + "name":"Chapelle Saint-Thomas", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7620563, + 4.822602 + ], + "osm_type":"way", + "osm_id":60752859, + "attractiveness":179, + "n_tags":13, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "54":{ + "uuid":"8636c976-7342-49be-9749-7e3aa24b8fe5", + "name":"Église de l'Annonciation", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7478048, + 4.8461438 + ], + "osm_type":"way", + "osm_id":60792215, + "attractiveness":128, + "n_tags":9, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "55":{ + "uuid":"f8cac3dd-9d74-43df-8be9-8a9faf1a5163", + "name":"Église Saint-Paul", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7666454, + 4.827052 + ], + "osm_type":"way", + "osm_id":60798750, + "attractiveness":187, + "n_tags":13, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "56":{ + "uuid":"872d43b1-9e5c-4d41-bdb6-3ded2a7057df", + "name":"Église Saint-Nizier", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7647159, + 4.8336967 + ], + "osm_type":"way", + "osm_id":60801683, + "attractiveness":200, + "n_tags":15, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "57":{ + "uuid":"5d0190dd-fbf8-40c5-bb3d-ffcaea041a70", + "name":"Basilique Saint-Bonaventure", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.763057, + 4.8369311 + ], + "osm_type":"way", + "osm_id":60803772, + "attractiveness":288, + "n_tags":21, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "58":{ + "uuid":"d21d3aa5-ca50-4f49-a686-6ee6665b7cb3", + "name":"Église Sainte-Croix", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7514446, + 4.8300054 + ], + "osm_type":"way", + "osm_id":60807055, + "attractiveness":116, + "n_tags":8, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "59":{ + "uuid":"c0d85d19-f515-4e44-92ae-1d1677457c1b", + "name":"Église de l'Immaculée Conception", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7597907, + 4.8451318 + ], + "osm_type":"way", + "osm_id":60807892, + "attractiveness":143, + "n_tags":10, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "60":{ + "uuid":"c8fc859d-a98b-44fe-a2fa-92afc251f581", + "name":"Église Notre-Dame Saint-Louis", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7506538, + 4.8483835 + ], + "osm_type":"way", + "osm_id":60808540, + "attractiveness":147, + "n_tags":10, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "61":{ + "uuid":"e99e487f-66b2-4940-98b4-292857c29022", + "name":"Église Saint-Pothin", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7661734, + 4.846011 + ], + "osm_type":"way", + "osm_id":60809446, + "attractiveness":208, + "n_tags":14, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "62":{ + "uuid":"3fb925aa-4797-4b60-9d34-8e15fd4a0cf7", + "name":"Mission Ciel Ouvert", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7602786, + 4.8501284 + ], + "osm_type":"way", + "osm_id":62265756, + "attractiveness":104, + "n_tags":6, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "63":{ + "uuid":"d387edc1-af4b-43bc-a403-2c81bce484c9", + "name":"Grand Temple", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7619093, + 4.8416182 + ], + "osm_type":"way", + "osm_id":62265899, + "attractiveness":177, + "n_tags":14, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "64":{ + "uuid":"950e846a-10f0-42ec-bb77-96341a4d8a9a", + "name":"Église du Saint-Sacrement", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7577831, + 4.8515956 + ], + "osm_type":"way", + "osm_id":62279347, + "attractiveness":103, + "n_tags":8, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "65":{ + "uuid":"5c2ab4c1-df39-40cc-bad3-b512b8839aa5", + "name":"Église arménienne Saint-Jacques", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7544445, + 4.8520227 + ], + "osm_type":"way", + "osm_id":62280106, + "attractiveness":158, + "n_tags":12, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "66":{ + "uuid":"1e2ae2a5-aa4c-4807-a605-3ca26772079e", + "name":"Église Adventiste du 7e jour", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7703542, + 4.8444067 + ], + "osm_type":"way", + "osm_id":62291596, + "attractiveness":123, + "n_tags":7, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "67":{ + "uuid":"a8bec083-b8c3-4b76-bb92-907b51524920", + "name":"Église Luthérienne", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7641482, + 4.8439366 + ], + "osm_type":"way", + "osm_id":62293986, + "attractiveness":108, + "n_tags":7, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "68":{ + "uuid":"7853c61a-bc77-4437-a8f5-56dff0c30754", + "name":"Chapelle Sainte-Croix de Lyon", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7643865, + 4.8474914 + ], + "osm_type":"way", + "osm_id":62296662, + "attractiveness":128, + "n_tags":9, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "69":{ + "uuid":"e990182b-4470-469c-8ad3-72787d0b83c8", + "name":"Synagogue", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7515163, + 4.8508844 + ], + "osm_type":"way", + "osm_id":64930509, + "attractiveness":87, + "n_tags":5, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "70":{ + "uuid":"36e248b7-8440-466e-88a5-7ae8bd944d2b", + "name":"Mosquée El Oulfa", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7457844, + 4.8502008 + ], + "osm_type":"way", + "osm_id":64930572, + "attractiveness":153, + "n_tags":8, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "71":{ + "uuid":"b27cc0a7-3471-4bb9-b485-92b1671476af", + "name":"Paroisses de la Guillotière", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7515214, + 4.8477444 + ], + "osm_type":"way", + "osm_id":64931334, + "attractiveness":94, + "n_tags":6, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "72":{ + "uuid":"53d995c4-3b19-4bfa-944e-41c402d48953", + "name":"Chapelle Paul Couturier", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7522046, + 4.8273948 + ], + "osm_type":"way", + "osm_id":82252920, + "attractiveness":131, + "n_tags":8, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "73":{ + "uuid":"092ded51-fd2e-4576-b04e-0d566ca3add4", + "name":"Église Saint-François-de-Sales", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7553013, + 4.8318315 + ], + "osm_type":"way", + "osm_id":82260098, + "attractiveness":127, + "n_tags":9, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "74":{ + "uuid":"e7946da0-3129-41f8-a05a-668fa0983f29", + "name":"Temple de la Lanterne", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.766545, + 4.8315365 + ], + "osm_type":"way", + "osm_id":84629522, + "attractiveness":164, + "n_tags":9, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "75":{ + "uuid":"9e8ad52f-491f-4d19-8b11-3d597f74d83f", + "name":"Église Notre-Dame-Saint-Vincent", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.767545, + 4.8296684 + ], + "osm_type":"way", + "osm_id":84629829, + "attractiveness":226, + "n_tags":16, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "76":{ + "uuid":"c66e234c-8923-45cc-9ad7-82b3b2995487", + "name":"Église de la Scientologie", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7685969, + 4.8325018 + ], + "osm_type":"way", + "osm_id":84631580, + "attractiveness":135, + "n_tags":6, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "77":{ + "uuid":"e6e75556-9fa4-42b5-bb1e-ab0f1ffc3fd0", + "name":"Église Saint-Polycarpe", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7701427, + 4.8338043 + ], + "osm_type":"way", + "osm_id":84631832, + "attractiveness":249, + "n_tags":16, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "78":{ + "uuid":"38599ae3-e984-4428-8c5e-3885ddf86ecf", + "name":"Église La Bonne Nouvelle", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7685247, + 4.8263618 + ], + "osm_type":"way", + "osm_id":84727970, + "attractiveness":144, + "n_tags":8, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "79":{ + "uuid":"9e7e44a8-3dca-4d10-9403-0129f2ed28a4", + "name":"Sacré-Choeur des Chartreux", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7704787, + 4.8201398 + ], + "osm_type":"way", + "osm_id":85565189, + "attractiveness":73, + "n_tags":6, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "80":{ + "uuid":"cb1552b4-c143-4fcc-a076-7a98b109da0e", + "name":"Chapelle des Chartreux", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7706859, + 4.8243204 + ], + "osm_type":"way", + "osm_id":85634846, + "attractiveness":82, + "n_tags":6, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "81":{ + "uuid":"fbbe08f2-50ac-40c6-b274-093164d89880", + "name":"Chapelle de l'Hôtel-Dieu", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7591126, + 4.8363118 + ], + "osm_type":"way", + "osm_id":112734769, + "attractiveness":140, + "n_tags":10, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "82":{ + "uuid":"45a95bfb-feae-4052-9136-566962692727", + "name":"Chapelle de Marie-Thérèse", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7580796, + 4.822556 + ], + "osm_type":"way", + "osm_id":118530480, + "attractiveness":115, + "n_tags":6, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "83":{ + "uuid":"e29477f0-f18f-49c0-b7c0-ab5bf2f39c42", + "name":"Église Saint-Georges", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.757672, + 4.825388 + ], + "osm_type":"way", + "osm_id":122356299, + "attractiveness":248, + "n_tags":19, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "84":{ + "uuid":"891c42be-8e2d-42c7-8d41-0592f5429901", + "name":"Église Évangélique du Vieux Lyon", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7585855, + 4.8257545 + ], + "osm_type":"way", + "osm_id":122356387, + "attractiveness":174, + "n_tags":8, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "85":{ + "uuid":"40ee1646-0aaa-4fc0-aea0-4725f262dbf7", + "name":"Chapelle du Carmel", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.759811, + 4.8176605 + ], + "osm_type":"way", + "osm_id":125687396, + "attractiveness":105, + "n_tags":8, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "86":{ + "uuid":"315c31db-8b51-426e-afa3-31d0dfc73d55", + "name":"Temple du Change", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7644769, + 4.827924 + ], + "osm_type":"way", + "osm_id":125782865, + "attractiveness":242, + "n_tags":17, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "87":{ + "uuid":"177eb559-d05e-4bc6-b8c3-16653ff2753f", + "name":"Cathédrale Saint-Jean", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.760715, + 4.8274333 + ], + "osm_type":"way", + "osm_id":136200148, + "attractiveness":221, + "n_tags":18, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "88":{ + "uuid":"34152cb2-0cfa-479e-a20f-0033d05230fc", + "name":"Le Lien", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7467935, + 4.8168496 + ], + "osm_type":"way", + "osm_id":139266975, + "attractiveness":120, + "n_tags":10, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "89":{ + "uuid":"31d1bdc2-1f77-473b-9e10-1edc6b7738cc", + "name":"Chapelle du Bienheureux Père Chevrier", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7501397, + 4.842154 + ], + "osm_type":"way", + "osm_id":139900402, + "attractiveness":92, + "n_tags":5, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "90":{ + "uuid":"8b172619-a622-48c4-8a54-98581e8933c3", + "name":"Chapelle des Jésuites", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7558343, + 4.8287635 + ], + "osm_type":"way", + "osm_id":144904193, + "attractiveness":118, + "n_tags":8, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "91":{ + "uuid":"8c98bf09-b38a-471e-93ce-244067bc5e18", + "name":"Tour de la Force", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.762454, + 4.8220997 + ], + "osm_type":"way", + "osm_id":167592991, + "attractiveness":121, + "n_tags":7, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "92":{ + "uuid":"f4251476-282f-4ab5-820d-81d33ef8ec2d", + "name":"Tour de la Justice", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7622128, + 4.8220582 + ], + "osm_type":"way", + "osm_id":167592993, + "attractiveness":133, + "n_tags":8, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "93":{ + "uuid":"1bc409c8-c6e7-47f2-9601-8ab3dd765ef0", + "name":"Tour de la Prudence", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7623942, + 4.8228365 + ], + "osm_type":"way", + "osm_id":167592998, + "attractiveness":125, + "n_tags":8, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "94":{ + "uuid":"f4af1f2b-e855-444f-ae6a-77fa238f86d8", + "name":"Tour de la Tempérance", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7621519, + 4.8227997 + ], + "osm_type":"way", + "osm_id":167593000, + "attractiveness":131, + "n_tags":8, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "95":{ + "uuid":"1a75f855-3dd6-43ce-bca5-a1fdb5ab626d", + "name":"Chapelle Sainte-Claudine Thévenet", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7624916, + 4.8212685 + ], + "osm_type":"way", + "osm_id":246700929, + "attractiveness":133, + "n_tags":10, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "96":{ + "uuid":"a348ab4d-793b-416f-8b47-b7b7b8403a8d", + "name":"Église Saint-Bruno-les-Chartreux", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7710544, + 4.8222501 + ], + "osm_type":"relation", + "osm_id":1275295, + "attractiveness":175, + "n_tags":15, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "97":{ + "uuid":"dad56906-b1eb-4d32-9781-3e616a9a4ebb", + "name":"Basilique Notre-Dame de Fourvière", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7623011, + 4.822527 + ], + "osm_type":"relation", + "osm_id":3318254, + "attractiveness":216, + "n_tags":15, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "98":{ + "uuid":"a232d38a-f639-47c6-beb9-96f54fe75e69", + "name":"Grande Synagogue de Lyon", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7570917, + 4.8277901 + ], + "osm_type":"relation", + "osm_id":6693983, + "attractiveness":180, + "n_tags":12, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "99":{ + "uuid":"16b8b353-ff8f-470a-a471-258aed70680e", + "name":"Fontaine des Jacobins", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7605087, + 4.8335377 + ], + "osm_type":"way", + "osm_id":25101472, + "attractiveness":126, + "n_tags":7, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "100":{ + "uuid":"83fd5468-dd11-4198-b0ef-40b588a671c4", + "name":"Fontaine Bartholdi", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7676311, + 4.8334608 + ], + "osm_type":"way", + "osm_id":55071957, + "attractiveness":230, + "n_tags":18, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "101":{ + "uuid":"eb21bbfb-fe34-42f4-bf7f-39ea61830643", + "name":"Le bassin central de la place", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7604679, + 4.8356317 + ], + "osm_type":"way", + "osm_id":129172257, + "attractiveness":102, + "n_tags":6, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "102":{ + "uuid":"af185fbd-ce31-4603-a00b-579ebd65dc75", + "name":"Le Soleil", + "type":{ + "landmark_type":"sightseeing" + }, + "location":[ + 45.7682853, + 4.8361114 + ], + "osm_type":"way", + "osm_id":345622422, + "attractiveness":231, + "n_tags":8, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "103":{ + "uuid":"27ed74b6-c603-46fc-8caa-6080c7bace4d", + "name":"Jardin des Chartreux", + "type":{ + "landmark_type":"nature" + }, + "location":[ + 45.768828, + 4.8207569 + ], + "osm_type":"way", + "osm_id":23486517, + "attractiveness":84, + "n_tags":4, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "104":{ + "uuid":"57b105e2-047f-4c94-a2c1-cd4884871772", + "name":"Parc des Hauteurs et jardins du Rosaire", + "type":{ + "landmark_type":"nature" + }, + "location":[ + 45.7617042, + 4.8237768 + ], + "osm_type":"way", + "osm_id":29176499, + "attractiveness":91, + "n_tags":4, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "105":{ + "uuid":"038b620f-376f-4be6-9321-3e9f39f8cf7a", + "name":"Place Saint-Alexandre", + "type":{ + "landmark_type":"nature" + }, + "location":[ + 45.7560195, + 4.8153677 + ], + "osm_type":"way", + "osm_id":40635479, + "attractiveness":195, + "n_tags":3, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "106":{ + "uuid":"d7adb95d-ad83-47e9-847e-0a05a349ec6e", + "name":"Stade Vert Simone Prelle", + "type":{ + "landmark_type":"nature" + }, + "location":[ + 45.7581824, + 4.8483987 + ], + "osm_type":"way", + "osm_id":42360453, + "attractiveness":150, + "n_tags":3, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "107":{ + "uuid":"53fe1c4d-997f-401b-a0c8-c6b881de72f3", + "name":"Square Ollier", + "type":{ + "landmark_type":"nature" + }, + "location":[ + 45.7525682, + 4.8380744 + ], + "osm_type":"way", + "osm_id":48124213, + "attractiveness":116, + "n_tags":4, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "108":{ + "uuid":"0c7f034e-e895-4967-a3da-3eb477c05319", + "name":"Square Jules Guesde", + "type":{ + "landmark_type":"nature" + }, + "location":[ + 45.7477816, + 4.8385128 + ], + "osm_type":"way", + "osm_id":48164427, + "attractiveness":72, + "n_tags":2, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "109":{ + "uuid":"5e5d7005-e53d-4159-91b7-87ccb5a2c8f4", + "name":"Jardin Général Charles Delestraint", + "type":{ + "landmark_type":"nature" + }, + "location":[ + 45.76096, + 4.8416333 + ], + "osm_type":"way", + "osm_id":55346753, + "attractiveness":112, + "n_tags":5, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "110":{ + "uuid":"6e95d122-7bbd-4145-b7ca-28dda2c54303", + "name":"Square des Estrées", + "type":{ + "landmark_type":"nature" + }, + "location":[ + 45.7613238, + 4.8278969 + ], + "osm_type":"way", + "osm_id":55346762, + "attractiveness":90, + "n_tags":3, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "111":{ + "uuid":"31319e10-926c-484c-bbbd-9743ef069cd2", + "name":"Square Sainte-Marie Perrin", + "type":{ + "landmark_type":"nature" + }, + "location":[ + 45.7572662, + 4.8518593 + ], + "osm_type":"way", + "osm_id":55346766, + "attractiveness":68, + "n_tags":4, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "112":{ + "uuid":"6f6cb058-f0d5-4c58-b79d-f7d24b18cc1f", + "name":"Place Aristide Briand", + "type":{ + "landmark_type":"nature" + }, + "location":[ + 45.7531331, + 4.8497417 + ], + "osm_type":"way", + "osm_id":55556100, + "attractiveness":105, + "n_tags":4, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "113":{ + "uuid":"53d43546-ec5d-48c8-bf84-f28f0ce4c6da", + "name":"Place Général Delfosse", + "type":{ + "landmark_type":"nature" + }, + "location":[ + 45.746847, + 4.8175585 + ], + "osm_type":"way", + "osm_id":69421660, + "attractiveness":87, + "n_tags":5, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "114":{ + "uuid":"2cec8301-2124-4db0-903a-bb8146efaecf", + "name":"Square Pierre Pelloux", + "type":{ + "landmark_type":"nature" + }, + "location":[ + 45.7546295, + 4.8504647 + ], + "osm_type":"way", + "osm_id":83800993, + "attractiveness":99, + "n_tags":6, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "115":{ + "uuid":"c5d5f08a-55f2-4c8d-8293-7758ea5627bf", + "name":"Jardin Edison", + "type":{ + "landmark_type":"nature" + }, + "location":[ + 45.7580355, + 4.847964 + ], + "osm_type":"way", + "osm_id":89233053, + "attractiveness":117, + "n_tags":2, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "116":{ + "uuid":"e9b994bb-868a-4dae-a5e7-7e4410990c27", + "name":"Place Croix-Paquet", + "type":{ + "landmark_type":"nature" + }, + "location":[ + 45.770825, + 4.8363205 + ], + "osm_type":"way", + "osm_id":116075878, + "attractiveness":144, + "n_tags":3, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "117":{ + "uuid":"b2c43b64-2a94-4aa0-af94-256b0587ae45", + "name":"Square du Cardinal Villot", + "type":{ + "landmark_type":"nature" + }, + "location":[ + 45.7665566, + 4.8524217 + ], + "osm_type":"way", + "osm_id":122493458, + "attractiveness":104, + "n_tags":6, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "118":{ + "uuid":"f76f9b33-723c-4c03-8c6a-dfe58a5e7384", + "name":"Parc du musée Gadagne", + "type":{ + "landmark_type":"nature" + }, + "location":[ + 45.7640563, + 4.8270104 + ], + "osm_type":"way", + "osm_id":129147127, + "attractiveness":75, + "n_tags":2, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "119":{ + "uuid":"5a0f1750-e9da-47ca-9fdb-8aabebc0c2e4", + "name":"Square Louis Blanc", + "type":{ + "landmark_type":"nature" + }, + "location":[ + 45.7644092, + 4.8479543 + ], + "osm_type":"way", + "osm_id":167140503, + "attractiveness":176, + "n_tags":11, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "120":{ + "uuid":"1a8a2e68-e229-4567-aaa7-66aea6ebf6d4", + "name":"Place Gensoul", + "type":{ + "landmark_type":"nature" + }, + "location":[ + 45.7513414, + 4.8240688 + ], + "osm_type":"way", + "osm_id":182688463, + "attractiveness":192, + "n_tags":3, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "121":{ + "uuid":"e66c9e11-d346-4196-a733-bab6af23a430", + "name":"Square Jussieu", + "type":{ + "landmark_type":"nature" + }, + "location":[ + 45.7627451, + 4.8416091 + ], + "osm_type":"way", + "osm_id":200743579, + "attractiveness":112, + "n_tags":5, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "122":{ + "uuid":"c1062bfb-3bc8-4c51-863c-8bb1b283cc7a", + "name":"Place Raspail", + "type":{ + "landmark_type":"nature" + }, + "location":[ + 45.755203, + 4.8399633 + ], + "osm_type":"way", + "osm_id":200887568, + "attractiveness":109, + "n_tags":3, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "123":{ + "uuid":"9d961a35-82c4-4005-abd5-a44b5136d23c", + "name":"Jardin Pierre Thévenin", + "type":{ + "landmark_type":"nature" + }, + "location":[ + 45.7634725, + 4.8137224 + ], + "osm_type":"way", + "osm_id":233962981, + "attractiveness":58, + "n_tags":2, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "124":{ + "uuid":"4bd13c44-429e-4b4a-9398-069fc33c09ad", + "name":"Square du Béguin", + "type":{ + "landmark_type":"nature" + }, + "location":[ + 45.7481552, + 4.8500268 + ], + "osm_type":"way", + "osm_id":237182480, + "attractiveness":163, + "n_tags":11, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "125":{ + "uuid":"f614645a-b09f-45ff-b5a8-d2539f3b3bb4", + "name":"Le Jardin de la Visitation", + "type":{ + "landmark_type":"nature" + }, + "location":[ + 45.7589653, + 4.8161755 + ], + "osm_type":"way", + "osm_id":254282488, + "attractiveness":97, + "n_tags":5, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "126":{ + "uuid":"d5e5d0ee-5c30-4a68-a249-d1e948178e2c", + "name":"Jardin du Père François Marty", + "type":{ + "landmark_type":"nature" + }, + "location":[ + 45.7493343, + 4.8276949 + ], + "osm_type":"way", + "osm_id":272128357, + "attractiveness":320, + "n_tags":9, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "127":{ + "uuid":"270e1b3f-9b2f-4780-b636-dc0c2dbfd80a", + "name":"Place Camille Georges", + "type":{ + "landmark_type":"nature" + }, + "location":[ + 45.7455606, + 4.8209475 + ], + "osm_type":"way", + "osm_id":279569867, + "attractiveness":104, + "n_tags":5, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "128":{ + "uuid":"eef31dc0-6175-45fc-9175-e3b89bb6e4e1", + "name":"Jardin du Palais Saint-Pierre", + "type":{ + "landmark_type":"nature" + }, + "location":[ + 45.7668411, + 4.8336247 + ], + "osm_type":"way", + "osm_id":291674801, + "attractiveness":180, + "n_tags":4, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "129":{ + "uuid":"106cc113-83c1-4d4c-a4d2-cc93c18cce38", + "name":"Parc Sergent Blandan", + "type":{ + "landmark_type":"nature" + }, + "location":[ + 45.7452108, + 4.8546571 + ], + "osm_type":"way", + "osm_id":293179334, + "attractiveness":129, + "n_tags":9, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "130":{ + "uuid":"05f4d0b8-9657-4696-a011-7c348425b6af", + "name":"Square Charles Depéret", + "type":{ + "landmark_type":"nature" + }, + "location":[ + 45.7515927, + 4.8384466 + ], + "osm_type":"way", + "osm_id":295664354, + "attractiveness":151, + "n_tags":4, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "131":{ + "uuid":"0aa8398f-74b8-414e-8b89-345657dab222", + "name":"Square Marsha P. Johnson", + "type":{ + "landmark_type":"nature" + }, + "location":[ + 45.7704915, + 4.8329581 + ], + "osm_type":"way", + "osm_id":304778049, + "attractiveness":117, + "n_tags":3, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "132":{ + "uuid":"aa76704c-1fbf-4a19-aa17-42640873242e", + "name":"Place des Minimes et parc St Just", + "type":{ + "landmark_type":"nature" + }, + "location":[ + 45.7575707, + 4.821354 + ], + "osm_type":"way", + "osm_id":308117835, + "attractiveness":78, + "n_tags":3, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "133":{ + "uuid":"b382d3f0-79a4-4152-8cd3-784d76186c63", + "name":"Jardin Partagé Vol'Terre", + "type":{ + "landmark_type":"nature" + }, + "location":[ + 45.758104, + 4.8485947 + ], + "osm_type":"way", + "osm_id":378905897, + "attractiveness":127, + "n_tags":3, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "134":{ + "uuid":"b0526d78-50c7-408d-9200-982bdf4a26ed", + "name":"Jardin André Malraux", + "type":{ + "landmark_type":"nature" + }, + "location":[ + 45.7585166, + 4.8215829 + ], + "osm_type":"way", + "osm_id":416674640, + "attractiveness":138, + "n_tags":4, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "135":{ + "uuid":"5aa9656e-ee18-437e-84ec-ad47378e338b", + "name":"Piste de la Sarra", + "type":{ + "landmark_type":"nature" + }, + "location":[ + 45.7643365, + 4.8160108 + ], + "osm_type":"way", + "osm_id":439820822, + "attractiveness":33, + "n_tags":2, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "136":{ + "uuid":"8c7fc123-6438-44ad-addd-ba9e27fa522c", + "name":"Jardin aquatique Ouagadougou", + "type":{ + "landmark_type":"nature" + }, + "location":[ + 45.7440048, + 4.815635 + ], + "osm_type":"way", + "osm_id":440270630, + "attractiveness":50, + "n_tags":3, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "137":{ + "uuid":"f381651b-4692-4de1-a864-e93b9235093d", + "name":"Place Edgar Quinet", + "type":{ + "landmark_type":"nature" + }, + "location":[ + 45.7661152, + 4.845016 + ], + "osm_type":"way", + "osm_id":472459817, + "attractiveness":128, + "n_tags":6, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "138":{ + "uuid":"a96f249b-86cd-46e8-a332-173faa817129", + "name":"composte", + "type":{ + "landmark_type":"nature" + }, + "location":[ + 45.7458747, + 4.8473859 + ], + "osm_type":"way", + "osm_id":607937373, + "attractiveness":207, + "n_tags":11, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "139":{ + "uuid":"3ac36022-b67b-48cc-85c6-6137afa53496", + "name":"Clos Saint-Benoît", + "type":{ + "landmark_type":"nature" + }, + "location":[ + 45.7689459, + 4.8254867 + ], + "osm_type":"way", + "osm_id":670918822, + "attractiveness":88, + "n_tags":2, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "140":{ + "uuid":"5cc2ee8e-fb3e-4249-ba47-9f1afc778f0a", + "name":"Jardin Rozier", + "type":{ + "landmark_type":"nature" + }, + "location":[ + 45.7695654, + 4.8337167 + ], + "osm_type":"way", + "osm_id":682588806, + "attractiveness":147, + "n_tags":3, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "141":{ + "uuid":"2e48d409-a83c-49bf-a472-003bb87762d0", + "name":"Place du Maréchal Lyautey", + "type":{ + "landmark_type":"nature" + }, + "location":[ + 45.7685261, + 4.8420589 + ], + "osm_type":"relation", + "osm_id":4819638, + "attractiveness":130, + "n_tags":5, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "142":{ + "uuid":"8c5a036f-4299-4d00-984f-0c54055af147", + "name":"Jardin des Plantes", + "type":{ + "landmark_type":"nature" + }, + "location":[ + 45.7704653, + 4.8297596 + ], + "osm_type":"relation", + "osm_id":6825770, + "attractiveness":115, + "n_tags":6, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "143":{ + "uuid":"4485587a-0ef9-4f52-82d4-7710110b9f48", + "name":"Esplanade", + "type":{ + "landmark_type":"nature" + }, + "location":[ + 45.7603525, + 4.8205849 + ], + "osm_type":"way", + "osm_id":113305759, + "attractiveness":110, + "n_tags":8, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + }, + "144":{ + "uuid":"91e1fb48-e62f-4303-8494-1077f9afc4e6", + "name":"Grand Hôtel-Dieu", + "type":{ + "landmark_type":"shopping" + }, + "location":[ + 45.7586955, + 4.8364597 + ], + "osm_type":"relation", + "osm_id":300128, + "attractiveness":226, + "n_tags":19, + "image_url":null, + "description":null, + "duration":0, + "must_do":false, + "is_secondary":false, + "time_to_reach_next":0, + "next_uuid":null + } +} \ No newline at end of file