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 e399a4f..25a5e31 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_PATH=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..64dceb7 100644 --- a/backend/src/constants.py +++ b/backend/src/constants.py @@ -15,13 +15,21 @@ 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": + MEMCACHED_HOST_PATH = None diff --git a/backend/src/log_config.yaml b/backend/src/log_config.yaml deleted file mode 100644 index 3e9c977..0000000 --- a/backend/src/log_config.yaml +++ /dev/null @@ -1,34 +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 - # 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 diff --git a/backend/src/main.py b/backend/src/main.py index cb245cb..313df93 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 @@ -27,15 +29,17 @@ def get_route(preferences: Preferences, start: tuple[float, float], end: tuple[f :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.") 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( @@ -47,22 +51,37 @@ 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) + 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, 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/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/persistence.py b/backend/src/persistence.py new file mode 100644 index 0000000..21f41d6 --- /dev/null +++ b/backend/src/persistence.py @@ -0,0 +1,26 @@ +from pymemcache.client.base import Client + +import constants + + +class DummyClient: + _data = {} + 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.MEMCACHED_HOST_PATH is None: + client = DummyClient() +else: + client = Client( + constants.MEMCACHED_HOST_PATH, + timeout=1, + allow_unicode_keys=True, + encoding='utf-8' + ) 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..6d6e242 --- /dev/null +++ b/backend/src/structs/trip.py @@ -0,0 +1,30 @@ +from pydantic import BaseModel, Field +from pymemcache.client.base import Client + +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: 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) + 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/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..6cde21c 100644 --- a/backend/src/utils/landmarks_manager.py +++ b/backend/src/utils/landmarks_manager.py @@ -15,17 +15,12 @@ from .take_most_important import take_most_important import constants -SIGHTSEEING = 'sightseeing' -NATURE = 'nature' -SHOPPING = 'shopping' - 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 @@ -40,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) @@ -69,30 +69,33 @@ 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) - L1 = self.fetch_landmarks(bbox, self.amenity_selectors['sightseeing'], SIGHTSEEING, score_function) - self.correct_score(L1, preferences.sightseeing) + 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) - L2 = self.fetch_landmarks(bbox, self.amenity_selectors['nature'], NATURE, score_function) - self.correct_score(L2, preferences.nature) + 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)) - L3 = self.fetch_landmarks(bbox, self.amenity_selectors['shopping'], SHOPPING, score_function) - self.correct_score(L3, preferences.shopping) + 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 + 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 +126,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 +135,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: @@ -191,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 @@ -207,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 @@ -296,21 +296,26 @@ 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 landmarktype != "shopping": if "shop" in tag: skip = True break @@ -318,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/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/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/layout.dart b/frontend/lib/layout.dart index abc8b24..7a214d3 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,71 @@ 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: '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: sightseeing, + 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: 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" + ), + ); + t1.landmarks.add( + Landmark( + uuid: "3", + name: "Louvre palace", + location: [48.8606, 2.3376], + type: sightseeing, + 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: 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" + ), + ); + t1.landmarks.add( + Landmark( + uuid: "5", + name: "Panthéon", + location: [48.847, 2.347], + 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/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 f07f9f7..b0771d5 100644 --- a/frontend/lib/modules/greeter.dart +++ b/frontend/lib/modules/greeter.dart @@ -1,14 +1,15 @@ +import 'dart:developer'; + import 'package:anyway/structs/trip.dart'; +import 'package:auto_size_text/auto_size_text.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 @@ -16,43 +17,91 @@ 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); - String cityName = ""; - if (snapshot.hasData) { - cityName = snapshot.data?.cityName ?? '...'; - } else if (snapshot.hasError) { - cityName = "error"; - } else { // still awaiting the cityname - cityName = "..."; - } + TextStyle greeterStyle = TextStyle(color: theme.primaryColor, fontWeight: FontWeight.bold, fontSize: 24); - Widget topGreeter = Text( - 'Welcome to $cityName!', - style: TextStyle(color: theme.primaryColor, fontWeight: FontWeight.bold, fontSize: 24), - ); + Widget topGreeter; - if (widget.standalone) { - return Center( - child: Padding( - padding: EdgeInsets.only(top: 24.0), - child: topGreeter, - ), + if (widget.trip.uuid != 'pending') { + topGreeter = FutureBuilder( + future: widget.trip.cityName, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.hasData) { + return AutoSizeText( + maxLines: 1, + 'Welcome to ${snapshot.data}!', + style: greeterStyle + ); + } else if (snapshot.hasError) { + log('Error while fetching city name'); + return AutoSizeText( + maxLines: 1, + 'Welcome to your trip!', + style: greeterStyle + ); + } else { + return AutoSizeText( + maxLines: 1, + 'Welcome to ...', + style: greeterStyle + ); + } + } ); } else { - return Center( - child: Column( - children: [ - Padding(padding: EdgeInsets.only(top: 24.0)), - topGreeter, - bottomGreeter, - Padding(padding: EdgeInsets.only(bottom: 24.0)), - ], - ) + // 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: [ + FutureBuilder( + future: widget.trip.cityName, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.hasData) { + return AutoSizeText( + maxLines: 1, + 'Generating your trip to ${snapshot.data}...', + style: greeterStyle + ); + } else if (snapshot.hasError) { + // 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 AutoSizeText( + maxLines: 1, + 'Generating your trip...', + style: greeterStyle + ); + } + ), + Padding( + padding: EdgeInsets.all(5), + child: const LinearProgressIndicator() + ) + ] ); } + + return Center( + child: Column( + children: [ + // Padding(padding: EdgeInsets.only(top: 20)), + topGreeter, + Padding( + padding: EdgeInsets.all(20), + child: bottomGreeter + ), + ], + ) + ); } Widget bottomGreeter = const Text( @@ -65,9 +114,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..a368e3d 100644 --- a/frontend/lib/modules/landmarks_overview.dart +++ b/frontend/lib/modules/landmarks_overview.dart @@ -1,17 +1,16 @@ -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 @@ -19,22 +18,37 @@ class LandmarksOverview extends StatefulWidget { } class _LandmarksOverviewState extends State { - // final Future> _landmarks = fetchLandmarks(); @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' && trip.uuid != 'error') { + log("Trip ${trip.uuid} ${trip.landmarks.length} landmarks"); + if (trip.landmarks.length <= 1) { + children = [ + const Text("No landmarks in this trip"), + ]; + } else { + children = [ + landmarksWithSteps(), + 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, @@ -42,20 +56,15 @@ class _LandmarksOverviewState extends State { ), Padding( padding: const EdgeInsets.only(top: 16), - child: Text('Error: ${snapshot.error}', style: TextStyle(fontSize: 12)), + child: Text('Error: ${trip.errorDescription}'), ), ]; - } else { - children = [Center(child: CircularProgressIndicator())]; - } - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: children, - ), - ); - }, - ), + } + + return Column( + children: children, + ); + }, ); } Widget saveButton() => ElevatedButton( @@ -67,55 +76,56 @@ 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, landmark.next!); - children.add(step); - } } - - return Column( - children: children - ); } -Widget stepBetweenLandmarks(Landmark before, Landmark after) { - // 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 +Widget stepBetweenLandmarks(Landmark current, Landmark next) { + int timeRounded = 5 * (current.tripTime?.inMinutes ?? 0) ~/ 5; + // ~/ is integer division (rounding) return Container( margin: EdgeInsets.all(10), padding: EdgeInsets.all(10), @@ -134,7 +144,7 @@ Widget stepBetweenLandmarks(Landmark before, Landmark after) { Column( children: [ Icon(Icons.directions_walk), - Text("5 min", style: TextStyle(fontSize: 10)), + Text("~$timeRounded min", style: TextStyle(fontSize: 10)), ], ), Spacer(), @@ -142,15 +152,17 @@ Widget stepBetweenLandmarks(Landmark before, Landmark after) { onPressed: () { // Open navigation instructions }, - child: Text("Navigate"), - ), + child: Row( + children: [ + Icon(Icons.directions), + Text("Directions"), + ], + ), + ) ], ), ); } -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 339792c..d47f88f 100644 --- a/frontend/lib/modules/map.dart +++ b/frontend/lib/modules/map.dart @@ -1,13 +1,16 @@ import 'dart:collection'; +import 'dart:developer'; +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:widget_to_marker/widget_to_marker.dart'; + class MapWidget extends StatefulWidget { - final Future? trip; + final Trip? trip; MapWidget({ this.trip @@ -19,58 +22,130 @@ 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, ); - Set markers = {}; + Set mapMarkers = {}; void _onMapCreated(GoogleMapController controller) async { mapController = controller; - Trip? trip = await widget.trip; - List? newLocation = 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(); + setMapMarkers(); } - void _onCameraIdle() { // print(mapController.getLatLng(ScreenCoordinate(x: 0, y: 0))); } - void drawLandmarks() async { - // (re)draws landmarks on the map - 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), - )); - } - }); + 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) + ), + ); + newMarkers.add(marker); } + setState(() { + mapMarkers = newMarkers; + }); } - @override Widget build(BuildContext context) { + widget.trip?.addListener(setMapMarkers); return GoogleMap( onMapCreated: _onMapCreated, initialCameraPosition: _cameraPosition, onCameraIdle: _onCameraIdle, // onLongPress: , - markers: markers, + markers: mapMarkers, cloudMapId: '41c21ac9b81dbfd8', ); } } + + +class CustomMarker extends StatelessWidget { + final Landmark landmark; + final int position; + + 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 == sightseeing) { + icon = Icon(Icons.church, color: Colors.black, size: 50); + } else if (landmark.type == nature) { + icon = Icon(Icons.park, color: Colors.black, size: 50); + } 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: [ + 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( + 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, + ), + positionIndicator ?? Container(), + ], + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/modules/trips_overview.dart b/frontend/lib/modules/trips_overview.dart index 8501013..765ef0e 100644 --- a/frontend/lib/modules/trips_overview.dart +++ b/frontend/lib/modules/trips_overview.dart @@ -25,12 +25,23 @@ 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( 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 47e2c5b..f3e97d3 100644 --- a/frontend/lib/pages/new_trip.dart +++ b/frontend/lib/pages/new_trip.dart @@ -1,5 +1,13 @@ - +import 'package:anyway/structs/landmark.dart'; import 'package:flutter/material.dart'; +import 'package:geocoding/geocoding.dart'; + +import 'package:anyway/layout.dart'; +import 'package:anyway/utils/fetch_trip.dart'; +import 'package:anyway/structs/preferences.dart'; +import "package:anyway/structs/trip.dart"; + + class NewTripPage extends StatefulWidget { const NewTripPage({Key? key}) : super(key: key); @@ -9,22 +17,77 @@ 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( + 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) + ) + ); + } + }, + ), + ], + ), + ) + ) ); } -} \ No newline at end of file +} 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/pages/profile.dart b/frontend/lib/pages/profile.dart index 4101895..f9d3ca7 100644 --- a/frontend/lib/pages/profile.dart +++ b/frontend/lib/pages/profile.dart @@ -1,7 +1,9 @@ +import 'package:anyway/constants.dart'; import 'package:anyway/structs/preferences.dart'; import 'package:flutter/material.dart'; +bool debugMode = false; class ProfilePage extends StatefulWidget { @override @@ -9,6 +11,56 @@ class ProfilePage extends StatefulWidget { } 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!; + 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(); + }, + ), + ], + ); + } + ); + }); + } + ) + ], + ) + ); + } + + + @override Widget build(BuildContext context) { return ListView( @@ -24,66 +76,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), + debugButton() ] ); } + + 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 PreferenceSliders extends StatefulWidget { + final List prefs; -class ImportanceSliders extends StatefulWidget { + 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/landmark.dart b/frontend/lib/structs/landmark.dart index 9e5e1ef..50918af 100644 --- a/frontend/lib/structs/landmark.dart +++ b/frontend/lib/structs/landmark.dart @@ -3,6 +3,15 @@ import 'dart:convert'; import 'package:shared_preferences/shared_preferences.dart'; +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{ // A linked node of a list of Landmarks final String uuid; @@ -47,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); @@ -55,11 +64,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 +91,8 @@ final class Landmark extends LinkedListEntry{ 'image_url': imageURL, 'description': description, 'duration': duration?.inMinutes, - 'visited': visited + 'visited': visited, + 'trip_time': tripTime?.inMinutes, }; } @@ -96,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/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/preferences.dart b/frontend/lib/structs/preferences.dart index 9a31fc7..074bbde 100644 --- a/frontend/lib/structs/preferences.dart +++ b/frontend/lib/structs/preferences.dart @@ -3,80 +3,100 @@ 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(); } } + + 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 + }; + } +} + + +Future loadUserPreferences() async { + UserPreferences prefs = UserPreferences(); + await prefs.load(); + return prefs; } \ 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/structs/trip.dart b/frontend/lib/structs/trip.dart index c43188c..ec0f228 100644 --- a/frontend/lib/structs/trip.dart +++ b/frontend/lib/structs/trip.dart @@ -5,31 +5,71 @@ 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 LinkedList landmarks; +class Trip with ChangeNotifier { + String uuid; + int totalTime; + LinkedList landmarks; // could be empty as well + String? errorDescription; + + Future get cityName async { + List? location = landmarks.firstOrNull?.location; + if (GeocodingPlatform.instance == null) { + return '$location'; + } else if (location == null) { + return 'Unknown'; + } else{ + List placemarks = await placemarkFromCoordinates(location[0], location[1]); + return placemarks.first.locality ?? 'Unknown'; + } + } Trip({ - required this.uuid, - required this.cityName, - required this.landmarks, - }); - + 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) { - return Trip( + Trip trip = Trip( uuid: json['uuid'], - cityName: json['city_name'], - 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(); + } + + void removeLandmark(Landmark landmark) { + landmarks.remove(landmark); + notifyListeners(); + } + + void updateError(String error) { + errorDescription = error; + notifyListeners(); + } factory Trip.fromPrefs(SharedPreferences prefs, String uuid) { String? content = prefs.getString('trip_$uuid'); @@ -43,8 +83,8 @@ class Trip { Map toJson() => { 'uuid': uuid, - 'city_name': cityName, - 'entry_uuid': landmarks.first?.uuid ?? '' + 'total_time': totalTime, + 'first_landmark_uuid': landmarks.first.uuid }; 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..8ed04d3 --- /dev/null +++ b/frontend/lib/utils/fetch_trip.dart @@ -0,0 +1,93 @@ +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"; + + +Dio dio = Dio( + BaseOptions( + baseUrl: API_URL_BASE, + 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', + // 'api': '1.0.0', + // }, + contentType: Headers.jsonContentType, + responseType: ResponseType.json, + + ), +); + +fetchTrip( + Trip trip, + Future preferences, +) async { + UserPreferences prefs = await preferences; + Map data = { + "preferences": prefs.toJson(), + "start": trip.landmarks!.first.location, + }; + String dataString = jsonEncode(data); + log(dataString); + + final response = await dio.post( + "/trip/new", + data: data + ); + + // handle errors + if (response.statusCode != 200) { + 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()); + } +} + + + +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["detail"] != null) { + throw Exception(response.data["detail"]); + } + log(response.data.toString()); + Map json = response.data; + String? nextUUID = json["next_uuid"]; + return (Landmark.fromJson(json), nextUUID); +} 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/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..6f808e0 100644 --- a/frontend/pubspec.lock +++ b/frontend/pubspec.lock @@ -9,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: @@ -17,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: @@ -41,6 +73,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 +137,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 +170,10 @@ 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_test: dependency: "direct dev" description: flutter @@ -128,54 +184,86 @@ 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: 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 +276,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 +288,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: @@ -272,6 +344,22 @@ 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: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" path: dependency: transitive description: @@ -280,6 +368,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + 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 +412,18 @@ 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" 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 +432,22 @@ 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: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" sanitize_html: dependency: transitive description: @@ -332,58 +460,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 +533,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 +589,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: @@ -461,6 +621,14 @@ 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_math: dependency: transitive description: @@ -485,14 +653,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.1" - win32: - dependency: transitive + widget_to_marker: + dependency: "direct main" description: - name: win32 - sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4 + name: widget_to_marker + sha256: badc36f23c76f3ca9d43d7780058096be774adf0f661bdb6eb6f6b893f648ab9 url: "https://pub.dev" source: hosted - version: "5.5.1" + version: "1.0.6" xdg_directories: dependency: transitive description: @@ -503,4 +671,4 @@ packages: version: "1.0.4" 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..bb46eb7 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -36,10 +36,15 @@ 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 + 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: