Tentatively enable communication between front + backend #15
| @@ -43,8 +43,10 @@ jobs: | |||||||
|       working-directory: ./frontend |       working-directory: ./frontend | ||||||
|  |  | ||||||
|     - name: Add required secrets |     - name: Add required secrets | ||||||
|  |       env: | ||||||
|  |         ANDROID_SECRETS_PROPERTIES: ${{ secrets.ANDROID_SECRETS_PROPERTIES }} | ||||||
|       run: | |       run: | | ||||||
|         echo ${{ secrets.ANDROID_SECRETS_PROPERTIES }} > ./android/secrets.properties |         echo "$ANDROID_SECRETS_PROPERTIES" >> ./android/secrets.properties | ||||||
|       working-directory: ./frontend |       working-directory: ./frontend | ||||||
|  |  | ||||||
|     - name: Sanity check |     - name: Sanity check | ||||||
|   | |||||||
| @@ -13,5 +13,6 @@ EXPOSE 8000 | |||||||
| # Set environment variables used by the deployment. These can be overridden by the user using this image. | # Set environment variables used by the deployment. These can be overridden by the user using this image. | ||||||
| ENV NUM_WORKERS=1 | ENV NUM_WORKERS=1 | ||||||
| ENV OSM_CACHE_DIR=/cache | ENV OSM_CACHE_DIR=/cache | ||||||
|  | ENV MEMCACHED_HOST_PATH=none | ||||||
|  |  | ||||||
| CMD fastapi run src/main.py --port 8000 --workers $NUM_WORKERS | CMD fastapi run src/main.py --port 8000 --workers $NUM_WORKERS | ||||||
|   | |||||||
| @@ -14,3 +14,4 @@ shapely = "*" | |||||||
| scipy = "*" | scipy = "*" | ||||||
| osmpythontools = "*" | osmpythontools = "*" | ||||||
| pywikibot = "*" | pywikibot = "*" | ||||||
|  | pymemcache = "*" | ||||||
|   | |||||||
							
								
								
									
										25
									
								
								backend/Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										25
									
								
								backend/Pipfile.lock
									
									
									
										generated
									
									
									
								
							| @@ -1,7 +1,7 @@ | |||||||
| { | { | ||||||
|     "_meta": { |     "_meta": { | ||||||
|         "hash": { |         "hash": { | ||||||
|             "sha256": "f0de801038593d42d8b780d14c2c72bb4f5f5e66df02f72244917ede5d5ebce6" |             "sha256": "4f8b3f0395b4e5352330616870da13acf41e16d1b69ba31b15fd688e90b8b628" | ||||||
|         }, |         }, | ||||||
|         "pipfile-spec": 6, |         "pipfile-spec": 6, | ||||||
|         "requires": {}, |         "requires": {}, | ||||||
| @@ -1102,6 +1102,15 @@ | |||||||
|             "markers": "python_version >= '3.8'", |             "markers": "python_version >= '3.8'", | ||||||
|             "version": "==2.18.0" |             "version": "==2.18.0" | ||||||
|         }, |         }, | ||||||
|  |         "pymemcache": { | ||||||
|  |             "hashes": [ | ||||||
|  |                 "sha256:27bf9bd1bbc1e20f83633208620d56de50f14185055e49504f4f5e94e94aff94", | ||||||
|  |                 "sha256:f507bc20e0dc8d562f8df9d872107a278df049fa496805c1431b926f3ddd0eab" | ||||||
|  |             ], | ||||||
|  |             "index": "pypi", | ||||||
|  |             "markers": "python_version >= '3.7'", | ||||||
|  |             "version": "==4.0.0" | ||||||
|  |         }, | ||||||
|         "pyparsing": { |         "pyparsing": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad", |                 "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad", | ||||||
| @@ -1142,12 +1151,12 @@ | |||||||
|         }, |         }, | ||||||
|         "pywikibot": { |         "pywikibot": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:3f4fbc57f1765aa0fa1ccf84125bcfa475cae95b9cc0291867b751f3d4ac8fa2", |                 "sha256:0dd8291f1a26abb9fce2c2108a90dc338274988e60d21723aec1d3b0de321b5e", | ||||||
|                 "sha256:a26d918cf88ef56fdb1421b65b09def200cc28031cdc922d72a4198fbfddd225" |                 "sha256:7953fc4a6c498057e6eb7d9b762bbccb61348af0a599b89d7e246d5175b20a9b" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "markers": "python_full_version >= '3.7.0'", |             "markers": "python_full_version >= '3.7.0'", | ||||||
|             "version": "==9.2.1" |             "version": "==9.3.0" | ||||||
|         }, |         }, | ||||||
|         "pyyaml": { |         "pyyaml": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @@ -1349,7 +1358,7 @@ | |||||||
|                 "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", |                 "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", | ||||||
|                 "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" |                 "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.8'", |             "markers": "python_version < '3.13'", | ||||||
|             "version": "==4.12.2" |             "version": "==4.12.2" | ||||||
|         }, |         }, | ||||||
|         "tzdata": { |         "tzdata": { | ||||||
| @@ -1658,11 +1667,11 @@ | |||||||
|         }, |         }, | ||||||
|         "xarray": { |         "xarray": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:0b91e0bc4dc0296947947640fe31ec6e867ce258d2f7cbc10bedf4a6d68340c7", |                 "sha256:1b0fd51ec408474aa1f4a355d75c00cc1c02bd425d97b2c2e551fd21810e7f64", | ||||||
|                 "sha256:721a7394e8ec3d592b2d8ebe21eed074ac077dc1bb1bd777ce00e41700b4866c" |                 "sha256:4cae512d121a8522d41e66d942fb06c526bc1fd32c2c181d5fe62fe65b671638" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.9'", |             "markers": "python_version >= '3.9'", | ||||||
|             "version": "==2024.6.0" |             "version": "==2024.7.0" | ||||||
|         } |         } | ||||||
|     }, |     }, | ||||||
|     "develop": {} |     "develop": {} | ||||||
|   | |||||||
| @@ -15,13 +15,21 @@ OSM_CACHE_DIR = Path(cache_dir_string) | |||||||
|  |  | ||||||
|  |  | ||||||
| import logging | import logging | ||||||
| import yaml | # if we are in a debug session, set verbose and rich logging | ||||||
|  |  | ||||||
| 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 os.getenv('DEBUG', False): | 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 | ||||||
|   | |||||||
| @@ -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 |  | ||||||
| @@ -1,12 +1,14 @@ | |||||||
| import logging | import logging | ||||||
| from fastapi import FastAPI, Query, Body | from fastapi import FastAPI, Query, Body, HTTPException | ||||||
|  |  | ||||||
| from structs.landmark import Landmark | from structs.landmark import Landmark | ||||||
| from structs.preferences import Preferences | from structs.preferences import Preferences | ||||||
| from structs.linked_landmarks import LinkedLandmarks | from structs.linked_landmarks import LinkedLandmarks | ||||||
|  | from structs.trip import Trip | ||||||
| from utils.landmarks_manager import LandmarkManager | from utils.landmarks_manager import LandmarkManager | ||||||
| from utils.optimizer import Optimizer | from utils.optimizer import Optimizer | ||||||
| from utils.refiner import Refiner | from utils.refiner import Refiner | ||||||
|  | from persistence import client as cache_client | ||||||
|  |  | ||||||
|  |  | ||||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||||
| @@ -17,8 +19,8 @@ optimizer = Optimizer() | |||||||
| refiner = Refiner(optimizer=optimizer) | refiner = Refiner(optimizer=optimizer) | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.post("/route/new") | @app.post("/trip/new") | ||||||
| def get_route(preferences: Preferences, start: tuple[float, float], end: tuple[float, float] | None = None) -> str: | def new_trip(preferences: Preferences, start: tuple[float, float], end: tuple[float, float] | None = None) -> Trip: | ||||||
|     ''' |     ''' | ||||||
|     Main function to call the optimizer. |     Main function to call the optimizer. | ||||||
|     :param preferences: the preferences specified by the user as the post body |     :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 |     :return: the uuid of the first landmark in the optimized route | ||||||
|     ''' |     ''' | ||||||
|     if preferences is None: |     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: |     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: |     if end is None: | ||||||
|         end = start |         end = start | ||||||
|         logger.info("No end coordinates provided. Using start=end.") |         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) |     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 |     # Generate the landmarks from the start location | ||||||
|     landmarks, landmarks_short = manager.generate_landmarks_list( |     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.insert(0, start_landmark) | ||||||
|     landmarks_short.append(end_landmark) |     landmarks_short.append(end_landmark) | ||||||
|      |      | ||||||
|     # TODO infer these parameters from the preferences |  | ||||||
|     max_walking_time = 4    # hours |  | ||||||
|     detour = 30             # minutes |  | ||||||
|  |  | ||||||
|     # First stage optimization |     # 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 |     # 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) |     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}") | @app.get("/landmark/{landmark_uuid}") | ||||||
| def get_landmark(landmark_uuid: str) -> Landmark: | def get_landmark(landmark_uuid: str) -> Landmark: | ||||||
|     #cherche dans linked_tour et retourne le landmark correspondant |     try: | ||||||
|     pass |         landmark = cache_client.get(f"landmark_{landmark_uuid}") | ||||||
|  |         return landmark | ||||||
|  |     except KeyError: | ||||||
|  |         raise HTTPException(status_code=404, detail="Landmark not found") | ||||||
| @@ -1,6 +1,6 @@ | |||||||
| city_bbox_side: 5000 #m | city_bbox_side: 5000 #m | ||||||
| radius_close_to: 50 | radius_close_to: 50 | ||||||
| church_coeff: 0.8 | church_coeff: 0.8 | ||||||
| park_coeff: 1.2 | park_coeff: 1.0 | ||||||
| tag_coeff: 10 | tag_coeff: 10 | ||||||
| N_important: 40 | N_important: 40 | ||||||
|   | |||||||
							
								
								
									
										26
									
								
								backend/src/persistence.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								backend/src/persistence.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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' | ||||||
|  |     ) | ||||||
| @@ -1,4 +1,3 @@ | |||||||
| import uuid |  | ||||||
| from .landmark import Landmark | from .landmark import Landmark | ||||||
| from utils.get_time_separation import get_time | from utils.get_time_separation import get_time | ||||||
|  |  | ||||||
| @@ -9,8 +8,7 @@ class LinkedLandmarks: | |||||||
|     """ |     """ | ||||||
|      |      | ||||||
|     _landmarks = list[Landmark] |     _landmarks = list[Landmark] | ||||||
|     total_time = int |     total_time: int = 0 | ||||||
|     uuid = str |  | ||||||
|  |  | ||||||
|     def __init__(self, data: list[Landmark] = None) -> None: |     def __init__(self, data: list[Landmark] = None) -> None: | ||||||
|         """ |         """ | ||||||
| @@ -19,7 +17,6 @@ class LinkedLandmarks: | |||||||
|         Args: |         Args: | ||||||
|             data (list[Landmark], optional): The list of landmarks that are linked together. Defaults to None. |             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._landmarks = data if data else [] | ||||||
|         self._link_landmarks() |         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. |         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]): |         for i, landmark in enumerate(self._landmarks[:-1]): | ||||||
|             landmark.next_uuid = self._landmarks[i + 1].uuid |             landmark.next_uuid = self._landmarks[i + 1].uuid | ||||||
|             time_to_next = get_time(landmark.location, self._landmarks[i + 1].location) |             time_to_next = get_time(landmark.location, self._landmarks[i + 1].location) | ||||||
| @@ -44,18 +40,4 @@ class LinkedLandmarks: | |||||||
|      |      | ||||||
|  |  | ||||||
|     def __str__(self) -> str: |     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])}]" |         return f"LinkedLandmarks [{' ->'.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] |  | ||||||
|         } |  | ||||||
|   | |||||||
| @@ -2,7 +2,6 @@ from pydantic import BaseModel | |||||||
| from typing import Optional, Literal | from typing import Optional, Literal | ||||||
|  |  | ||||||
| class Preference(BaseModel) : | class Preference(BaseModel) : | ||||||
|     name: str |  | ||||||
|     type: Literal['sightseeing', 'nature', 'shopping', 'start', 'finish'] |     type: Literal['sightseeing', 'nature', 'shopping', 'start', 'finish'] | ||||||
|     score: int          # score could be from 1 to 5 |     score: int          # score could be from 1 to 5 | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										30
									
								
								backend/src/structs/trip.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								backend/src/structs/trip.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||||
