Tentatively enable communication between front + backend #15

Merged
remoll merged 9 commits from feature/frontend-backend-interoperability into main 2024-08-06 13:51:32 +00:00
38 changed files with 1268 additions and 647 deletions

View File

@ -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

View File

@ -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

View File

@ -14,3 +14,4 @@ shapely = "*"
scipy = "*" scipy = "*"
osmpythontools = "*" osmpythontools = "*"
pywikibot = "*" pywikibot = "*"
pymemcache = "*"

25
backend/Pipfile.lock generated
View File

@ -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": {}

View File

@ -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

View File

@ -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

View File

@ -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")

View File

@ -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

View 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'
)

View File

@ -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]
}

View File

@ -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

View 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

View File

@ -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 :

View File

@ -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

View File

@ -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

View File

@ -1,4 +1,4 @@
package com.example.anyway package com.anydev.anyway
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity

View File

@ -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';

View File

@ -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;
} }

View File

@ -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]),
); );
} }
} }

View File

@ -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,
); );
} }
} }

View File

@ -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,

View File

@ -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;
}

View File

@ -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(),
],
),
);
}
}

View File

@ -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)
) )
); );
}, },

View File

@ -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)
)
);
}
},
),
],
),
)
)
); );
} }
} }

View File

@ -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)
]
)
)
],
); );
} }
} }

View File

@ -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);
} }
} }

View File

@ -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;
}
}
} }

View File

@ -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;
// }
// }
// }

View File

@ -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;
}

View File

@ -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
});
}

View File

@ -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
}; };

View File

@ -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°5129.6N 2°1740.2E
// 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');
// // }
// }

View 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);
}

View File

@ -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(

View File

@ -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"))
} }

View File

@ -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"

View File

@ -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: