From e6e9bc1b79dea562dfe8f631590b31c3bda16889 Mon Sep 17 00:00:00 2001
From: Kilian Scheidecker <kilian.scheidecker@orange.fr>
Date: Tue, 4 Jun 2024 00:20:54 +0200
Subject: [PATCH] fixed input as coordinates

---
 .gitignore                         |   1 +
 backend/src/landmarks_manager.py   | 219 ++++++++++++++++++++++-------
 backend/src/main.py                |  35 ++++-
 backend/src/optimizer.py           |   4 +-
 backend/src/structs/landmarks.py   |  20 +--
 backend/src/structs/preferences.py |   8 +-
 backend/src/tester.py              |  67 +++++++++
 7 files changed, 274 insertions(+), 80 deletions(-)
 create mode 100644 .gitignore
 create mode 100644 backend/src/tester.py

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e934adf
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+cache/
diff --git a/backend/src/landmarks_manager.py b/backend/src/landmarks_manager.py
index 67141c7..3f8a62f 100644
--- a/backend/src/landmarks_manager.py
+++ b/backend/src/landmarks_manager.py
@@ -2,77 +2,190 @@ from OSMPythonTools.api import Api
 from OSMPythonTools.overpass import Overpass, overpassQueryBuilder, Nominatim
 from dataclasses import dataclass
 from pydantic import BaseModel
+import math as m
+from structs.landmarks import Landmark, LandmarkType
+from structs.preferences import Preferences, Preference
+from typing import List
+from typing import Tuple
+
+RADIUS = 0.0005             # size of the bbox in degrees. 0.0005 ~ 50m
+BBOX_SIDE = 10              # size of bbox in km for general area, 10km
+RADIUS_CLOSE_TO = 50       # size of area in m for close features, 5àm radius
+MIN_SCORE = 100             # discard elements with score < 100
+MIN_TAGS = 5                # discard elements withs less than 5 tags
 
 
-# Defines the landmark class (aka some place there is to visit)
-@dataclass
-class Landmarkkkk :
-    name : str
-    attractiveness : int
-    id : int
+# Include th json here
+# Create a list of all things to visit given some preferences and a city. Ready for the optimizer
+def generate_landmarks(coordinates: Tuple[float, float], preferences: Preferences) :
+
+    l_sights = ["'tourism'='museum'", "'tourism'='attraction'", "'tourism'='gallery'", 'historic', "'amenity'='arts_centre'", "'amenity'='planetarium'", "'amenity'='place_of_worship'", "'amenity'='fountain'", '"water"="reflecting_pool"'] 
+    l_nature = ["'leisure'='park'", 'geological', "'natural'='geyser'", "'natural'='hot_spring'", '"natural"="arch"', '"natural"="cave_entrance"', '"natural"="volcano"', '"natural"="stone"', '"tourism"="alpine_hut"', '"tourism"="picnic_site"', '"tourism"="viewpoint"', '"tourism"="zoo"', '"waterway"="waterfall"'] 
+    l_shop = ["'shop'='department_store'", "'shop'='mall'"] #, '"shop"="collector"', '"shop"="antiques"'] 
+  
+    # List for sightseeing
+    L1 = get_landmarks(coordinates, l_sights, LandmarkType(landmark_type='sightseeing'))
+    correct_score(L1, preferences.sightseeing)
     
-class Landmark(BaseModel) :
-    name : str
-    attractiveness : int
-    loc : tuple
+    # List for nature
+    L2 = get_landmarks(coordinates, l_nature, LandmarkType(landmark_type='nature'))
+    correct_score(L2, preferences.nature)
+    
+    # List for shopping
+    L3 = get_landmarks(coordinates, l_shop, LandmarkType(landmark_type='shopping'))
+    correct_score(L3, preferences.shopping)
 
-# Converts a OSM id to a landmark
-def add_from_id(id: int, score: int) :
+    L = L1 + L2 + L3
 
-    try :
-        s = 'way/' + str(id)           # prepare string for query
-        obj =  api.query(s)                             # object to add
+    return cleanup_list(L)
+
+# Determines if two locations are close to each other
+def is_close_to(loc1: Tuple[float, float], loc2: Tuple[float, float]) :
+
+    alpha = (180*RADIUS_CLOSE_TO)/(6371000*m.pi)
+    if abs(loc1[0] - loc2[0]) + abs(loc1[1] - loc2[1]) < alpha*2 :
+        return True
+    else : 
+        return False
+
+# Remove duplicate elements and elements with low score
+def cleanup_list(L: List[Landmark]) :
+    L_clean = []
+    names = []
+
+    for landmark in L :
+
+        if landmark.name in names :                 # Remove duplicates
+            continue     
+
+        elif landmark.attractiveness < MIN_SCORE :  # Remove uninteresting
+            continue
+
+        elif landmark.n_tags < MIN_TAGS :           # Remove uninteresting 2.0
+            continue
+
+        else :
+            names.append(landmark.name)
+            L_clean.append(landmark)
+    
+    return L_clean
+
+
+# Correct the score of a list of landmarks by taking into account preferences and the number of tags
+def correct_score(L: List[Landmark], preference: Preference) :
+
+    if len(L) == 0 :
+        return
+    
+    if L[0].type != preference.type :
+        raise TypeError(f"LandmarkType {preference.type} does not match the type of Landmark {L[0].name}")
+
+    for elem in L :
+        elem.attractiveness = int(elem.attractiveness/100) + elem.n_tags      # arbitrary correction of the balance score vs number of tags
+        elem.attractiveness = elem.attractiveness*preference.score        # arbitrary computation
+
+# Correct the score of a list of landmarks by taking into account preferences and the number of tags
+def correct_score_test(L: List[Landmark], preference: Preference) :
+
+    if len(L) == 0 :
+        return
+    
+    if L[0].type != preference.type :
+        raise TypeError(f"LandmarkType {preference.type} does not match the type of Landmark {L[0].name}")
+
+    for elem in L :
+        elem.attractiveness = int(elem.attractiveness/100) + elem.n_tags      # arbitrary correction of the balance score vs number of tags
+        elem.attractiveness = elem.attractiveness*preference.score        # arbitrary computation
+
+# Function to count elements within a 25m radius of a location
+def count_elements_within_radius(coordinates: Tuple[float, float]) -> int:
+    
+    lat = coordinates[0]
+    lon = coordinates[1]
+
+    bbox = {'latLower':lat-RADIUS,'lonLower':lon-RADIUS,'latHigher':lat+RADIUS,'lonHigher': lon+RADIUS}
+    overpass = Overpass()
+    
+    # Build the query to find elements within the radius
+    radius_query = overpassQueryBuilder(bbox=[bbox['latLower'],bbox['lonLower'],bbox['latHigher'],bbox['lonHigher']],
+                             elementType=['node', 'way', 'relation'])
+
+    try : 
+        radius_result = overpass.query(radius_query)
+    
+        # The count is the number of elements found
+        return radius_result.countElements()
+    
     except :
-        s = 'relation/' + str(id)           # prepare string for query
-        obj =  api.query(s)                             # object to add
+        return None
 
-    return Landmarkkkk(obj.tag('name:fr'), score, id)      # create Landmark out of it
+# Creates a bounding box around precise coordinates
+def create_bbox(coordinates: Tuple[float, float], side_length: int) -> Tuple[float, float, float, float]:
+    """
+    Create a simple bounding box around given coordinates.
+    :param coordinates: tuple (lat, lon)
+    -> lat: Latitude of the center point.
+    -> lon: Longitude of the center point.
+    :param side_length: int     - side length of the bbox in km
+    :return: Bounding box as (min_lat, min_lon, max_lat, max_lon).
+    """
+    lat = coordinates[0]
+    lon = coordinates[1]
 
+    # Half the side length in km (since it's a square bbox)
+    half_side_length_km = side_length / 2.0
+
+    # Convert distance to degrees
+    lat_diff = half_side_length_km / 111  # 1 degree latitude is approximately 111 km
+    lon_diff = half_side_length_km / (111 * m.cos(m.radians(lat)))  # Adjust for longitude based on latitude
+
+    # Calculate bbox
+    min_lat = lat - lat_diff
+    max_lat = lat + lat_diff
+    min_lon = lon - lon_diff
+    max_lon = lon + lon_diff
+
+    return min_lat, min_lon, max_lat, max_lon
+
+# Generates the list of landmarks for a given Landmarktype. Needs coordinates, a list of amenities and the corresponding LandmarkType
+def get_landmarks(coordinates: Tuple[float, float], l: List[Landmark], landmarktype: LandmarkType):
 
-def get_sights(city_country: str):
-    nominatim = Nominatim()
-    areaId = nominatim.query(city_country).areaId()
     overpass = Overpass()
 
-    # list of stuff we want to define as sights
-    l = ["'tourism'='museum'", "'tourism'='attraction'", "'tourism'='gallery'", 'historic', "'amenity'='arts_centre'", "'amenity'='planetarium'", '"amenity"="place_of_worship"'] 
-    score = 0
+    # Generate a bbox around currunt coordinates
+    bbox = create_bbox(coordinates, BBOX_SIDE)
+
+    # Initialize some variables
+    N = 0
+    L = []
 
     for amenity in l :
