From 07dde5ab58f8408fc1bad24bb67c79ecbe31cc5d Mon Sep 17 00:00:00 2001 From: Remy Moll 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 From 86bcec6b29170c9508b64bb8b18defbff9bc4fee Mon Sep 17 00:00:00 2001 From: Remy Moll 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 { zoom: 11.0, ); Set markers = {}; + final GlobalKey globalKey = GlobalKey(); void _onMapCreated(GoogleMapController controller) async { @@ -49,28 +51,81 @@ class _MapWidgetState extends State { Trip? trip = await widget.trip; LinkedList? 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 { + final GlobalKey _formKey = GlobalKey(); + 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: [ - const Text( - 'Create a new trip', + body: Form( + key: _formKey, + child: + Padding( + padding: const EdgeInsets.all(15.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + + children: [ + 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 startPoint = [ + double.parse(latController.text), + double.parse(lonController.text) + ]; + UserPreferences preferences = UserPreferences(); + preferences.load(); + Future 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 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> 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 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 fetchTrip( + List 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: From bf129b201d741e53fb9b1a8f5e79fffb66f96ba0 Mon Sep 17 00:00:00 2001 From: Remy Moll 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 From 016622c7af7e338d98af1dabf05236a536d8133d Mon Sep 17 00:00:00 2001 From: Remy Moll 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 { + Future _prefs = loadUserPreferences(); + + @override Widget build(BuildContext context) { return ListView( @@ -24,66 +27,82 @@ class _ProfilePageState extends State { 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 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 prefs; + + PreferenceSliders({required this.prefs}); @override - State createState() => _ImportanceSlidersState(); + State createState() => _PreferenceSlidersState(); } -class _ImportanceSlidersState extends State { - - UserPreferences _prefs = UserPreferences(); - - List _createSliders() { - List 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 { @override Widget build(BuildContext context) { + List 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 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 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 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 fetchTrip( final response = await dio.post( "/trip/new", data: { - // 'preferences': preferences.toJson(), - 'start': [48,2.3] + 'preferences': preferences.toJson(), + 'start': startPoint } ); From 5748630b99cd8d87316320ba8aaff9074dee92b0 Mon Sep 17 00:00:00 2001 From: Remy Moll 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 { Widget greeterBuild (BuildContext context, AsyncSnapshot 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 { double.parse(latController.text), double.parse(lonController.text) ]; - UserPreferences preferences = UserPreferences(); - preferences.load(); - Future trip = fetchTrip(startPoint, preferences); + Future preferences = loadUserPreferences(); + Future? 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 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 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 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 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 fetchTrip( +Future? fetchTrip( List startPoint, - UserPreferences preferences, + Future preferences, ) async { + UserPreferences prefs = await preferences; + Map 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 fetchTrip( if (response.data["error"] != null) { throw Exception(response.data["error"]); } - return Trip.fromJson(response.data); + log(response.data.toString()); + Map 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 json = response.data; + String? nextUUID = json["next_uuid"]; + return (Landmark.fromJson(json), nextUUID); +} From c87a01b2e8a813b227e7f55764692bb694bf4112 Mon Sep 17 00:00:00 2001 From: Remy Moll 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; - + final Trip? trip; + const BasePage({ super.key, required this.mainScreen, - this.trip + this.trip, }); @override @@ -53,13 +56,13 @@ class _BasePageState extends State { 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 { } } - - -Future getFirstTrip (Future> trips) async { - List 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> trips) { + Trip t1 = Trip(uuid: '1', landmarks: LinkedList()); + 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; - 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 { - Widget greeterBuild (BuildContext context, AsyncSnapshot 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 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 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 { @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 { 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; + final Trip? trip; const LandmarksOverview({super.key, this.trip}); @override @@ -23,18 +23,17 @@ class _LandmarksOverviewState extends State { @override Widget build(BuildContext context) { - final Future> _landmarks = getLandmarks(widget.trip); - return DefaultTextStyle( - style: Theme.of(context).textTheme.displayMedium!, - textAlign: TextAlign.center, - child: FutureBuilder>( - future: _landmarks, - builder: (BuildContext context, AsyncSnapshot> snapshot) { - List children; - if (snapshot.hasData) { - children = [landmarksWithSteps(snapshot.data!), saveButton()]; - } else if (snapshot.hasError) { - children = [ + return ListenableBuilder(//> + listenable: widget.trip!, + builder: (BuildContext context, Widget? child) { + Trip trip = widget.trip!; + log("Trip ${trip.uuid} ${trip.landmarks.length} landmarks"); + List 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 { ), 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 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 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> getLandmarks (Future? 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; + final Trip? trip; MapWidget({ this.trip @@ -31,8 +31,7 @@ class _MapWidgetState extends State { void _onMapCreated(GoogleMapController controller) async { mapController = controller; - Trip? trip = await widget.trip; - List? newLocation = trip?.landmarks.first.location; + List? 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 { void drawLandmarks() async { // (re)draws landmarks on the map - Trip? trip = await widget.trip; - LinkedList? landmarks = trip?.landmarks; + LinkedList? 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 { 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 { double.parse(lonController.text) ]; Future preferences = loadUserPreferences(); - Future? 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; + final Trip trip; NavigationOverview({ - required this.trip + required this.trip, }); @override @@ -27,53 +27,56 @@ class _NavigationOverviewState extends State { @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: [ - 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 landmarks; +class Trip with ChangeNotifier { + String uuid; + int totalTime; + LinkedList landmarks; // could be empty as well + Future get cityName async { + if (GeocodingPlatform.instance == null) { + return '${landmarks.first.location[0]}, ${landmarks.first.location[1]}'; + } + List 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? landmarks + // a trip can be created with no landmarks, but the list should be initialized anyway + }) : landmarks = landmarks ?? LinkedList(); + factory Trip.fromJson(Map json) { Trip trip = Trip( uuid: json['uuid'], - cityName: json['city_name'] ?? 'Not communicated', - landmarks: LinkedList() + totalTime: json['total_time'], ); return trip; } + void loadFromJson(Map 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 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? fetchTrip( - List startPoint, +fetchTrip( + Trip trip, Future preferences, ) async { UserPreferences prefs = await preferences; Map data = { "preferences": prefs.toJson(), - "start": startPoint + "start": trip.landmarks!.first.location, }; String dataString = jsonEncode(data); log(dataString); @@ -44,24 +44,25 @@ Future? 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 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> loadTrips() async { } if (trips.isEmpty) { - Trip t1 = Trip(uuid: '1', cityName: 'Paris', landmarks: LinkedList()); + Trip t1 = Trip(uuid: '1', landmarks: LinkedList()); t1.landmarks.add( Landmark( uuid: '1', @@ -66,7 +66,7 @@ Future> loadTrips() async { trips.add(t1); - Trip t2 = Trip(uuid: '2', cityName: 'Vienna', landmarks: LinkedList()); + Trip t2 = Trip(uuid: '2', landmarks: LinkedList()); 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: From 71d9554d97ea786c1bfbcc5561c47fee44df94dc Mon Sep 17 00:00:00 2001 From: Remy Moll 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> 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> 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> 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> 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> 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 { future: widget.trip.cityName, builder: (BuildContext context, AsyncSnapshot 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 { ]; } else { children = [ - landmarksWithSteps(trip.landmarks), + landmarksWithSteps(), saveButton(), ]; } @@ -71,55 +71,61 @@ class _LandmarksOverviewState extends State { child: const Text('Save'), ); -} - -Widget landmarksWithSteps(LinkedList landmarks) { - List children = []; - int lkey = 0; - for (Landmark landmark in landmarks) { - children.add( - Dismissible( - key: ValueKey(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 children = []; + for (Landmark landmark in widget.trip!.landmarks) { + children.add( + Dismissible( + key: ValueKey(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 { target: LatLng(48.8566, 2.3522), zoom: 11.0, ); - Set markers = {}; - final GlobalKey globalKey = GlobalKey(); + Set mapMarkers = {}; void _onMapCreated(GoogleMapController controller) async { mapController = controller; - List? newLocation = widget.trip?.landmarks.first.location; + List? 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? 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 { @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 { children = List.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 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 { Future _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 { ), ), - FutureBuilder(future: _prefs, builder: futureSliders) + FutureBuilder(future: _prefs, builder: futureSliders), + debugButton() ] ); } @@ -59,7 +82,6 @@ class _ProfilePageState extends State { } - class PreferenceSliders extends StatefulWidget { final List 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{ // A linked node of a list of Landmarks final String uuid; @@ -55,11 +63,12 @@ final class Landmark extends LinkedListEntry{ 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{ '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? 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 get cityName async { + List? 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 placemarks = await placemarkFromCoordinates(location[0], location[1]); + return placemarks.first.locality ?? 'Unknown'; } - List 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 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: From 89511f39cbdce4ff5080fcc1948bca6fd5fcef75 Mon Sep 17 00:00:00 2001 From: Remy Moll 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 { + 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 snapshot) { @@ -28,17 +33,20 @@ class _GreeterState extends State { 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 { future: widget.trip.cityName, builder: (BuildContext context, AsyncSnapshot 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 { - // final Future> _landmarks = fetchLandmarks(); @override Widget build(BuildContext context) { - return ListenableBuilder(//> + return ListenableBuilder( listenable: widget.trip!, builder: (BuildContext context, Widget? child) { Trip trip = widget.trip!; log("Trip ${trip.uuid} ${trip.landmarks.length} landmarks"); + List 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 { 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 { 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 { 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 landmarks; // could be empty as well + String? errorDescription; Future get cityName async { List? 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 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 json = response.data; From f71b9b19a6b5ec474d1bd3f550d4d213f8a3bef7 Mon Sep 17 00:00:00 2001 From: Remy Moll 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 { // TODO: Implement this function Trip getFirstTrip(Future> trips) { Trip t1 = Trip(uuid: '1', landmarks: LinkedList()); + 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> 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> 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> 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> 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 { 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 { CameraUpdate update = CameraUpdate.newLatLng(LatLng(newLocation[0], newLocation[1])); controller.moveCamera(update); } - // addLandmarkMarker(); + setMapMarkers(); } void _onCameraIdle() { @@ -44,45 +45,37 @@ class _MapWidgetState extends State { } - void addLandmarkMarker() async { - LinkedList? 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 landmarks = widget.trip?.landmarks.toList() ?? []; + Set newMarkers = {}; + for (int i = 0; i < landmarks.length; i++) { + Landmark landmark = landmarks[i]; + List 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 { ), body: Form( key: _formKey, - child: - Padding( - padding: const EdgeInsets.all(15.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - - children: [ - 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: [ + 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 startPoint = [ - double.parse(latController.text), - double.parse(lonController.text) - ]; - Future 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 startPoint = [ + double.parse(latController.text), + double.parse(lonController.text) + ]; + Future 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{ @@ -55,7 +56,7 @@ final class Landmark extends LinkedListEntry{ 'location': List location, 'type': String type, }) { - // refine the parsing on a few + // refine the parsing on a few fields List locationFixed = List.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 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 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; } }