From 07dde5ab58f8408fc1bad24bb67c79ecbe31cc5d Mon Sep 17 00:00:00 2001
From: Remy Moll <me@moll.re>
Date: Wed, 31 Jul 2024 12:54:25 +0200
Subject: [PATCH 1/9] persistence for recurring api calls

---
 backend/Dockerfile                      |  1 +
 backend/Pipfile                         |  1 +
 backend/Pipfile.lock                    | 25 +++++++++++-----
 backend/src/constants.py                |  4 +++
 backend/src/main.py                     | 36 ++++++++++++++--------
 backend/src/persistence.py              | 18 +++++++++++
 backend/src/structs/linked_landmarks.py | 22 ++------------
 backend/src/structs/preferences.py      |  3 +-
 backend/src/structs/trip.py             | 28 +++++++++++++++++
 backend/src/tester.py                   | 21 ++++---------
 backend/src/utils/landmarks_manager.py  | 40 ++++++++++---------------
 11 files changed, 118 insertions(+), 81 deletions(-)
 create mode 100644 backend/src/persistence.py
 create mode 100644 backend/src/structs/trip.py

diff --git a/backend/Dockerfile b/backend/Dockerfile
index e399a4f..4a3ca4d 100644
--- a/backend/Dockerfile
+++ b/backend/Dockerfile
@@ -13,5 +13,6 @@ EXPOSE 8000
 # Set environment variables used by the deployment. These can be overridden by the user using this image.
 ENV NUM_WORKERS=1
 ENV OSM_CACHE_DIR=/cache
+ENV MEMCACHED_HOST=none
 
 CMD fastapi run src/main.py --port 8000 --workers $NUM_WORKERS
diff --git a/backend/Pipfile b/backend/Pipfile
index c590ded..1ca056b 100644
--- a/backend/Pipfile
+++ b/backend/Pipfile
@@ -14,3 +14,4 @@ shapely = "*"
 scipy = "*"
 osmpythontools = "*"
 pywikibot = "*"
+pymemcache = "*"
diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock
index 387fcbd..a6b756c 100644
--- a/backend/Pipfile.lock
+++ b/backend/Pipfile.lock
@@ -1,7 +1,7 @@
 {
     "_meta": {
         "hash": {
-            "sha256": "f0de801038593d42d8b780d14c2c72bb4f5f5e66df02f72244917ede5d5ebce6"
+            "sha256": "4f8b3f0395b4e5352330616870da13acf41e16d1b69ba31b15fd688e90b8b628"
         },
         "pipfile-spec": 6,
         "requires": {},
@@ -1102,6 +1102,15 @@
             "markers": "python_version >= '3.8'",
             "version": "==2.18.0"
         },
+        "pymemcache": {
+            "hashes": [
+                "sha256:27bf9bd1bbc1e20f83633208620d56de50f14185055e49504f4f5e94e94aff94",
+                "sha256:f507bc20e0dc8d562f8df9d872107a278df049fa496805c1431b926f3ddd0eab"
+            ],
+            "index": "pypi",
+            "markers": "python_version >= '3.7'",
+            "version": "==4.0.0"
+        },
         "pyparsing": {
             "hashes": [
                 "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad",
@@ -1142,12 +1151,12 @@
         },
         "pywikibot": {
             "hashes": [
-                "sha256:3f4fbc57f1765aa0fa1ccf84125bcfa475cae95b9cc0291867b751f3d4ac8fa2",
-                "sha256:a26d918cf88ef56fdb1421b65b09def200cc28031cdc922d72a4198fbfddd225"
+                "sha256:0dd8291f1a26abb9fce2c2108a90dc338274988e60d21723aec1d3b0de321b5e",
+                "sha256:7953fc4a6c498057e6eb7d9b762bbccb61348af0a599b89d7e246d5175b20a9b"
             ],
             "index": "pypi",
             "markers": "python_full_version >= '3.7.0'",
-            "version": "==9.2.1"
+            "version": "==9.3.0"
         },
         "pyyaml": {
             "hashes": [
@@ -1349,7 +1358,7 @@
                 "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d",
                 "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"
             ],
-            "markers": "python_version >= '3.8'",
+            "markers": "python_version < '3.13'",
             "version": "==4.12.2"
         },
         "tzdata": {
@@ -1658,11 +1667,11 @@
         },
         "xarray": {
             "hashes": [
-                "sha256:0b91e0bc4dc0296947947640fe31ec6e867ce258d2f7cbc10bedf4a6d68340c7",
-                "sha256:721a7394e8ec3d592b2d8ebe21eed074ac077dc1bb1bd777ce00e41700b4866c"
+                "sha256:1b0fd51ec408474aa1f4a355d75c00cc1c02bd425d97b2c2e551fd21810e7f64",
+                "sha256:4cae512d121a8522d41e66d942fb06c526bc1fd32c2c181d5fe62fe65b671638"
             ],
             "markers": "python_version >= '3.9'",
-            "version": "==2024.6.0"
+            "version": "==2024.7.0"
         }
     },
     "develop": {}
diff --git a/backend/src/constants.py b/backend/src/constants.py
index 1b57a3a..7118666 100644
--- a/backend/src/constants.py
+++ b/backend/src/constants.py
@@ -25,3 +25,7 @@ logging.config.dictConfig(config)
 # if we are in a debug session, set the log level to debug
 if os.getenv('DEBUG', False):
     logging.getLogger().setLevel(logging.DEBUG)
+
+MEMCACHE_HOST = os.getenv('MEMCACHE_HOST', None)
+if MEMCACHE_HOST == "none":
+    MEMCACHE_HOST = None
diff --git a/backend/src/main.py b/backend/src/main.py
index cb245cb..c59d864 100644
--- a/backend/src/main.py
+++ b/backend/src/main.py
@@ -1,12 +1,14 @@
 import logging
-from fastapi import FastAPI, Query, Body
+from fastapi import FastAPI, Query, Body, HTTPException
 
 from structs.landmark import Landmark
 from structs.preferences import Preferences
 from structs.linked_landmarks import LinkedLandmarks
+from structs.trip import Trip
 from utils.landmarks_manager import LandmarkManager
 from utils.optimizer import Optimizer
 from utils.refiner import Refiner
+from persistence import client as cache_client
 
 
 logger = logging.getLogger(__name__)
@@ -17,8 +19,8 @@ optimizer = Optimizer()
 refiner = Refiner(optimizer=optimizer)
 
 
-@app.post("/route/new")
-def get_route(preferences: Preferences, start: tuple[float, float], end: tuple[float, float] | None = None) -> str:
+@app.post("/trip/new")
+def new_trip(preferences: Preferences, start: tuple[float, float], end: tuple[float, float] | None = None) -> Trip:
     '''
     Main function to call the optimizer.
     :param preferences: the preferences specified by the user as the post body
@@ -47,22 +49,32 @@ def get_route(preferences: Preferences, start: tuple[float, float], end: tuple[f
     landmarks_short.insert(0, start_landmark)
     landmarks_short.append(end_landmark)
     
-    # TODO infer these parameters from the preferences
-    max_walking_time = 4    # hours
-    detour = 30             # minutes
-
     # First stage optimization
-    base_tour = optimizer.solve_optimization(max_walking_time*60, landmarks_short)
+    base_tour = optimizer.solve_optimization(preferences.max_time_minute, landmarks_short)
     
     # Second stage optimization
-    refined_tour = refiner.refine_optimization(landmarks, base_tour, max_walking_time*60, detour)
+    refined_tour = refiner.refine_optimization(landmarks, base_tour, preferences.max_time_minute, preferences.detour_tolerance_minute)
 
     linked_tour = LinkedLandmarks(refined_tour)
-    return linked_tour[0].uuid
+    # upon creation of the trip, persistence of both the trip and its landmarks is ensured. Ca
+    trip = Trip.from_linked_landmarks(linked_tour, cache_client)
+    return trip
 
 
+#### For already existing trips/landmarks
+@app.get("/trip/{trip_uuid}")
+def get_trip(trip_uuid: str) -> Trip:
+    try:
+        trip = cache_client.get(f"trip_{trip_uuid}")
+        return trip
+    except KeyError:
+        raise HTTPException(status_code=404, detail="Trip not found")
+
 
 @app.get("/landmark/{landmark_uuid}")
 def get_landmark(landmark_uuid: str) -> Landmark:
-    #cherche dans linked_tour et retourne le landmark correspondant
-    pass
+    try:
+        landmark = cache_client.get(f"landmark_{landmark_uuid}")
+        return landmark
+    except KeyError:
+        raise HTTPException(status_code=404, detail="Landmark not found")
\ No newline at end of file
diff --git a/backend/src/persistence.py b/backend/src/persistence.py
new file mode 100644
index 0000000..249edc6
--- /dev/null
+++ b/backend/src/persistence.py
@@ -0,0 +1,18 @@
+from pymemcache.client.base import Client
+
+import constants
+
+
+class DummyClient:
+    _data = {}
+    def set(self, key, value, **kwargs):
+        self._data[key] = value
+
+    def get(self, key, **kwargs):
+        return self._data[key]
+
+
+if constants.MEMCACHE_HOST is None:
+    client = DummyClient()
+else:
+    client = Client(constants.MEMCACHE_HOST, timeout=1)
diff --git a/backend/src/structs/linked_landmarks.py b/backend/src/structs/linked_landmarks.py
index a0799f3..7d350f5 100644
--- a/backend/src/structs/linked_landmarks.py
+++ b/backend/src/structs/linked_landmarks.py
@@ -1,4 +1,3 @@
-import uuid
 from .landmark import Landmark
 from utils.get_time_separation import get_time
 
@@ -9,8 +8,7 @@ class LinkedLandmarks:
     """
     
     _landmarks = list[Landmark]
-    total_time = int
-    uuid = str
+    total_time: int = 0
 
     def __init__(self, data: list[Landmark] = None) -> None:
         """
@@ -19,7 +17,6 @@ class LinkedLandmarks:
         Args:
             data (list[Landmark], optional): The list of landmarks that are linked together. Defaults to None.
         """
-        self.uuid = uuid.uuid4()
         self._landmarks = data if data else []
         self._link_landmarks()
 
@@ -28,7 +25,6 @@ class LinkedLandmarks:
         """
         Create the links between the landmarks in the list by setting their .next_uuid and the .time_to_next attributes.
         """
-        self.total_time = 0
         for i, landmark in enumerate(self._landmarks[:-1]):
             landmark.next_uuid = self._landmarks[i + 1].uuid
             time_to_next = get_time(landmark.location, self._landmarks[i + 1].location)
@@ -44,18 +40,4 @@ class LinkedLandmarks:
     
 
     def __str__(self) -> str:
-        return f"LinkedLandmarks, total time: {self.total_time} minutes, {len(self._landmarks)} stops: [{','.join([str(landmark) for landmark in self._landmarks])}]"
-    
-
-    def asdict(self) -> dict:
-        """
-        Convert the linked landmarks to a json serializable dictionary.
-        
-        Returns:
-            dict: A dictionary representation of the linked landmarks.
-        """
-        return {
-            'uuid': self.uuid,
-            'total_time': self.total_time,
-            'landmarks': [landmark.dict() for landmark in self._landmarks]
-        }
+        return f"LinkedLandmarks [{' ->'.join([str(landmark) for landmark in self._landmarks])}]"
diff --git a/backend/src/structs/preferences.py b/backend/src/structs/preferences.py
index d370fc8..ebb15b7 100644
--- a/backend/src/structs/preferences.py
+++ b/backend/src/structs/preferences.py
@@ -2,7 +2,6 @@ from pydantic import BaseModel
 from typing import Optional, Literal
 
 class Preference(BaseModel) :
-    name: str
     type: Literal['sightseeing', 'nature', 'shopping', 'start', 'finish']
     score: int          # score could be from 1 to 5
 
@@ -17,5 +16,5 @@ class Preferences(BaseModel) :
     # Shopping (diriger plutôt vers des zones / rues commerçantes)
     shopping : Preference
 
-    max_time_minute: Optional[int] = 6*60 
+    max_time_minute: Optional[int] = 6*60
     detour_tolerance_minute: Optional[int] = 0
diff --git a/backend/src/structs/trip.py b/backend/src/structs/trip.py
new file mode 100644
index 0000000..c2435a3
--- /dev/null
+++ b/backend/src/structs/trip.py
@@ -0,0 +1,28 @@
+from pydantic import BaseModel, Field
+
+from .landmark import Landmark
+from .linked_landmarks import LinkedLandmarks
+import uuid
+
+class Trip(BaseModel):
+    uuid: str = Field(default_factory=uuid.uuid4)
+    total_time: int
+    first_landmark_uuid: str
+
+
+    @classmethod
+    def from_linked_landmarks(self, landmarks: LinkedLandmarks, cache_client) -> "Trip":
+        """
+        Initialize a new Trip object and ensure it is stored in the cache.
+        """
+        trip = Trip(
+            total_time = landmarks.total_time,
+            first_landmark_uuid = str(landmarks[0].uuid)
+        )
+
+        # Store the trip in the cache
+        cache_client.set(f"trip_{trip.uuid}", trip)
+        for landmark in landmarks:
+            cache_client.set(f"landmark_{landmark.uuid}", landmark)
+
+        return trip
\ No newline at end of file
diff --git a/backend/src/tester.py b/backend/src/tester.py
index 8a39967..f83c4a7 100644
--- a/backend/src/tester.py
+++ b/backend/src/tester.py
@@ -20,22 +20,13 @@ def test(start_coords: tuple[float, float], finish_coords: tuple[float, float] =
 
     
     preferences = Preferences(
-                    sightseeing=Preference(
-                                  name='sightseeing', 
-                                  type='sightseeing',
-                                  score = 5),
-                    nature=Preference(
-                                  name='nature', 
-                                  type='nature',
-                                  score = 5),
-                    shopping=Preference(
-                                  name='shopping', 
-                                  type='shopping',
-                                  score = 5),
+        sightseeing=Preference(type='sightseeing', score = 5),
+        nature=Preference(type='nature', score = 5),
+        shopping=Preference(type='shopping', score = 5),
 
-                    max_time_minute=180,
-                    detour_tolerance_minute=30
-                    )
+        max_time_minute=180,
+        detour_tolerance_minute=30
+    )
 
     # Create start and finish 
     if finish_coords is None :
diff --git a/backend/src/utils/landmarks_manager.py b/backend/src/utils/landmarks_manager.py
index 2ba9100..2422c11 100644
--- a/backend/src/utils/landmarks_manager.py
+++ b/backend/src/utils/landmarks_manager.py
@@ -15,10 +15,6 @@ from .take_most_important import take_most_important
 import constants
 
 
-SIGHTSEEING = 'sightseeing'
-NATURE = 'nature'
-SHOPPING = 'shopping'
-
 
 
 class LandmarkManager:
@@ -74,25 +70,25 @@ class LandmarkManager:
         # list for sightseeing
         if preferences.sightseeing.score != 0:
             score_function = lambda loc, n_tags: int((self.count_elements_close_to(loc) + ((n_tags**1.2)*self.tag_coeff) )*self.church_coeff)  
-            L1 = self.fetch_landmarks(bbox, self.amenity_selectors['sightseeing'], SIGHTSEEING, score_function)
-            self.correct_score(L1, preferences.sightseeing)
+            L1 = self.fetch_landmarks(bbox, self.amenity_selectors['sightseeing'], preferences.sightseeing.type, score_function)
             L += L1
 
         # list for nature
         if preferences.nature.score != 0:
             score_function = lambda loc, n_tags: int((self.count_elements_close_to(loc) + ((n_tags**1.2)*self.tag_coeff) )*self.park_coeff)  
-            L2 = self.fetch_landmarks(bbox, self.amenity_selectors['nature'], NATURE, score_function)
-            self.correct_score(L2, preferences.nature)
+            L2 = self.fetch_landmarks(bbox, self.amenity_selectors['nature'], preferences.nature.type, score_function)
             L += L2
 
         # list for shopping
         if preferences.shopping.score != 0:
             score_function = lambda loc, n_tags: int(self.count_elements_close_to(loc) + ((n_tags**1.2)*self.tag_coeff))
-            L3 = self.fetch_landmarks(bbox, self.amenity_selectors['shopping'], SHOPPING, score_function)
-            self.correct_score(L3, preferences.shopping)
+            L3 = self.fetch_landmarks(bbox, self.amenity_selectors['shopping'], preferences.shopping.type, score_function)
             L += L3
 
+
         L = self.remove_duplicates(L)
+        self.correct_score(L, preferences)
+
         L_constrained = take_most_important(L, self.N_important)
         self.logger.info(f'Generated {len(L)} landmarks around {center_coordinates}, and constrained to {len(L_constrained)} most important ones.')
 
@@ -123,7 +119,7 @@ class LandmarkManager:
         return L_clean
         
 
-    def correct_score(self, landmarks: list[Landmark], preference: Preference):
+    def correct_score(self, landmarks: list[Landmark], preferences: Preferences) -> None:
         """
         Adjust the attractiveness score of each landmark in the list based on user preferences.
 
@@ -132,20 +128,16 @@ class LandmarkManager:
 
         Args:
             landmarks (list[Landmark]): A list of landmarks whose scores need to be corrected.
-            preference (Preference): The user's preference settings that influence the attractiveness score adjustment.
-
-        Raises:
-            TypeError: If the type of any landmark in the list does not match the expected type in the preference.
+            preferences (Preferences): The user's preference settings that influence the attractiveness score adjustment.
         """
 
-        if len(landmarks) == 0:
-            return
-        
-        if landmarks[0].type != preference.type:
-            raise TypeError(f"LandmarkType {preference.type} does not match the type of Landmark {landmarks[0].name}")
-
-        for elem in landmarks:
-            elem.attractiveness = int(elem.attractiveness*preference.score/5)     # arbitrary computation
+        score_dict = {
+            preferences.sightseeing.type: preferences.sightseeing.score,
+            preferences.nature.type: preferences.nature.score,
+            preferences.shopping.type: preferences.shopping.score
+        }
+        for landmark in landmarks:
+            landmark.attractiveness = int(landmark.attractiveness * score_dict[landmark.type] / 5)        
 
 
     def count_elements_close_to(self, coordinates: tuple[float, float]) -> int:
@@ -310,7 +302,7 @@ class LandmarkManager:
                         if "leisure" in tag and elem.tag('leisure') == "park":
                             elem_type = "nature"
 
-                    if landmarktype != SHOPPING:
+                    if landmarktype != "shopping":
                         if "shop" in tag:
                             skip = True
                             break
-- 
2.47.2


From 86bcec6b29170c9508b64bb8b18defbff9bc4fee Mon Sep 17 00:00:00 2001
From: Remy Moll <me@moll.re>
Date: Thu, 1 Aug 2024 14:39:15 +0200
Subject: [PATCH 2/9] frontend groundwork

---
 .gitea/workflows/frontend_build-android.yaml  |   4 +-
 backend/Dockerfile                            |   2 +-
 backend/src/constants.py                      |   6 +-
 backend/src/log_config.yaml                   |   1 +
 backend/src/persistence.py                    |  12 +-
 backend/src/structs/trip.py                   |  10 +-
 frontend/android/app/build.gradle             |   2 +-
 .../fast_network_navigation/MainActivity.kt   |   2 +-
 frontend/lib/modules/map.dart                 |  89 ++++--
 frontend/lib/pages/new_trip.dart              |  75 ++++-
 frontend/lib/structs/route.dart               |  14 -
 frontend/lib/utils/fetch_landmarks.dart       |  54 ----
 frontend/lib/utils/fetch_trip.dart            |  45 +++
 .../Flutter/GeneratedPluginRegistrant.swift   |   4 +
 frontend/pubspec.lock                         | 262 ++++++++++++++----
 frontend/pubspec.yaml                         |   3 +-
 16 files changed, 417 insertions(+), 168 deletions(-)
 delete mode 100644 frontend/lib/structs/route.dart
 delete mode 100644 frontend/lib/utils/fetch_landmarks.dart
 create mode 100644 frontend/lib/utils/fetch_trip.dart

diff --git a/.gitea/workflows/frontend_build-android.yaml b/.gitea/workflows/frontend_build-android.yaml
index 2979c23..fa89f2b 100644
--- a/.gitea/workflows/frontend_build-android.yaml
+++ b/.gitea/workflows/frontend_build-android.yaml
@@ -43,8 +43,10 @@ jobs:
       working-directory: ./frontend
 
     - name: Add required secrets
+      env:
+        ANDROID_SECRETS_PROPERTIES: ${{ secrets.ANDROID_SECRETS_PROPERTIES }}
       run: |
-        echo ${{ secrets.ANDROID_SECRETS_PROPERTIES }} > ./android/secrets.properties
+        echo "$ANDROID_SECRETS_PROPERTIES" >> ./android/secrets.properties
       working-directory: ./frontend
 
     - name: Sanity check
diff --git a/backend/Dockerfile b/backend/Dockerfile
index 4a3ca4d..25a5e31 100644
--- a/backend/Dockerfile
+++ b/backend/Dockerfile
@@ -13,6 +13,6 @@ EXPOSE 8000
 # Set environment variables used by the deployment. These can be overridden by the user using this image.
 ENV NUM_WORKERS=1
 ENV OSM_CACHE_DIR=/cache
-ENV MEMCACHED_HOST=none
+ENV MEMCACHED_HOST_PATH=none
 
 CMD fastapi run src/main.py --port 8000 --workers $NUM_WORKERS
diff --git a/backend/src/constants.py b/backend/src/constants.py
index 7118666..e9a4be2 100644
--- a/backend/src/constants.py
+++ b/backend/src/constants.py
@@ -26,6 +26,6 @@ logging.config.dictConfig(config)
 if os.getenv('DEBUG', False):
     logging.getLogger().setLevel(logging.DEBUG)
 
-MEMCACHE_HOST = os.getenv('MEMCACHE_HOST', None)
-if MEMCACHE_HOST == "none":
-    MEMCACHE_HOST = None
+MEMCACHED_HOST_PATH = os.getenv('MEMCACHED_HOST_PATH', None)
+if MEMCACHED_HOST_PATH == "none":
+    MEMCACHED_HOST_PATH = None
diff --git a/backend/src/log_config.yaml b/backend/src/log_config.yaml
index 3e9c977..4cb27f9 100644
--- a/backend/src/log_config.yaml
+++ b/backend/src/log_config.yaml
@@ -7,6 +7,7 @@ handlers:
   console:
     class: rich.logging.RichHandler
     formatter: simple
+    width: 255
   # access:
   #   class: logging.FileHandler
   #   filename: logs/access.log
diff --git a/backend/src/persistence.py b/backend/src/persistence.py
index 249edc6..21f41d6 100644
--- a/backend/src/persistence.py
+++ b/backend/src/persistence.py
@@ -8,11 +8,19 @@ class DummyClient:
     def set(self, key, value, **kwargs):
         self._data[key] = value
 
+    def set_many(self, data, **kwargs):
+        self._data.update(data)
+
     def get(self, key, **kwargs):
         return self._data[key]
 
 
-if constants.MEMCACHE_HOST is None:
+if constants.MEMCACHED_HOST_PATH is None:
     client = DummyClient()
 else:
-    client = Client(constants.MEMCACHE_HOST, timeout=1)
+    client = Client(
+        constants.MEMCACHED_HOST_PATH,
+        timeout=1,
+        allow_unicode_keys=True,
+        encoding='utf-8'
+    )
diff --git a/backend/src/structs/trip.py b/backend/src/structs/trip.py
index c2435a3..6d6e242 100644
--- a/backend/src/structs/trip.py
+++ b/backend/src/structs/trip.py
@@ -1,6 +1,6 @@
 from pydantic import BaseModel, Field
+from pymemcache.client.base import Client
 
-from .landmark import Landmark
 from .linked_landmarks import LinkedLandmarks
 import uuid
 
@@ -11,7 +11,7 @@ class Trip(BaseModel):
 
 
     @classmethod
-    def from_linked_landmarks(self, landmarks: LinkedLandmarks, cache_client) -> "Trip":
+    def from_linked_landmarks(self, landmarks: LinkedLandmarks, cache_client: Client) -> "Trip":
         """
         Initialize a new Trip object and ensure it is stored in the cache.
         """
@@ -22,7 +22,9 @@ class Trip(BaseModel):
 
         # Store the trip in the cache
         cache_client.set(f"trip_{trip.uuid}", trip)
-        for landmark in landmarks:
-            cache_client.set(f"landmark_{landmark.uuid}", landmark)
+        cache_client.set_many({f"landmark_{landmark.uuid}": landmark for landmark in landmarks}, expire=3600)
+        # is equivalent to:
+        # for landmark in landmarks:
+        #     cache_client.set(f"landmark_{landmark.uuid}", landmark, expire=3600)
 
         return trip
\ No newline at end of file
diff --git a/frontend/android/app/build.gradle b/frontend/android/app/build.gradle
index 7f145d2..c0f4089 100644
--- a/frontend/android/app/build.gradle
+++ b/frontend/android/app/build.gradle
@@ -61,7 +61,7 @@ android {
 
     defaultConfig {
         // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
-        applicationId "com.example.anyway"
+        applicationId "com.anydev.anyway"
         // You can update the following values to match your application needs.
         // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
         // Minimum Android version for Google Maps SDK
diff --git a/frontend/android/app/src/main/kotlin/com/example/fast_network_navigation/MainActivity.kt b/frontend/android/app/src/main/kotlin/com/example/fast_network_navigation/MainActivity.kt
index 0cddadf..95565a7 100644
--- a/frontend/android/app/src/main/kotlin/com/example/fast_network_navigation/MainActivity.kt
+++ b/frontend/android/app/src/main/kotlin/com/example/fast_network_navigation/MainActivity.kt
@@ -1,4 +1,4 @@
-package com.example.anyway
+package com.anydev.anyway
 
 import io.flutter.embedding.android.FlutterActivity
 
diff --git a/frontend/lib/modules/map.dart b/frontend/lib/modules/map.dart
index 339792c..d0ab511 100644
--- a/frontend/lib/modules/map.dart
+++ b/frontend/lib/modules/map.dart
@@ -1,9 +1,10 @@
 import 'dart:collection';
 
+import 'package:flutter/material.dart';
 import 'package:anyway/structs/landmark.dart';
 import 'package:anyway/structs/trip.dart';
-import 'package:flutter/material.dart';
 import 'package:google_maps_flutter/google_maps_flutter.dart';
+import 'package:the_widget_marker/the_widget_marker.dart';
 
 class MapWidget extends StatefulWidget {
 
@@ -25,6 +26,7 @@ class _MapWidgetState extends State<MapWidget> {
     zoom: 11.0,
   );
   Set<Marker> markers = <Marker>{};
+  final GlobalKey globalKey = GlobalKey();
   
 
   void _onMapCreated(GoogleMapController controller) async {
@@ -49,28 +51,81 @@ class _MapWidgetState extends State<MapWidget> {
     Trip? trip = await widget.trip;
     LinkedList<Landmark>? landmarks = trip?.landmarks;
     if (landmarks != null){
-      setState(() {
-        for (Landmark landmark in landmarks) {
-          markers.add(Marker(
-            markerId: MarkerId(landmark.name),
-            position: LatLng(landmark.location[0], landmark.location[1]),
-            infoWindow: InfoWindow(title: landmark.name, snippet: landmark.type.name),
-          ));
-        }
-      });
+      for (Landmark landmark in landmarks) {
+        markers.add(Marker(
+          markerId: MarkerId(landmark.name),
+          position: LatLng(landmark.location[0], landmark.location[1]),
+          // infoWindow: InfoWindow(title: landmark.name, snippet: landmark.type.name),
+          icon: await MarkerIcon.widgetToIcon(globalKey),
+        ));
+      }
+      setState(() {});
     }
   }
 
 
   @override
   Widget build(BuildContext context) {
-    return GoogleMap(
-      onMapCreated: _onMapCreated,
-      initialCameraPosition: _cameraPosition,
-      onCameraIdle: _onCameraIdle,
-      // onLongPress: ,
-      markers: markers,
-      cloudMapId: '41c21ac9b81dbfd8',
+    return Stack(
+      children: [
+        MyMarker(globalKey),
+
+        GoogleMap(
+              onMapCreated: _onMapCreated,
+              initialCameraPosition: _cameraPosition,
+              onCameraIdle: _onCameraIdle,
+              // onLongPress: ,
+              markers: markers,
+              cloudMapId: '41c21ac9b81dbfd8',
+            )
+      ]
     );
   }
 }
+
+
+class MyMarker extends StatelessWidget {
+  // declare a global key and get it trough Constructor
+
+  MyMarker(this.globalKeyMyWidget);
+  final GlobalKey globalKeyMyWidget;
+
+  @override
+  Widget build(BuildContext context) {
+    // This returns an outlined circle, with an icon corresponding to the landmark type
+    // As a small dot, the number of the landmark is displayed in the top right
+    return RepaintBoundary(
+      key: globalKeyMyWidget,
+      child: Stack(
+        children: [
+          Container(
+            width: 75,
+            height: 75,
+            decoration: BoxDecoration(
+              gradient: LinearGradient(
+                begin: Alignment.topLeft,
+                end: Alignment.bottomRight,
+                colors: [Colors.red, Colors.yellow]
+              ),
+              shape: BoxShape.circle,
+              border: Border.all(color: Colors.black, width: 5),
+            ),
+            child: Icon(Icons.location_on, color: Colors.black, size: 50),
+          ),
+          Positioned(
+            top: 0,
+            right: 0,
+            child: Container(
+              padding: EdgeInsets.all(5),
+              decoration: BoxDecoration(
+                color: Theme.of(context).primaryColor,
+                shape: BoxShape.circle,
+              ),
+              child: Text('1', style: TextStyle(color: Colors.white, fontSize: 20)),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}
\ No newline at end of file
diff --git a/frontend/lib/pages/new_trip.dart b/frontend/lib/pages/new_trip.dart
index 47e2c5b..a10a046 100644
--- a/frontend/lib/pages/new_trip.dart
+++ b/frontend/lib/pages/new_trip.dart
@@ -1,5 +1,11 @@
 
+import 'package:anyway/layout.dart';
+import 'package:anyway/structs/preferences.dart';
+import 'package:anyway/utils/fetch_trip.dart';
 import 'package:flutter/material.dart';
+import "package:anyway/structs/trip.dart";
+
+
 
 class NewTripPage extends StatefulWidget {
   const NewTripPage({Key? key}) : super(key: key);
@@ -9,22 +15,71 @@ class NewTripPage extends StatefulWidget {
 }
 
 class _NewTripPageState extends State<NewTripPage> {
+  final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
+  final TextEditingController latController = TextEditingController();
+  final TextEditingController lonController = TextEditingController();
+
   @override
   Widget build(BuildContext context) {
     return Scaffold(
       appBar: AppBar(
         title: const Text('New Trip'),
       ),
-      body: Center(
-        child: Column(
-          mainAxisAlignment: MainAxisAlignment.center,
-          children: <Widget>[
-            const Text(
-              'Create a new trip',
+      body: Form(
+        key: _formKey,
+        child: 
+          Padding(
+            padding: const EdgeInsets.all(15.0),
+            child: Column(
+              crossAxisAlignment: CrossAxisAlignment.start,
+              
+              children: <Widget>[
+                TextFormField(
+                  decoration: const InputDecoration(hintText: 'Lat'),
+                  controller: latController,
+                  validator: (String? value) {
+                    if (value == null || value.isEmpty || double.tryParse(value) == null){
+                      return 'Please enter a floating point number';
+                    }
+                    return null;
+                  },
+                ),
+                TextFormField(
+                  decoration: const InputDecoration(hintText: 'Lon'),
+                  controller: lonController,
+
+                  validator: (String? value) {
+                    if (value == null || value.isEmpty || double.tryParse(value) == null){
+                      return 'Please enter a floating point number';
+                    }
+                    return null;
+                  },
+                ),
+                Divider(height: 15, color: Colors.transparent),
+                ElevatedButton(
+                  onPressed: () {
+                    if (_formKey.currentState!.validate()) {
+                      List<double> startPoint = [
+                        double.parse(latController.text),
+                        double.parse(lonController.text)
+                      ];
+                      UserPreferences preferences = UserPreferences();
+                      preferences.load();
+                      Future<Trip> trip = fetchTrip(startPoint, preferences);
+                        Navigator.of(context).push(
+                          MaterialPageRoute(
+                            builder: (context) => BasePage(mainScreen: "map", trip: trip)
+                          )
+                        );
+                    }
+                  },
+                  child: const Text('Create trip'),
+                ),
+              ],
             ),
-          ],
-        ),
-      ),
+          )
+        
+      )
     );
   }
-}
\ No newline at end of file
+}
diff --git a/frontend/lib/structs/route.dart b/frontend/lib/structs/route.dart
deleted file mode 100644
index 20b1625..0000000
--- a/frontend/lib/structs/route.dart
+++ /dev/null
@@ -1,14 +0,0 @@
-import "package:anyway/structs/landmark.dart";
-
-
-class Route {
-  final String name;
-  final Duration duration;
-  final List<Landmark> landmarks;
-
-  Route({
-    required this.name,
-    required this.duration,
-    required this.landmarks
-  });
-}
\ No newline at end of file
diff --git a/frontend/lib/utils/fetch_landmarks.dart b/frontend/lib/utils/fetch_landmarks.dart
deleted file mode 100644
index c014f5d..0000000
--- a/frontend/lib/utils/fetch_landmarks.dart
+++ /dev/null
@@ -1,54 +0,0 @@
-import "package:anyway/structs/landmark.dart";
-import "package:anyway/structs/linked_landmarks.dart";
-import 'package:dio/dio.dart';
-
-final dio = Dio();
-
-// Future<List<Landmark>> fetchLandmarks() async {
-//   // final response = await http
-//   //     .get(Uri.parse('https://nav.kluster.moll.re/v1/destination/1'));
-
-//   // if (response.statusCode == 200) {
-//     // If the server did return a 200 OK response,
-//     // then parse the JSON.
-//     List<Landmark> landmarks = [
-//       // 48°51′29.6″N 2°17′40.2″E
-//       Landmark(
-//         name: "Eiffel Tower",
-//         location: [48.51296, 2.17402],
-//         type: LandmarkType(name: "Tower"),
-//         imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a8/Tour_Eiffel_Wikimedia_Commons.jpg/1037px-Tour_Eiffel_Wikimedia_Commons.jpg"
-//         ),
-//       Landmark(
-//         name: "Notre Dame Cathedral",
-//         location: [48.8530, 2.3498],
-//         type: LandmarkType(name: "Monument"),
-//         imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f7/Notre-Dame_de_Paris%2C_4_October_2017.jpg/440px-Notre-Dame_de_Paris%2C_4_October_2017.jpg"
-//         ),
-//       Landmark(
-//         name: "Louvre palace",
-//         location: [48.8606, 2.3376],
-//         type: LandmarkType(name: "Museum"),
-//         imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/6/66/Louvre_Museum_Wikimedia_Commons.jpg/540px-Louvre_Museum_Wikimedia_Commons.jpg"
-//         ),
-//       Landmark(
-//         name: "Pont-des-arts",
-//         location: [48.5130, 2.2015],
-//         type: LandmarkType(name: "Bridge"),
-//         imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d1/Pont_des_Arts%2C_6e_Arrondissement%2C_Paris_%28HDR%29_20140320_1.jpg/560px-Pont_des_Arts%2C_6e_Arrondissement%2C_Paris_%28HDR%29_20140320_1.jpg"),
-//       Landmark(
-//         name: "Panthéon",
-//         location: [48.5046, 2.2046],
-//         type: LandmarkType(name: "Monument"),
-//         imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/8/80/Pantheon_of_Paris_007.JPG/1280px-Pantheon_of_Paris_007.JPG"
-//         ),
-//     ];
-//     // sleep 10 seconds
-//     await Future.delayed(Duration(seconds: 5));
-//     return landmarks;
-//   // } else {
-//   //   // If the server did not return a 200 OK response,
-//   //   // then throw an exception.
-//   //   throw Exception('Failed to load destination');
-//   // }
-// }
\ No newline at end of file
diff --git a/frontend/lib/utils/fetch_trip.dart b/frontend/lib/utils/fetch_trip.dart
new file mode 100644
index 0000000..fd55751
--- /dev/null
+++ b/frontend/lib/utils/fetch_trip.dart
@@ -0,0 +1,45 @@
+import 'package:dio/dio.dart';
+import 'package:anyway/constants.dart';
+import "package:anyway/structs/landmark.dart";
+import "package:anyway/structs/trip.dart";
+import "package:anyway/structs/preferences.dart";
+
+import "package:anyway/structs/linked_landmarks.dart";
+
+Dio dio = Dio(
+    BaseOptions(
+      baseUrl: API_URL_BASE,
+      connectTimeout: const Duration(seconds: 5),
+      receiveTimeout: const Duration(seconds: 60),
+      // api is notoriously slow
+      // headers: {
+      //   HttpHeaders.userAgentHeader: 'dio',
+      //   'api': '1.0.0',
+      // },
+      contentType: Headers.jsonContentType,
+      responseType: ResponseType.json,
+  ),
+);
+
+Future<Trip> fetchTrip(
+  List<double> startPoint,
+  UserPreferences preferences,
+) async {
+  final response = await dio.post(
+    "/trip/new",
+    data: {
+      // 'preferences': preferences.toJson(),
+      'start': [48,2.3]
+    }
+  );
+
+  // handle errors
+  if (response.statusCode != 200) {
+    throw Exception('Failed to load trip');
+  }
+  if (response.data["error"] != null) {
+    throw Exception(response.data["error"]);
+  }
+  return Trip.fromJson(response.data);
+}
+
diff --git a/frontend/macos/Flutter/GeneratedPluginRegistrant.swift b/frontend/macos/Flutter/GeneratedPluginRegistrant.swift
index 724bb2a..eefcc6d 100644
--- a/frontend/macos/Flutter/GeneratedPluginRegistrant.swift
+++ b/frontend/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -5,8 +5,12 @@
 import FlutterMacOS
 import Foundation
 
+import path_provider_foundation
 import shared_preferences_foundation
+import sqflite
 
 func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
+  PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
   SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
+  SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
 }
diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock
index eeca771..8d3dc88 100644
--- a/frontend/pubspec.lock
+++ b/frontend/pubspec.lock
@@ -1,6 +1,14 @@
 # Generated by pub
 # See https://dart.dev/tools/pub/glossary#lockfile
 packages:
+  args:
+    dependency: transitive
+    description:
+      name: args
+      sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.5.0"
   async:
     dependency: transitive
     description:
@@ -41,6 +49,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.18.0"
+  crypto:
+    dependency: transitive
+    description:
+      name: crypto
+      sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.0.3"
   csslib:
     dependency: transitive
     description:
@@ -97,11 +113,27 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "7.0.0"
+  fixnum:
+    dependency: transitive
+    description:
+      name: fixnum
+      sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.1.0"
   flutter:
     dependency: "direct main"
     description: flutter
     source: sdk
     version: "0.0.0"
+  flutter_cache_manager:
+    dependency: transitive
+    description:
+      name: flutter_cache_manager
+      sha256: a77f77806a790eb9ba0118a5a3a936e81c4fea2b61533033b2b0c3d50bbde5ea
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.4.0"
   flutter_lints:
     dependency: "direct dev"
     description:
@@ -114,10 +146,18 @@ packages:
     dependency: transitive
     description:
       name: flutter_plugin_android_lifecycle
-      sha256: "8cf40eebf5dec866a6d1956ad7b4f7016e6c0cc69847ab946833b7d43743809f"
+      sha256: "9d98bd47ef9d34e803d438f17fd32b116d31009f534a6fa5ce3a1167f189a6de"
       url: "https://pub.dev"
     source: hosted
-    version: "2.0.19"
+    version: "2.0.21"
+  flutter_svg:
+    dependency: transitive
+    description:
+      name: flutter_svg
+      sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.0.10+1"
   flutter_test:
     dependency: "direct dev"
     description: flutter
@@ -132,50 +172,50 @@ packages:
     dependency: transitive
     description:
       name: google_maps
-      sha256: "47eef3836b49bb030d5cb3afc60b8451408bf34cf753e571b645d6529eb4251a"
+      sha256: "463b38e5a92a05cde41220a11fd5eef3847031fef3e8cf295ac76ec453246907"
       url: "https://pub.dev"
     source: hosted
-    version: "7.1.0"
+    version: "8.0.0"
   google_maps_flutter:
     dependency: "direct main"
     description:
       name: google_maps_flutter
-      sha256: c1972cbad779bc5346c49045f26ae45550a0958b1cbca5b524dd3c8954995d28
+      sha256: acf0ec482d86b2ac55ade80597ce7f797a47971f5210ebfd030f0d58130e0a94
       url: "https://pub.dev"
     source: hosted
-    version: "2.6.1"
+    version: "2.7.0"
   google_maps_flutter_android:
     dependency: transitive
     description:
       name: google_maps_flutter_android
-      sha256: "0bcadb80eba39afda77dede89a6caafd3b68f2786b90491eceea4a01c3db181c"
+      sha256: "5d444f4135559488d7ea325eae710ae3284e6951b1b61729a0ac026456fe1548"
       url: "https://pub.dev"
     source: hosted
-    version: "2.8.0"
+    version: "2.12.1"
   google_maps_flutter_ios:
     dependency: transitive
     description:
       name: google_maps_flutter_ios
-      sha256: e5132d17f051600d90d79d9f574b177c24231da702453a036db2490f9ced4646
+      sha256: a6e3c6ecdda6c985053f944be13a0645ebb919da2ef0f5bc579c5e1670a5b2a8
       url: "https://pub.dev"
     source: hosted
-    version: "2.6.0"
+    version: "2.10.0"
   google_maps_flutter_platform_interface:
     dependency: transitive
     description:
       name: google_maps_flutter_platform_interface
-      sha256: "167af879da4d004cd58771f1469b91dcc3b9b0a2c5334cc6bf71fd41d4b35403"
+      sha256: bd60ca330e3c7763b95b477054adec338a522d982af73ecc520b232474063ac5
       url: "https://pub.dev"
     source: hosted
-    version: "2.6.0"
+    version: "2.8.0"
   google_maps_flutter_web:
     dependency: transitive
     description:
       name: google_maps_flutter_web
-      sha256: "0c0d5c723d94b295cf86dd1c45ff91d2ac1fff7c05ddca4f01bef9fa0a014690"
+      sha256: "8d5d0f58bfc4afac0bbe3d399f2018fcea691e3ea3d35254b7aae56df5827659"
       url: "https://pub.dev"
     source: hosted
-    version: "0.5.7"
+    version: "0.5.9+1"
   html:
     dependency: transitive
     description:
@@ -188,10 +228,10 @@ packages:
     dependency: "direct main"
     description:
       name: http
-      sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938"
+      sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
       url: "https://pub.dev"
     source: hosted
-    version: "1.2.1"
+    version: "1.2.2"
   http_parser:
     dependency: transitive
     description:
@@ -200,22 +240,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "4.0.2"
-  js:
-    dependency: transitive
-    description:
-      name: js
-      sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
-      url: "https://pub.dev"
-    source: hosted
-    version: "0.6.7"
-  js_wrapping:
-    dependency: transitive
-    description:
-      name: js_wrapping
-      sha256: e385980f7c76a8c1c9a560dfb623b890975841542471eade630b2871d243851c
-      url: "https://pub.dev"
-    source: hosted
-    version: "0.7.4"
   leak_tracker:
     dependency: transitive
     description:
@@ -280,6 +304,38 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.9.0"
+  path_parsing:
+    dependency: transitive
+    description:
+      name: path_parsing
+      sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.0.1"
+  path_provider:
+    dependency: transitive
+    description:
+      name: path_provider
+      sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.4"
+  path_provider_android:
+    dependency: transitive
+    description:
+      name: path_provider_android
+      sha256: "490539678396d4c3c0b06efdaab75ae60675c3e0c66f72bc04c2e2c1e0e2abeb"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.2.9"
+  path_provider_foundation:
+    dependency: transitive
+    description:
+      name: path_provider_foundation
+      sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.4.0"
   path_provider_linux:
     dependency: transitive
     description:
@@ -300,18 +356,26 @@ packages:
     dependency: transitive
     description:
       name: path_provider_windows
-      sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
+      sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
       url: "https://pub.dev"
     source: hosted
-    version: "2.2.1"
+    version: "2.3.0"
+  petitparser:
+    dependency: transitive
+    description:
+      name: petitparser
+      sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
+      url: "https://pub.dev"
+    source: hosted
+    version: "6.0.2"
   platform:
     dependency: transitive
     description:
       name: platform
-      sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
+      sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
       url: "https://pub.dev"
     source: hosted
-    version: "3.1.4"
+    version: "3.1.5"
   plugin_platform_interface:
     dependency: transitive
     description:
@@ -320,6 +384,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.1.8"
+  rxdart:
+    dependency: transitive
+    description:
+      name: rxdart
+      sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.28.0"
   sanitize_html:
     dependency: transitive
     description:
@@ -332,58 +404,58 @@ packages:
     dependency: "direct main"
     description:
       name: shared_preferences
-      sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180
+      sha256: c3f888ba2d659f3e75f4686112cc1e71f46177f74452d40d8307edc332296ead
       url: "https://pub.dev"
     source: hosted
-    version: "2.2.3"
+    version: "2.3.0"
   shared_preferences_android:
     dependency: transitive
     description:
       name: shared_preferences_android
-      sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2"
+      sha256: "041be4d9d2dc6079cf342bc8b761b03787e3b71192d658220a56cac9c04a0294"
       url: "https://pub.dev"
     source: hosted
-    version: "2.2.2"
+    version: "2.3.0"
   shared_preferences_foundation:
     dependency: transitive
     description:
       name: shared_preferences_foundation
-      sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7"
+      sha256: "671e7a931f55a08aa45be2a13fe7247f2a41237897df434b30d2012388191833"
       url: "https://pub.dev"
     source: hosted
-    version: "2.4.0"
+    version: "2.5.0"
   shared_preferences_linux:
     dependency: transitive
     description:
       name: shared_preferences_linux
-      sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa"
+      sha256: "2ba0510d3017f91655b7543e9ee46d48619de2a2af38e5c790423f7007c7ccc1"
       url: "https://pub.dev"
     source: hosted
-    version: "2.3.2"
+    version: "2.4.0"
   shared_preferences_platform_interface:
     dependency: transitive
     description:
       name: shared_preferences_platform_interface
-      sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b"
+      sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
       url: "https://pub.dev"
     source: hosted
-    version: "2.3.2"
+    version: "2.4.1"
   shared_preferences_web:
     dependency: transitive
     description:
       name: shared_preferences_web
-      sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a"
+      sha256: "3a293170d4d9403c3254ee05b84e62e8a9b3c5808ebd17de6a33fe9ea6457936"
       url: "https://pub.dev"
     source: hosted
-    version: "2.3.0"
+    version: "2.4.0"
   shared_preferences_windows:
     dependency: transitive
     description:
       name: shared_preferences_windows
-      sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59"
+      sha256: "398084b47b7f92110683cac45c6dc4aae853db47e470e5ddcd52cab7f7196ab2"
       url: "https://pub.dev"
     source: hosted
-    version: "2.3.2"
+    version: "2.4.0"
   sky_engine:
     dependency: transitive
     description: flutter
@@ -405,6 +477,30 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.10.0"
+  sprintf:
+    dependency: transitive
+    description:
+      name: sprintf
+      sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
+      url: "https://pub.dev"
+    source: hosted
+    version: "7.0.0"
+  sqflite:
+    dependency: transitive
+    description:
+      name: sqflite
+      sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.3.3+1"
+  sqflite_common:
+    dependency: transitive
+    description:
+      name: sqflite_common
+      sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.5.4"
   stack_trace:
     dependency: transitive
     description:
@@ -437,6 +533,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.2.0"
+  synchronized:
+    dependency: transitive
+    description:
+      name: synchronized
+      sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.1.0+1"
   term_glyph:
     dependency: transitive
     description:
@@ -453,6 +557,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "0.7.0"
+  the_widget_marker:
+    dependency: "direct main"
+    description:
+      name: the_widget_marker
+      sha256: "2476ae6b1fe29bbffa3596546871bd26f724c223ea7da74775801d9b70d64811"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.0.0"
   typed_data:
     dependency: transitive
     description:
@@ -461,6 +573,38 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.3.2"
+  uuid:
+    dependency: transitive
+    description:
+      name: uuid
+      sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90"
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.4.2"
+  vector_graphics:
+    dependency: transitive
+    description:
+      name: vector_graphics
+      sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.1.11+1"
+  vector_graphics_codec:
+    dependency: transitive
+    description:
+      name: vector_graphics_codec
+      sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.1.11+1"
+  vector_graphics_compiler:
+    dependency: transitive
+    description:
+      name: vector_graphics_compiler
+      sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.1.11+1"
   vector_math:
     dependency: transitive
     description:
@@ -485,14 +629,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "0.5.1"
-  win32:
-    dependency: transitive
-    description:
-      name: win32
-      sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4
-      url: "https://pub.dev"
-    source: hosted
-    version: "5.5.1"
   xdg_directories:
     dependency: transitive
     description:
@@ -501,6 +637,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.0.4"
+  xml:
+    dependency: transitive
+    description:
+      name: xml
+      sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
+      url: "https://pub.dev"
+    source: hosted
+    version: "6.5.0"
 sdks:
   dart: ">=3.4.0 <4.0.0"
-  flutter: ">=3.19.0"
+  flutter: ">=3.22.0"
diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml
index 1d31915..6f6d8cf 100644
--- a/frontend/pubspec.yaml
+++ b/frontend/pubspec.yaml
@@ -36,10 +36,11 @@ dependencies:
   # Use with the CupertinoIcons class for iOS style icons.
   cupertino_icons: ^1.0.6
   sliding_up_panel: ^2.0.0+1
-  google_maps_flutter: ^2.6.1
   http: ^1.2.1
   shared_preferences: ^2.2.3
   dio: ^5.5.0+1
+  google_maps_flutter: ^2.7.0
+  the_widget_marker: ^1.0.0
 
 dev_dependencies:
   flutter_test:
-- 
2.47.2


From bf129b201d741e53fb9b1a8f5e79fffb66f96ba0 Mon Sep 17 00:00:00 2001
From: Remy Moll <me@moll.re>
Date: Thu, 1 Aug 2024 19:34:48 +0200
Subject: [PATCH 3/9] more straightforward logging

---
 backend/src/constants.py    | 22 +++++++++++++---------
 backend/src/log_config.yaml | 35 -----------------------------------
 2 files changed, 13 insertions(+), 44 deletions(-)
 delete mode 100644 backend/src/log_config.yaml

diff --git a/backend/src/constants.py b/backend/src/constants.py
index e9a4be2..64dceb7 100644
--- a/backend/src/constants.py
+++ b/backend/src/constants.py
@@ -15,16 +15,20 @@ OSM_CACHE_DIR = Path(cache_dir_string)
 
 
 import logging
-import yaml
-
-LOGGING_CONFIG = LOCATION_PREFIX / 'log_config.yaml'
-config = yaml.safe_load(LOGGING_CONFIG.read_text())
-
-logging.config.dictConfig(config)
-
-# if we are in a debug session, set the log level to debug
+# if we are in a debug session, set verbose and rich logging
 if os.getenv('DEBUG', False):
-    logging.getLogger().setLevel(logging.DEBUG)
+    from rich.logging import RichHandler
+    logging.basicConfig(
+        level=logging.DEBUG,
+        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
+        handlers=[RichHandler()]
+    )
+else:
+    logging.basicConfig(
+        level=logging.INFO,
+        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
+    )
+
 
 MEMCACHED_HOST_PATH = os.getenv('MEMCACHED_HOST_PATH', None)
 if MEMCACHED_HOST_PATH == "none":
diff --git a/backend/src/log_config.yaml b/backend/src/log_config.yaml
deleted file mode 100644
index 4cb27f9..0000000
--- a/backend/src/log_config.yaml
+++ /dev/null
@@ -1,35 +0,0 @@
-version: 1
-disable_existing_loggers: False
-formatters:
-  simple:
-    format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
-handlers:
-  console:
-    class: rich.logging.RichHandler
-    formatter: simple
-    width: 255
-  # access:
-  #   class: logging.FileHandler
-  #   filename: logs/access.log
-  #   level: INFO
-  #   formatter: simple
-
-
-
-
-loggers:
-  uvicorn.error:
-    level: INFO
-    handlers:
-      - console
-    propagate: no
-  # uvicorn.access:
-  #   level: INFO
-  #   handlers:
-  #     - access
-  #   propagate: no
-root:
-  level: INFO
-  handlers:
-    - console
-  propagate: yes
-- 
2.47.2


From 016622c7af7e338d98af1dabf05236a536d8133d Mon Sep 17 00:00:00 2001
From: Remy Moll <me@moll.re>
Date: Thu, 1 Aug 2024 19:35:25 +0200
Subject: [PATCH 4/9] frontend compliant with backend

---
 frontend/lib/pages/profile.dart       | 103 ++++++++++++--------
 frontend/lib/structs/preferences.dart | 134 +++++++++++++++-----------
 frontend/lib/utils/fetch_trip.dart    |   4 +-
 3 files changed, 141 insertions(+), 100 deletions(-)

diff --git a/frontend/lib/pages/profile.dart b/frontend/lib/pages/profile.dart
index 4101895..9371e87 100644
--- a/frontend/lib/pages/profile.dart
+++ b/frontend/lib/pages/profile.dart
@@ -9,6 +9,9 @@ class ProfilePage extends StatefulWidget {
 }
 
 class _ProfilePageState extends State<ProfilePage> {
+  Future<UserPreferences> _prefs = loadUserPreferences();
+
+
   @override
   Widget build(BuildContext context) {
     return ListView(
@@ -24,66 +27,82 @@ class _ProfilePageState extends State<ProfilePage> {
           child: Text('Curious traveler', style: TextStyle(fontSize: 24))
         ),
 
-        Padding(padding: EdgeInsets.all(10)),
-        Divider(indent: 25, endIndent: 25),
-        Padding(padding: EdgeInsets.all(10)),
+        Divider(indent: 25, endIndent: 25, height: 50),
 
-        Padding(
-          padding: EdgeInsets.only(left: 10, right: 10, top: 0, bottom: 10),
-          child: Text('Please rate your personal preferences so that we can taylor your experience.', style: TextStyle(fontSize: 18))
+        Center(
+          child: Padding(
+           padding: EdgeInsets.only(left: 10, right: 10, top: 0, bottom: 10),
+            child: Text('For a tailored experience, please rate your discovery preferences.', style: TextStyle(fontSize: 18))
+          ),
         ),
 
-        // Now the sliders
-        ImportanceSliders()
+        FutureBuilder(future: _prefs, builder: futureSliders)
       ]
     );
   }
+
+  Widget futureSliders(BuildContext context, AsyncSnapshot<UserPreferences> snapshot) {
+    if (snapshot.connectionState == ConnectionState.done) {
+      UserPreferences prefs = snapshot.data!;
+
+      return Column(
+        children: [
+          PreferenceSliders(prefs: [prefs.maxTime, prefs.maxDetour]),
+          Divider(indent: 25, endIndent: 25, height: 50),
+          PreferenceSliders(prefs: [prefs.sightseeing, prefs.shopping, prefs.nature])
+        ]
+      );
+    } else {
+      return CircularProgressIndicator();
+    }
+  }
 }
 
 
 
-class ImportanceSliders extends StatefulWidget {
+class PreferenceSliders extends StatefulWidget {
+  final List<SinglePreference> prefs;
+
+  PreferenceSliders({required this.prefs});
 
   @override
-  State<ImportanceSliders> createState() => _ImportanceSlidersState();
+  State<PreferenceSliders> createState() => _PreferenceSlidersState();
 }
 
 
-class _ImportanceSlidersState extends State<ImportanceSliders> {
-
-  UserPreferences _prefs = UserPreferences();
-
-  List<Card> _createSliders() {
-    List<Card> sliders = [];
-    for (SinglePreference pref in _prefs.preferences) {
-      sliders.add(Card(
-        child: ListTile(
-          leading: pref.icon,
-          title: Text(pref.name),
-          subtitle: Slider(
-            value: pref.value.toDouble(),
-            min: 0,
-            max: 10,
-            divisions: 10,
-            label: pref.value.toString(),
-            onChanged: (double newValue) {
-              setState(() {
-                pref.value = newValue.toInt();
-                _prefs.save();
-              });
-            },
-          )
-        ),
-        margin: const EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 0),
-        shadowColor: Colors.grey,
-      ));
-    }
-    return sliders;
-  }
+class _PreferenceSlidersState extends State<PreferenceSliders> {
 
   @override
   Widget build(BuildContext context) {
+    List<Card> sliders = [];
+      for (SinglePreference pref in widget.prefs) {
+      sliders.add(
+        Card(
+          child: ListTile(
+            leading: pref.icon,
+            title: Text(pref.name),
+            subtitle: Slider(
+              value: pref.value.toDouble(),
+              min: pref.minVal.toDouble(),
+              max: pref.maxVal.toDouble(),
+              divisions: pref.maxVal - pref.minVal,
+              label: pref.value.toString(),
+              onChanged: (double newValue) {
+                setState(() {
+                  pref.value = newValue.toInt();
+                  pref.save();
+                });
+              },
+            )
+          ),
+          margin: const EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 0),
+          shadowColor: Colors.grey,
+        )
+      );
+    }
 
-    return Column(children: _createSliders());
+    return Column(
+      children: sliders);
   }
 }
+
diff --git a/frontend/lib/structs/preferences.dart b/frontend/lib/structs/preferences.dart
index 9a31fc7..d86b361 100644
--- a/frontend/lib/structs/preferences.dart
+++ b/frontend/lib/structs/preferences.dart
@@ -3,80 +3,102 @@ import 'package:shared_preferences/shared_preferences.dart';
 
 
 class SinglePreference {
+  String slug;
   String name;
   String description;
   int value;
+  int minVal;
+  int maxVal;
   Icon icon;
-  String key;
 
   SinglePreference({
+    required this.slug,
     required this.name,
     required this.description,
     required this.value,
     required this.icon,
-    required this.key,
+    this.minVal = 0,
+    this.maxVal = 5,
   });
-}
-
-
-class UserPreferences {
-  List<SinglePreference> preferences = [
-    SinglePreference(
-      name: "Sightseeing",
-      description: "How much do you like sightseeing?",
-      value: 0,
-      icon: Icon(Icons.church),
-      key: "sightseeing",
-    ),
-    SinglePreference(
-      name: "Shopping",
-      description: "How much do you like shopping?",
-      value: 0,
-      icon: Icon(Icons.shopping_bag),
-      key: "shopping",
-    ),
-    SinglePreference(
-      name: "Foods & Drinks",
-      description: "How much do you like eating?",
-      value: 0,
-      icon: Icon(Icons.restaurant),
-      key: "eating",
-    ),
-    SinglePreference(
-      name: "Nightlife",
-      description: "How much do you like nightlife?",
-      value: 0,
-      icon: Icon(Icons.wine_bar),
-      key: "nightlife",
-    ),
-    SinglePreference(
-      name: "Nature",
-      description: "How much do you like nature?",
-      value: 0,
-      icon: Icon(Icons.landscape),
-      key: "nature",
-    ),
-    SinglePreference(
-      name: "Culture",
-      description: "How much do you like culture?",
-      value: 0,
-      icon: Icon(Icons.palette),
-      key: "culture",
-    ),
-  ];
-
 
   void save() async {
     SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
-    for (SinglePreference pref in preferences) {
-      sharedPrefs.setInt(pref.key, pref.value);
-    }
+      sharedPrefs.setInt('pref_$slug', value);
   }
 
   void load() async {
     SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
-    for (SinglePreference pref in preferences) {
-      pref.value = sharedPrefs.getInt(pref.key) ?? 0;
+      value = sharedPrefs.getInt('pref_$slug') ?? minVal;
+  }
+}
+
+
+class UserPreferences {
+  SinglePreference sightseeing = SinglePreference(
+    name: "Sightseeing",
+    slug: "sightseeing",
+    description: "How much do you like sightseeing?",
+    value: 0,
+    icon: Icon(Icons.church),
+  );
+  SinglePreference shopping = SinglePreference(
+    name: "Shopping",
+    slug: "shopping",
+    description: "How much do you like shopping?",
+    value: 0,
+    icon: Icon(Icons.shopping_bag),
+  );
+  SinglePreference nature = SinglePreference(
+    name: "Nature",
+    slug: "nature",
+    description: "How much do you like nature?",
+    value: 0,
+    icon: Icon(Icons.landscape),
+  );
+
+  SinglePreference maxTime = SinglePreference(
+    name: "Trip duration",
+    slug: "duration",
+    description: "How long do you want your trip to be?",
+    value: 30,
+    minVal: 30,
+    maxVal: 720,
+    icon: Icon(Icons.timer),
+  );
+  SinglePreference maxDetour = SinglePreference(
+    name: "Trip detours",
+    slug: "detours",
+    description: "Are you okay with roaming even if makes the trip longer?",
+    value: 0,
+    maxVal: 30,
+    icon: Icon(Icons.loupe_sharp),
+  );
+
+
+
+  Future<void> load() async {
+    for (SinglePreference pref in [sightseeing, shopping, nature, maxTime, maxDetour]) {
+      pref.load();
     }
   }
+
+  String toJson() {
+    // This is "opinionated" JSON, corresponding to the backend's expectations
+    return '''
+    {
+      "sightseeing": {"type": "sightseeing", "score": ${sightseeing.value}},
+      "shopping": {"type": "shopping", "score": ${shopping.value}},
+      "nature": {"type": "nature", "score": ${nature.value}},
+      "max_time_minutes": ${maxTime.value},
+      "detour_tolerance_minute": ${maxDetour.value}
+    }
+    ''';
+  }
+}
+
+
+Future<UserPreferences> loadUserPreferences() async {
+  UserPreferences prefs = UserPreferences();
+  await prefs.load();
+  return prefs;
 }
\ No newline at end of file
diff --git a/frontend/lib/utils/fetch_trip.dart b/frontend/lib/utils/fetch_trip.dart
index fd55751..c1d17e7 100644
--- a/frontend/lib/utils/fetch_trip.dart
+++ b/frontend/lib/utils/fetch_trip.dart
@@ -28,8 +28,8 @@ Future<Trip> fetchTrip(
   final response = await dio.post(
     "/trip/new",
     data: {
-      // 'preferences': preferences.toJson(),
-      'start': [48,2.3]
+      'preferences': preferences.toJson(),
+      'start': startPoint
     }
   );
 
-- 
2.47.2


From 5748630b99cd8d87316320ba8aaff9074dee92b0 Mon Sep 17 00:00:00 2001
From: Remy Moll <me@moll.re>
Date: Thu, 1 Aug 2024 22:48:28 +0200
Subject: [PATCH 5/9] bare implementation of comuncation with the api

---
 frontend/lib/modules/greeter.dart     | 40 ++++++++++++------
 frontend/lib/pages/new_trip.dart      |  5 +--
 frontend/lib/structs/preferences.dart | 20 +++++----
 frontend/lib/structs/trip.dart        | 10 +++--
 frontend/lib/utils/fetch_trip.dart    | 58 ++++++++++++++++++++++-----
 5 files changed, 94 insertions(+), 39 deletions(-)

diff --git a/frontend/lib/modules/greeter.dart b/frontend/lib/modules/greeter.dart
index f07f9f7..77e55de 100644
--- a/frontend/lib/modules/greeter.dart
+++ b/frontend/lib/modules/greeter.dart
@@ -20,26 +20,40 @@ class Greeter extends StatefulWidget {
 class _GreeterState extends State<Greeter> {
   Widget greeterBuild (BuildContext context, AsyncSnapshot<Trip> snapshot) {
     ThemeData theme = Theme.of(context);
-    String cityName = "";
+    Widget topGreeter;
     if (snapshot.hasData) {
-      cityName = snapshot.data?.cityName ?? '...';
+      topGreeter = Padding(
+        padding: const EdgeInsets.only(top: 20, bottom: 20),
+        child: Text(
+          'Welcome to ${snapshot.data?.cityName}!',
+          style: TextStyle(color: theme.primaryColor, fontWeight: FontWeight.bold, fontSize: 24),
+        )
+      );
     } else if (snapshot.hasError) {
-      cityName = "error";
-    } else { // still awaiting the cityname
-      cityName = "...";
+      topGreeter = const Padding(
+        padding: EdgeInsets.only(top: 20, bottom: 20),
+        child: Text('Error while fetching trip')
+      );
+    } else {
+      // still awaiting the cityname
+      // Show a linear loader at the bottom and an info message above
+      topGreeter = Column(
+        mainAxisAlignment: MainAxisAlignment.end,
+        children: [
+          Padding(
+            padding: const EdgeInsets.only(top: 20, bottom: 20),
+            child: const Text('Generating your trip...', style: TextStyle(fontSize: 20),)
+          ),
+          const LinearProgressIndicator()
+        ]
+      );
     }
 
-    Widget topGreeter = Text(
-      'Welcome to $cityName!',
-      style: TextStyle(color: theme.primaryColor, fontWeight: FontWeight.bold, fontSize: 24),
-    );
+    
 
     if (widget.standalone) {
       return Center(
-        child: Padding(
-          padding: EdgeInsets.only(top: 24.0),
-          child: topGreeter,
-        ),
+        child: topGreeter,
       );
     } else {
       return Center(
diff --git a/frontend/lib/pages/new_trip.dart b/frontend/lib/pages/new_trip.dart
index a10a046..2464177 100644
--- a/frontend/lib/pages/new_trip.dart
+++ b/frontend/lib/pages/new_trip.dart
@@ -63,9 +63,8 @@ class _NewTripPageState extends State<NewTripPage> {
                         double.parse(latController.text),
                         double.parse(lonController.text)
                       ];
-                      UserPreferences preferences = UserPreferences();
-                      preferences.load();
-                      Future<Trip> trip = fetchTrip(startPoint, preferences);
+                      Future<UserPreferences> preferences = loadUserPreferences();
+                      Future<Trip>? trip = fetchTrip(startPoint, preferences);
                         Navigator.of(context).push(
                           MaterialPageRoute(
                             builder: (context) => BasePage(mainScreen: "map", trip: trip)
diff --git a/frontend/lib/structs/preferences.dart b/frontend/lib/structs/preferences.dart
index d86b361..074bbde 100644
--- a/frontend/lib/structs/preferences.dart
+++ b/frontend/lib/structs/preferences.dart
@@ -82,18 +82,16 @@ class UserPreferences {
     }
   }
 
-  String toJson() {
-    // This is "opinionated" JSON, corresponding to the backend's expectations
-    return '''
-    {
-      "sightseeing": {"type": "sightseeing", "score": ${sightseeing.value}},
-      "shopping": {"type": "shopping", "score": ${shopping.value}},
-      "nature": {"type": "nature", "score": ${nature.value}},
-      "max_time_minutes": ${maxTime.value},
-      "detour_tolerance_minute": ${maxDetour.value}
+  Map<String, dynamic> toJson() {
+      // This is "opinionated" JSON, corresponding to the backend's expectations
+      return {
+        "sightseeing": {"type": "sightseeing", "score": sightseeing.value},
+        "shopping": {"type": "shopping", "score": shopping.value},
+        "nature": {"type": "nature", "score": nature.value},
+        "max_time_minute": maxTime.value,
+        "detour_tolerance_minute": maxDetour.value
+      };
     }
-    ''';
-  }
 }
 
 
diff --git a/frontend/lib/structs/trip.dart b/frontend/lib/structs/trip.dart
index c43188c..f6d94e3 100644
--- a/frontend/lib/structs/trip.dart
+++ b/frontend/lib/structs/trip.dart
@@ -11,6 +11,7 @@ class Trip {
   final String uuid;
   final String cityName;
   // TODO: cityName should be inferred from coordinates of the Landmarks
+  final int totalTime;
   final LinkedList<Landmark> landmarks;
   // could be empty as well
 
@@ -19,15 +20,18 @@ class Trip {
     required this.uuid,
     required this.cityName,
     required this.landmarks,
+    this.totalTime = 0
   });
 
 
   factory Trip.fromJson(Map<String, dynamic> json) {
-    return Trip(
+    Trip trip = Trip(
       uuid: json['uuid'],
-      cityName: json['city_name'],
+      cityName: json['city_name'] ?? 'Not communicated',
       landmarks: LinkedList()
     );
+
+    return trip;
   }
 
 
@@ -44,7 +48,7 @@ class Trip {
   Map<String, dynamic> toJson() => {
     'uuid': uuid,
     'city_name': cityName,
-    'entry_uuid': landmarks.first?.uuid ?? ''
+    'first_landmark_uuid': landmarks.first.uuid
   };
 
 
diff --git a/frontend/lib/utils/fetch_trip.dart b/frontend/lib/utils/fetch_trip.dart
index c1d17e7..aeba3b8 100644
--- a/frontend/lib/utils/fetch_trip.dart
+++ b/frontend/lib/utils/fetch_trip.dart
@@ -1,16 +1,19 @@
+import "dart:convert";
+import "dart:developer";
+
 import 'package:dio/dio.dart';
 import 'package:anyway/constants.dart';
 import "package:anyway/structs/landmark.dart";
 import "package:anyway/structs/trip.dart";
 import "package:anyway/structs/preferences.dart";
 
-import "package:anyway/structs/linked_landmarks.dart";
 
 Dio dio = Dio(
     BaseOptions(
       baseUrl: API_URL_BASE,
+      // baseUrl: 'http://localhost:8000',
       connectTimeout: const Duration(seconds: 5),
-      receiveTimeout: const Duration(seconds: 60),
+      receiveTimeout: const Duration(seconds: 120),
       // api is notoriously slow
       // headers: {
       //   HttpHeaders.userAgentHeader: 'dio',
@@ -18,19 +21,25 @@ Dio dio = Dio(
       // },
       contentType: Headers.jsonContentType,
       responseType: ResponseType.json,
+        
   ),
 );
 
-Future<Trip> fetchTrip(
+Future<Trip>? fetchTrip(
   List<double> startPoint,
-  UserPreferences preferences,
+  Future<UserPreferences> preferences,
 ) async {
+  UserPreferences prefs = await preferences;
+  Map<String, dynamic> data = {
+    "preferences": prefs.toJson(),
+    "start": startPoint
+  };
+  String dataString = jsonEncode(data);
+  log(dataString);
+
   final response = await dio.post(
     "/trip/new",
-    data: {
-      'preferences': preferences.toJson(),
-      'start': startPoint
-    }
+    data: data
   );
 
   // handle errors
@@ -40,6 +49,37 @@ Future<Trip> fetchTrip(
   if (response.data["error"] != null) {
     throw Exception(response.data["error"]);
   }
-  return Trip.fromJson(response.data);
+  log(response.data.toString());
+  Map<String, dynamic> json = response.data;
+
+  // only fetch the trip "meta" data for now
+  Trip trip = Trip.fromJson(json);
+
+  String? nextUUID = json["first_landmark_uuid"];
+  while (nextUUID != null) {
+    var (landmark, newUUID) = await fetchLandmark(nextUUID);
+    trip.landmarks.add(landmark);
+    nextUUID = newUUID;
+  }
+  return trip;
 }
 
+
+
+Future<(Landmark, String?)> fetchLandmark(String uuid) async {
+  final response = await dio.get(
+    "/landmark/$uuid"
+  );
+
+  // handle errors
+  if (response.statusCode != 200) {
+    throw Exception('Failed to load landmark');
+  }
+  if (response.data["error"] != null) {
+    throw Exception(response.data["error"]);
+  }
+  log(response.data.toString());
+  Map<String, dynamic> json = response.data;
+  String? nextUUID = json["next_uuid"];
+  return (Landmark.fromJson(json), nextUUID);
+}
-- 
2.47.2


From c87a01b2e8a813b227e7f55764692bb694bf4112 Mon Sep 17 00:00:00 2001
From: Remy Moll <me@moll.re>
Date: Sat, 3 Aug 2024 16:52:29 +0200
Subject: [PATCH 6/9] overhaul using a trip struct that notifies its ui
 dependencies

---
 backend/src/main.py                          |  2 +-
 frontend/lib/layout.dart                     | 68 +++++++++++---
 frontend/lib/main.dart                       |  2 +-
 frontend/lib/modules/greeter.dart            | 95 +++++++++++---------
 frontend/lib/modules/landmark_card.dart      |  8 +-
 frontend/lib/modules/landmarks_overview.dart | 67 +++++++-------
 frontend/lib/modules/map.dart                |  8 +-
 frontend/lib/modules/trips_overview.dart     |  2 +-
 frontend/lib/pages/new_trip.dart             | 17 +++-
 frontend/lib/pages/overview.dart             | 81 +++++++++--------
 frontend/lib/structs/trip.dart               | 51 +++++++----
 frontend/lib/utils/fetch_trip.dart           | 17 ++--
 frontend/lib/utils/load_trips.dart           |  4 +-
 frontend/pubspec.lock                        | 64 +++++++++++++
 frontend/pubspec.yaml                        |  2 +
 15 files changed, 324 insertions(+), 164 deletions(-)

diff --git a/backend/src/main.py b/backend/src/main.py
index c59d864..b9c1837 100644
--- a/backend/src/main.py
+++ b/backend/src/main.py
@@ -37,7 +37,7 @@ def new_trip(preferences: Preferences, start: tuple[float, float], end: tuple[fl
         logger.info("No end coordinates provided. Using start=end.")
 
     start_landmark = Landmark(name='start', type='start', location=(start[0], start[1]), osm_type='start', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
-    end_landmark = Landmark(name='end', type='finish', location=(end[0], end[1]), osm_type='end', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
+    end_landmark = Landmark(name='finish', type='finish', location=(end[0], end[1]), osm_type='end', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
 
     # Generate the landmarks from the start location
     landmarks, landmarks_short = manager.generate_landmarks_list(
diff --git a/frontend/lib/layout.dart b/frontend/lib/layout.dart
index abc8b24..fbfc6ca 100644
--- a/frontend/lib/layout.dart
+++ b/frontend/lib/layout.dart
@@ -1,3 +1,6 @@
+import 'dart:collection';
+
+import 'package:anyway/structs/landmark.dart';
 import 'package:flutter/material.dart';
 
 import 'package:anyway/constants.dart';
@@ -15,12 +18,12 @@ import 'package:anyway/pages/profile.dart';
 // A side drawer is used to switch between pages
 class BasePage extends StatefulWidget {
   final String mainScreen;
-  final Future<Trip>? trip;
-  
+  final Trip? trip;
+
   const BasePage({
     super.key,
     required this.mainScreen,
-    this.trip
+    this.trip,
   });
 
   @override
@@ -53,13 +56,13 @@ class _BasePageState extends State<BasePage> {
           children: [
             DrawerHeader(
               decoration: BoxDecoration(
-                gradient: LinearGradient(colors: [Colors.cyan, theme.primaryColor])
+                gradient: LinearGradient(colors: [Colors.red, Colors.yellow])
               ),
               child: Center(
                 child: Text(
                   APP_NAME,
                   style: TextStyle(
-                    color: Colors.white,
+                    color: Colors.grey[800],
                     fontSize: 24,
                     fontWeight: FontWeight.bold,
                   ),
@@ -129,9 +132,54 @@ class _BasePageState extends State<BasePage> {
   }
 }
 
-
-
-Future<Trip> getFirstTrip (Future<List<Trip>> trips) async {
-  List<Trip> tripsf = await trips;
-  return tripsf[0];
+// This function is used to get the first trip from a list of trips
+// TODO: Implement this function
+Trip getFirstTrip(Future<List<Trip>> trips) {
+  Trip t1 = Trip(uuid: '1', landmarks: LinkedList<Landmark>());
+  t1.landmarks.add(
+    Landmark(
+      uuid: '1',
+      name: "Eiffel Tower",
+      location: [48.859, 2.295],
+      type: LandmarkType(name: "Tower"),
+      imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a8/Tour_Eiffel_Wikimedia_Commons.jpg/1037px-Tour_Eiffel_Wikimedia_Commons.jpg"
+    ),
+  );
+  t1.landmarks.add(
+    Landmark(
+      uuid: "2",
+      name: "Notre Dame Cathedral",
+      location: [48.8530, 2.3498],
+      type: LandmarkType(name: "Monument"),
+      imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f7/Notre-Dame_de_Paris%2C_4_October_2017.jpg/440px-Notre-Dame_de_Paris%2C_4_October_2017.jpg"
+    ),
+  );
+  t1.landmarks.add(
+    Landmark(
+      uuid: "3",
+      name: "Louvre palace",
+      location: [48.8606, 2.3376],
+      type: LandmarkType(name: "Museum"),
+      imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/6/66/Louvre_Museum_Wikimedia_Commons.jpg/540px-Louvre_Museum_Wikimedia_Commons.jpg"
+    ),
+  );
+  t1.landmarks.add(
+    Landmark(
+      uuid: "4",
+      name: "Pont-des-arts",
+      location: [48.8585, 2.3376],
+      type: LandmarkType(name: "Bridge"),
+      imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d1/Pont_des_Arts%2C_6e_Arrondissement%2C_Paris_%28HDR%29_20140320_1.jpg/560px-Pont_des_Arts%2C_6e_Arrondissement%2C_Paris_%28HDR%29_20140320_1.jpg"
+    ),
+  );
+  t1.landmarks.add(
+    Landmark(
+      uuid: "5",
+      name: "Panthéon",
+      location: [48.847, 2.347],
+      type: LandmarkType(name: "Monument"),
+      imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/8/80/Pantheon_of_Paris_007.JPG/1280px-Pantheon_of_Paris_007.JPG"
+    ),
+  );
+  return t1;
 }
\ No newline at end of file
diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart
index 7fbb6f8..44c3922 100644
--- a/frontend/lib/main.dart
+++ b/frontend/lib/main.dart
@@ -13,7 +13,7 @@ class App extends StatelessWidget {
     return MaterialApp(
       title: APP_NAME,
       home: BasePage(mainScreen: "map"),
-      theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.green),
+      theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.red[600]),
     );
   }
 }
diff --git a/frontend/lib/modules/greeter.dart b/frontend/lib/modules/greeter.dart
index 77e55de..6b74c13 100644
--- a/frontend/lib/modules/greeter.dart
+++ b/frontend/lib/modules/greeter.dart
@@ -3,12 +3,10 @@ import 'package:anyway/structs/trip.dart';
 import 'package:flutter/material.dart';
 
 class Greeter extends StatefulWidget {
-  final Future<Trip> trip;
-  final bool standalone;
+  final Trip trip;
 
   Greeter({
-    required this.standalone,
-    required this.trip
+    required this.trip,
   });
 
   @override
@@ -18,55 +16,66 @@ class Greeter extends StatefulWidget {
 
 
 class _GreeterState extends State<Greeter> {
-  Widget greeterBuild (BuildContext context, AsyncSnapshot<Trip> snapshot) {
+  Widget greeterBuilder (BuildContext context, Widget? child) {
     ThemeData theme = Theme.of(context);
     Widget topGreeter;
-    if (snapshot.hasData) {
-      topGreeter = Padding(
-        padding: const EdgeInsets.only(top: 20, bottom: 20),
-        child: Text(
-          'Welcome to ${snapshot.data?.cityName}!',
-          style: TextStyle(color: theme.primaryColor, fontWeight: FontWeight.bold, fontSize: 24),
-        )
-      );
-    } else if (snapshot.hasError) {
-      topGreeter = const Padding(
-        padding: EdgeInsets.only(top: 20, bottom: 20),
-        child: Text('Error while fetching trip')
+    if (widget.trip.landmarks.length > 1) {
+      topGreeter = FutureBuilder(
+        future: widget.trip.cityName,
+        builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
+          if (snapshot.hasData) {
+            return Text(
+              'Welcome to ${snapshot.data}!',
+              style: TextStyle(color: theme.primaryColor, fontWeight: FontWeight.bold, fontSize: 24),
+            );
+          } else if (snapshot.hasError) {
+            return const Text('Welcome to your trip!');
+          } else {
+            return const Text('Welcome to ...');
+          }
+        }
       );
     } else {
-      // still awaiting the cityname
+      // still awaiting the trip
+      // We can hopefully infer the city name from the cityName future
       // Show a linear loader at the bottom and an info message above
       topGreeter = Column(
         mainAxisAlignment: MainAxisAlignment.end,
         children: [
-          Padding(
-            padding: const EdgeInsets.only(top: 20, bottom: 20),
-            child: const Text('Generating your trip...', style: TextStyle(fontSize: 20),)
+          FutureBuilder(
+            future: widget.trip.cityName,
+            builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
+              if (snapshot.hasData) {
+                return Text(
+                  'Generating your trip to ${snapshot.data}...',
+                  style: TextStyle(color: theme.primaryColor, fontWeight: FontWeight.bold, fontSize: 24),
+                );
+              } else if (snapshot.hasError) {
+                return const Text('Error while fetching city name');
+              }
+              return const Text('Generating your trip...');
+            }
           ),
-          const LinearProgressIndicator()
+          Padding(
+            padding: EdgeInsets.all(5),
+            child: const LinearProgressIndicator()
+          )
         ]
       );
     }
 
-    
-
-    if (widget.standalone) {
-      return Center(
-        child: topGreeter,
-      );
-    } else {
-      return Center(
-        child: Column(
-          children: [
-            Padding(padding: EdgeInsets.only(top: 24.0)),
-            topGreeter,
-            bottomGreeter,
-            Padding(padding: EdgeInsets.only(bottom: 24.0)),
-          ],
-        )
-      );
-    }
+    return Center(
+      child: Column(
+        children: [
+          // Padding(padding: EdgeInsets.only(top: 20)),
+          topGreeter,
+          Padding(
+            padding: EdgeInsets.all(20),
+            child: bottomGreeter
+          ),
+        ],
+      )
+    );
   }
 
   Widget bottomGreeter = const Text(
@@ -79,9 +88,9 @@ class _GreeterState extends State<Greeter> {
 
   @override
   Widget build(BuildContext context) {
-    return FutureBuilder(
-      future: widget.trip,
-      builder: greeterBuild,
+    return ListenableBuilder(
+      listenable: widget.trip,
+      builder: greeterBuilder,
     );
   }
 }
\ No newline at end of file
diff --git a/frontend/lib/modules/landmark_card.dart b/frontend/lib/modules/landmark_card.dart
index ca663b6..579ebca 100644
--- a/frontend/lib/modules/landmark_card.dart
+++ b/frontend/lib/modules/landmark_card.dart
@@ -1,4 +1,5 @@
 import 'package:anyway/structs/landmark.dart';
+import 'package:cached_network_image/cached_network_image.dart';
 import 'package:flutter/material.dart';
 
 
@@ -31,9 +32,10 @@ class _LandmarkCardState extends State<LandmarkCard> {
               height: double.infinity,
               // force a fixed width
               width: 160,
-              child: Image.network(
-                widget.landmark.imageURL ?? '',
-                errorBuilder: (context, error, stackTrace) => Icon(Icons.question_mark_outlined),
+              child: CachedNetworkImage(
+                imageUrl: widget.landmark.imageURL ?? '',
+                placeholder: (context, url) => CircularProgressIndicator(),
+                errorWidget: (context, error, stackTrace) => Icon(Icons.question_mark_outlined),
                 // TODO: make this a switch statement to load a placeholder if null
                 // cover the whole container meaning the image will be cropped
                 fit: BoxFit.cover,
diff --git a/frontend/lib/modules/landmarks_overview.dart b/frontend/lib/modules/landmarks_overview.dart
index 720fe49..387bf11 100644
--- a/frontend/lib/modules/landmarks_overview.dart
+++ b/frontend/lib/modules/landmarks_overview.dart
@@ -1,17 +1,17 @@
 import 'dart:collection';
+import 'dart:developer';
+import 'package:flutter/material.dart';
+import 'package:shared_preferences/shared_preferences.dart';
 
 import 'package:anyway/modules/landmark_card.dart';
 import 'package:anyway/structs/landmark.dart';
-
 import 'package:anyway/structs/trip.dart';
-import 'package:flutter/material.dart';
-import 'package:shared_preferences/shared_preferences.dart';
 
 
 
 
 class LandmarksOverview extends StatefulWidget {
-  final Future<Trip>? trip;
+  final Trip? trip;
   const LandmarksOverview({super.key, this.trip});
 
   @override
@@ -23,18 +23,17 @@ class _LandmarksOverviewState extends State<LandmarksOverview> {
 
   @override
   Widget build(BuildContext context) {
-    final Future<LinkedList<Landmark>> _landmarks = getLandmarks(widget.trip);
-    return DefaultTextStyle(
-      style: Theme.of(context).textTheme.displayMedium!,
-      textAlign: TextAlign.center,
-      child: FutureBuilder<LinkedList<Landmark>>(
-        future: _landmarks,
-        builder: (BuildContext context, AsyncSnapshot<LinkedList<Landmark>> snapshot) {
-          List<Widget> children;
-          if (snapshot.hasData) {
-            children = [landmarksWithSteps(snapshot.data!), saveButton()];
-          } else if (snapshot.hasError) {
-            children = <Widget>[
+    return ListenableBuilder(//<LinkedList<Landmark>>
+      listenable: widget.trip!,
+      builder: (BuildContext context, Widget? child) {
+        Trip trip = widget.trip!;
+        log("Trip ${trip.uuid} ${trip.landmarks.length} landmarks");
+        List<Widget> children;
+        if (trip.uuid == 'pending') {
+          // the trip is still being fetched from the api
+          children = [Center(child: CircularProgressIndicator())];
+        } else if (trip.uuid == 'error') {
+            children = [
               const Icon(
                 Icons.error_outline,
                 color: Colors.red,
@@ -42,20 +41,25 @@ class _LandmarksOverviewState extends State<LandmarksOverview> {
               ),
               Padding(
                 padding: const EdgeInsets.only(top: 16),
-                child: Text('Error: ${snapshot.error}', style: TextStyle(fontSize: 12)),
+                child: Text('Error: ${trip.cityName}'),
               ),
             ];
+        } else {
+          if (trip.landmarks.length <= 1) {
+            children = [
+              const Text("No landmarks in this trip"),
+            ];
           } else {
-            children = [Center(child: CircularProgressIndicator())];
+            children = [
+              landmarksWithSteps(trip.landmarks),
+              saveButton(),
+            ];
           }
-          return Center(
-            child: Column(
-              mainAxisAlignment: MainAxisAlignment.center,
-              children: children,
-            ),
-          );
-        },
-      ),
+        }
+        return Column(
+          children: children,
+        );
+      },
     );
   }
   Widget saveButton() => ElevatedButton(
@@ -100,7 +104,7 @@ Widget landmarksWithSteps(LinkedList<Landmark> landmarks) {
     );
     lkey++;
     if (landmark.next != null) {
-      Widget step = stepBetweenLandmarks(landmark, landmark.next!);
+      Widget step = stepBetweenLandmarks(landmark);
       children.add(step);
     }
   }
@@ -111,7 +115,7 @@ Widget landmarksWithSteps(LinkedList<Landmark> landmarks) {
 }
 
 
-Widget stepBetweenLandmarks(Landmark before, Landmark after) {
+Widget stepBetweenLandmarks(Landmark landmark) {
   // This is a simple widget that draws a line between landmark-cards
   // It's a vertical dotted line
   // Next to the line is the icon for the mode of transport (walking for now) and the estimated time
@@ -134,7 +138,7 @@ Widget stepBetweenLandmarks(Landmark before, Landmark after) {
         Column(
           children: [
             Icon(Icons.directions_walk),
-            Text("5 min", style: TextStyle(fontSize: 10)),
+            Text("${landmark.tripTime} min", style: TextStyle(fontSize: 10)),
           ],
         ),
         Spacer(),
@@ -149,8 +153,5 @@ Widget stepBetweenLandmarks(Landmark before, Landmark after) {
   );
 }
 
-Future<LinkedList<Landmark>> getLandmarks (Future<Trip>? trip) async {
-  Trip tripf = await trip!;
-  return tripf.landmarks;
-}
+
 
diff --git a/frontend/lib/modules/map.dart b/frontend/lib/modules/map.dart
index d0ab511..2e95ccd 100644
--- a/frontend/lib/modules/map.dart
+++ b/frontend/lib/modules/map.dart
@@ -8,7 +8,7 @@ import 'package:the_widget_marker/the_widget_marker.dart';
 
 class MapWidget extends StatefulWidget {
 
-  final Future<Trip>? trip;
+  final Trip? trip;
 
   MapWidget({
     this.trip
@@ -31,8 +31,7 @@ class _MapWidgetState extends State<MapWidget> {
 
   void _onMapCreated(GoogleMapController controller) async {
     mapController = controller;
-    Trip? trip = await widget.trip;
-    List<double>? newLocation = trip?.landmarks.first.location;
+    List<double>? newLocation = widget.trip?.landmarks.first.location;
     if (newLocation != null) {
       CameraUpdate update = CameraUpdate.newLatLng(LatLng(newLocation[0], newLocation[1]));
       controller.moveCamera(update);
@@ -48,8 +47,7 @@ class _MapWidgetState extends State<MapWidget> {
 
   void drawLandmarks() async {
     // (re)draws landmarks on the map
-    Trip? trip = await widget.trip;
-    LinkedList<Landmark>? landmarks = trip?.landmarks;
+    LinkedList<Landmark>? landmarks = widget.trip?.landmarks;
     if (landmarks != null){
       for (Landmark landmark in landmarks) {
         markers.add(Marker(
diff --git a/frontend/lib/modules/trips_overview.dart b/frontend/lib/modules/trips_overview.dart
index 8501013..fec7f2a 100644
--- a/frontend/lib/modules/trips_overview.dart
+++ b/frontend/lib/modules/trips_overview.dart
@@ -30,7 +30,7 @@ class _TripsOverviewState extends State<TripsOverview> {
           onTap: () {
             Navigator.of(context).push(
               MaterialPageRoute(
-                builder: (context) => BasePage(mainScreen: "map", trip: Future.value(trip))
+                builder: (context) => BasePage(mainScreen: "map", trip: trip)
               )
             );
           },
diff --git a/frontend/lib/pages/new_trip.dart b/frontend/lib/pages/new_trip.dart
index 2464177..79c15ae 100644
--- a/frontend/lib/pages/new_trip.dart
+++ b/frontend/lib/pages/new_trip.dart
@@ -1,8 +1,10 @@
+import 'package:anyway/structs/landmark.dart';
+import 'package:flutter/material.dart';
+import 'package:geocoding/geocoding.dart';
 
 import 'package:anyway/layout.dart';
-import 'package:anyway/structs/preferences.dart';
 import 'package:anyway/utils/fetch_trip.dart';
-import 'package:flutter/material.dart';
+import 'package:anyway/structs/preferences.dart';
 import "package:anyway/structs/trip.dart";
 
 
@@ -64,7 +66,16 @@ class _NewTripPageState extends State<NewTripPage> {
                         double.parse(lonController.text)
                       ];
                       Future<UserPreferences> preferences = loadUserPreferences();
-                      Future<Trip>? trip = fetchTrip(startPoint, preferences);
+                      Trip trip = Trip();
+                      trip.landmarks.add(
+                        Landmark(
+                          location: startPoint,
+                          name: "start",
+                          type: LandmarkType(name: 'start'),
+                          uuid: "pending"
+                        )
+                      );
+                      fetchTrip(trip, preferences);
                         Navigator.of(context).push(
                           MaterialPageRoute(
                             builder: (context) => BasePage(mainScreen: "map", trip: trip)
diff --git a/frontend/lib/pages/overview.dart b/frontend/lib/pages/overview.dart
index ce3646b..e98d91b 100644
--- a/frontend/lib/pages/overview.dart
+++ b/frontend/lib/pages/overview.dart
@@ -10,10 +10,10 @@ import 'package:anyway/modules/greeter.dart';
 
 
 class NavigationOverview extends StatefulWidget {
-  final Future<Trip> trip;
+  final Trip trip;
 
   NavigationOverview({
-    required this.trip
+    required this.trip,
   });
 
   @override
@@ -27,53 +27,56 @@ class _NavigationOverviewState extends State<NavigationOverview> {
   @override
   Widget build(BuildContext context) {
     return SlidingUpPanel(
-        renderPanelSheet: false,
         panel: _floatingPanel(),
-        collapsed: _floatingCollapsed(),
-        body: MapWidget(trip: widget.trip)
+        // collapsed: _floatingCollapsed(),
+        body: MapWidget(trip: widget.trip),
+        // renderPanelSheet: false,
+        // backdropEnabled: true,
+        maxHeight: MediaQuery.of(context).size.height * 0.8,
+        padding: EdgeInsets.all(10),
+        // panelSnapping: false,
+        borderRadius: BorderRadius.only(topLeft: Radius.circular(25), topRight: Radius.circular(25)),
+        boxShadow: [
+          BoxShadow(
+            blurRadius: 20.0,
+            color: Colors.black,
+          )
+        ],
     );
   }
 
   Widget _floatingCollapsed(){
-    final ThemeData theme = Theme.of(context);
-    return Container(
-      decoration: BoxDecoration(
-        color: theme.canvasColor,
-        borderRadius: BorderRadius.only(topLeft: Radius.circular(24.0), topRight: Radius.circular(24.0)),
-        boxShadow: []
-      ),
-      
-      child: Greeter(standalone: true, trip: widget.trip)
+    return Greeter(
+      trip: widget.trip
     );
   }
 
   Widget _floatingPanel(){
-    final ThemeData theme = Theme.of(context);
-    return Container(
-      decoration: BoxDecoration(
-        color: Colors.white,
-        borderRadius: BorderRadius.all(Radius.circular(24.0)),
-        boxShadow: [
-          BoxShadow(
-            blurRadius: 20.0,
-            color: theme.shadowColor,
-          ),
-        ]
-      ),
-      child: Center(
-        child: Padding(
-        padding: EdgeInsets.all(8.0),
-        child: SingleChildScrollView(
-          child: Column(
-            children: <Widget>[
-              Greeter(standalone: false, trip: widget.trip),
-              LandmarksOverview(trip: widget.trip),
-            ],
-          ),
+    return Column(
+      children: [
+        Padding(
+          padding: const EdgeInsets.all(15),
+          child: 
+            Center(
+              child: Container(
+                width: 40,
+                height: 5,
+                decoration: BoxDecoration(
+                  color: Colors.grey[300],
+                  borderRadius: BorderRadius.all(Radius.circular(12.0)),
+                ),
+              ),
+            ),
         ),
-      ),
-    ),
+        Expanded(
+          child: ListView(
+            children: [
+              Greeter(trip: widget.trip),
+              LandmarksOverview(trip: widget.trip)
+            ]
+          )
+        )
+      ],
     );
   }
-
 }
diff --git a/frontend/lib/structs/trip.dart b/frontend/lib/structs/trip.dart
index f6d94e3..3b8c1ff 100644
--- a/frontend/lib/structs/trip.dart
+++ b/frontend/lib/structs/trip.dart
@@ -5,35 +5,56 @@ import 'dart:collection';
 import 'dart:convert';
 
 import 'package:anyway/structs/landmark.dart';
+import 'package:flutter/foundation.dart';
+import 'package:geocoding/geocoding.dart';
 import 'package:shared_preferences/shared_preferences.dart';
 
-class Trip {
-  final String uuid;
-  final String cityName;
-  // TODO: cityName should be inferred from coordinates of the Landmarks
-  final int totalTime;
-  final LinkedList<Landmark> landmarks;
+class Trip with ChangeNotifier {
+  String uuid;
+  int totalTime;
+  LinkedList<Landmark> landmarks;
   // could be empty as well
 
+  Future<String> get cityName async {
+    if (GeocodingPlatform.instance == null) {
+      return '${landmarks.first.location[0]}, ${landmarks.first.location[1]}';
+    }
+    List<Placemark> placemarks = await placemarkFromCoordinates(landmarks.first.location[0], landmarks.first.location[1]);
+    return placemarks.first.locality ?? 'Unknown';
+  }
+
 
   Trip({
-    required this.uuid,
-    required this.cityName,
-    required this.landmarks,
-    this.totalTime = 0
-  });
-
+    this.uuid = 'pending',
+    this.totalTime = 0,
+    LinkedList<Landmark>? landmarks
+    // a trip can be created with no landmarks, but the list should be initialized anyway
+  }) : landmarks = landmarks ?? LinkedList<Landmark>();
+  
 
   factory Trip.fromJson(Map<String, dynamic> json) {
     Trip trip = Trip(
       uuid: json['uuid'],
-      cityName: json['city_name'] ?? 'Not communicated',
-      landmarks: LinkedList()
+      totalTime: json['total_time'],
     );
 
     return trip;
   }
 
+  void loadFromJson(Map<String, dynamic> json) {
+    uuid = json['uuid'];
+    totalTime = json['total_time'];
+    notifyListeners();
+  }
+  void addLandmark(Landmark landmark) {
+    landmarks.add(landmark);
+    notifyListeners();
+  }
+
+  void updateUUID(String newUUID) {
+    uuid = newUUID;
+    notifyListeners();
+  }
 
   factory Trip.fromPrefs(SharedPreferences prefs, String uuid) {
     String? content = prefs.getString('trip_$uuid');
@@ -47,7 +68,7 @@ class Trip {
 
   Map<String, dynamic> toJson() => {
     'uuid': uuid,
-    'city_name': cityName,
+    'total_time': totalTime,
     'first_landmark_uuid': landmarks.first.uuid
   };
 
diff --git a/frontend/lib/utils/fetch_trip.dart b/frontend/lib/utils/fetch_trip.dart
index aeba3b8..54d9365 100644
--- a/frontend/lib/utils/fetch_trip.dart
+++ b/frontend/lib/utils/fetch_trip.dart
@@ -1,7 +1,7 @@
 import "dart:convert";
 import "dart:developer";
-
 import 'package:dio/dio.dart';
+
 import 'package:anyway/constants.dart';
 import "package:anyway/structs/landmark.dart";
 import "package:anyway/structs/trip.dart";
@@ -25,14 +25,14 @@ Dio dio = Dio(
   ),
 );
 
-Future<Trip>? fetchTrip(
-  List<double> startPoint,
+fetchTrip(
+  Trip trip,
   Future<UserPreferences> preferences,
 ) async {
   UserPreferences prefs = await preferences;
   Map<String, dynamic> data = {
     "preferences": prefs.toJson(),
-    "start": startPoint
+    "start": trip.landmarks!.first.location,
   };
   String dataString = jsonEncode(data);
   log(dataString);
@@ -44,24 +44,25 @@ Future<Trip>? fetchTrip(
 
   // handle errors
   if (response.statusCode != 200) {
+    trip.updateUUID("error");
     throw Exception('Failed to load trip');
   }
   if (response.data["error"] != null) {
+    trip.updateUUID("error");
     throw Exception(response.data["error"]);
   }
   log(response.data.toString());
   Map<String, dynamic> json = response.data;
 
-  // only fetch the trip "meta" data for now
-  Trip trip = Trip.fromJson(json);
+  // only fill in the trip "meta" data for now
+  trip.loadFromJson(json);
 
   String? nextUUID = json["first_landmark_uuid"];
   while (nextUUID != null) {
     var (landmark, newUUID) = await fetchLandmark(nextUUID);
-    trip.landmarks.add(landmark);
+    trip.addLandmark(landmark);
     nextUUID = newUUID;
   }
-  return trip;
 }
 
 
diff --git a/frontend/lib/utils/load_trips.dart b/frontend/lib/utils/load_trips.dart
index 22b9ced..f608294 100644
--- a/frontend/lib/utils/load_trips.dart
+++ b/frontend/lib/utils/load_trips.dart
@@ -17,7 +17,7 @@ Future<List<Trip>> loadTrips() async {
   }
 
   if (trips.isEmpty) {
-    Trip t1 = Trip(uuid: '1', cityName: 'Paris', landmarks: LinkedList<Landmark>());
+    Trip t1 = Trip(uuid: '1', landmarks: LinkedList<Landmark>());
     t1.landmarks.add(
       Landmark(
         uuid: '1',
@@ -66,7 +66,7 @@ Future<List<Trip>> loadTrips() async {
     trips.add(t1);
 
 
-    Trip t2 = Trip(uuid: '2', cityName: 'Vienna', landmarks: LinkedList<Landmark>());
+    Trip t2 = Trip(uuid: '2', landmarks: LinkedList<Landmark>());
 
     t2.landmarks.add(
       Landmark(
diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock
index 8d3dc88..a891618 100644
--- a/frontend/pubspec.lock
+++ b/frontend/pubspec.lock
@@ -25,6 +25,30 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.1.1"
+  cached_network_image:
+    dependency: "direct main"
+    description:
+      name: cached_network_image
+      sha256: "4a5d8d2c728b0f3d0245f69f921d7be90cae4c2fd5288f773088672c0893f819"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.4.0"
+  cached_network_image_platform_interface:
+    dependency: transitive
+    description:
+      name: cached_network_image_platform_interface
+      sha256: ff0c949e323d2a1b52be73acce5b4a7b04063e61414c8ca542dbba47281630a7
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.1.0"
+  cached_network_image_web:
+    dependency: transitive
+    description:
+      name: cached_network_image_web
+      sha256: "6322dde7a5ad92202e64df659241104a43db20ed594c41ca18de1014598d7996"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.3.0"
   characters:
     dependency: transitive
     description:
@@ -168,6 +192,38 @@ packages:
     description: flutter
     source: sdk
     version: "0.0.0"
+  geocoding:
+    dependency: "direct main"
+    description:
+      name: geocoding
+      sha256: d580c801cba9386b4fac5047c4c785a4e19554f46be42f4f5e5b7deacd088a66
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.0.0"
+  geocoding_android:
+    dependency: transitive
+    description:
+      name: geocoding_android
+      sha256: "1b13eca79b11c497c434678fed109c2be020b158cec7512c848c102bc7232603"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.3.1"
+  geocoding_ios:
+    dependency: transitive
+    description:
+      name: geocoding_ios
+      sha256: "94ddba60387501bd1c11e18dca7c5a9e8c645d6e3da9c38b9762434941870c24"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.0.1"
+  geocoding_platform_interface:
+    dependency: transitive
+    description:
+      name: geocoding_platform_interface
+      sha256: "8c2c8226e5c276594c2e18bfe88b19110ed770aeb7c1ab50ede570be8b92229b"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.2.0"
   google_maps:
     dependency: transitive
     description:
@@ -296,6 +352,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.12.0"
+  octo_image:
+    dependency: transitive
+    description:
+      name: octo_image
+      sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.0"
   path:
     dependency: transitive
     description:
diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml
index 6f6d8cf..a2f41d4 100644
--- a/frontend/pubspec.yaml
+++ b/frontend/pubspec.yaml
@@ -41,6 +41,8 @@ dependencies:
   dio: ^5.5.0+1
   google_maps_flutter: ^2.7.0
   the_widget_marker: ^1.0.0
+  cached_network_image: ^3.4.0
+  geocoding: ^3.0.0
 
 dev_dependencies:
   flutter_test:
-- 
2.47.2


From 71d9554d97ea786c1bfbcc5561c47fee44df94dc Mon Sep 17 00:00:00 2001
From: Remy Moll <me@moll.re>
Date: Mon, 5 Aug 2024 10:18:00 +0200
Subject: [PATCH 7/9] ui improvements for trips and landmarks

---
 frontend/lib/layout.dart                     |  10 +-
 frontend/lib/modules/greeter.dart            |  14 ++-
 frontend/lib/modules/landmarks_overview.dart |  99 ++++++++++--------
 frontend/lib/modules/map.dart                | 100 +++++++++++-------
 frontend/lib/modules/trips_overview.dart     |  13 ++-
 frontend/lib/pages/profile.dart              |  26 ++++-
 frontend/lib/structs/landmark.dart           |  16 ++-
 frontend/lib/structs/linked_landmarks.dart   |  46 --------
 frontend/lib/structs/trip.dart               |  15 ++-
 frontend/lib/utils/fetch_trip.dart           |   7 ++
 frontend/pubspec.lock                        | 104 ++++++-------------
 frontend/pubspec.yaml                        |   4 +-
 12 files changed, 237 insertions(+), 217 deletions(-)
 delete mode 100644 frontend/lib/structs/linked_landmarks.dart

diff --git a/frontend/lib/layout.dart b/frontend/lib/layout.dart
index fbfc6ca..43aebde 100644
--- a/frontend/lib/layout.dart
+++ b/frontend/lib/layout.dart
@@ -141,7 +141,7 @@ Trip getFirstTrip(Future<List<Trip>> trips) {
       uuid: '1',
       name: "Eiffel Tower",
       location: [48.859, 2.295],
-      type: LandmarkType(name: "Tower"),
+      type: monument,
       imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a8/Tour_Eiffel_Wikimedia_Commons.jpg/1037px-Tour_Eiffel_Wikimedia_Commons.jpg"
     ),
   );
@@ -150,7 +150,7 @@ Trip getFirstTrip(Future<List<Trip>> trips) {
       uuid: "2",
       name: "Notre Dame Cathedral",
       location: [48.8530, 2.3498],
-      type: LandmarkType(name: "Monument"),
+      type: monument,
       imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f7/Notre-Dame_de_Paris%2C_4_October_2017.jpg/440px-Notre-Dame_de_Paris%2C_4_October_2017.jpg"
     ),
   );
@@ -159,7 +159,7 @@ Trip getFirstTrip(Future<List<Trip>> trips) {
       uuid: "3",
       name: "Louvre palace",
       location: [48.8606, 2.3376],
-      type: LandmarkType(name: "Museum"),
+      type: museum,
       imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/6/66/Louvre_Museum_Wikimedia_Commons.jpg/540px-Louvre_Museum_Wikimedia_Commons.jpg"
     ),
   );
@@ -168,7 +168,7 @@ Trip getFirstTrip(Future<List<Trip>> trips) {
       uuid: "4",
       name: "Pont-des-arts",
       location: [48.8585, 2.3376],
-      type: LandmarkType(name: "Bridge"),
+      type: monument,
       imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d1/Pont_des_Arts%2C_6e_Arrondissement%2C_Paris_%28HDR%29_20140320_1.jpg/560px-Pont_des_Arts%2C_6e_Arrondissement%2C_Paris_%28HDR%29_20140320_1.jpg"
     ),
   );
@@ -177,7 +177,7 @@ Trip getFirstTrip(Future<List<Trip>> trips) {
       uuid: "5",
       name: "Panthéon",
       location: [48.847, 2.347],
-      type: LandmarkType(name: "Monument"),
+      type: monument,
       imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/8/80/Pantheon_of_Paris_007.JPG/1280px-Pantheon_of_Paris_007.JPG"
     ),
   );
diff --git a/frontend/lib/modules/greeter.dart b/frontend/lib/modules/greeter.dart
index 6b74c13..034720e 100644
--- a/frontend/lib/modules/greeter.dart
+++ b/frontend/lib/modules/greeter.dart
@@ -1,4 +1,5 @@
 import 'package:anyway/structs/trip.dart';
+import 'package:auto_size_text/auto_size_text.dart';
 
 import 'package:flutter/material.dart';
 
@@ -24,14 +25,21 @@ class _GreeterState extends State<Greeter> {
         future: widget.trip.cityName,
         builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
           if (snapshot.hasData) {
-            return Text(
+            return AutoSizeText(
+              maxLines: 1,
               'Welcome to ${snapshot.data}!',
               style: TextStyle(color: theme.primaryColor, fontWeight: FontWeight.bold, fontSize: 24),
             );
           } else if (snapshot.hasError) {
-            return const Text('Welcome to your trip!');
+            return const AutoSizeText(
+              maxLines: 1,
+              'Welcome to your trip!'
+            );
           } else {
-            return const Text('Welcome to ...');
+            return const AutoSizeText(
+              maxLines: 1,
+              'Welcome to ...'
+            );
           }
         }
       );
diff --git a/frontend/lib/modules/landmarks_overview.dart b/frontend/lib/modules/landmarks_overview.dart
index 387bf11..844673d 100644
--- a/frontend/lib/modules/landmarks_overview.dart
+++ b/frontend/lib/modules/landmarks_overview.dart
@@ -51,7 +51,7 @@ class _LandmarksOverviewState extends State<LandmarksOverview> {
             ];
           } else {
             children = [
-              landmarksWithSteps(trip.landmarks),
+              landmarksWithSteps(),
               saveButton(),
             ];
           }
@@ -71,55 +71,61 @@ class _LandmarksOverviewState extends State<LandmarksOverview> {
     child: const Text('Save'),
   );
 
-}
-
-Widget landmarksWithSteps(LinkedList<Landmark> landmarks) {
-  List<Widget> children = [];
-  int lkey = 0;
-  for (Landmark landmark in landmarks) {
-    children.add(
-      Dismissible(
-        key: ValueKey<int>(lkey),
-        child: LandmarkCard(landmark),
-        // onDismissed: (direction) {
-        //   // Remove the item from the data source.
-        //   setState(() {
-        //     landmarks.remove(landmark);
-        //   });
-        //   // Then show a snackbar.
-        //   ScaffoldMessenger.of(context)
-        //       .showSnackBar(SnackBar(content: Text("${landmark.name} dismissed")));
-        // },
-        background: Container(color: Colors.red),
-        secondaryBackground: Container(
-          color: Colors.red,
-          child: Icon(
-            Icons.delete,
-            color: Colors.white,
-          ),
-          padding: EdgeInsets.all(15),
-          alignment: Alignment.centerRight,
-        ),
-      )
+  Widget landmarksWithSteps() {
+    return ListenableBuilder(
+      listenable: widget.trip!,
+      builder: (BuildContext context, Widget? child) {
+        List<Widget> children = [];
+        for (Landmark landmark in widget.trip!.landmarks) {
+          children.add(
+            Dismissible(
+              key: ValueKey<int>(landmark.hashCode),
+              child: LandmarkCard(landmark),
+              dismissThresholds: {DismissDirection.endToStart: 0.6},
+              onDismissed: (direction) {
+                // Remove the item from the data source.
+                  log(landmark.name);
+                setState(() {
+                  widget.trip!.removeLandmark(landmark);
+                });
+                // Then show a snackbar.
+                ScaffoldMessenger.of(context)
+                    .showSnackBar(SnackBar(content: Text("We won't show ${landmark.name} again")));
+              },
+              background: Container(color: Colors.red),
+              secondaryBackground: Container(
+                color: Colors.red,
+                child: Icon(
+                  Icons.delete,
+                  color: Colors.white,
+                ),
+                padding: EdgeInsets.all(15),
+                alignment: Alignment.centerRight,
+              ),
+            )
+          );
+          if (landmark.next != null) {
+            Widget step = stepBetweenLandmarks(landmark, landmark.next!);
+            children.add(step);
+          }
+        }
+        return Column(
+          children: children  
+        );
+      },
     );
-    lkey++;
-    if (landmark.next != null) {
-      Widget step = stepBetweenLandmarks(landmark);
-      children.add(step);
-    }
   }
-
-  return Column(
-    children: children
-  );
 }
 
 
-Widget stepBetweenLandmarks(Landmark landmark) {
+Widget stepBetweenLandmarks(Landmark current, Landmark next) {
   // This is a simple widget that draws a line between landmark-cards
   // It's a vertical dotted line
   // Next to the line is the icon for the mode of transport (walking for now) and the estimated time
   // There is also a button to open the navigation instructions as a new intent
+  // next landmark is not actually required, but it ensures that the widget is deleted when the next landmark is removed (which makes sense, because then there will be another step)
+  int timeRounded = 5 * (current.tripTime?.inMinutes ?? 0) ~/ 5;
+  // ~/ is integer division (rounding)
   return Container(
     margin: EdgeInsets.all(10),
     padding: EdgeInsets.all(10),
@@ -138,7 +144,7 @@ Widget stepBetweenLandmarks(Landmark landmark) {
         Column(
           children: [
             Icon(Icons.directions_walk),
-            Text("${landmark.tripTime} min", style: TextStyle(fontSize: 10)),
+            Text("~$timeRounded min", style: TextStyle(fontSize: 10)),
           ],
         ),
         Spacer(),
@@ -146,8 +152,13 @@ Widget stepBetweenLandmarks(Landmark landmark) {
           onPressed: () {
             // Open navigation instructions
           },
-          child: Text("Navigate"),
-        ),
+          child: Row(
+            children: [
+              Icon(Icons.directions),
+              Text("Directions"),
+            ],
+          ),
+        )
       ],
     ),
   );
diff --git a/frontend/lib/modules/map.dart b/frontend/lib/modules/map.dart
index 2e95ccd..199388b 100644
--- a/frontend/lib/modules/map.dart
+++ b/frontend/lib/modules/map.dart
@@ -4,7 +4,8 @@ import 'package:flutter/material.dart';
 import 'package:anyway/structs/landmark.dart';
 import 'package:anyway/structs/trip.dart';
 import 'package:google_maps_flutter/google_maps_flutter.dart';
-import 'package:the_widget_marker/the_widget_marker.dart';
+import 'package:widget_to_marker/widget_to_marker.dart';
+
 
 class MapWidget extends StatefulWidget {
 
@@ -25,38 +26,42 @@ class _MapWidgetState extends State<MapWidget> {
     target: LatLng(48.8566, 2.3522),
     zoom: 11.0,
   );
-  Set<Marker> markers = <Marker>{};
-  final GlobalKey globalKey = GlobalKey();
+  Set<Marker> mapMarkers = <Marker>{};
   
 
   void _onMapCreated(GoogleMapController controller) async {
     mapController = controller;
-    List<double>? newLocation = widget.trip?.landmarks.first.location;
+    List<double>? newLocation = widget.trip?.landmarks.firstOrNull?.location;
     if (newLocation != null) {
       CameraUpdate update = CameraUpdate.newLatLng(LatLng(newLocation[0], newLocation[1]));
       controller.moveCamera(update);
     }
-    drawLandmarks();
+    // addLandmarkMarker();
   }
 
-
   void _onCameraIdle() {
     // print(mapController.getLatLng(ScreenCoordinate(x: 0, y: 0)));
   }
 
 
-  void drawLandmarks() async {
-    // (re)draws landmarks on the map
+  void addLandmarkMarker() async {
     LinkedList<Landmark>? landmarks = widget.trip?.landmarks;
-    if (landmarks != null){
-      for (Landmark landmark in landmarks) {
-        markers.add(Marker(
-          markerId: MarkerId(landmark.name),
-          position: LatLng(landmark.location[0], landmark.location[1]),
-          // infoWindow: InfoWindow(title: landmark.name, snippet: landmark.type.name),
-          icon: await MarkerIcon.widgetToIcon(globalKey),
-        ));
-      }
+    int i = mapMarkers.length;
+    Landmark? current = landmarks!.elementAtOrNull(i);
+    if (current != null){
+      mapMarkers.add(
+        Marker(
+          markerId: MarkerId(current.name),
+          position: LatLng(current.location[0], current.location[1]),
+          icon: await CustomMarker(
+            landmark: current,
+            position: i+1
+          ).toBitmapDescriptor(
+            logicalSize: const Size(150, 150),
+            imageSize: const Size(150, 150)
+          )
+        )
+      );
       setState(() {});
     }
   }
@@ -64,39 +69,60 @@ class _MapWidgetState extends State<MapWidget> {
 
   @override
   Widget build(BuildContext context) {
-    return Stack(
-      children: [
-        MyMarker(globalKey),
-
-        GoogleMap(
-              onMapCreated: _onMapCreated,
-              initialCameraPosition: _cameraPosition,
-              onCameraIdle: _onCameraIdle,
-              // onLongPress: ,
-              markers: markers,
-              cloudMapId: '41c21ac9b81dbfd8',
-            )
-      ]
+    return ListenableBuilder(
+      listenable: widget.trip!,
+      builder: (context, child) {
+        addLandmarkMarker();
+        return GoogleMap(
+          onMapCreated: _onMapCreated,
+          initialCameraPosition: _cameraPosition,
+          onCameraIdle: _onCameraIdle,
+          
+          // onLongPress: ,
+          markers: mapMarkers,
+          cloudMapId: '41c21ac9b81dbfd8',
+        );
+      }
     );
   }
 }
 
 
-class MyMarker extends StatelessWidget {
-  // declare a global key and get it trough Constructor
+class CustomMarker extends StatelessWidget {
+  final Landmark landmark;
+  final int position;
 
-  MyMarker(this.globalKeyMyWidget);
-  final GlobalKey globalKeyMyWidget;
+  CustomMarker({
+    super.key,
+    required this.landmark,
+    required this.position
+    });
 
   @override
   Widget build(BuildContext context) {
     // This returns an outlined circle, with an icon corresponding to the landmark type
     // As a small dot, the number of the landmark is displayed in the top right
+    Icon icon;
+    if (landmark.type == museum) {
+      icon = Icon(Icons.museum, color: Colors.black, size: 50);
+    } else if (landmark.type == monument) {
+      icon = Icon(Icons.church, color: Colors.black, size: 50);
+    } else if (landmark.type == park) {
+      icon = Icon(Icons.park, color: Colors.black, size: 50);
+    } else if (landmark.type == restaurant) {
+      icon = Icon(Icons.restaurant, color: Colors.black, size: 50);
+    } else if (landmark.type == shop) {
+      icon = Icon(Icons.shopping_cart, color: Colors.black, size: 50);
+    } else {
+      icon = Icon(Icons.location_on, color: Colors.black, size: 50);
+    }
+
     return RepaintBoundary(
-      key: globalKeyMyWidget,
       child: Stack(
         children: [
           Container(
+            // these are not the final sizes, since the final size is set in the toBitmapDescriptor method
+            // they are useful nevertheless to ensure the scale of the components are correct
             width: 75,
             height: 75,
             decoration: BoxDecoration(
@@ -108,7 +134,7 @@ class MyMarker extends StatelessWidget {
               shape: BoxShape.circle,
               border: Border.all(color: Colors.black, width: 5),
             ),
-            child: Icon(Icons.location_on, color: Colors.black, size: 50),
+            child: icon,
           ),
           Positioned(
             top: 0,
@@ -119,7 +145,7 @@ class MyMarker extends StatelessWidget {
                 color: Theme.of(context).primaryColor,
                 shape: BoxShape.circle,
               ),
-              child: Text('1', style: TextStyle(color: Colors.white, fontSize: 20)),
+              child: Text('$position', style: TextStyle(color: Colors.white, fontSize: 20)),
             ),
           ),
         ],
diff --git a/frontend/lib/modules/trips_overview.dart b/frontend/lib/modules/trips_overview.dart
index fec7f2a..765ef0e 100644
--- a/frontend/lib/modules/trips_overview.dart
+++ b/frontend/lib/modules/trips_overview.dart
@@ -25,7 +25,18 @@ class _TripsOverviewState extends State<TripsOverview> {
       children = List<Widget>.generate(snapshot.data!.length, (index) {
         Trip trip = snapshot.data![index];
         return ListTile(
-          title: Text("Trip to ${trip.cityName}"),
+          title: FutureBuilder(
+            future: trip.cityName,
+            builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
+              if (snapshot.hasData) {
+                return Text("Trip to ${snapshot.data}");
+              } else if (snapshot.hasError) {
+                return Text("Error: ${snapshot.error}");
+              } else {
+                return const Text("Trip to ...");
+              }
+            },
+          ),
           leading: Icon(Icons.pin_drop),
           onTap: () {
             Navigator.of(context).push(
diff --git a/frontend/lib/pages/profile.dart b/frontend/lib/pages/profile.dart
index 9371e87..0ee2b8f 100644
--- a/frontend/lib/pages/profile.dart
+++ b/frontend/lib/pages/profile.dart
@@ -2,6 +2,7 @@ import 'package:anyway/structs/preferences.dart';
 import 'package:flutter/material.dart';
 
 
+bool debugMode = false;
 
 class ProfilePage extends StatefulWidget {
   @override
@@ -12,6 +13,27 @@ class _ProfilePageState extends State<ProfilePage> {
   Future<UserPreferences> _prefs = loadUserPreferences();
 
 
+  Widget debugButton() {
+    return Padding(
+      padding: EdgeInsets.only(top: 20),
+      child: Row(
+        children: [
+          Text('Debug mode'),
+          Switch(
+            value: debugMode,
+            onChanged: (bool? newValue) {
+              setState(() {
+                debugMode = newValue!;
+              });
+            }
+          )
+        ],
+      )
+    );
+  }
+
+
+
   @override
   Widget build(BuildContext context) {
     return ListView(
@@ -36,7 +58,8 @@ class _ProfilePageState extends State<ProfilePage> {
           ),
         ),
 
-        FutureBuilder(future: _prefs, builder: futureSliders)
+        FutureBuilder(future: _prefs, builder: futureSliders),
+        debugButton()
       ]
     );
   }
@@ -59,7 +82,6 @@ class _ProfilePageState extends State<ProfilePage> {
 }
 
 
-
 class PreferenceSliders extends StatefulWidget {
   final List<SinglePreference> prefs;
 
diff --git a/frontend/lib/structs/landmark.dart b/frontend/lib/structs/landmark.dart
index 9e5e1ef..0f387af 100644
--- a/frontend/lib/structs/landmark.dart
+++ b/frontend/lib/structs/landmark.dart
@@ -3,6 +3,14 @@ import 'dart:convert';
 
 import 'package:shared_preferences/shared_preferences.dart';
 
+
+const LandmarkType museum = LandmarkType(name: 'Museum');
+const LandmarkType monument = LandmarkType(name: 'Monument');
+const LandmarkType park = LandmarkType(name: 'Park');
+const LandmarkType restaurant = LandmarkType(name: 'Restaurant');
+const LandmarkType shop = LandmarkType(name: 'Shop');
+
+
 final class Landmark extends LinkedListEntry<Landmark>{
   // A linked node of a list of Landmarks
   final String uuid;
@@ -55,11 +63,12 @@ final class Landmark extends LinkedListEntry<Landmark>{
       final imageURL = json['image_url'] as String?;
       final description = json['description'] as String?;
       var duration = Duration(minutes: json['duration'] ?? 0) as Duration?;
-      if (duration == const Duration()) {duration = null;};
+      // if (duration == const Duration()) {duration = null;};
       final visited = json['visited'] as bool?;
+      var tripTime = Duration(minutes: json['time_to_reach_next'] ?? 0) as Duration?;
       
       return Landmark(
-        uuid: uuid, name: name, location: locationFixed, type: typeFixed, isSecondary: isSecondary, imageURL: imageURL, description: description, duration: duration, visited: visited);
+        uuid: uuid, name: name, location: locationFixed, type: typeFixed, isSecondary: isSecondary, imageURL: imageURL, description: description, duration: duration, visited: visited, tripTime: tripTime);
     } else {
       throw FormatException('Invalid JSON: $json');
     }
@@ -81,7 +90,8 @@ final class Landmark extends LinkedListEntry<Landmark>{
     'image_url': imageURL,
     'description': description,
     'duration': duration?.inMinutes,
-    'visited': visited
+    'visited': visited,
+    'trip_time': tripTime?.inMinutes,
   };
 }
 
diff --git a/frontend/lib/structs/linked_landmarks.dart b/frontend/lib/structs/linked_landmarks.dart
deleted file mode 100644
index c995ed0..0000000
--- a/frontend/lib/structs/linked_landmarks.dart
+++ /dev/null
@@ -1,46 +0,0 @@
-// import "package:anyway/structs/landmark.dart";
-
-// class Linked<Landmark> {
-//   Landmark? head;
-  
-//   Linked();
-
-//   // class methods
-//   bool get isEmpty => head == null;
-
-//   // Add a new node to the end of the list
-//   void add(Landmark value) {
-//     if (isEmpty) {
-//       // If the list is empty, set the new node as the head
-//       head = value;
-//     } else {
-//       Landmark? current = head;
-//       while (current!.next != null) {
-//         // Traverse the list to find the last node
-//         current = current.next;
-//       }
-//       current.next = value; // Set the new node as the next node of the last node
-//     }
-//   }
-
-//   // Remove the first node with the given value
-//   void remove(Landmark value) {
-//     if (isEmpty) return;
-
-//     // If the value is in the head node, update the head to the next node
-//     if (head! == value) {
-//       head = head.next;
-//       return;
-//     }
-
-//     var current = head;
-//     while (current!.next != null) {
-//       if (current.next! == value) {
-//         // If the value is found in the next node, skip the next node
-//         current.next = current.next.next;
-//         return;
-//       }
-//       current = current.next;
-//     }
-//   }
-// }
\ No newline at end of file
diff --git a/frontend/lib/structs/trip.dart b/frontend/lib/structs/trip.dart
index 3b8c1ff..b8176db 100644
--- a/frontend/lib/structs/trip.dart
+++ b/frontend/lib/structs/trip.dart
@@ -16,11 +16,15 @@ class Trip with ChangeNotifier {
   // could be empty as well
 
   Future<String> get cityName async {
+    List<double>? location = landmarks.firstOrNull?.location; 
     if (GeocodingPlatform.instance == null) {
-      return '${landmarks.first.location[0]}, ${landmarks.first.location[1]}';
+      return '$location';
+    } else if (location == null) {
+      return 'Unknown';
+    } else{
+      List<Placemark> placemarks = await placemarkFromCoordinates(location[0], location[1]);
+      return placemarks.first.locality ?? 'Unknown';
     }
-    List<Placemark> placemarks = await placemarkFromCoordinates(landmarks.first.location[0], landmarks.first.location[1]);
-    return placemarks.first.locality ?? 'Unknown';
   }
 
 
@@ -56,6 +60,11 @@ class Trip with ChangeNotifier {
     notifyListeners();
   }
 
+  void removeLandmark(Landmark landmark) {
+    landmarks.remove(landmark);
+    notifyListeners();
+  }
+
   factory Trip.fromPrefs(SharedPreferences prefs, String uuid) {
     String? content = prefs.getString('trip_$uuid');
     Map<String, dynamic> json = jsonDecode(content!);
diff --git a/frontend/lib/utils/fetch_trip.dart b/frontend/lib/utils/fetch_trip.dart
index 54d9365..cf76d8b 100644
--- a/frontend/lib/utils/fetch_trip.dart
+++ b/frontend/lib/utils/fetch_trip.dart
@@ -57,6 +57,13 @@ fetchTrip(
   // only fill in the trip "meta" data for now
   trip.loadFromJson(json);
 
+
+  // now fill the trip with landmarks
+  // if (trip.landmarks.isNotEmpty) {
+  //   trip.landmarks.clear();
+  // }
+  // we are going to recreate all the landmarks from the information given by the api
+  trip.landmarks.remove(trip.landmarks.first);
   String? nextUUID = json["first_landmark_uuid"];
   while (nextUUID != null) {
     var (landmark, newUUID) = await fetchLandmark(nextUUID);
diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock
index a891618..6f808e0 100644
--- a/frontend/pubspec.lock
+++ b/frontend/pubspec.lock
@@ -1,14 +1,6 @@
 # Generated by pub
 # See https://dart.dev/tools/pub/glossary#lockfile
 packages:
-  args:
-    dependency: transitive
-    description:
-      name: args
-      sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a"
-      url: "https://pub.dev"
-    source: hosted
-    version: "2.5.0"
   async:
     dependency: transitive
     description:
@@ -17,6 +9,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.11.0"
+  auto_size_text:
+    dependency: "direct main"
+    description:
+      name: auto_size_text
+      sha256: "3f5261cd3fb5f2a9ab4e2fc3fba84fd9fcaac8821f20a1d4e71f557521b22599"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.0.0"
   boolean_selector:
     dependency: transitive
     description:
@@ -174,14 +174,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.0.21"
-  flutter_svg:
-    dependency: transitive
-    description:
-      name: flutter_svg
-      sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2"
-      url: "https://pub.dev"
-    source: hosted
-    version: "2.0.10+1"
   flutter_test:
     dependency: "direct dev"
     description: flutter
@@ -352,6 +344,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.12.0"
+  nested:
+    dependency: transitive
+    description:
+      name: nested
+      sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.0.0"
   octo_image:
     dependency: transitive
     description:
@@ -368,14 +368,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.9.0"
-  path_parsing:
-    dependency: transitive
-    description:
-      name: path_parsing
-      sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf
-      url: "https://pub.dev"
-    source: hosted
-    version: "1.0.1"
   path_provider:
     dependency: transitive
     description:
@@ -424,14 +416,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.3.0"
-  petitparser:
-    dependency: transitive
-    description:
-      name: petitparser
-      sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
-      url: "https://pub.dev"
-    source: hosted
-    version: "6.0.2"
   platform:
     dependency: transitive
     description:
@@ -448,6 +432,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.1.8"
+  provider:
+    dependency: "direct main"
+    description:
+      name: provider
+      sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c
+      url: "https://pub.dev"
+    source: hosted
+    version: "6.1.2"
   rxdart:
     dependency: transitive
     description:
@@ -621,14 +613,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "0.7.0"
-  the_widget_marker:
-    dependency: "direct main"
-    description:
-      name: the_widget_marker
-      sha256: "2476ae6b1fe29bbffa3596546871bd26f724c223ea7da74775801d9b70d64811"
-      url: "https://pub.dev"
-    source: hosted
-    version: "1.0.0"
   typed_data:
     dependency: transitive
     description:
@@ -645,30 +629,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "4.4.2"
-  vector_graphics:
-    dependency: transitive
-    description:
-      name: vector_graphics
-      sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3"
-      url: "https://pub.dev"
-    source: hosted
-    version: "1.1.11+1"
-  vector_graphics_codec:
-    dependency: transitive
-    description:
-      name: vector_graphics_codec
-      sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da
-      url: "https://pub.dev"
-    source: hosted
-    version: "1.1.11+1"
-  vector_graphics_compiler:
-    dependency: transitive
-    description:
-      name: vector_graphics_compiler
-      sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81"
-      url: "https://pub.dev"
-    source: hosted
-    version: "1.1.11+1"
   vector_math:
     dependency: transitive
     description:
@@ -693,6 +653,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "0.5.1"
+  widget_to_marker:
+    dependency: "direct main"
+    description:
+      name: widget_to_marker
+      sha256: badc36f23c76f3ca9d43d7780058096be774adf0f661bdb6eb6f6b893f648ab9
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.0.6"
   xdg_directories:
     dependency: transitive
     description:
@@ -701,14 +669,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.0.4"
-  xml:
-    dependency: transitive
-    description:
-      name: xml
-      sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
-      url: "https://pub.dev"
-    source: hosted
-    version: "6.5.0"
 sdks:
   dart: ">=3.4.0 <4.0.0"
   flutter: ">=3.22.0"
diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml
index a2f41d4..bb46eb7 100644
--- a/frontend/pubspec.yaml
+++ b/frontend/pubspec.yaml
@@ -40,9 +40,11 @@ dependencies:
   shared_preferences: ^2.2.3
   dio: ^5.5.0+1
   google_maps_flutter: ^2.7.0
-  the_widget_marker: ^1.0.0
   cached_network_image: ^3.4.0
   geocoding: ^3.0.0
+  widget_to_marker: ^1.0.6
+  provider: ^6.1.2
+  auto_size_text: ^3.0.0
 
 dev_dependencies:
   flutter_test:
-- 
2.47.2


From 89511f39cbdce4ff5080fcc1948bca6fd5fcef75 Mon Sep 17 00:00:00 2001
From: Remy Moll <me@moll.re>
Date: Mon, 5 Aug 2024 15:27:25 +0200
Subject: [PATCH 8/9] better errorhandling, slimmed down optimizer

---
 backend/src/main.py                           | 13 +++--
 .../src/parameters/landmark_parameters.yaml   |  2 +-
 backend/src/utils/landmarks_manager.py        | 46 +++++++++++-------
 frontend/lib/constants.dart                   |  2 +-
 frontend/lib/modules/greeter.dart             | 40 +++++++++++-----
 frontend/lib/modules/landmarks_overview.dart  | 48 +++++++++----------
 frontend/lib/pages/profile.dart               | 27 +++++++++++
 frontend/lib/structs/trip.dart                |  6 +++
 frontend/lib/utils/fetch_trip.dart            | 24 +++++-----
 9 files changed, 138 insertions(+), 70 deletions(-)

diff --git a/backend/src/main.py b/backend/src/main.py
index b9c1837..313df93 100644
--- a/backend/src/main.py
+++ b/backend/src/main.py
@@ -29,9 +29,11 @@ def new_trip(preferences: Preferences, start: tuple[float, float], end: tuple[fl
     :return: the uuid of the first landmark in the optimized route
     '''
     if preferences is None:
-        raise ValueError("Please provide preferences in the form of a 'Preference' BaseModel class.")
+        raise HTTPException(status_code=406, detail="Preferences not provided")
+    if preferences.shopping.score == 0 and preferences.sightseeing.score == 0 and preferences.nature.score == 0:
+        raise HTTPException(status_code=406, detail="All preferences are 0.")
     if start is None:
-        raise ValueError("Please provide the starting coordinates as a tuple of floats.")
+        raise HTTPException(status_code=406, detail="Start coordinates not provided")
     if end is None:
         end = start
         logger.info("No end coordinates provided. Using start=end.")
@@ -50,7 +52,12 @@ def new_trip(preferences: Preferences, start: tuple[float, float], end: tuple[fl
     landmarks_short.append(end_landmark)
     
     # First stage optimization
-    base_tour = optimizer.solve_optimization(preferences.max_time_minute, landmarks_short)
+    try:
+        base_tour = optimizer.solve_optimization(preferences.max_time_minute, landmarks_short)
+    except ArithmeticError:
+        raise HTTPException(status_code=500, detail="No solution found")
+    except TimeoutError:
+        raise HTTPException(status_code=500, detail="Optimzation took too long")
     
     # Second stage optimization
     refined_tour = refiner.refine_optimization(landmarks, base_tour, preferences.max_time_minute, preferences.detour_tolerance_minute)
diff --git a/backend/src/parameters/landmark_parameters.yaml b/backend/src/parameters/landmark_parameters.yaml
index 5de9c48..777c18f 100644
--- a/backend/src/parameters/landmark_parameters.yaml
+++ b/backend/src/parameters/landmark_parameters.yaml
@@ -1,6 +1,6 @@
 city_bbox_side: 5000 #m
 radius_close_to: 50
 church_coeff: 0.8
-park_coeff: 1.2
+park_coeff: 1.0
 tag_coeff: 10
 N_important: 40
diff --git a/backend/src/utils/landmarks_manager.py b/backend/src/utils/landmarks_manager.py
index 2422c11..6cde21c 100644
--- a/backend/src/utils/landmarks_manager.py
+++ b/backend/src/utils/landmarks_manager.py
@@ -21,7 +21,6 @@ class LandmarkManager:
 
     logger = logging.getLogger(__name__)
 
-    city_bbox_side: int     # bbox side in meters
     radius_close_to: int    # radius in meters
     church_coeff: float     # coeff to adjsut score of churches
     park_coeff: float       # coeff to adjust score of parks
@@ -36,12 +35,17 @@ class LandmarkManager:
 
         with constants.LANDMARK_PARAMETERS_PATH.open('r') as f:
             parameters = yaml.safe_load(f)
-            self.city_bbox_side = parameters['city_bbox_side']
+            self.max_bbox_side = parameters['city_bbox_side']
             self.radius_close_to = parameters['radius_close_to']
             self.church_coeff = parameters['church_coeff']
             self.park_coeff = parameters['park_coeff']
             self.tag_coeff = parameters['tag_coeff']
             self.N_important = parameters['N_important']
+            
+        with constants.OPTIMIZER_PARAMETERS_PATH.open('r') as f:
+            parameters = yaml.safe_load(f)
+            self.walking_speed = parameters['average_walking_speed']
+            self.detour_factor = parameters['detour_factor']
 
         self.overpass = Overpass()
         CachingStrategy.use(JSON, cacheDir=constants.OSM_CACHE_DIR)
@@ -65,23 +69,26 @@ class LandmarkManager:
                 - A list of the most important landmarks based on the user's preferences.
         """
 
+        max_walk_dist = (preferences.max_time_minute/2)/60*self.walking_speed*1000/self.detour_factor
+        reachable_bbox_side = min(max_walk_dist, self.max_bbox_side)
+
         L = []
-        bbox = self.create_bbox(center_coordinates)
+        bbox = self.create_bbox(center_coordinates, reachable_bbox_side)
         # list for sightseeing
         if preferences.sightseeing.score != 0:
-            score_function = lambda loc, n_tags: int((self.count_elements_close_to(loc) + ((n_tags**1.2)*self.tag_coeff) )*self.church_coeff)  
+            score_function = lambda loc, n_tags: int((((n_tags**1.2)*self.tag_coeff) )*self.church_coeff)   # self.count_elements_close_to(loc) +
             L1 = self.fetch_landmarks(bbox, self.amenity_selectors['sightseeing'], preferences.sightseeing.type, score_function)
             L += L1
 
         # list for nature
         if preferences.nature.score != 0:
-            score_function = lambda loc, n_tags: int((self.count_elements_close_to(loc) + ((n_tags**1.2)*self.tag_coeff) )*self.park_coeff)  
+            score_function = lambda loc, n_tags: int((((n_tags**1.2)*self.tag_coeff) )*self.park_coeff)   # self.count_elements_close_to(loc) +
             L2 = self.fetch_landmarks(bbox, self.amenity_selectors['nature'], preferences.nature.type, score_function)
             L += L2
 
         # list for shopping
         if preferences.shopping.score != 0:
-            score_function = lambda loc, n_tags: int(self.count_elements_close_to(loc) + ((n_tags**1.2)*self.tag_coeff))
+            score_function = lambda loc, n_tags: int(((n_tags**1.2)*self.tag_coeff)) # self.count_elements_close_to(loc) +
             L3 = self.fetch_landmarks(bbox, self.amenity_selectors['shopping'], preferences.shopping.type, score_function)
             L += L3
 
@@ -183,12 +190,13 @@ class LandmarkManager:
             return 0
 
 
-    def create_bbox(self, coordinates: tuple[float, float]) -> tuple[float, float, float, float]:
+    def create_bbox(self, coordinates: tuple[float, float], reachable_bbox_side: int) -> tuple[float, float, float, float]:
         """
         Create a bounding box around the given coordinates.
 
         Args:
             coordinates (tuple[float, float]): The latitude and longitude of the center of the bounding box.
+            reachable_bbox_side (int): The side length of the bounding box in meters.
 
         Returns:
             tuple[float, float, float, float]: The minimum latitude, minimum longitude, maximum latitude, and maximum longitude
@@ -199,7 +207,7 @@ class LandmarkManager:
         lon = coordinates[1]
 
         # Half the side length in km (since it's a square bbox)
-        half_side_length_km = self.city_bbox_side / 2 / 1000
+        half_side_length_km = reachable_bbox_side / 2 / 1000
 
         # Convert distance to degrees
         lat_diff = half_side_length_km / 111  # 1 degree latitude is approximately 111 km
@@ -288,19 +296,24 @@ class LandmarkManager:
                         break
 
                     if "wikipedia" in tag:
-                        n_tags += 3             # wikipedia entries count more
+                        n_tags += 1             # wikipedia entries count more
 
-                    if tag == "wikidata":
-                        Q = elem.tag('wikidata')
-                        site = Site("wikidata", "wikidata")
-                        item = ItemPage(site, Q)
-                        item.get()
-                        n_languages = len(item.labels)
-                        n_tags += n_languages/10
+                    # if tag == "wikidata":
+                    #     Q = elem.tag('wikidata')
+                    #     site = Site("wikidata", "wikidata")
+                    #     item = ItemPage(site, Q)
+                    #     item.get()
+                    #     n_languages = len(item.labels)
+                    #     n_tags += n_languages/10
+                    if "viewpoint" in tag:
+                        n_tags += 10
 
                     if elem_type != "nature":
                         if "leisure" in tag and elem.tag('leisure') == "park":
                             elem_type = "nature"
+                    
+                    if elem_type == "nature":
+                        n_tags += 1 
 
                     if landmarktype != "shopping":
                         if "shop" in tag:
@@ -310,7 +323,6 @@ class LandmarkManager:
                         if tag == "building" and elem.tag('building') in ['retail', 'supermarket', 'parking']:
                             skip = True
                             break
-
                 if skip:
                     continue
 
diff --git a/frontend/lib/constants.dart b/frontend/lib/constants.dart
index dbd009a..efa0a1f 100644
--- a/frontend/lib/constants.dart
+++ b/frontend/lib/constants.dart
@@ -1,4 +1,4 @@
 const String APP_NAME = 'AnyWay';
 
-const String API_URL_BASE = 'https://anyway.kluster.moll.re';
+String API_URL_BASE = 'https://anyway.kluster.moll.re';
 
diff --git a/frontend/lib/modules/greeter.dart b/frontend/lib/modules/greeter.dart
index 034720e..b0771d5 100644
--- a/frontend/lib/modules/greeter.dart
+++ b/frontend/lib/modules/greeter.dart
@@ -1,3 +1,5 @@
+import 'dart:developer';
+
 import 'package:anyway/structs/trip.dart';
 import 'package:auto_size_text/auto_size_text.dart';
 
@@ -15,12 +17,15 @@ class Greeter extends StatefulWidget {
 }
 
 
-
 class _GreeterState extends State<Greeter> {
+  
   Widget greeterBuilder (BuildContext context, Widget? child) {
     ThemeData theme = Theme.of(context);
+    TextStyle greeterStyle = TextStyle(color: theme.primaryColor, fontWeight: FontWeight.bold, fontSize: 24);
+
     Widget topGreeter;
-    if (widget.trip.landmarks.length > 1) {
+
+    if (widget.trip.uuid != 'pending') {
       topGreeter = FutureBuilder(
         future: widget.trip.cityName,
         builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
@@ -28,17 +33,20 @@ class _GreeterState extends State<Greeter> {
             return AutoSizeText(
               maxLines: 1,
               'Welcome to ${snapshot.data}!',
-              style: TextStyle(color: theme.primaryColor, fontWeight: FontWeight.bold, fontSize: 24),
+              style: greeterStyle
             );
           } else if (snapshot.hasError) {
-            return const AutoSizeText(
+            log('Error while fetching city name');
+            return AutoSizeText(
               maxLines: 1,
-              'Welcome to your trip!'
+              'Welcome to your trip!',
+              style: greeterStyle
             );
           } else {
-            return const AutoSizeText(
+            return AutoSizeText(
               maxLines: 1,
-              'Welcome to ...'
+              'Welcome to ...',
+              style: greeterStyle
             );
           }
         }
@@ -54,14 +62,24 @@ class _GreeterState extends State<Greeter> {
             future: widget.trip.cityName,
             builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
               if (snapshot.hasData) {
-                return Text(
+                return AutoSizeText(
+                  maxLines: 1,
                   'Generating your trip to ${snapshot.data}...',
-                  style: TextStyle(color: theme.primaryColor, fontWeight: FontWeight.bold, fontSize: 24),
+                  style: greeterStyle
                 );
               } else if (snapshot.hasError) {
-                return const Text('Error while fetching city name');
+                // the exact error is shown in the central part of the trip overview. No need to show it here
+                return AutoSizeText(
+                  maxLines: 1,
+                  'Error while loading trip.',
+                  style: greeterStyle
+                  );
               }
-              return const Text('Generating your trip...');
+              return AutoSizeText(
+                  maxLines: 1,
+                  'Generating your trip...',
+                  style: greeterStyle
+                  );
             }
           ),
           Padding(
diff --git a/frontend/lib/modules/landmarks_overview.dart b/frontend/lib/modules/landmarks_overview.dart
index 844673d..a368e3d 100644
--- a/frontend/lib/modules/landmarks_overview.dart
+++ b/frontend/lib/modules/landmarks_overview.dart
@@ -1,4 +1,3 @@
-import 'dart:collection';
 import 'dart:developer';
 import 'package:flutter/material.dart';
 import 'package:shared_preferences/shared_preferences.dart';
@@ -19,32 +18,19 @@ class LandmarksOverview extends StatefulWidget {
 }
 
 class _LandmarksOverviewState extends State<LandmarksOverview> {
-  // final Future<List<Landmark>> _landmarks = fetchLandmarks();
 
   @override
   Widget build(BuildContext context) {
-    return ListenableBuilder(//<LinkedList<Landmark>>
+    return ListenableBuilder(
       listenable: widget.trip!,
       builder: (BuildContext context, Widget? child) {
         Trip trip = widget.trip!;
         log("Trip ${trip.uuid} ${trip.landmarks.length} landmarks");
+        
         List<Widget> children;
-        if (trip.uuid == 'pending') {
-          // the trip is still being fetched from the api
-          children = [Center(child: CircularProgressIndicator())];
-        } else if (trip.uuid == 'error') {
-            children = [
-              const Icon(
-                Icons.error_outline,
-                color: Colors.red,
-                size: 60,
-              ),
-              Padding(
-                padding: const EdgeInsets.only(top: 16),
-                child: Text('Error: ${trip.cityName}'),
-              ),
-            ];
-        } else {
+        
+        if (trip.uuid != 'pending' && trip.uuid != 'error') {
+          log("Trip ${trip.uuid} ${trip.landmarks.length} landmarks");
           if (trip.landmarks.length <= 1) {
             children = [
               const Text("No landmarks in this trip"),
@@ -55,7 +41,26 @@ class _LandmarksOverviewState extends State<LandmarksOverview> {
               saveButton(),
             ];
           }
+        } else if(trip.uuid == 'pending') {
+          // the trip is still being fetched from the api
+          children = [Center(child: CircularProgressIndicator())];
+        } else {
+            // trip.uuid == 'error'
+            // show the error raised by the api
+            // String error = 
+            children = [
+              const Icon(
+                Icons.error_outline,
+                color: Colors.red,
+                size: 60,
+              ),
+              Padding(
+                padding: const EdgeInsets.only(top: 16),
+                child: Text('Error: ${trip.errorDescription}'),
+              ),
+            ];
         }
+
         return Column(
           children: children,
         );
@@ -119,11 +124,6 @@ class _LandmarksOverviewState extends State<LandmarksOverview> {
 
 
 Widget stepBetweenLandmarks(Landmark current, Landmark next) {
-  // This is a simple widget that draws a line between landmark-cards
-  // It's a vertical dotted line
-  // Next to the line is the icon for the mode of transport (walking for now) and the estimated time
-  // There is also a button to open the navigation instructions as a new intent
-  // next landmark is not actually required, but it ensures that the widget is deleted when the next landmark is removed (which makes sense, because then there will be another step)
   int timeRounded = 5 * (current.tripTime?.inMinutes ?? 0) ~/ 5;
   // ~/ is integer division (rounding)
   return Container(
diff --git a/frontend/lib/pages/profile.dart b/frontend/lib/pages/profile.dart
index 0ee2b8f..f9d3ca7 100644
--- a/frontend/lib/pages/profile.dart
+++ b/frontend/lib/pages/profile.dart
@@ -1,3 +1,4 @@
+import 'package:anyway/constants.dart';
 import 'package:anyway/structs/preferences.dart';
 import 'package:flutter/material.dart';
 
@@ -24,6 +25,32 @@ class _ProfilePageState extends State<ProfilePage> {
             onChanged: (bool? newValue) {
               setState(() {
                 debugMode = newValue!;
+                showDialog(
+                  context: context,
+                  builder: (BuildContext context) {
+                    return AlertDialog(
+                      title: Text('Debug mode - custom API'),
+                      content: TextField(
+                        decoration: InputDecoration(
+                          hintText: 'http://localhost:8000'
+                        ),
+                      onChanged: (value) {
+                        setState(() {
+                          API_URL_BASE = value;
+                        });
+                      },
+                    ),
+                    actions: [
+                      TextButton(
+                        child: Text('OK'),
+                        onPressed: () {
+                          Navigator.of(context).pop();
+                        },
+                      ),
+                    ],
+                    );
+                  }
+                );
               });
             }
           )
diff --git a/frontend/lib/structs/trip.dart b/frontend/lib/structs/trip.dart
index b8176db..ec0f228 100644
--- a/frontend/lib/structs/trip.dart
+++ b/frontend/lib/structs/trip.dart
@@ -14,6 +14,7 @@ class Trip with ChangeNotifier {
   int totalTime;
   LinkedList<Landmark> landmarks;
   // could be empty as well
+  String? errorDescription;
 
   Future<String> get cityName async {
     List<double>? location = landmarks.firstOrNull?.location; 
@@ -64,6 +65,11 @@ class Trip with ChangeNotifier {
     landmarks.remove(landmark);
     notifyListeners();
   }
+  
+  void updateError(String error) {
+    errorDescription = error;
+    notifyListeners();
+  }
 
   factory Trip.fromPrefs(SharedPreferences prefs, String uuid) {
     String? content = prefs.getString('trip_$uuid');
diff --git a/frontend/lib/utils/fetch_trip.dart b/frontend/lib/utils/fetch_trip.dart
index cf76d8b..d85ab76 100644
--- a/frontend/lib/utils/fetch_trip.dart
+++ b/frontend/lib/utils/fetch_trip.dart
@@ -11,9 +11,11 @@ import "package:anyway/structs/preferences.dart";
 Dio dio = Dio(
     BaseOptions(
       baseUrl: API_URL_BASE,
-      // baseUrl: 'http://localhost:8000',
       connectTimeout: const Duration(seconds: 5),
       receiveTimeout: const Duration(seconds: 120),
+      // also accept 500 errors, since we cannot rule out that the server is at fault. We still want to gracefully handle these errors
+      validateStatus: (status) => status! <= 500,
+      receiveDataWhenStatusError: true,
       // api is notoriously slow
       // headers: {
       //   HttpHeaders.userAgentHeader: 'dio',
@@ -45,24 +47,20 @@ fetchTrip(
   // handle errors
   if (response.statusCode != 200) {
     trip.updateUUID("error");
-    throw Exception('Failed to load trip');
-  }
-  if (response.data["error"] != null) {
-    trip.updateUUID("error");
-    throw Exception(response.data["error"]);
+    if (response.data["detail"] != null) {
+      trip.updateError(response.data["detail"]);
+      // throw Exception(response.data["detail"]);
+    }
   }
+
   log(response.data.toString());
   Map<String, dynamic> json = response.data;
 
   // only fill in the trip "meta" data for now
   trip.loadFromJson(json);
 
-
   // now fill the trip with landmarks
-  // if (trip.landmarks.isNotEmpty) {
-  //   trip.landmarks.clear();
-  // }
-  // we are going to recreate all the landmarks from the information given by the api
+  // we are going to recreate ALL the landmarks from the information given by the api
   trip.landmarks.remove(trip.landmarks.first);
   String? nextUUID = json["first_landmark_uuid"];
   while (nextUUID != null) {
@@ -83,8 +81,8 @@ Future<(Landmark, String?)> fetchLandmark(String uuid) async {
   if (response.statusCode != 200) {
     throw Exception('Failed to load landmark');
   }
-  if (response.data["error"] != null) {
-    throw Exception(response.data["error"]);
+  if (response.data["detail"] != null) {
+    throw Exception(response.data["detail"]);
   }
   log(response.data.toString());
   Map<String, dynamic> json = response.data;
-- 
2.47.2


From f71b9b19a6b5ec474d1bd3f550d4d213f8a3bef7 Mon Sep 17 00:00:00 2001
From: Remy Moll <me@moll.re>
Date: Tue, 6 Aug 2024 14:34:12 +0200
Subject: [PATCH 9/9] show correct landmark types when fetching from api

---
 frontend/lib/layout.dart           |  27 +++++--
 frontend/lib/modules/map.dart      | 104 +++++++++++++-------------
 frontend/lib/pages/new_trip.dart   | 116 ++++++++++++++---------------
 frontend/lib/structs/landmark.dart |  23 ++++--
 frontend/lib/utils/fetch_trip.dart |  30 ++++----
 5 files changed, 161 insertions(+), 139 deletions(-)

diff --git a/frontend/lib/layout.dart b/frontend/lib/layout.dart
index 43aebde..7a214d3 100644
--- a/frontend/lib/layout.dart
+++ b/frontend/lib/layout.dart
@@ -136,12 +136,20 @@ class _BasePageState extends State<BasePage> {
 // TODO: Implement this function
 Trip getFirstTrip(Future<List<Trip>> trips) {
   Trip t1 = Trip(uuid: '1', landmarks: LinkedList<Landmark>());
+  t1.landmarks.add(
+    Landmark(
+      uuid: '0',
+      name: "Start",
+      location: [48.85, 2.32],
+      type: start,
+    ),
+  );
   t1.landmarks.add(
     Landmark(
       uuid: '1',
       name: "Eiffel Tower",
       location: [48.859, 2.295],
-      type: monument,
+      type: sightseeing,
       imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a8/Tour_Eiffel_Wikimedia_Commons.jpg/1037px-Tour_Eiffel_Wikimedia_Commons.jpg"
     ),
   );
@@ -150,7 +158,7 @@ Trip getFirstTrip(Future<List<Trip>> trips) {
       uuid: "2",
       name: "Notre Dame Cathedral",
       location: [48.8530, 2.3498],
-      type: monument,
+      type: sightseeing,
       imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f7/Notre-Dame_de_Paris%2C_4_October_2017.jpg/440px-Notre-Dame_de_Paris%2C_4_October_2017.jpg"
     ),
   );
@@ -159,7 +167,7 @@ Trip getFirstTrip(Future<List<Trip>> trips) {
       uuid: "3",
       name: "Louvre palace",
       location: [48.8606, 2.3376],
-      type: museum,
+      type: sightseeing,
       imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/6/66/Louvre_Museum_Wikimedia_Commons.jpg/540px-Louvre_Museum_Wikimedia_Commons.jpg"
     ),
   );
@@ -168,7 +176,7 @@ Trip getFirstTrip(Future<List<Trip>> trips) {
       uuid: "4",
       name: "Pont-des-arts",
       location: [48.8585, 2.3376],
-      type: monument,
+      type: sightseeing,
       imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d1/Pont_des_Arts%2C_6e_Arrondissement%2C_Paris_%28HDR%29_20140320_1.jpg/560px-Pont_des_Arts%2C_6e_Arrondissement%2C_Paris_%28HDR%29_20140320_1.jpg"
     ),
   );
@@ -177,9 +185,18 @@ Trip getFirstTrip(Future<List<Trip>> trips) {
       uuid: "5",
       name: "Panthéon",
       location: [48.847, 2.347],
-      type: monument,
+      type: sightseeing,
       imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/8/80/Pantheon_of_Paris_007.JPG/1280px-Pantheon_of_Paris_007.JPG"
     ),
   );
+  t1.landmarks.add(
+    Landmark(
+      uuid: "6",
+      name: "Galeries Lafayette",
+      location: [48.87, 2.32],
+      type: shopping,
+      imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/d/de/GaleriesLafayetteNuit.jpg/220px-GaleriesLafayetteNuit.jpg"
+    ),
+  );
   return t1;
 }
\ No newline at end of file
diff --git a/frontend/lib/modules/map.dart b/frontend/lib/modules/map.dart
index 199388b..d47f88f 100644
--- a/frontend/lib/modules/map.dart
+++ b/frontend/lib/modules/map.dart
@@ -1,4 +1,5 @@
 import 'dart:collection';
+import 'dart:developer';
 
 import 'package:flutter/material.dart';
 import 'package:anyway/structs/landmark.dart';
@@ -21,7 +22,7 @@ class MapWidget extends StatefulWidget {
 
 class _MapWidgetState extends State<MapWidget> {
   late GoogleMapController mapController;
-  // coordinates of Paris
+
   CameraPosition _cameraPosition = CameraPosition(
     target: LatLng(48.8566, 2.3522),
     zoom: 11.0,
@@ -36,7 +37,7 @@ class _MapWidgetState extends State<MapWidget> {
       CameraUpdate update = CameraUpdate.newLatLng(LatLng(newLocation[0], newLocation[1]));
       controller.moveCamera(update);
     }
-    // addLandmarkMarker();
+    setMapMarkers();
   }
 
   void _onCameraIdle() {
@@ -44,45 +45,37 @@ class _MapWidgetState extends State<MapWidget> {
   }
 
 
-  void addLandmarkMarker() async {
-    LinkedList<Landmark>? landmarks = widget.trip?.landmarks;
-    int i = mapMarkers.length;
-    Landmark? current = landmarks!.elementAtOrNull(i);
-    if (current != null){
-      mapMarkers.add(
-        Marker(
-          markerId: MarkerId(current.name),
-          position: LatLng(current.location[0], current.location[1]),
-          icon: await CustomMarker(
-            landmark: current,
-            position: i+1
-          ).toBitmapDescriptor(
-            logicalSize: const Size(150, 150),
-            imageSize: const Size(150, 150)
-          )
-        )
+  void setMapMarkers() async {
+    List<Landmark> landmarks = widget.trip?.landmarks.toList() ?? [];
+    Set<Marker> newMarkers = <Marker>{};
+    for (int i = 0; i < landmarks.length; i++) {
+      Landmark landmark = landmarks[i];
+      List<double> location = landmark.location;
+      Marker marker = Marker(
+        markerId: MarkerId(landmark.uuid),
+        position: LatLng(location[0], location[1]),
+        icon: await CustomMarker(landmark: landmark, position: i).toBitmapDescriptor(
+          logicalSize: const Size(150, 150),
+          imageSize: const Size(150, 150)
+        ),
       );
-      setState(() {});
+      newMarkers.add(marker);
     }
+    setState(() {
+      mapMarkers = newMarkers;
+    });
   }
 
-
   @override
   Widget build(BuildContext context) {
-    return ListenableBuilder(
-      listenable: widget.trip!,
-      builder: (context, child) {
-        addLandmarkMarker();
-        return GoogleMap(
-          onMapCreated: _onMapCreated,
-          initialCameraPosition: _cameraPosition,
-          onCameraIdle: _onCameraIdle,
-          
-          // onLongPress: ,
-          markers: mapMarkers,
-          cloudMapId: '41c21ac9b81dbfd8',
-        );
-      }
+    widget.trip?.addListener(setMapMarkers);
+    return GoogleMap(
+      onMapCreated: _onMapCreated,
+      initialCameraPosition: _cameraPosition,
+      onCameraIdle: _onCameraIdle,
+      // onLongPress: ,
+      markers: mapMarkers,
+      cloudMapId: '41c21ac9b81dbfd8',
     );
   }
 }
@@ -103,20 +96,34 @@ class CustomMarker extends StatelessWidget {
     // This returns an outlined circle, with an icon corresponding to the landmark type
     // As a small dot, the number of the landmark is displayed in the top right
     Icon icon;
-    if (landmark.type == museum) {
-      icon = Icon(Icons.museum, color: Colors.black, size: 50);
-    } else if (landmark.type == monument) {
+    if (landmark.type == sightseeing) {
       icon = Icon(Icons.church, color: Colors.black, size: 50);
-    } else if (landmark.type == park) {
+    } else if (landmark.type == nature) {
       icon = Icon(Icons.park, color: Colors.black, size: 50);
-    } else if (landmark.type == restaurant) {
-      icon = Icon(Icons.restaurant, color: Colors.black, size: 50);
-    } else if (landmark.type == shop) {
+    } else if (landmark.type == shopping) {
       icon = Icon(Icons.shopping_cart, color: Colors.black, size: 50);
+    } else if (landmark.type == start || landmark.type == finish) {
+      icon = Icon(Icons.flag, color: Colors.black, size: 50);
     } else {
       icon = Icon(Icons.location_on, color: Colors.black, size: 50);
     }
 
+    Widget? positionIndicator;
+    if (landmark.type != start && landmark.type != finish) {
+      positionIndicator = Positioned(
+        top: 0,
+        right: 0,
+        child: Container(
+          padding: EdgeInsets.all(5),
+          decoration: BoxDecoration(
+            color: Theme.of(context).primaryColor,
+            shape: BoxShape.circle,
+          ),
+          child: Text('$position', style: TextStyle(color: Colors.white, fontSize: 20)),
+        ),
+      );
+    }
+
     return RepaintBoundary(
       child: Stack(
         children: [
@@ -136,18 +143,7 @@ class CustomMarker extends StatelessWidget {
             ),
             child: icon,
           ),
-          Positioned(
-            top: 0,
-            right: 0,
-            child: Container(
-              padding: EdgeInsets.all(5),
-              decoration: BoxDecoration(
-                color: Theme.of(context).primaryColor,
-                shape: BoxShape.circle,
-              ),
-              child: Text('$position', style: TextStyle(color: Colors.white, fontSize: 20)),
-            ),
-          ),
+          positionIndicator ?? Container(),
         ],
       ),
     );
diff --git a/frontend/lib/pages/new_trip.dart b/frontend/lib/pages/new_trip.dart
index 79c15ae..f3e97d3 100644
--- a/frontend/lib/pages/new_trip.dart
+++ b/frontend/lib/pages/new_trip.dart
@@ -29,66 +29,64 @@ class _NewTripPageState extends State<NewTripPage> {
       ),
       body: Form(
         key: _formKey,
-        child: 
-          Padding(
-            padding: const EdgeInsets.all(15.0),
-            child: Column(
-              crossAxisAlignment: CrossAxisAlignment.start,
-              
-              children: <Widget>[
-                TextFormField(
-                  decoration: const InputDecoration(hintText: 'Lat'),
-                  controller: latController,
-                  validator: (String? value) {
-                    if (value == null || value.isEmpty || double.tryParse(value) == null){
-                      return 'Please enter a floating point number';
-                    }
-                    return null;
-                  },
-                ),
-                TextFormField(
-                  decoration: const InputDecoration(hintText: 'Lon'),
-                  controller: lonController,
+        child: Padding(
+          padding: const EdgeInsets.all(15.0),
+          child: Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            
+            children: <Widget>[
+              TextFormField(
+                decoration: const InputDecoration(hintText: 'Lat'),
+                controller: latController,
+                validator: (String? value) {
+                  if (value == null || value.isEmpty || double.tryParse(value) == null){
+                    return 'Please enter a floating point number';
+                  }
+                  return null;
+                },
+              ),
+              TextFormField(
+                decoration: const InputDecoration(hintText: 'Lon'),
+                controller: lonController,
 
-                  validator: (String? value) {
-                    if (value == null || value.isEmpty || double.tryParse(value) == null){
-                      return 'Please enter a floating point number';
-                    }
-                    return null;
-                  },
-                ),
-                Divider(height: 15, color: Colors.transparent),
-                ElevatedButton(
-                  onPressed: () {
-                    if (_formKey.currentState!.validate()) {
-                      List<double> startPoint = [
-                        double.parse(latController.text),
-                        double.parse(lonController.text)
-                      ];
-                      Future<UserPreferences> preferences = loadUserPreferences();
-                      Trip trip = Trip();
-                      trip.landmarks.add(
-                        Landmark(
-                          location: startPoint,
-                          name: "start",
-                          type: LandmarkType(name: 'start'),
-                          uuid: "pending"
-                        )
-                      );
-                      fetchTrip(trip, preferences);
-                        Navigator.of(context).push(
-                          MaterialPageRoute(
-                            builder: (context) => BasePage(mainScreen: "map", trip: trip)
-                          )
-                        );
-                    }
-                  },
-                  child: const Text('Create trip'),
-                ),
-              ],
-            ),
-          )
-        
+                validator: (String? value) {
+                  if (value == null || value.isEmpty || double.tryParse(value) == null){
+                    return 'Please enter a floating point number';
+                  }
+                  return null;
+                },
+              ),
+              Divider(height: 15, color: Colors.transparent),
+              ElevatedButton(
+                child: const Text('Create trip'),
+                onPressed: () {
+                  if (_formKey.currentState!.validate()) {
+                    List<double> startPoint = [
+                      double.parse(latController.text),
+                      double.parse(lonController.text)
+                    ];
+                    Future<UserPreferences> preferences = loadUserPreferences();
+                    Trip trip = Trip();
+                    trip.landmarks.add(
+                      Landmark(
+                        location: startPoint,
+                        name: "Start",
+                        type: start,
+                        uuid: "pending"
+                      )
+                    );
+                    fetchTrip(trip, preferences);
+                    Navigator.of(context).push(
+                      MaterialPageRoute(
+                        builder: (context) => BasePage(mainScreen: "map", trip: trip)
+                      )
+                    );
+                  }
+                },
+              ),
+            ],
+          ),
+        )
       )
     );
   }
diff --git a/frontend/lib/structs/landmark.dart b/frontend/lib/structs/landmark.dart
index 0f387af..50918af 100644
--- a/frontend/lib/structs/landmark.dart
+++ b/frontend/lib/structs/landmark.dart
@@ -3,12 +3,13 @@ import 'dart:convert';
 
 import 'package:shared_preferences/shared_preferences.dart';
 
-
-const LandmarkType museum = LandmarkType(name: 'Museum');
-const LandmarkType monument = LandmarkType(name: 'Monument');
-const LandmarkType park = LandmarkType(name: 'Park');
-const LandmarkType restaurant = LandmarkType(name: 'Restaurant');
-const LandmarkType shop = LandmarkType(name: 'Shop');
+const LandmarkType sightseeing = LandmarkType(name: 'sightseeing');
+const LandmarkType nature = LandmarkType(name: 'nature');
+const LandmarkType shopping = LandmarkType(name: 'shopping');
+// const LandmarkType museum = LandmarkType(name: 'Museum');
+// const LandmarkType restaurant = LandmarkType(name: 'Restaurant');
+const LandmarkType start = LandmarkType(name: 'start');
+const LandmarkType finish = LandmarkType(name: 'finish');
 
 
 final class Landmark extends LinkedListEntry<Landmark>{
@@ -55,7 +56,7 @@ final class Landmark extends LinkedListEntry<Landmark>{
         'location': List<dynamic> location,
         'type': String type,
       }) {
-      // refine the parsing on a few
+      // refine the parsing on a few fields
       List<double> locationFixed = List<double>.from(location);
       // parse the rest separately, they could be missing
       LandmarkType typeFixed = LandmarkType(name: type);
@@ -106,6 +107,14 @@ class LandmarkType {
     // required this.description,
     // required this.icon,
   });
+  @override
+  bool operator ==(Object other) {
+    if (other is LandmarkType) {
+      return name == other.name;
+    } else {
+      return false;
+    }
+  }
 }
 
 
diff --git a/frontend/lib/utils/fetch_trip.dart b/frontend/lib/utils/fetch_trip.dart
index d85ab76..8ed04d3 100644
--- a/frontend/lib/utils/fetch_trip.dart
+++ b/frontend/lib/utils/fetch_trip.dart
@@ -49,24 +49,26 @@ fetchTrip(
     trip.updateUUID("error");
     if (response.data["detail"] != null) {
       trip.updateError(response.data["detail"]);
+      log(response.data["detail"]);
       // throw Exception(response.data["detail"]);
     }
-  }
+  } else {
+    Map<String, dynamic> json = response.data;
+
+    // only fill in the trip "meta" data for now
+    trip.loadFromJson(json);
+
+    // now fill the trip with landmarks
+    // we are going to recreate ALL the landmarks from the information given by the api
+    trip.landmarks.remove(trip.landmarks.first);
+    String? nextUUID = json["first_landmark_uuid"];
+    while (nextUUID != null) {
+      var (landmark, newUUID) = await fetchLandmark(nextUUID);
+      trip.addLandmark(landmark);
+      nextUUID = newUUID;
+    }
 
   log(response.data.toString());
-  Map<String, dynamic> json = response.data;
-
-  // only fill in the trip "meta" data for now
-  trip.loadFromJson(json);
-
-  // now fill the trip with landmarks
-  // we are going to recreate ALL the landmarks from the information given by the api
-  trip.landmarks.remove(trip.landmarks.first);
-  String? nextUUID = json["first_landmark_uuid"];
-  while (nextUUID != null) {
-    var (landmark, newUUID) = await fetchLandmark(nextUUID);
-    trip.addLandmark(landmark);
-    nextUUID = newUUID;
   }
 }
 
-- 
2.47.2