-        query = overpassQueryBuilder(area=areaId, elementType=['way', 'relation'], selector=amenity, includeGeometry=True)
+        query = overpassQueryBuilder(bbox=bbox, elementType=['way', 'relation'], selector=amenity, includeCenter=True, out='body')
         result = overpass.query(query)
-        score += result.countElements()
+        N += result.countElements()
 
-    return score
+        for elem in result.elements():
 
+            name = elem.tag('name')                             # Add name
+            location = (elem.centerLat(), elem.centerLon())     # Add coordinates (lat, lon)
 
-# take a lsit of tuples (id, score) to generate a list of landmarks
-def generate_landmarks(ids_and_scores: list) :
+            # skip if unprecise location
+            if name is None or location[0] is None:
+                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
+                score = count_elements_within_radius(location)
 
-    L = []
-    for tup in ids_and_scores :
-        L.append(add_from_id(tup[0], tup[1]))
+                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
-"""
-api = Api()
 
-
-l = (7515426, 70)
-t = (5013364, 100)
-n = (201611261, 99)
-a = (226413508, 50)
-m = (23762981, 30)
-
-
-ids_and_scores = [t, l, n, a, m]
-
-landmarks = generate_landmarks(ids_and_scores)
-
-
-for obj in landmarks :
-    print(obj)"""
-
-
-
-print(get_sights('Paris, France'))
\ No newline at end of file
diff --git a/backend/src/main.py b/backend/src/main.py
index d24346f..3118f96 100644
--- a/backend/src/main.py
+++ b/backend/src/main.py
@@ -1,7 +1,9 @@
 from optimizer import solve_optimization
+from landmarks_manager import generate_landmarks
 from structs.landmarks import LandmarkTest
 from structs.landmarks import Landmark
-from structs.preferences import Preferences
+from structs.landmarktype import LandmarkType
+from structs.preferences import Preferences, Preference
 from fastapi import FastAPI, Query, Body
 from typing import List
 
@@ -12,13 +14,22 @@ app = FastAPI()
 #"http://127.0.0.1:8000/process?param1={param1}&param2={param2}"
 # This should become main at some point
 @app.post("/optimizer/{longitude}/{latitude}")
-def main(longitude: float, latitude: float, prefrences: Preferences = Body(...)) -> List[Landmark]:
+def main(longitude: float, latitude: float, preferences: Preferences = Body(...)) -> List[Landmark]:
     # From frontend get longitude, latitude and prefence list
+    
+    # Generate the landmark list
+    landmarks = generate_landmarks(tuple((longitude, latitude)), preferences)
+
+    # Set the max distance
+    max_steps = 90
+
+    # Compute the visiting order
+    visiting_order = solve_optimization(landmarks, max_steps, True)
+
+    return visiting_order
 
-    landmarks = []
 
 
-    return landmarks
 
 @app.get("test")
 def test():
@@ -46,6 +57,16 @@ def test():
     #return("max steps :", max_steps, "\n", visiting_order)
 
 
-"""# keep this for debug
-if __name__ == "__main__":
-    main()"""
\ No newline at end of file
+# input city, country in the form of 'Paris, France'
+@app.post("/test2/{city_country}")
+def test2(city_country: str, preferences: Preferences = Body(...)) -> List[Landmark]:
+
+    landmarks = generate_landmarks(city_country, preferences)
+
+    max_steps = 9000000
+
+    visiting_order = solve_optimization(landmarks, max_steps, True)
+
+
+
+
diff --git a/backend/src/optimizer.py b/backend/src/optimizer.py
index 78f34a0..907647a 100644
--- a/backend/src/optimizer.py
+++ b/backend/src/optimizer.py
@@ -212,7 +212,7 @@ def init_ub_dist(landmarks: list, max_steps: int):
         dist_table = [0]*len(landmarks)
         c.append(-spot1.attractiveness)
         for j, spot2 in enumerate(landmarks) :
-            dist_table[j] = manhattan_distance(spot1.loc, spot2.loc)
+            dist_table[j] = manhattan_distance(spot1.location, spot2.location)
         A.append(dist_table)
     c = c*len(landmarks)
     A_ub = []
@@ -238,7 +238,7 @@ def respect_user_mustsee(landmarks: list, A_eq: list, b_eq: list) :
                 for k in range(L-1) :
                         l[k*L+L-1] = 1  
 
-            H += manhattan_distance(elem.loc, elem_prev.loc)
+            H += manhattan_distance(elem.location, elem_prev.location)
             elem_prev = elem
 
             """for i in range(7):
diff --git a/backend/src/structs/landmarks.py b/backend/src/structs/landmarks.py
index 48e6729..61715bf 100644
--- a/backend/src/structs/landmarks.py
+++ b/backend/src/structs/landmarks.py
@@ -1,4 +1,5 @@
 from pydantic import BaseModel
+from OSMPythonTools.api import Api
 from .landmarktype import LandmarkType
 from .preferences import Preferences
 
@@ -12,19 +13,10 @@ class Landmark(BaseModel) :
     name : str
     type: LandmarkType      # De facto mapping depending on how the query was executed with overpass. Should still EXACTLY correspond to the preferences
     location : tuple
-
-    # loop through the preferences and assign a score to the landmark
-    def score(self, preferences: Preferences):
-
-        for preference_name, preference in preferences.__dict__.items():
-            
-            if (preference_name == self.type.landmark_type) :
-                score = preference.score
-        
-        if (not score) :
-            raise Exception(f"Could not determine score for landmark {self.name}")
-
-        else :
-            return score
+    osm_type : str
+    osm_id : int
+    attractiveness : int
+    must_do : bool
+    n_tags : int
 
 
diff --git a/backend/src/structs/preferences.py b/backend/src/structs/preferences.py
index 94e0836..500ebec 100644
--- a/backend/src/structs/preferences.py
+++ b/backend/src/structs/preferences.py
@@ -3,8 +3,8 @@ from .landmarktype import LandmarkType
 
 class Preference(BaseModel) :
     name: str
-    type: LandmarkType
-    score: int
+    type: LandmarkType  # should match the attributes of the Preferences class
+    score: int          # score could be from 1 to 5
 
 # Input for optimization
 class Preferences(BaseModel) :
@@ -17,11 +17,11 @@ class Preferences(BaseModel) :
     # Shopping (diriger plutôt vers des zones / rues commerçantes)
     shopping : Preference
 
-    # Food (price low or high. Combien on veut dépenser pour manger à midi/soir)
+"""    # Food (price low or high. Combien on veut dépenser pour manger à midi/soir)
     food_budget : Preference
     
     # Tolérance au détour (ce qui détermine (+ ou -) le chemin emprunté)
-    detour_tol : Preference
+    detour_tol : Preference"""
 
 
 
diff --git a/backend/src/tester.py b/backend/src/tester.py
new file mode 100644
index 0000000..e531783
--- /dev/null
+++ b/backend/src/tester.py
@@ -0,0 +1,67 @@
+from optimizer import solve_optimization
+from landmarks_manager import generate_landmarks
+from structs.landmarks import LandmarkTest
+from structs.landmarks import Landmark
+from structs.landmarktype import LandmarkType
+from structs.preferences import Preferences, Preference
+from fastapi import FastAPI, Query, Body
+from typing import List
+
+
+def test3(city_country: str) -> List[Landmark]:
+
+
+    preferences = Preferences(
+                    sightseeing=Preference(
+                                  name='sightseeing', 
+                                  type=LandmarkType(landmark_type='sightseeing'),
+                                  score = 5),
+                    nature=Preference(
+                                  name='nature', 
+                                  type=LandmarkType(landmark_type='nature'),
+                                  score = 0),
+                    shopping=Preference(
+                                  name='shopping', 
+                                  type=LandmarkType(landmark_type='shopping'),
+                                  score = 5))
+
+    landmarks = generate_landmarks(city_country, preferences)
+
+    max_steps = 9
+
+    visiting_order = solve_optimization(landmarks, max_steps, True)
+
+    print(len(visiting_order))
+
+    return len(visiting_order)
+
+
+def test4(coordinates: tuple[float, float]) -> List[Landmark]:
+
+
+    preferences = Preferences(
+                    sightseeing=Preference(
+                                  name='sightseeing', 
+                                  type=LandmarkType(landmark_type='sightseeing'),
+                                  score = 5),
+                    nature=Preference(
+                                  name='nature', 
+                                  type=LandmarkType(landmark_type='nature'),
+                                  score = 0),
+                    shopping=Preference(
+                                  name='shopping', 
+                                  type=LandmarkType(landmark_type='shopping'),
+                                  score = 5))
+
+    landmarks = generate_landmarks(coordinates, preferences)
+
+    max_steps = 90
+
+    visiting_order = solve_optimization(landmarks, max_steps, True)
+
+    print(len(visiting_order))
+
+    return len(visiting_order)
+
+
+test3(tuple((48.834378, 2.322113)))
\ No newline at end of file