| @@ -20,22 +20,13 @@ def test(start_coords: tuple[float, float], finish_coords: tuple[float, float] = | |||||||
|  |  | ||||||
|      |      | ||||||
|     preferences = Preferences( |     preferences = Preferences( | ||||||
|                     sightseeing=Preference( |         sightseeing=Preference(type='sightseeing', score = 5), | ||||||
|                                   name='sightseeing',  |         nature=Preference(type='nature', score = 5), | ||||||
|                                   type='sightseeing', |         shopping=Preference(type='shopping', score = 5), | ||||||
|                                   score = 5), |  | ||||||
|                     nature=Preference( |  | ||||||
|                                   name='nature',  |  | ||||||
|                                   type='nature', |  | ||||||
|                                   score = 5), |  | ||||||
|                     shopping=Preference( |  | ||||||
|                                   name='shopping',  |  | ||||||
|                                   type='shopping', |  | ||||||
|                                   score = 5), |  | ||||||
|  |  | ||||||
|                     max_time_minute=180, |         max_time_minute=180, | ||||||
|                     detour_tolerance_minute=30 |         detour_tolerance_minute=30 | ||||||
|                     ) |     ) | ||||||
|  |  | ||||||
|     # Create start and finish  |     # Create start and finish  | ||||||
|     if finish_coords is None : |     if finish_coords is None : | ||||||
|   | |||||||
| @@ -15,17 +15,12 @@ from .take_most_important import take_most_important | |||||||
| import constants | import constants | ||||||
|  |  | ||||||
|  |  | ||||||
| SIGHTSEEING = 'sightseeing' |  | ||||||
| NATURE = 'nature' |  | ||||||
| SHOPPING = 'shopping' |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class LandmarkManager: | class LandmarkManager: | ||||||
|  |  | ||||||
|     logger = logging.getLogger(__name__) |     logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|     city_bbox_side: int     # bbox side in meters |  | ||||||
|     radius_close_to: int    # radius in meters |     radius_close_to: int    # radius in meters | ||||||
|     church_coeff: float     # coeff to adjsut score of churches |     church_coeff: float     # coeff to adjsut score of churches | ||||||
|     park_coeff: float       # coeff to adjust score of parks |     park_coeff: float       # coeff to adjust score of parks | ||||||
| @@ -40,13 +35,18 @@ class LandmarkManager: | |||||||
|  |  | ||||||
|         with constants.LANDMARK_PARAMETERS_PATH.open('r') as f: |         with constants.LANDMARK_PARAMETERS_PATH.open('r') as f: | ||||||
|             parameters = yaml.safe_load(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.radius_close_to = parameters['radius_close_to'] | ||||||
|             self.church_coeff = parameters['church_coeff'] |             self.church_coeff = parameters['church_coeff'] | ||||||
|             self.park_coeff = parameters['park_coeff'] |             self.park_coeff = parameters['park_coeff'] | ||||||
|             self.tag_coeff = parameters['tag_coeff'] |             self.tag_coeff = parameters['tag_coeff'] | ||||||
|             self.N_important = parameters['N_important'] |             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() |         self.overpass = Overpass() | ||||||
|         CachingStrategy.use(JSON, cacheDir=constants.OSM_CACHE_DIR) |         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. |                 - 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 = [] |         L = [] | ||||||
|         bbox = self.create_bbox(center_coordinates) |         bbox = self.create_bbox(center_coordinates, reachable_bbox_side) | ||||||
|         # list for sightseeing |         # list for sightseeing | ||||||
|         if preferences.sightseeing.score != 0: |         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'], SIGHTSEEING, score_function) |             L1 = self.fetch_landmarks(bbox, self.amenity_selectors['sightseeing'], preferences.sightseeing.type, score_function) | ||||||
|             self.correct_score(L1, preferences.sightseeing) |  | ||||||
|             L += L1 |             L += L1 | ||||||
|  |  | ||||||
|         # list for nature |         # list for nature | ||||||
|         if preferences.nature.score != 0: |         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'], NATURE, score_function) |             L2 = self.fetch_landmarks(bbox, self.amenity_selectors['nature'], preferences.nature.type, score_function) | ||||||
|             self.correct_score(L2, preferences.nature) |  | ||||||
|             L += L2 |             L += L2 | ||||||
|  |  | ||||||
|         # list for shopping |         # list for shopping | ||||||
|         if preferences.shopping.score != 0: |         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'], SHOPPING, score_function) |             L3 = self.fetch_landmarks(bbox, self.amenity_selectors['shopping'], preferences.shopping.type, score_function) | ||||||
|             self.correct_score(L3, preferences.shopping) |  | ||||||
|             L += L3 |             L += L3 | ||||||
|  |  | ||||||
|  |  | ||||||
|         L = self.remove_duplicates(L) |         L = self.remove_duplicates(L) | ||||||
|  |         self.correct_score(L, preferences) | ||||||
|  |  | ||||||
|         L_constrained = take_most_important(L, self.N_important) |         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.') |         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 |         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. |         Adjust the attractiveness score of each landmark in the list based on user preferences. | ||||||
|  |  | ||||||
| @@ -132,20 +135,16 @@ class LandmarkManager: | |||||||
|  |  | ||||||
|         Args: |         Args: | ||||||
|             landmarks (list[Landmark]): A list of landmarks whose scores need to be corrected. |             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. |             preferences (Preferences): 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. |  | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
|         if len(landmarks) == 0: |         score_dict = { | ||||||
|             return |             preferences.sightseeing.type: preferences.sightseeing.score, | ||||||
|          |             preferences.nature.type: preferences.nature.score, | ||||||
|         if landmarks[0].type != preference.type: |             preferences.shopping.type: preferences.shopping.score | ||||||
|             raise TypeError(f"LandmarkType {preference.type} does not match the type of Landmark {landmarks[0].name}") |         } | ||||||
|  |         for landmark in landmarks: | ||||||
|         for elem in landmarks: |             landmark.attractiveness = int(landmark.attractiveness * score_dict[landmark.type] / 5)         | ||||||
|             elem.attractiveness = int(elem.attractiveness*preference.score/5)     # arbitrary computation |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     def count_elements_close_to(self, coordinates: tuple[float, float]) -> int: |     def count_elements_close_to(self, coordinates: tuple[float, float]) -> int: | ||||||
| @@ -191,12 +190,13 @@ class LandmarkManager: | |||||||
|             return 0 |             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. |         Create a bounding box around the given coordinates. | ||||||
|  |  | ||||||
|         Args: |         Args: | ||||||
|             coordinates (tuple[float, float]): The latitude and longitude of the center of the bounding box. |             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: |         Returns: | ||||||
|             tuple[float, float, float, float]: The minimum latitude, minimum longitude, maximum latitude, and maximum longitude |             tuple[float, float, float, float]: The minimum latitude, minimum longitude, maximum latitude, and maximum longitude | ||||||
| @@ -207,7 +207,7 @@ class LandmarkManager: | |||||||
|         lon = coordinates[1] |         lon = coordinates[1] | ||||||
|  |  | ||||||
|         # Half the side length in km (since it's a square bbox) |         # 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 |         # Convert distance to degrees | ||||||
|         lat_diff = half_side_length_km / 111  # 1 degree latitude is approximately 111 km |         lat_diff = half_side_length_km / 111  # 1 degree latitude is approximately 111 km | ||||||
| @@ -296,21 +296,26 @@ class LandmarkManager: | |||||||
|                         break |                         break | ||||||
|  |  | ||||||
|                     if "wikipedia" in tag: |                     if "wikipedia" in tag: | ||||||
|                         n_tags += 3             # wikipedia entries count more |                         n_tags += 1             # wikipedia entries count more | ||||||
|  |  | ||||||
|                     if tag == "wikidata": |                     # if tag == "wikidata": | ||||||
|                         Q = elem.tag('wikidata') |                     #     Q = elem.tag('wikidata') | ||||||
|                         site = Site("wikidata", "wikidata") |                     #     site = Site("wikidata", "wikidata") | ||||||
|                         item = ItemPage(site, Q) |                     #     item = ItemPage(site, Q) | ||||||
|                         item.get() |                     #     item.get() | ||||||
|                         n_languages = len(item.labels) |                     #     n_languages = len(item.labels) | ||||||
|                         n_tags += n_languages/10 |                     #     n_tags += n_languages/10 | ||||||
|  |                     if "viewpoint" in tag: | ||||||
|  |                         n_tags += 10 | ||||||
|  |  | ||||||
|                     if elem_type != "nature": |                     if elem_type != "nature": | ||||||
|                         if "leisure" in tag and elem.tag('leisure') == "park": |                         if "leisure" in tag and elem.tag('leisure') == "park": | ||||||
|                             elem_type = "nature" |                             elem_type = "nature" | ||||||
|                      |                      | ||||||
|                     if landmarktype != SHOPPING: |                     if elem_type == "nature": | ||||||
|  |                         n_tags += 1  | ||||||
|  |  | ||||||
|  |                     if landmarktype != "shopping": | ||||||
|                         if "shop" in tag: |                         if "shop" in tag: | ||||||
|                             skip = True |                             skip = True | ||||||
|                             break |                             break | ||||||
| @@ -318,7 +323,6 @@ class LandmarkManager: | |||||||
|                         if tag == "building" and elem.tag('building') in ['retail', 'supermarket', 'parking']: |                         if tag == "building" and elem.tag('building') in ['retail', 'supermarket', 'parking']: | ||||||
|                             skip = True |                             skip = True | ||||||
|                             break |                             break | ||||||
|  |  | ||||||
|                 if skip: |                 if skip: | ||||||
|                     continue |                     continue | ||||||
|  |  | ||||||
|   | |||||||
| @@ -61,7 +61,7 @@ android { | |||||||
|  |  | ||||||
|     defaultConfig { |     defaultConfig { | ||||||
|         // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). |         // 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. |         // 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. |         // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. | ||||||
|         // Minimum Android version for Google Maps SDK |         // Minimum Android version for Google Maps SDK | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| package com.example.anyway | package com.anydev.anyway | ||||||
|  |  | ||||||
| import io.flutter.embedding.android.FlutterActivity | import io.flutter.embedding.android.FlutterActivity | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| const String APP_NAME = 'AnyWay'; | const String APP_NAME = 'AnyWay'; | ||||||
|  |  | ||||||
| const String API_URL_BASE = 'https://anyway.kluster.moll.re'; | String API_URL_BASE = 'https://anyway.kluster.moll.re'; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,3 +1,6 @@ | |||||||
|  | import 'dart:collection'; | ||||||
|  |  | ||||||
|  | import 'package:anyway/structs/landmark.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
|  |  | ||||||
| import 'package:anyway/constants.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 | // A side drawer is used to switch between pages | ||||||
| class BasePage extends StatefulWidget { | class BasePage extends StatefulWidget { | ||||||
|   final String mainScreen; |   final String mainScreen; | ||||||
|   final Future<Trip>? trip; |   final Trip? trip; | ||||||
|  |  | ||||||
|   const BasePage({ |   const BasePage({ | ||||||
|     super.key, |     super.key, | ||||||
|     required this.mainScreen, |     required this.mainScreen, | ||||||
|     this.trip |     this.trip, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
| @@ -53,13 +56,13 @@ class _BasePageState extends State<BasePage> { | |||||||
|           children: [ |           children: [ | ||||||
|             DrawerHeader( |             DrawerHeader( | ||||||
|               decoration: BoxDecoration( |               decoration: BoxDecoration( | ||||||
|                 gradient: LinearGradient(colors: [Colors.cyan, theme.primaryColor]) |                 gradient: LinearGradient(colors: [Colors.red, Colors.yellow]) | ||||||
|               ), |               ), | ||||||
|               child: Center( |               child: Center( | ||||||
|                 child: Text( |                 child: Text( | ||||||
|                   APP_NAME, |                   APP_NAME, | ||||||
|                   style: TextStyle( |                   style: TextStyle( | ||||||
|                     color: Colors.white, |                     color: Colors.grey[800], | ||||||
|                     fontSize: 24, |                     fontSize: 24, | ||||||
|                     fontWeight: FontWeight.bold, |                     fontWeight: FontWeight.bold, | ||||||
|                   ), |                   ), | ||||||
| @@ -129,9 +132,71 @@ class _BasePageState extends State<BasePage> { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // This function is used to get the first trip from a list of trips | ||||||
|  | // TODO: Implement this function | ||||||
| Future<Trip> getFirstTrip (Future<List<Trip>> trips) async { | Trip getFirstTrip(Future<List<Trip>> trips) { | ||||||
|   List<Trip> tripsf = await trips; |   Trip t1 = Trip(uuid: '1', landmarks: LinkedList<Landmark>()); | ||||||
|   return tripsf[0]; |   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; | ||||||
| } | } | ||||||
| @@ -13,7 +13,7 @@ class App extends StatelessWidget { | |||||||
|     return MaterialApp( |     return MaterialApp( | ||||||
|       title: APP_NAME, |       title: APP_NAME, | ||||||
|       home: BasePage(mainScreen: "map"), |       home: BasePage(mainScreen: "map"), | ||||||
|       theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.green), |       theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.red[600]), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,14 +1,15 @@ | |||||||
|  | import 'dart:developer'; | ||||||
|  |  | ||||||
| import 'package:anyway/structs/trip.dart'; | import 'package:anyway/structs/trip.dart'; | ||||||
|  | import 'package:auto_size_text/auto_size_text.dart'; | ||||||
|  |  | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
|  |  | ||||||
| class Greeter extends StatefulWidget { | class Greeter extends StatefulWidget { | ||||||
|   final Future<Trip> trip; |   final Trip trip; | ||||||
|   final bool standalone; |  | ||||||
|  |  | ||||||
|   Greeter({ |   Greeter({ | ||||||
|     required this.standalone, |     required this.trip, | ||||||
|     required this.trip |  | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
| @@ -16,43 +17,91 @@ class Greeter extends StatefulWidget { | |||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class _GreeterState extends State<Greeter> { | class _GreeterState extends State<Greeter> { | ||||||
|   Widget greeterBuild (BuildContext context, AsyncSnapshot<Trip> snapshot) { |    | ||||||
|  |   Widget greeterBuilder (BuildContext context, Widget? child) { | ||||||
|     ThemeData theme = Theme.of(context); |     ThemeData theme = Theme.of(context); | ||||||
|     String cityName = ""; |     TextStyle greeterStyle = TextStyle(color: theme.primaryColor, fontWeight: FontWeight.bold, fontSize: 24); | ||||||
|     if (snapshot.hasData) { |  | ||||||
|       cityName = snapshot.data?.cityName ?? '...'; |  | ||||||
|     } else if (snapshot.hasError) { |  | ||||||
|       cityName = "error"; |  | ||||||
|     } else { // still awaiting the cityname |  | ||||||
|       cityName = "..."; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     Widget topGreeter = Text( |     Widget topGreeter; | ||||||
|       'Welcome to $cityName!', |  | ||||||
|       style: TextStyle(color: theme.primaryColor, fontWeight: FontWeight.bold, fontSize: 24), |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     if (widget.standalone) { |     if (widget.trip.uuid != 'pending') { | ||||||
|       return Center( |       topGreeter = FutureBuilder( | ||||||
|         child: Padding( |         future: widget.trip.cityName, | ||||||
|           padding: EdgeInsets.only(top: 24.0), |         builder: (BuildContext context, AsyncSnapshot<String> snapshot) { | ||||||
|           child: topGreeter, |           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 { |     } else { | ||||||
|       return Center( |       // still awaiting the trip | ||||||
|         child: Column( |       // We can hopefully infer the city name from the cityName future | ||||||
|           children: [ |       // Show a linear loader at the bottom and an info message above | ||||||
|             Padding(padding: EdgeInsets.only(top: 24.0)), |       topGreeter = Column( | ||||||
|             topGreeter, |         mainAxisAlignment: MainAxisAlignment.end, | ||||||
|             bottomGreeter, |         children: [ | ||||||
|             Padding(padding: EdgeInsets.only(bottom: 24.0)), |           FutureBuilder( | ||||||
|           ], |             future: widget.trip.cityName, | ||||||
|         ) |             builder: (BuildContext context, AsyncSnapshot<String> 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( |   Widget bottomGreeter = const Text( | ||||||
| @@ -65,9 +114,9 @@ class _GreeterState extends State<Greeter> { | |||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     return FutureBuilder( |     return ListenableBuilder( | ||||||
|       future: widget.trip, |       listenable: widget.trip, | ||||||
|       builder: greeterBuild, |       builder: greeterBuilder, | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @@ -1,4 +1,5 @@ | |||||||
| import 'package:anyway/structs/landmark.dart'; | import 'package:anyway/structs/landmark.dart'; | ||||||
|  | import 'package:cached_network_image/cached_network_image.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -31,9 +32,10 @@ class _LandmarkCardState extends State<LandmarkCard> { | |||||||
|               height: double.infinity, |               height: double.infinity, | ||||||
|               // force a fixed width |               // force a fixed width | ||||||
|               width: 160, |               width: 160, | ||||||
|               child: Image.network( |               child: CachedNetworkImage( | ||||||
|                 widget.landmark.imageURL ?? '', |                 imageUrl: widget.landmark.imageURL ?? '', | ||||||
|                 errorBuilder: (context, error, stackTrace) => Icon(Icons.question_mark_outlined), |                 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 |                 // TODO: make this a switch statement to load a placeholder if null | ||||||
|                 // cover the whole container meaning the image will be cropped |                 // cover the whole container meaning the image will be cropped | ||||||
|                 fit: BoxFit.cover, |                 fit: BoxFit.cover, | ||||||
|   | |||||||
| @@ -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/modules/landmark_card.dart'; | ||||||
| import 'package:anyway/structs/landmark.dart'; | import 'package:anyway/structs/landmark.dart'; | ||||||
|  |  | ||||||
| import 'package:anyway/structs/trip.dart'; | import 'package:anyway/structs/trip.dart'; | ||||||
| import 'package:flutter/material.dart'; |  | ||||||
| import 'package:shared_preferences/shared_preferences.dart'; |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class LandmarksOverview extends StatefulWidget { | class LandmarksOverview extends StatefulWidget { | ||||||
|   final Future<Trip>? trip; |   final Trip? trip; | ||||||
|   const LandmarksOverview({super.key, this.trip}); |   const LandmarksOverview({super.key, this.trip}); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
| @@ -19,22 +18,37 @@ class LandmarksOverview extends StatefulWidget { | |||||||
| } | } | ||||||
|  |  | ||||||
| class _LandmarksOverviewState extends State<LandmarksOverview> { | class _LandmarksOverviewState extends State<LandmarksOverview> { | ||||||
|   // final Future<List<Landmark>> _landmarks = fetchLandmarks(); |  | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     final Future<LinkedList<Landmark>> _landmarks = getLandmarks(widget.trip); |     return ListenableBuilder( | ||||||
|     return DefaultTextStyle( |       listenable: widget.trip!, | ||||||
|       style: Theme.of(context).textTheme.displayMedium!, |       builder: (BuildContext context, Widget? child) { | ||||||
|       textAlign: TextAlign.center, |         Trip trip = widget.trip!; | ||||||
|       child: FutureBuilder<LinkedList<Landmark>>( |         log("Trip ${trip.uuid} ${trip.landmarks.length} landmarks"); | ||||||
|         future: _landmarks, |          | ||||||
|         builder: (BuildContext context, AsyncSnapshot<LinkedList<Landmark>> snapshot) { |         List<Widget> children; | ||||||
|           List<Widget> children; |          | ||||||
|           if (snapshot.hasData) { |         if (trip.uuid != 'pending' && trip.uuid != 'error') { | ||||||
|             children = [landmarksWithSteps(snapshot.data!), saveButton()]; |           log("Trip ${trip.uuid} ${trip.landmarks.length} landmarks"); | ||||||
|           } else if (snapshot.hasError) { |           if (trip.landmarks.length <= 1) { | ||||||
|             children = <Widget>[ |             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( |               const Icon( | ||||||
|                 Icons.error_outline, |                 Icons.error_outline, | ||||||
|                 color: Colors.red, |                 color: Colors.red, | ||||||
| @@ -42,20 +56,15 @@ class _LandmarksOverviewState extends State<LandmarksOverview> { | |||||||
|               ), |               ), | ||||||
|               Padding( |               Padding( | ||||||
|                 padding: const EdgeInsets.only(top: 16), |                 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 Column( | ||||||
|           return Center( |           children: children, | ||||||
|             child: Column( |         ); | ||||||
|               mainAxisAlignment: MainAxisAlignment.center, |       }, | ||||||
|               children: children, |  | ||||||
|             ), |  | ||||||
|           ); |  | ||||||
|         }, |  | ||||||
|       ), |  | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|   Widget saveButton() => ElevatedButton( |   Widget saveButton() => ElevatedButton( | ||||||
| @@ -67,55 +76,56 @@ class _LandmarksOverviewState extends State<LandmarksOverview> { | |||||||
|     child: const Text('Save'), |     child: const Text('Save'), | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
| } |   Widget landmarksWithSteps() { | ||||||
|  |     return ListenableBuilder( | ||||||
| Widget landmarksWithSteps(LinkedList<Landmark> landmarks) { |       listenable: widget.trip!, | ||||||
|   List<Widget> children = []; |       builder: (BuildContext context, Widget? child) { | ||||||
|   int lkey = 0; |         List<Widget> children = []; | ||||||
|   for (Landmark landmark in landmarks) { |         for (Landmark landmark in widget.trip!.landmarks) { | ||||||
|     children.add( |           children.add( | ||||||
|       Dismissible( |             Dismissible( | ||||||
|         key: ValueKey<int>(lkey), |               key: ValueKey<int>(landmark.hashCode), | ||||||
|         child: LandmarkCard(landmark), |               child: LandmarkCard(landmark), | ||||||
|         // onDismissed: (direction) { |               dismissThresholds: {DismissDirection.endToStart: 0.6}, | ||||||
|         //   // Remove the item from the data source. |               onDismissed: (direction) { | ||||||
|         //   setState(() { |                 // Remove the item from the data source. | ||||||
|         //     landmarks.remove(landmark); |                   log(landmark.name); | ||||||
|         //   }); |                 setState(() { | ||||||
|         //   // Then show a snackbar. |                   widget.trip!.removeLandmark(landmark); | ||||||
|         //   ScaffoldMessenger.of(context) |                 }); | ||||||
|         //       .showSnackBar(SnackBar(content: Text("${landmark.name} dismissed"))); |                 // Then show a snackbar. | ||||||
|         // }, |                 ScaffoldMessenger.of(context) | ||||||
|         background: Container(color: Colors.red), |                     .showSnackBar(SnackBar(content: Text("We won't show ${landmark.name} again"))); | ||||||
|         secondaryBackground: Container( |               }, | ||||||
|           color: Colors.red, |               background: Container(color: Colors.red), | ||||||
|           child: Icon( |               secondaryBackground: Container( | ||||||
|             Icons.delete, |                 color: Colors.red, | ||||||
|             color: Colors.white, |                 child: Icon( | ||||||
|           ), |                   Icons.delete, | ||||||
|           padding: EdgeInsets.all(15), |                   color: Colors.white, | ||||||
|           alignment: Alignment.centerRight, |                 ), | ||||||
|         ), |                 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) { | Widget stepBetweenLandmarks(Landmark current, Landmark next) { | ||||||
|   // This is a simple widget that draws a line between landmark-cards |   int timeRounded = 5 * (current.tripTime?.inMinutes ?? 0) ~/ 5; | ||||||
|   // It's a vertical dotted line |   // ~/ is integer division (rounding) | ||||||
|   // 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 |  | ||||||
|   return Container( |   return Container( | ||||||
|     margin: EdgeInsets.all(10), |     margin: EdgeInsets.all(10), | ||||||
|     padding: EdgeInsets.all(10), |     padding: EdgeInsets.all(10), | ||||||
| @@ -134,7 +144,7 @@ Widget stepBetweenLandmarks(Landmark before, Landmark after) { | |||||||
|         Column( |         Column( | ||||||
|           children: [ |           children: [ | ||||||
|             Icon(Icons.directions_walk), |             Icon(Icons.directions_walk), | ||||||
|             Text("5 min", style: TextStyle(fontSize: 10)), |             Text("~$timeRounded min", style: TextStyle(fontSize: 10)), | ||||||
|           ], |           ], | ||||||
|         ), |         ), | ||||||
|         Spacer(), |         Spacer(), | ||||||
| @@ -142,15 +152,17 @@ Widget stepBetweenLandmarks(Landmark before, Landmark after) { | |||||||
|           onPressed: () { |           onPressed: () { | ||||||
|             // Open navigation instructions |             // Open navigation instructions | ||||||
|           }, |           }, | ||||||
|           child: Text("Navigate"), |           child: Row( | ||||||
|         ), |             children: [ | ||||||
|  |               Icon(Icons.directions), | ||||||
|  |               Text("Directions"), | ||||||
|  |             ], | ||||||
|  |           ), | ||||||
|  |         ) | ||||||
|       ], |       ], | ||||||
|     ), |     ), | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  |  | ||||||
| Future<LinkedList<Landmark>> getLandmarks (Future<Trip>? trip) async { |  | ||||||
|   Trip tripf = await trip!; |  | ||||||
|   return tripf.landmarks; |  | ||||||
| } |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,13 +1,16 @@ | |||||||
| import 'dart:collection'; | import 'dart:collection'; | ||||||
|  | import 'dart:developer'; | ||||||
|  |  | ||||||
|  | import 'package:flutter/material.dart'; | ||||||
| import 'package:anyway/structs/landmark.dart'; | import 'package:anyway/structs/landmark.dart'; | ||||||
| import 'package:anyway/structs/trip.dart'; | import 'package:anyway/structs/trip.dart'; | ||||||
| import 'package:flutter/material.dart'; |  | ||||||
| import 'package:google_maps_flutter/google_maps_flutter.dart'; | import 'package:google_maps_flutter/google_maps_flutter.dart'; | ||||||
|  | import 'package:widget_to_marker/widget_to_marker.dart'; | ||||||
|  |  | ||||||
|  |  | ||||||
| class MapWidget extends StatefulWidget { | class MapWidget extends StatefulWidget { | ||||||
|  |  | ||||||
|   final Future<Trip>? trip; |   final Trip? trip; | ||||||
|  |  | ||||||
|   MapWidget({ |   MapWidget({ | ||||||
|     this.trip |     this.trip | ||||||
| @@ -19,58 +22,130 @@ class MapWidget extends StatefulWidget { | |||||||
|  |  | ||||||
| class _MapWidgetState extends State<MapWidget> { | class _MapWidgetState extends State<MapWidget> { | ||||||
|   late GoogleMapController mapController; |   late GoogleMapController mapController; | ||||||
|   // coordinates of Paris |  | ||||||
|   CameraPosition _cameraPosition = CameraPosition( |   CameraPosition _cameraPosition = CameraPosition( | ||||||
|     target: LatLng(48.8566, 2.3522), |     target: LatLng(48.8566, 2.3522), | ||||||
|     zoom: 11.0, |     zoom: 11.0, | ||||||
|   ); |   ); | ||||||
|   Set<Marker> markers = <Marker>{}; |   Set<Marker> mapMarkers = <Marker>{}; | ||||||
|    |    | ||||||
|  |  | ||||||
|   void _onMapCreated(GoogleMapController controller) async { |   void _onMapCreated(GoogleMapController controller) async { | ||||||
|     mapController = controller; |     mapController = controller; | ||||||
|     Trip? trip = await widget.trip; |     List<double>? newLocation = widget.trip?.landmarks.firstOrNull?.location; | ||||||
|     List<double>? newLocation = trip?.landmarks.first.location; |  | ||||||
|     if (newLocation != null) { |     if (newLocation != null) { | ||||||
|       CameraUpdate update = CameraUpdate.newLatLng(LatLng(newLocation[0], newLocation[1])); |       CameraUpdate update = CameraUpdate.newLatLng(LatLng(newLocation[0], newLocation[1])); | ||||||
|       controller.moveCamera(update); |       controller.moveCamera(update); | ||||||
|     } |     } | ||||||
|     drawLandmarks(); |     setMapMarkers(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|   void _onCameraIdle() { |   void _onCameraIdle() { | ||||||
|     // print(mapController.getLatLng(ScreenCoordinate(x: 0, y: 0))); |     // print(mapController.getLatLng(ScreenCoordinate(x: 0, y: 0))); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|   void drawLandmarks() async { |   void setMapMarkers() async { | ||||||
|     // (re)draws landmarks on the map |     List<Landmark> landmarks = widget.trip?.landmarks.toList() ?? []; | ||||||
|     Trip? trip = await widget.trip; |     Set<Marker> newMarkers = <Marker>{}; | ||||||
|     LinkedList<Landmark>? landmarks = trip?.landmarks; |     for (int i = 0; i < landmarks.length; i++) { | ||||||
|     if (landmarks != null){ |       Landmark landmark = landmarks[i]; | ||||||
|       setState(() { |       List<double> location = landmark.location; | ||||||
|         for (Landmark landmark in landmarks) { |       Marker marker = Marker( | ||||||
|           markers.add(Marker( |         markerId: MarkerId(landmark.uuid), | ||||||
|             markerId: MarkerId(landmark.name), |         position: LatLng(location[0], location[1]), | ||||||
|             position: LatLng(landmark.location[0], landmark.location[1]), |         icon: await CustomMarker(landmark: landmark, position: i).toBitmapDescriptor( | ||||||
|             infoWindow: InfoWindow(title: landmark.name, snippet: landmark.type.name), |           logicalSize: const Size(150, 150), | ||||||
|           )); |           imageSize: const Size(150, 150) | ||||||
|         } |         ), | ||||||
|       }); |       ); | ||||||
|  |       newMarkers.add(marker); | ||||||
|     } |     } | ||||||
|  |     setState(() { | ||||||
|  |       mapMarkers = newMarkers; | ||||||
|  |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|  |     widget.trip?.addListener(setMapMarkers); | ||||||
|     return GoogleMap( |     return GoogleMap( | ||||||
|       onMapCreated: _onMapCreated, |       onMapCreated: _onMapCreated, | ||||||
|       initialCameraPosition: _cameraPosition, |       initialCameraPosition: _cameraPosition, | ||||||
|       onCameraIdle: _onCameraIdle, |       onCameraIdle: _onCameraIdle, | ||||||
|       // onLongPress: , |       // onLongPress: , | ||||||
|       markers: markers, |       markers: mapMarkers, | ||||||
|       cloudMapId: '41c21ac9b81dbfd8', |       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(), | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -25,12 +25,23 @@ class _TripsOverviewState extends State<TripsOverview> { | |||||||
|       children = List<Widget>.generate(snapshot.data!.length, (index) { |       children = List<Widget>.generate(snapshot.data!.length, (index) { | ||||||
|         Trip trip = snapshot.data![index]; |         Trip trip = snapshot.data![index]; | ||||||
|         return ListTile( |         return ListTile( | ||||||
|           title: Text("Trip to ${trip.cityName}"), |           title: FutureBuilder( | ||||||
|  |             future: trip.cityName, | ||||||
|  |             builder: (BuildContext context, AsyncSnapshot<String> snapshot) { | ||||||
|  |               if (snapshot.hasData) { | ||||||
|  |                 return Text("Trip to ${snapshot.data}"); | ||||||
|  |               } else if (snapshot.hasError) { | ||||||
|  |                 return Text("Error: ${snapshot.error}"); | ||||||
|  |               } else { | ||||||
|  |                 return const Text("Trip to ..."); | ||||||
|  |               } | ||||||
|  |             }, | ||||||
|  |           ), | ||||||
|           leading: Icon(Icons.pin_drop), |           leading: Icon(Icons.pin_drop), | ||||||
|           onTap: () { |           onTap: () { | ||||||
|             Navigator.of(context).push( |             Navigator.of(context).push( | ||||||
|               MaterialPageRoute( |               MaterialPageRoute( | ||||||
|                 builder: (context) => BasePage(mainScreen: "map", trip: Future.value(trip)) |                 builder: (context) => BasePage(mainScreen: "map", trip: trip) | ||||||
|               ) |               ) | ||||||
|             ); |             ); | ||||||
|           }, |           }, | ||||||
|   | |||||||
| @@ -1,5 +1,13 @@ | |||||||
|  | import 'package:anyway/structs/landmark.dart'; | ||||||
| import 'package:flutter/material.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 { | class NewTripPage extends StatefulWidget { | ||||||
|   const NewTripPage({Key? key}) : super(key: key); |   const NewTripPage({Key? key}) : super(key: key); | ||||||
| @@ -9,22 +17,77 @@ class NewTripPage extends StatefulWidget { | |||||||
| } | } | ||||||
|  |  | ||||||
| class _NewTripPageState extends State<NewTripPage> { | class _NewTripPageState extends State<NewTripPage> { | ||||||
|  |   final GlobalKey<FormState> _formKey = GlobalKey<FormState>(); | ||||||
|  |   final TextEditingController latController = TextEditingController(); | ||||||
|  |   final TextEditingController lonController = TextEditingController(); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     return Scaffold( |     return Scaffold( | ||||||
|       appBar: AppBar( |       appBar: AppBar( | ||||||
|         title: const Text('New Trip'), |         title: const Text('New Trip'), | ||||||
|       ), |       ), | ||||||
|       body: Center( |       body: Form( | ||||||
|         child: Column( |         key: _formKey, | ||||||
|           mainAxisAlignment: MainAxisAlignment.center, |         child: Padding( | ||||||
|           children: <Widget>[ |           padding: const EdgeInsets.all(15.0), | ||||||
|             const Text( |           child: Column( | ||||||
|               'Create a new trip', |             crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|             ), |              | ||||||
|           ], |             children: <Widget>[ | ||||||
|         ), |               TextFormField( | ||||||
|       ), |                 decoration: const InputDecoration(hintText: 'Lat'), | ||||||
|  |                 controller: latController, | ||||||
|  |                 validator: (String? value) { | ||||||
|  |                   if (value == null || value.isEmpty || double.tryParse(value) == null){ | ||||||
|  |                     return 'Please enter a floating point number'; | ||||||
|  |                   } | ||||||
|  |                   return null; | ||||||
|  |                 }, | ||||||
|  |               ), | ||||||
|  |               TextFormField( | ||||||
|  |                 decoration: const InputDecoration(hintText: 'Lon'), | ||||||
|  |                 controller: lonController, | ||||||
|  |  | ||||||
|  |                 validator: (String? value) { | ||||||
|  |                   if (value == null || value.isEmpty || double.tryParse(value) == null){ | ||||||
|  |                     return 'Please enter a floating point number'; | ||||||
|  |                   } | ||||||
|  |                   return null; | ||||||
|  |                 }, | ||||||
|  |               ), | ||||||
|  |               Divider(height: 15, color: Colors.transparent), | ||||||
|  |               ElevatedButton( | ||||||
|  |                 child: const Text('Create trip'), | ||||||
|  |                 onPressed: () { | ||||||
|  |                   if (_formKey.currentState!.validate()) { | ||||||
|  |                     List<double> startPoint = [ | ||||||
|  |                       double.parse(latController.text), | ||||||
|  |                       double.parse(lonController.text) | ||||||
|  |                     ]; | ||||||
|  |                     Future<UserPreferences> preferences = loadUserPreferences(); | ||||||
|  |                     Trip trip = Trip(); | ||||||
|  |                     trip.landmarks.add( | ||||||
|  |                       Landmark( | ||||||
|  |                         location: startPoint, | ||||||
|  |                         name: "Start", | ||||||
|  |                         type: start, | ||||||
|  |                         uuid: "pending" | ||||||
|  |                       ) | ||||||
|  |                     ); | ||||||
|  |                     fetchTrip(trip, preferences); | ||||||
|  |                     Navigator.of(context).push( | ||||||
|  |                       MaterialPageRoute( | ||||||
|  |                         builder: (context) => BasePage(mainScreen: "map", trip: trip) | ||||||
|  |                       ) | ||||||
|  |                     ); | ||||||
|  |                   } | ||||||
|  |                 }, | ||||||
|  |               ), | ||||||
|  |             ], | ||||||
|  |           ), | ||||||
|  |         ) | ||||||
|  |       ) | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @@ -10,10 +10,10 @@ import 'package:anyway/modules/greeter.dart'; | |||||||
|  |  | ||||||
|  |  | ||||||
| class NavigationOverview extends StatefulWidget { | class NavigationOverview extends StatefulWidget { | ||||||
|   final Future<Trip> trip; |   final Trip trip; | ||||||
|  |  | ||||||
|   NavigationOverview({ |   NavigationOverview({ | ||||||
|     required this.trip |     required this.trip, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
| @@ -27,53 +27,56 @@ class _NavigationOverviewState extends State<NavigationOverview> { | |||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     return SlidingUpPanel( |     return SlidingUpPanel( | ||||||
|         renderPanelSheet: false, |  | ||||||
|         panel: _floatingPanel(), |         panel: _floatingPanel(), | ||||||
|         collapsed: _floatingCollapsed(), |         // collapsed: _floatingCollapsed(), | ||||||
|         body: MapWidget(trip: widget.trip) |         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(){ |   Widget _floatingCollapsed(){ | ||||||
|     final ThemeData theme = Theme.of(context); |     return Greeter( | ||||||
|     return Container( |       trip: widget.trip | ||||||
|       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) |  | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Widget _floatingPanel(){ |   Widget _floatingPanel(){ | ||||||
|     final ThemeData theme = Theme.of(context); |     return Column( | ||||||
|     return Container( |       children: [ | ||||||
|       decoration: BoxDecoration( |         Padding( | ||||||
|         color: Colors.white, |           padding: const EdgeInsets.all(15), | ||||||
|         borderRadius: BorderRadius.all(Radius.circular(24.0)), |           child:  | ||||||
|         boxShadow: [ |             Center( | ||||||
|           BoxShadow( |               child: Container( | ||||||
|             blurRadius: 20.0, |                 width: 40, | ||||||
|             color: theme.shadowColor, |                 height: 5, | ||||||
|           ), |                 decoration: BoxDecoration( | ||||||
|         ] |                   color: Colors.grey[300], | ||||||
|       ), |                   borderRadius: BorderRadius.all(Radius.circular(12.0)), | ||||||
|       child: Center( |                 ), | ||||||
|         child: Padding( |               ), | ||||||
|         padding: EdgeInsets.all(8.0), |             ), | ||||||
|         child: SingleChildScrollView( |  | ||||||
|           child: Column( |  | ||||||
|             children: <Widget>[ |  | ||||||
|               Greeter(standalone: false, trip: widget.trip), |  | ||||||
|               LandmarksOverview(trip: widget.trip), |  | ||||||
|             ], |  | ||||||
|           ), |  | ||||||
|         ), |         ), | ||||||
|       ), |         Expanded( | ||||||
|     ), |           child: ListView( | ||||||
|  |             children: [ | ||||||
|  |               Greeter(trip: widget.trip), | ||||||
|  |               LandmarksOverview(trip: widget.trip) | ||||||
|  |             ] | ||||||
|  |           ) | ||||||
|  |         ) | ||||||
|  |       ], | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,7 +1,9 @@ | |||||||
|  | import 'package:anyway/constants.dart'; | ||||||
| import 'package:anyway/structs/preferences.dart'; | import 'package:anyway/structs/preferences.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | bool debugMode = false; | ||||||
|  |  | ||||||
| class ProfilePage extends StatefulWidget { | class ProfilePage extends StatefulWidget { | ||||||
|   @override |   @override | ||||||
| @@ -9,6 +11,56 @@ class ProfilePage extends StatefulWidget { | |||||||
| } | } | ||||||
|  |  | ||||||
| class _ProfilePageState extends State<ProfilePage> { | class _ProfilePageState extends State<ProfilePage> { | ||||||
|  |   Future<UserPreferences> _prefs = loadUserPreferences(); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   Widget debugButton() { | ||||||
|  |     return Padding( | ||||||
|  |       padding: EdgeInsets.only(top: 20), | ||||||
|  |       child: Row( | ||||||
|  |         children: [ | ||||||
|  |           Text('Debug mode'), | ||||||
|  |           Switch( | ||||||
|  |             value: debugMode, | ||||||
|  |             onChanged: (bool? newValue) { | ||||||
|  |               setState(() { | ||||||
|  |                 debugMode = newValue!; | ||||||
|  |                 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 |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     return ListView( |     return ListView( | ||||||
| @@ -24,66 +76,82 @@ class _ProfilePageState extends State<ProfilePage> { | |||||||
|           child: Text('Curious traveler', style: TextStyle(fontSize: 24)) |           child: Text('Curious traveler', style: TextStyle(fontSize: 24)) | ||||||
|         ), |         ), | ||||||
|  |  | ||||||
|         Padding(padding: EdgeInsets.all(10)), |         Divider(indent: 25, endIndent: 25, height: 50), | ||||||
|         Divider(indent: 25, endIndent: 25), |  | ||||||
|         Padding(padding: EdgeInsets.all(10)), |  | ||||||
|  |  | ||||||
|         Padding( |         Center( | ||||||
|           padding: EdgeInsets.only(left: 10, right: 10, top: 0, bottom: 10), |           child: Padding( | ||||||
|           child: Text('Please rate your personal preferences so that we can taylor your experience.', style: TextStyle(fontSize: 18)) |            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 |         FutureBuilder(future: _prefs, builder: futureSliders), | ||||||
|         ImportanceSliders() |         debugButton() | ||||||
|       ] |       ] | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   Widget futureSliders(BuildContext context, AsyncSnapshot<UserPreferences> snapshot) { | ||||||
|  |     if (snapshot.connectionState == ConnectionState.done) { | ||||||
|  |       UserPreferences prefs = snapshot.data!; | ||||||
|  |  | ||||||
|  |       return Column( | ||||||
|  |         children: [ | ||||||
|  |           PreferenceSliders(prefs: [prefs.maxTime, prefs.maxDetour]), | ||||||
|  |           Divider(indent: 25, endIndent: 25, height: 50), | ||||||
|  |           PreferenceSliders(prefs: [prefs.sightseeing, prefs.shopping, prefs.nature]) | ||||||
|  |         ] | ||||||
|  |       ); | ||||||
|  |     } else { | ||||||
|  |       return CircularProgressIndicator(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class PreferenceSliders extends StatefulWidget { | ||||||
|  |   final List<SinglePreference> prefs; | ||||||
|  |  | ||||||
| class ImportanceSliders extends StatefulWidget { |   PreferenceSliders({required this.prefs}); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   State<ImportanceSliders> createState() => _ImportanceSlidersState(); |   State<PreferenceSliders> createState() => _PreferenceSlidersState(); | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| class _ImportanceSlidersState extends State<ImportanceSliders> { | class _PreferenceSlidersState extends State<PreferenceSliders> { | ||||||
|  |  | ||||||
|   UserPreferences _prefs = UserPreferences(); |  | ||||||
|  |  | ||||||
|   List<Card> _createSliders() { |  | ||||||
|     List<Card> sliders = []; |  | ||||||
|     for (SinglePreference pref in _prefs.preferences) { |  | ||||||
|       sliders.add(Card( |  | ||||||
|         child: ListTile( |  | ||||||
|           leading: pref.icon, |  | ||||||
|           title: Text(pref.name), |  | ||||||
|           subtitle: Slider( |  | ||||||
|             value: pref.value.toDouble(), |  | ||||||
|             min: 0, |  | ||||||
|             max: 10, |  | ||||||
|             divisions: 10, |  | ||||||
|             label: pref.value.toString(), |  | ||||||
|             onChanged: (double newValue) { |  | ||||||
|               setState(() { |  | ||||||
|                 pref.value = newValue.toInt(); |  | ||||||
|                 _prefs.save(); |  | ||||||
|               }); |  | ||||||
|             }, |  | ||||||
|           ) |  | ||||||
|         ), |  | ||||||
|         margin: const EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 0), |  | ||||||
|         shadowColor: Colors.grey, |  | ||||||
|       )); |  | ||||||
|     } |  | ||||||
|     return sliders; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|  |     List<Card> sliders = []; | ||||||
|  |       for (SinglePreference pref in widget.prefs) { | ||||||
|  |       sliders.add( | ||||||
|  |         Card( | ||||||
|  |           child: ListTile( | ||||||
|  |             leading: pref.icon, | ||||||
|  |             title: Text(pref.name), | ||||||
|  |             subtitle: Slider( | ||||||
|  |               value: pref.value.toDouble(), | ||||||
|  |               min: pref.minVal.toDouble(), | ||||||
|  |               max: pref.maxVal.toDouble(), | ||||||
|  |               divisions: pref.maxVal - pref.minVal, | ||||||
|  |               label: pref.value.toString(), | ||||||
|  |               onChanged: (double newValue) { | ||||||
|  |                 setState(() { | ||||||
|  |                   pref.value = newValue.toInt(); | ||||||
|  |                   pref.save(); | ||||||
|  |                 }); | ||||||
|  |               }, | ||||||
|  |             ) | ||||||
|  |           ), | ||||||
|  |           margin: const EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 0), | ||||||
|  |           shadowColor: Colors.grey, | ||||||
|  |         ) | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     return Column(children: _createSliders()); |     return Column( | ||||||
|  |       children: sliders); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,6 +3,15 @@ import 'dart:convert'; | |||||||
|  |  | ||||||
| import 'package:shared_preferences/shared_preferences.dart'; | 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<Landmark>{ | final class Landmark extends LinkedListEntry<Landmark>{ | ||||||
|   // A linked node of a list of Landmarks |   // A linked node of a list of Landmarks | ||||||
|   final String uuid; |   final String uuid; | ||||||
| @@ -47,7 +56,7 @@ final class Landmark extends LinkedListEntry<Landmark>{ | |||||||
|         'location': List<dynamic> location, |         'location': List<dynamic> location, | ||||||
|         'type': String type, |         'type': String type, | ||||||
|       }) { |       }) { | ||||||
|       // refine the parsing on a few |       // refine the parsing on a few fields | ||||||
|       List<double> locationFixed = List<double>.from(location); |       List<double> locationFixed = List<double>.from(location); | ||||||
|       // parse the rest separately, they could be missing |       // parse the rest separately, they could be missing | ||||||
|       LandmarkType typeFixed = LandmarkType(name: type); |       LandmarkType typeFixed = LandmarkType(name: type); | ||||||
| @@ -55,11 +64,12 @@ final class Landmark extends LinkedListEntry<Landmark>{ | |||||||
|       final imageURL = json['image_url'] as String?; |       final imageURL = json['image_url'] as String?; | ||||||
|       final description = json['description'] as String?; |       final description = json['description'] as String?; | ||||||
|       var duration = Duration(minutes: json['duration'] ?? 0) as Duration?; |       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?; |       final visited = json['visited'] as bool?; | ||||||
|  |       var tripTime = Duration(minutes: json['time_to_reach_next'] ?? 0) as Duration?; | ||||||
|        |        | ||||||
|       return Landmark( |       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 { |     } else { | ||||||
|       throw FormatException('Invalid JSON: $json'); |       throw FormatException('Invalid JSON: $json'); | ||||||
|     } |     } | ||||||
| @@ -81,7 +91,8 @@ final class Landmark extends LinkedListEntry<Landmark>{ | |||||||
|     'image_url': imageURL, |     'image_url': imageURL, | ||||||
|     'description': description, |     'description': description, | ||||||
|     'duration': duration?.inMinutes, |     'duration': duration?.inMinutes, | ||||||
|     'visited': visited |     'visited': visited, | ||||||
|  |     'trip_time': tripTime?.inMinutes, | ||||||
|   }; |   }; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -96,6 +107,14 @@ class LandmarkType { | |||||||
|     // required this.description, |     // required this.description, | ||||||
|     // required this.icon, |     // required this.icon, | ||||||
|   }); |   }); | ||||||
|  |   @override | ||||||
|  |   bool operator ==(Object other) { | ||||||
|  |     if (other is LandmarkType) { | ||||||
|  |       return name == other.name; | ||||||
|  |     } else { | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,46 +0,0 @@ | |||||||
| // import "package:anyway/structs/landmark.dart"; |  | ||||||
|  |  | ||||||
| // class Linked<Landmark> { |  | ||||||
| //   Landmark? head; |  | ||||||
|    |  | ||||||
| //   Linked(); |  | ||||||
|  |  | ||||||
| //   // class methods |  | ||||||
| //   bool get isEmpty => head == null; |  | ||||||
|  |  | ||||||
| //   // Add a new node to the end of the list |  | ||||||
| //   void add(Landmark value) { |  | ||||||
| //     if (isEmpty) { |  | ||||||
| //       // If the list is empty, set the new node as the head |  | ||||||
| //       head = value; |  | ||||||
| //     } else { |  | ||||||
| //       Landmark? current = head; |  | ||||||
| //       while (current!.next != null) { |  | ||||||
| //         // Traverse the list to find the last node |  | ||||||
| //         current = current.next; |  | ||||||
| //       } |  | ||||||
| //       current.next = value; // Set the new node as the next node of the last node |  | ||||||
| //     } |  | ||||||
| //   } |  | ||||||
|  |  | ||||||
| //   // Remove the first node with the given value |  | ||||||
| //   void remove(Landmark value) { |  | ||||||
| //     if (isEmpty) return; |  | ||||||
|  |  | ||||||
| //     // If the value is in the head node, update the head to the next node |  | ||||||
| //     if (head! == value) { |  | ||||||
| //       head = head.next; |  | ||||||
| //       return; |  | ||||||
| //     } |  | ||||||
|  |  | ||||||
| //     var current = head; |  | ||||||
| //     while (current!.next != null) { |  | ||||||
| //       if (current.next! == value) { |  | ||||||
| //         // If the value is found in the next node, skip the next node |  | ||||||
| //         current.next = current.next.next; |  | ||||||
| //         return; |  | ||||||
| //       } |  | ||||||
| //       current = current.next; |  | ||||||
| //     } |  | ||||||
| //   } |  | ||||||
| // } |  | ||||||
| @@ -3,80 +3,100 @@ import 'package:shared_preferences/shared_preferences.dart'; | |||||||
|  |  | ||||||
|  |  | ||||||
| class SinglePreference { | class SinglePreference { | ||||||
|  |   String slug; | ||||||
|   String name; |   String name; | ||||||
|   String description; |   String description; | ||||||
|   int value; |   int value; | ||||||
|  |   int minVal; | ||||||
|  |   int maxVal; | ||||||
|   Icon icon; |   Icon icon; | ||||||
|   String key; |  | ||||||
|  |  | ||||||
|   SinglePreference({ |   SinglePreference({ | ||||||
|  |     required this.slug, | ||||||
|     required this.name, |     required this.name, | ||||||
|     required this.description, |     required this.description, | ||||||
|     required this.value, |     required this.value, | ||||||
|     required this.icon, |     required this.icon, | ||||||
|     required this.key, |     this.minVal = 0, | ||||||
|  |     this.maxVal = 5, | ||||||
|   }); |   }); | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class UserPreferences { |  | ||||||
|   List<SinglePreference> preferences = [ |  | ||||||
|     SinglePreference( |  | ||||||
|       name: "Sightseeing", |  | ||||||
|       description: "How much do you like sightseeing?", |  | ||||||
|       value: 0, |  | ||||||
|       icon: Icon(Icons.church), |  | ||||||
|       key: "sightseeing", |  | ||||||
|     ), |  | ||||||
|     SinglePreference( |  | ||||||
|       name: "Shopping", |  | ||||||
|       description: "How much do you like shopping?", |  | ||||||
|       value: 0, |  | ||||||
|       icon: Icon(Icons.shopping_bag), |  | ||||||
|       key: "shopping", |  | ||||||
|     ), |  | ||||||
|     SinglePreference( |  | ||||||
|       name: "Foods & Drinks", |  | ||||||
|       description: "How much do you like eating?", |  | ||||||
|       value: 0, |  | ||||||
|       icon: Icon(Icons.restaurant), |  | ||||||
|       key: "eating", |  | ||||||
|     ), |  | ||||||
|     SinglePreference( |  | ||||||
|       name: "Nightlife", |  | ||||||
|       description: "How much do you like nightlife?", |  | ||||||
|       value: 0, |  | ||||||
|       icon: Icon(Icons.wine_bar), |  | ||||||
|       key: "nightlife", |  | ||||||
|     ), |  | ||||||
|     SinglePreference( |  | ||||||
|       name: "Nature", |  | ||||||
|       description: "How much do you like nature?", |  | ||||||
|       value: 0, |  | ||||||
|       icon: Icon(Icons.landscape), |  | ||||||
|       key: "nature", |  | ||||||
|     ), |  | ||||||
|     SinglePreference( |  | ||||||
|       name: "Culture", |  | ||||||
|       description: "How much do you like culture?", |  | ||||||
|       value: 0, |  | ||||||
|       icon: Icon(Icons.palette), |  | ||||||
|       key: "culture", |  | ||||||
|     ), |  | ||||||
|   ]; |  | ||||||
|  |  | ||||||
|  |  | ||||||
|   void save() async { |   void save() async { | ||||||
|     SharedPreferences sharedPrefs = await SharedPreferences.getInstance(); |     SharedPreferences sharedPrefs = await SharedPreferences.getInstance(); | ||||||
|     for (SinglePreference pref in preferences) { |       sharedPrefs.setInt('pref_$slug', value); | ||||||
|       sharedPrefs.setInt(pref.key, pref.value); |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void load() async { |   void load() async { | ||||||
|     SharedPreferences sharedPrefs = await SharedPreferences.getInstance(); |     SharedPreferences sharedPrefs = await SharedPreferences.getInstance(); | ||||||
|     for (SinglePreference pref in preferences) { |       value = sharedPrefs.getInt('pref_$slug') ?? minVal; | ||||||
|       pref.value = sharedPrefs.getInt(pref.key) ?? 0; |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class UserPreferences { | ||||||
|  |   SinglePreference sightseeing = SinglePreference( | ||||||
|  |     name: "Sightseeing", | ||||||
|  |     slug: "sightseeing", | ||||||
|  |     description: "How much do you like sightseeing?", | ||||||
|  |     value: 0, | ||||||
|  |     icon: Icon(Icons.church), | ||||||
|  |   ); | ||||||
|  |   SinglePreference shopping = SinglePreference( | ||||||
|  |     name: "Shopping", | ||||||
|  |     slug: "shopping", | ||||||
|  |     description: "How much do you like shopping?", | ||||||
|  |     value: 0, | ||||||
|  |     icon: Icon(Icons.shopping_bag), | ||||||
|  |   ); | ||||||
|  |   SinglePreference nature = SinglePreference( | ||||||
|  |     name: "Nature", | ||||||
|  |     slug: "nature", | ||||||
|  |     description: "How much do you like nature?", | ||||||
|  |     value: 0, | ||||||
|  |     icon: Icon(Icons.landscape), | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   SinglePreference maxTime = SinglePreference( | ||||||
|  |     name: "Trip duration", | ||||||
|  |     slug: "duration", | ||||||
|  |     description: "How long do you want your trip to be?", | ||||||
|  |     value: 30, | ||||||
|  |     minVal: 30, | ||||||
|  |     maxVal: 720, | ||||||
|  |     icon: Icon(Icons.timer), | ||||||
|  |   ); | ||||||
|  |   SinglePreference maxDetour = SinglePreference( | ||||||
|  |     name: "Trip detours", | ||||||
|  |     slug: "detours", | ||||||
|  |     description: "Are you okay with roaming even if makes the trip longer?", | ||||||
|  |     value: 0, | ||||||
|  |     maxVal: 30, | ||||||
|  |     icon: Icon(Icons.loupe_sharp), | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   Future<void> load() async { | ||||||
|  |     for (SinglePreference pref in [sightseeing, shopping, nature, maxTime, maxDetour]) { | ||||||
|  |       pref.load(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Map<String, dynamic> toJson() { | ||||||
|  |       // This is "opinionated" JSON, corresponding to the backend's expectations | ||||||
|  |       return { | ||||||
|  |         "sightseeing": {"type": "sightseeing", "score": sightseeing.value}, | ||||||
|  |         "shopping": {"type": "shopping", "score": shopping.value}, | ||||||
|  |         "nature": {"type": "nature", "score": nature.value}, | ||||||
|  |         "max_time_minute": maxTime.value, | ||||||
|  |         "detour_tolerance_minute": maxDetour.value | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | Future<UserPreferences> loadUserPreferences() async { | ||||||
|  |   UserPreferences prefs = UserPreferences(); | ||||||
|  |   await prefs.load(); | ||||||
|  |   return prefs; | ||||||
|  | } | ||||||
| @@ -1,14 +0,0 @@ | |||||||
| import "package:anyway/structs/landmark.dart"; |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Route { |  | ||||||
|   final String name; |  | ||||||
|   final Duration duration; |  | ||||||
|   final List<Landmark> landmarks; |  | ||||||
|  |  | ||||||
|   Route({ |  | ||||||
|     required this.name, |  | ||||||
|     required this.duration, |  | ||||||
|     required this.landmarks |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
| @@ -5,31 +5,71 @@ import 'dart:collection'; | |||||||
| import 'dart:convert'; | import 'dart:convert'; | ||||||
|  |  | ||||||
| import 'package:anyway/structs/landmark.dart'; | import 'package:anyway/structs/landmark.dart'; | ||||||
|  | import 'package:flutter/foundation.dart'; | ||||||
|  | import 'package:geocoding/geocoding.dart'; | ||||||
| import 'package:shared_preferences/shared_preferences.dart'; | import 'package:shared_preferences/shared_preferences.dart'; | ||||||
|  |  | ||||||
| class Trip { | class Trip with ChangeNotifier { | ||||||
|   final String uuid; |   String uuid; | ||||||
|   final String cityName; |   int totalTime; | ||||||
|   // TODO: cityName should be inferred from coordinates of the Landmarks |   LinkedList<Landmark> landmarks; | ||||||
|   final LinkedList<Landmark> landmarks; |  | ||||||
|   // could be empty as well |   // could be empty as well | ||||||
|  |   String? errorDescription; | ||||||
|  |  | ||||||
|  |   Future<String> get cityName async { | ||||||
|  |     List<double>? location = landmarks.firstOrNull?.location;  | ||||||
|  |     if (GeocodingPlatform.instance == null) { | ||||||
|  |       return '$location'; | ||||||
|  |     } else if (location == null) { | ||||||
|  |       return 'Unknown'; | ||||||
|  |     } else{ | ||||||
|  |       List<Placemark> placemarks = await placemarkFromCoordinates(location[0], location[1]); | ||||||
|  |       return placemarks.first.locality ?? 'Unknown'; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|   Trip({ |   Trip({ | ||||||
|     required this.uuid, |     this.uuid = 'pending', | ||||||
|     required this.cityName, |     this.totalTime = 0, | ||||||
|     required this.landmarks, |     LinkedList<Landmark>? landmarks | ||||||
|   }); |     // a trip can be created with no landmarks, but the list should be initialized anyway | ||||||
|  |   }) : landmarks = landmarks ?? LinkedList<Landmark>(); | ||||||
|    |    | ||||||
|  |  | ||||||
|   factory Trip.fromJson(Map<String, dynamic> json) { |   factory Trip.fromJson(Map<String, dynamic> json) { | ||||||
|     return Trip( |     Trip trip = Trip( | ||||||
|       uuid: json['uuid'], |       uuid: json['uuid'], | ||||||
|       cityName: json['city_name'], |       totalTime: json['total_time'], | ||||||
|       landmarks: LinkedList() |  | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|  |     return trip; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   void loadFromJson(Map<String, dynamic> json) { | ||||||
|  |     uuid = json['uuid']; | ||||||
|  |     totalTime = json['total_time']; | ||||||
|  |     notifyListeners(); | ||||||
|  |   } | ||||||
|  |   void addLandmark(Landmark landmark) { | ||||||
|  |     landmarks.add(landmark); | ||||||
|  |     notifyListeners(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void updateUUID(String newUUID) { | ||||||
|  |     uuid = newUUID; | ||||||
|  |     notifyListeners(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void removeLandmark(Landmark landmark) { | ||||||
|  |     landmarks.remove(landmark); | ||||||
|  |     notifyListeners(); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   void updateError(String error) { | ||||||
|  |     errorDescription = error; | ||||||
|  |     notifyListeners(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   factory Trip.fromPrefs(SharedPreferences prefs, String uuid) { |   factory Trip.fromPrefs(SharedPreferences prefs, String uuid) { | ||||||
|     String? content = prefs.getString('trip_$uuid'); |     String? content = prefs.getString('trip_$uuid'); | ||||||
| @@ -43,8 +83,8 @@ class Trip { | |||||||
|  |  | ||||||
|   Map<String, dynamic> toJson() => { |   Map<String, dynamic> toJson() => { | ||||||
|     'uuid': uuid, |     'uuid': uuid, | ||||||
|     'city_name': cityName, |     'total_time': totalTime, | ||||||
|     'entry_uuid': landmarks.first?.uuid ?? '' |     'first_landmark_uuid': landmarks.first.uuid | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,54 +0,0 @@ | |||||||
| import "package:anyway/structs/landmark.dart"; |  | ||||||
| import "package:anyway/structs/linked_landmarks.dart"; |  | ||||||
| import 'package:dio/dio.dart'; |  | ||||||
|  |  | ||||||
| final dio = Dio(); |  | ||||||
|  |  | ||||||
| // Future<List<Landmark>> fetchLandmarks() async { |  | ||||||
| //   // final response = await http |  | ||||||
| //   //     .get(Uri.parse('https://nav.kluster.moll.re/v1/destination/1')); |  | ||||||
|  |  | ||||||
| //   // if (response.statusCode == 200) { |  | ||||||
| //     // If the server did return a 200 OK response, |  | ||||||
| //     // then parse the JSON. |  | ||||||
| //     List<Landmark> landmarks = [ |  | ||||||
| //       // 48°51′29.6″N 2°17′40.2″E |  | ||||||
| //       Landmark( |  | ||||||
| //         name: "Eiffel Tower", |  | ||||||
| //         location: [48.51296, 2.17402], |  | ||||||
| //         type: LandmarkType(name: "Tower"), |  | ||||||
| //         imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a8/Tour_Eiffel_Wikimedia_Commons.jpg/1037px-Tour_Eiffel_Wikimedia_Commons.jpg" |  | ||||||
| //         ), |  | ||||||
| //       Landmark( |  | ||||||
| //         name: "Notre Dame Cathedral", |  | ||||||
| //         location: [48.8530, 2.3498], |  | ||||||
| //         type: LandmarkType(name: "Monument"), |  | ||||||
| //         imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f7/Notre-Dame_de_Paris%2C_4_October_2017.jpg/440px-Notre-Dame_de_Paris%2C_4_October_2017.jpg" |  | ||||||
| //         ), |  | ||||||
| //       Landmark( |  | ||||||
| //         name: "Louvre palace", |  | ||||||
| //         location: [48.8606, 2.3376], |  | ||||||
| //         type: LandmarkType(name: "Museum"), |  | ||||||
| //         imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/6/66/Louvre_Museum_Wikimedia_Commons.jpg/540px-Louvre_Museum_Wikimedia_Commons.jpg" |  | ||||||
| //         ), |  | ||||||
| //       Landmark( |  | ||||||
| //         name: "Pont-des-arts", |  | ||||||
| //         location: [48.5130, 2.2015], |  | ||||||
| //         type: LandmarkType(name: "Bridge"), |  | ||||||
| //         imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d1/Pont_des_Arts%2C_6e_Arrondissement%2C_Paris_%28HDR%29_20140320_1.jpg/560px-Pont_des_Arts%2C_6e_Arrondissement%2C_Paris_%28HDR%29_20140320_1.jpg"), |  | ||||||
| //       Landmark( |  | ||||||
| //         name: "Panthéon", |  | ||||||
| //         location: [48.5046, 2.2046], |  | ||||||
| //         type: LandmarkType(name: "Monument"), |  | ||||||
| //         imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/8/80/Pantheon_of_Paris_007.JPG/1280px-Pantheon_of_Paris_007.JPG" |  | ||||||
| //         ), |  | ||||||
| //     ]; |  | ||||||
| //     // sleep 10 seconds |  | ||||||
| //     await Future.delayed(Duration(seconds: 5)); |  | ||||||
| //     return landmarks; |  | ||||||
| //   // } else { |  | ||||||
| //   //   // If the server did not return a 200 OK response, |  | ||||||
| //   //   // then throw an exception. |  | ||||||
| //   //   throw Exception('Failed to load destination'); |  | ||||||
| //   // } |  | ||||||
| // } |  | ||||||
							
								
								
									
										93
									
								
								frontend/lib/utils/fetch_trip.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								frontend/lib/utils/fetch_trip.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -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<UserPreferences> preferences, | ||||||
|  | ) async { | ||||||
|  |   UserPreferences prefs = await preferences; | ||||||
|  |   Map<String, dynamic> 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<String, dynamic> json = response.data; | ||||||
|  |  | ||||||
|  |     // only fill in the trip "meta" data for now | ||||||
|  |     trip.loadFromJson(json); | ||||||
|  |  | ||||||
|  |     // now fill the trip with landmarks | ||||||
|  |     // we are going to recreate ALL the landmarks from the information given by the api | ||||||
|  |     trip.landmarks.remove(trip.landmarks.first); | ||||||
|  |     String? nextUUID = json["first_landmark_uuid"]; | ||||||
|  |     while (nextUUID != null) { | ||||||
|  |       var (landmark, newUUID) = await fetchLandmark(nextUUID); | ||||||
|  |       trip.addLandmark(landmark); | ||||||
|  |       nextUUID = newUUID; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |   log(response.data.toString()); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 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<String, dynamic> json = response.data; | ||||||
|  |   String? nextUUID = json["next_uuid"]; | ||||||
|  |   return (Landmark.fromJson(json), nextUUID); | ||||||
|  | } | ||||||
| @@ -17,7 +17,7 @@ Future<List<Trip>> loadTrips() async { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (trips.isEmpty) { |   if (trips.isEmpty) { | ||||||
|     Trip t1 = Trip(uuid: '1', cityName: 'Paris', landmarks: LinkedList<Landmark>()); |     Trip t1 = Trip(uuid: '1', landmarks: LinkedList<Landmark>()); | ||||||
|     t1.landmarks.add( |     t1.landmarks.add( | ||||||
|       Landmark( |       Landmark( | ||||||
|         uuid: '1', |         uuid: '1', | ||||||
| @@ -66,7 +66,7 @@ Future<List<Trip>> loadTrips() async { | |||||||
|     trips.add(t1); |     trips.add(t1); | ||||||
|  |  | ||||||
|  |  | ||||||
|     Trip t2 = Trip(uuid: '2', cityName: 'Vienna', landmarks: LinkedList<Landmark>()); |     Trip t2 = Trip(uuid: '2', landmarks: LinkedList<Landmark>()); | ||||||
|  |  | ||||||
|     t2.landmarks.add( |     t2.landmarks.add( | ||||||
|       Landmark( |       Landmark( | ||||||
|   | |||||||
| @@ -5,8 +5,12 @@ | |||||||
| import FlutterMacOS | import FlutterMacOS | ||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
|  | import path_provider_foundation | ||||||
| import shared_preferences_foundation | import shared_preferences_foundation | ||||||
|  | import sqflite | ||||||
|  |  | ||||||
| func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { | ||||||
|  |   PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) | ||||||
|   SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) |   SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) | ||||||
|  |   SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -9,6 +9,14 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.11.0" |     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: |   boolean_selector: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -17,6 +25,30 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.1.1" |     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: |   characters: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -41,6 +73,14 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.18.0" |     version: "1.18.0" | ||||||
|  |   crypto: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: crypto | ||||||
|  |       sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "3.0.3" | ||||||
|   csslib: |   csslib: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -97,11 +137,27 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "7.0.0" |     version: "7.0.0" | ||||||
|  |   fixnum: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: fixnum | ||||||
|  |       sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "1.1.0" | ||||||
|   flutter: |   flutter: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: flutter |     description: flutter | ||||||
|     source: sdk |     source: sdk | ||||||
|     version: "0.0.0" |     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: |   flutter_lints: | ||||||
|     dependency: "direct dev" |     dependency: "direct dev" | ||||||
|     description: |     description: | ||||||
| @@ -114,10 +170,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: flutter_plugin_android_lifecycle |       name: flutter_plugin_android_lifecycle | ||||||
|       sha256: "8cf40eebf5dec866a6d1956ad7b4f7016e6c0cc69847ab946833b7d43743809f" |       sha256: "9d98bd47ef9d34e803d438f17fd32b116d31009f534a6fa5ce3a1167f189a6de" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.0.19" |     version: "2.0.21" | ||||||
|   flutter_test: |   flutter_test: | ||||||
|     dependency: "direct dev" |     dependency: "direct dev" | ||||||
|     description: flutter |     description: flutter | ||||||
| @@ -128,54 +184,86 @@ packages: | |||||||
|     description: flutter |     description: flutter | ||||||
|     source: sdk |     source: sdk | ||||||
|     version: "0.0.0" |     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: |   google_maps: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: google_maps |       name: google_maps | ||||||
|       sha256: "47eef3836b49bb030d5cb3afc60b8451408bf34cf753e571b645d6529eb4251a" |       sha256: "463b38e5a92a05cde41220a11fd5eef3847031fef3e8cf295ac76ec453246907" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "7.1.0" |     version: "8.0.0" | ||||||
|   google_maps_flutter: |   google_maps_flutter: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: google_maps_flutter |       name: google_maps_flutter | ||||||
|       sha256: c1972cbad779bc5346c49045f26ae45550a0958b1cbca5b524dd3c8954995d28 |       sha256: acf0ec482d86b2ac55ade80597ce7f797a47971f5210ebfd030f0d58130e0a94 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.6.1" |     version: "2.7.0" | ||||||
|   google_maps_flutter_android: |   google_maps_flutter_android: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: google_maps_flutter_android |       name: google_maps_flutter_android | ||||||
|       sha256: "0bcadb80eba39afda77dede89a6caafd3b68f2786b90491eceea4a01c3db181c" |       sha256: "5d444f4135559488d7ea325eae710ae3284e6951b1b61729a0ac026456fe1548" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.8.0" |     version: "2.12.1" | ||||||
|   google_maps_flutter_ios: |   google_maps_flutter_ios: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: google_maps_flutter_ios |       name: google_maps_flutter_ios | ||||||
|       sha256: e5132d17f051600d90d79d9f574b177c24231da702453a036db2490f9ced4646 |       sha256: a6e3c6ecdda6c985053f944be13a0645ebb919da2ef0f5bc579c5e1670a5b2a8 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.6.0" |     version: "2.10.0" | ||||||
|   google_maps_flutter_platform_interface: |   google_maps_flutter_platform_interface: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: google_maps_flutter_platform_interface |       name: google_maps_flutter_platform_interface | ||||||
|       sha256: "167af879da4d004cd58771f1469b91dcc3b9b0a2c5334cc6bf71fd41d4b35403" |       sha256: bd60ca330e3c7763b95b477054adec338a522d982af73ecc520b232474063ac5 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.6.0" |     version: "2.8.0" | ||||||
|   google_maps_flutter_web: |   google_maps_flutter_web: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: google_maps_flutter_web |       name: google_maps_flutter_web | ||||||
|       sha256: "0c0d5c723d94b295cf86dd1c45ff91d2ac1fff7c05ddca4f01bef9fa0a014690" |       sha256: "8d5d0f58bfc4afac0bbe3d399f2018fcea691e3ea3d35254b7aae56df5827659" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "0.5.7" |     version: "0.5.9+1" | ||||||
|   html: |   html: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -188,10 +276,10 @@ packages: | |||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: http |       name: http | ||||||
|       sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" |       sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.2.1" |     version: "1.2.2" | ||||||
|   http_parser: |   http_parser: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -200,22 +288,6 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "4.0.2" |     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: |   leak_tracker: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -272,6 +344,22 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.12.0" |     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: |   path: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -280,6 +368,30 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.9.0" |     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: |   path_provider_linux: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -300,18 +412,18 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: path_provider_windows |       name: path_provider_windows | ||||||
|       sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" |       sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.2.1" |     version: "2.3.0" | ||||||
|   platform: |   platform: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: platform |       name: platform | ||||||
|       sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" |       sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.1.4" |     version: "3.1.5" | ||||||
|   plugin_platform_interface: |   plugin_platform_interface: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -320,6 +432,22 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.1.8" |     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: |   sanitize_html: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -332,58 +460,58 @@ packages: | |||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: shared_preferences |       name: shared_preferences | ||||||
|       sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 |       sha256: c3f888ba2d659f3e75f4686112cc1e71f46177f74452d40d8307edc332296ead | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.2.3" |     version: "2.3.0" | ||||||
|   shared_preferences_android: |   shared_preferences_android: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: shared_preferences_android |       name: shared_preferences_android | ||||||
|       sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2" |       sha256: "041be4d9d2dc6079cf342bc8b761b03787e3b71192d658220a56cac9c04a0294" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.2.2" |     version: "2.3.0" | ||||||
|   shared_preferences_foundation: |   shared_preferences_foundation: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: shared_preferences_foundation |       name: shared_preferences_foundation | ||||||
|       sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7" |       sha256: "671e7a931f55a08aa45be2a13fe7247f2a41237897df434b30d2012388191833" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.4.0" |     version: "2.5.0" | ||||||
|   shared_preferences_linux: |   shared_preferences_linux: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: shared_preferences_linux |       name: shared_preferences_linux | ||||||
|       sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" |       sha256: "2ba0510d3017f91655b7543e9ee46d48619de2a2af38e5c790423f7007c7ccc1" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.3.2" |     version: "2.4.0" | ||||||
|   shared_preferences_platform_interface: |   shared_preferences_platform_interface: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: shared_preferences_platform_interface |       name: shared_preferences_platform_interface | ||||||
|       sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" |       sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.3.2" |     version: "2.4.1" | ||||||
|   shared_preferences_web: |   shared_preferences_web: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: shared_preferences_web |       name: shared_preferences_web | ||||||
|       sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" |       sha256: "3a293170d4d9403c3254ee05b84e62e8a9b3c5808ebd17de6a33fe9ea6457936" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.3.0" |     version: "2.4.0" | ||||||
|   shared_preferences_windows: |   shared_preferences_windows: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: shared_preferences_windows |       name: shared_preferences_windows | ||||||
|       sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" |       sha256: "398084b47b7f92110683cac45c6dc4aae853db47e470e5ddcd52cab7f7196ab2" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.3.2" |     version: "2.4.0" | ||||||
|   sky_engine: |   sky_engine: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: flutter |     description: flutter | ||||||
| @@ -405,6 +533,30 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.10.0" |     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: |   stack_trace: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -437,6 +589,14 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.2.0" |     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: |   term_glyph: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -461,6 +621,14 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.3.2" |     version: "1.3.2" | ||||||
|  |   uuid: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: uuid | ||||||
|  |       sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90" | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "4.4.2" | ||||||
|   vector_math: |   vector_math: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -485,14 +653,14 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "0.5.1" |     version: "0.5.1" | ||||||
|   win32: |   widget_to_marker: | ||||||
|     dependency: transitive |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: win32 |       name: widget_to_marker | ||||||
|       sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4 |       sha256: badc36f23c76f3ca9d43d7780058096be774adf0f661bdb6eb6f6b893f648ab9 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "5.5.1" |     version: "1.0.6" | ||||||
|   xdg_directories: |   xdg_directories: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -503,4 +671,4 @@ packages: | |||||||
|     version: "1.0.4" |     version: "1.0.4" | ||||||
| sdks: | sdks: | ||||||
|   dart: ">=3.4.0 <4.0.0" |   dart: ">=3.4.0 <4.0.0" | ||||||
|   flutter: ">=3.19.0" |   flutter: ">=3.22.0" | ||||||
|   | |||||||
| @@ -36,10 +36,15 @@ dependencies: | |||||||
|   # Use with the CupertinoIcons class for iOS style icons. |   # Use with the CupertinoIcons class for iOS style icons. | ||||||
|   cupertino_icons: ^1.0.6 |   cupertino_icons: ^1.0.6 | ||||||
|   sliding_up_panel: ^2.0.0+1 |   sliding_up_panel: ^2.0.0+1 | ||||||
|   google_maps_flutter: ^2.6.1 |  | ||||||
|   http: ^1.2.1 |   http: ^1.2.1 | ||||||
|   shared_preferences: ^2.2.3 |   shared_preferences: ^2.2.3 | ||||||
|   dio: ^5.5.0+1 |   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: | dev_dependencies: | ||||||
|   flutter_test: |   flutter_test: | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user