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
- name: Add required secrets
env:
ANDROID_SECRETS_PROPERTIES: ${{ secrets.ANDROID_SECRETS_PROPERTIES }}
run: |
echo ${{ secrets.ANDROID_SECRETS_PROPERTIES }} > ./android/secrets.properties
echo "$ANDROID_SECRETS_PROPERTIES" >> ./android/secrets.properties
working-directory: ./frontend
- name: Sanity check

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.
ENV NUM_WORKERS=1
ENV OSM_CACHE_DIR=/cache
ENV MEMCACHED_HOST_PATH=none
CMD fastapi run src/main.py --port 8000 --workers $NUM_WORKERS

View File

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

25
backend/Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "f0de801038593d42d8b780d14c2c72bb4f5f5e66df02f72244917ede5d5ebce6"
"sha256": "4f8b3f0395b4e5352330616870da13acf41e16d1b69ba31b15fd688e90b8b628"
},
"pipfile-spec": 6,
"requires": {},
@ -1102,6 +1102,15 @@
"markers": "python_version >= '3.8'",
"version": "==2.18.0"
},
"pymemcache": {
"hashes": [
"sha256:27bf9bd1bbc1e20f83633208620d56de50f14185055e49504f4f5e94e94aff94",
"sha256:f507bc20e0dc8d562f8df9d872107a278df049fa496805c1431b926f3ddd0eab"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
"version": "==4.0.0"
},
"pyparsing": {
"hashes": [
"sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad",
@ -1142,12 +1151,12 @@
},
"pywikibot": {
"hashes": [
"sha256:3f4fbc57f1765aa0fa1ccf84125bcfa475cae95b9cc0291867b751f3d4ac8fa2",
"sha256:a26d918cf88ef56fdb1421b65b09def200cc28031cdc922d72a4198fbfddd225"
"sha256:0dd8291f1a26abb9fce2c2108a90dc338274988e60d21723aec1d3b0de321b5e",
"sha256:7953fc4a6c498057e6eb7d9b762bbccb61348af0a599b89d7e246d5175b20a9b"
],
"index": "pypi",
"markers": "python_full_version >= '3.7.0'",
"version": "==9.2.1"
"version": "==9.3.0"
},
"pyyaml": {
"hashes": [
@ -1349,7 +1358,7 @@
"sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d",
"sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"
],
"markers": "python_version >= '3.8'",
"markers": "python_version < '3.13'",
"version": "==4.12.2"
},
"tzdata": {
@ -1658,11 +1667,11 @@
},
"xarray": {
"hashes": [
"sha256:0b91e0bc4dc0296947947640fe31ec6e867ce258d2f7cbc10bedf4a6d68340c7",
"sha256:721a7394e8ec3d592b2d8ebe21eed074ac077dc1bb1bd777ce00e41700b4866c"
"sha256:1b0fd51ec408474aa1f4a355d75c00cc1c02bd425d97b2c2e551fd21810e7f64",
"sha256:4cae512d121a8522d41e66d942fb06c526bc1fd32c2c181d5fe62fe65b671638"
],
"markers": "python_version >= '3.9'",
"version": "==2024.6.0"
"version": "==2024.7.0"
}
},
"develop": {}

View File

@ -15,13 +15,21 @@ OSM_CACHE_DIR = Path(cache_dir_string)
import logging
import yaml
LOGGING_CONFIG = LOCATION_PREFIX / 'log_config.yaml'
config = yaml.safe_load(LOGGING_CONFIG.read_text())
logging.config.dictConfig(config)
# if we are in a debug session, set the log level to debug
# if we are in a debug session, set verbose and rich logging
if os.getenv('DEBUG', False):
logging.getLogger().setLevel(logging.DEBUG)
from rich.logging import RichHandler
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[RichHandler()]
)
else:
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
)
MEMCACHED_HOST_PATH = os.getenv('MEMCACHED_HOST_PATH', None)
if MEMCACHED_HOST_PATH == "none":
MEMCACHED_HOST_PATH = None

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
from fastapi import FastAPI, Query, Body
from fastapi import FastAPI, Query, Body, HTTPException
from structs.landmark import Landmark
from structs.preferences import Preferences
from structs.linked_landmarks import LinkedLandmarks
from structs.trip import Trip
from utils.landmarks_manager import LandmarkManager
from utils.optimizer import Optimizer
from utils.refiner import Refiner
from persistence import client as cache_client
logger = logging.getLogger(__name__)
@ -17,8 +19,8 @@ optimizer = Optimizer()
refiner = Refiner(optimizer=optimizer)
@app.post("/route/new")
def get_route(preferences: Preferences, start: tuple[float, float], end: tuple[float, float] | None = None) -> str:
@app.post("/trip/new")
def new_trip(preferences: Preferences, start: tuple[float, float], end: tuple[float, float] | None = None) -> Trip:
'''
Main function to call the optimizer.
:param preferences: the preferences specified by the user as the post body
@ -27,15 +29,17 @@ def get_route(preferences: Preferences, start: tuple[float, float], end: tuple[f
:return: the uuid of the first landmark in the optimized route
'''
if preferences is None:
raise ValueError("Please provide preferences in the form of a 'Preference' BaseModel class.")
raise HTTPException(status_code=406, detail="Preferences not provided")
if preferences.shopping.score == 0 and preferences.sightseeing.score == 0 and preferences.nature.score == 0:
raise HTTPException(status_code=406, detail="All preferences are 0.")
if start is None:
raise ValueError("Please provide the starting coordinates as a tuple of floats.")
raise HTTPException(status_code=406, detail="Start coordinates not provided")
if end is None:
end = start
logger.info("No end coordinates provided. Using start=end.")
start_landmark = Landmark(name='start', type='start', location=(start[0], start[1]), osm_type='start', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
end_landmark = Landmark(name='end', type='finish', location=(end[0], end[1]), osm_type='end', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
end_landmark = Landmark(name='finish', type='finish', location=(end[0], end[1]), osm_type='end', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
# Generate the landmarks from the start location
landmarks, landmarks_short = manager.generate_landmarks_list(
@ -47,22 +51,37 @@ def get_route(preferences: Preferences, start: tuple[float, float], end: tuple[f
landmarks_short.insert(0, start_landmark)
landmarks_short.append(end_landmark)
# TODO infer these parameters from the preferences
max_walking_time = 4 # hours
detour = 30 # minutes
# First stage optimization
base_tour = optimizer.solve_optimization(max_walking_time*60, landmarks_short)
try:
base_tour = optimizer.solve_optimization(preferences.max_time_minute, landmarks_short)
except ArithmeticError:
raise HTTPException(status_code=500, detail="No solution found")
except TimeoutError:
raise HTTPException(status_code=500, detail="Optimzation took too long")
# Second stage optimization
refined_tour = refiner.refine_optimization(landmarks, base_tour, max_walking_time*60, detour)
refined_tour = refiner.refine_optimization(landmarks, base_tour, preferences.max_time_minute, preferences.detour_tolerance_minute)
linked_tour = LinkedLandmarks(refined_tour)
return linked_tour[0].uuid
# upon creation of the trip, persistence of both the trip and its landmarks is ensured. Ca
trip = Trip.from_linked_landmarks(linked_tour, cache_client)
return trip
#### For already existing trips/landmarks
@app.get("/trip/{trip_uuid}")
def get_trip(trip_uuid: str) -> Trip:
try:
trip = cache_client.get(f"trip_{trip_uuid}")
return trip
except KeyError:
raise HTTPException(status_code=404, detail="Trip not found")
@app.get("/landmark/{landmark_uuid}")
def get_landmark(landmark_uuid: str) -> Landmark:
#cherche dans linked_tour et retourne le landmark correspondant
pass
try:
landmark = cache_client.get(f"landmark_{landmark_uuid}")
return landmark
except KeyError:
raise HTTPException(status_code=404, detail="Landmark not found")

View File

@ -1,6 +1,6 @@
city_bbox_side: 5000 #m
radius_close_to: 50
church_coeff: 0.8
park_coeff: 1.2
park_coeff: 1.0
tag_coeff: 10
N_important: 40

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 utils.get_time_separation import get_time
@ -9,8 +8,7 @@ class LinkedLandmarks:
"""
_landmarks = list[Landmark]
total_time = int
uuid = str
total_time: int = 0
def __init__(self, data: list[Landmark] = None) -> None:
"""
@ -19,7 +17,6 @@ class LinkedLandmarks:
Args:
data (list[Landmark], optional): The list of landmarks that are linked together. Defaults to None.
"""
self.uuid = uuid.uuid4()
self._landmarks = data if data else []
self._link_landmarks()
@ -28,7 +25,6 @@ class LinkedLandmarks:
"""
Create the links between the landmarks in the list by setting their .next_uuid and the .time_to_next attributes.
"""
self.total_time = 0
for i, landmark in enumerate(self._landmarks[:-1]):
landmark.next_uuid = self._landmarks[i + 1].uuid
time_to_next = get_time(landmark.location, self._landmarks[i + 1].location)
@ -44,18 +40,4 @@ class LinkedLandmarks:
def __str__(self) -> str:
return f"LinkedLandmarks, total time: {self.total_time} minutes, {len(self._landmarks)} stops: [{','.join([str(landmark) for landmark in self._landmarks])}]"
def asdict(self) -> dict:
"""
Convert the linked landmarks to a json serializable dictionary.
Returns:
dict: A dictionary representation of the linked landmarks.
"""
return {
'uuid': self.uuid,
'total_time': self.total_time,
'landmarks': [landmark.dict() for landmark in self._landmarks]
}
return f"LinkedLandmarks [{' ->'.join([str(landmark) for landmark in self._landmarks])}]"

View File

@ -2,7 +2,6 @@ from pydantic import BaseModel
from typing import Optional, Literal
class Preference(BaseModel) :
name: str
type: Literal['sightseeing', 'nature', 'shopping', 'start', 'finish']
score: int # score could be from 1 to 5
@ -17,5 +16,5 @@ class Preferences(BaseModel) :
# Shopping (diriger plutôt vers des zones / rues commerçantes)
shopping : Preference
max_time_minute: Optional[int] = 6*60
max_time_minute: Optional[int] = 6*60
detour_tolerance_minute: Optional[int] = 0

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(
sightseeing=Preference(
name='sightseeing',
type='sightseeing',
score = 5),
nature=Preference(
name='nature',
type='nature',
score = 5),
shopping=Preference(
name='shopping',
type='shopping',
score = 5),
sightseeing=Preference(type='sightseeing', score = 5),
nature=Preference(type='nature', score = 5),
shopping=Preference(type='shopping', score = 5),
max_time_minute=180,
detour_tolerance_minute=30
)
max_time_minute=180,
detour_tolerance_minute=30
)
# Create start and finish
if finish_coords is None :

View File

@ -15,17 +15,12 @@ from .take_most_important import take_most_important
import constants
SIGHTSEEING = 'sightseeing'
NATURE = 'nature'
SHOPPING = 'shopping'
class LandmarkManager:
logger = logging.getLogger(__name__)
city_bbox_side: int # bbox side in meters
radius_close_to: int # radius in meters
church_coeff: float # coeff to adjsut score of churches
park_coeff: float # coeff to adjust score of parks
@ -40,12 +35,17 @@ class LandmarkManager:
with constants.LANDMARK_PARAMETERS_PATH.open('r') as f:
parameters = yaml.safe_load(f)
self.city_bbox_side = parameters['city_bbox_side']
self.max_bbox_side = parameters['city_bbox_side']
self.radius_close_to = parameters['radius_close_to']
self.church_coeff = parameters['church_coeff']
self.park_coeff = parameters['park_coeff']
self.tag_coeff = parameters['tag_coeff']
self.N_important = parameters['N_important']
with constants.OPTIMIZER_PARAMETERS_PATH.open('r') as f:
parameters = yaml.safe_load(f)
self.walking_speed = parameters['average_walking_speed']
self.detour_factor = parameters['detour_factor']
self.overpass = Overpass()
CachingStrategy.use(JSON, cacheDir=constants.OSM_CACHE_DIR)
@ -69,30 +69,33 @@ class LandmarkManager:
- A list of the most important landmarks based on the user's preferences.
"""
max_walk_dist = (preferences.max_time_minute/2)/60*self.walking_speed*1000/self.detour_factor
reachable_bbox_side = min(max_walk_dist, self.max_bbox_side)
L = []
bbox = self.create_bbox(center_coordinates)
bbox = self.create_bbox(center_coordinates, reachable_bbox_side)
# list for sightseeing
if preferences.sightseeing.score != 0:
score_function = lambda loc, n_tags: int((self.count_elements_close_to(loc) + ((n_tags**1.2)*self.tag_coeff) )*self.church_coeff)
L1 = self.fetch_landmarks(bbox, self.amenity_selectors['sightseeing'], SIGHTSEEING, score_function)
self.correct_score(L1, preferences.sightseeing)
score_function = lambda loc, n_tags: int((((n_tags**1.2)*self.tag_coeff) )*self.church_coeff) # self.count_elements_close_to(loc) +
L1 = self.fetch_landmarks(bbox, self.amenity_selectors['sightseeing'], preferences.sightseeing.type, score_function)
L += L1
# list for nature
if preferences.nature.score != 0:
score_function = lambda loc, n_tags: int((self.count_elements_close_to(loc) + ((n_tags**1.2)*self.tag_coeff) )*self.park_coeff)
L2 = self.fetch_landmarks(bbox, self.amenity_selectors['nature'], NATURE, score_function)
self.correct_score(L2, preferences.nature)
score_function = lambda loc, n_tags: int((((n_tags**1.2)*self.tag_coeff) )*self.park_coeff) # self.count_elements_close_to(loc) +
L2 = self.fetch_landmarks(bbox, self.amenity_selectors['nature'], preferences.nature.type, score_function)
L += L2
# list for shopping
if preferences.shopping.score != 0:
score_function = lambda loc, n_tags: int(self.count_elements_close_to(loc) + ((n_tags**1.2)*self.tag_coeff))
L3 = self.fetch_landmarks(bbox, self.amenity_selectors['shopping'], SHOPPING, score_function)
self.correct_score(L3, preferences.shopping)
score_function = lambda loc, n_tags: int(((n_tags**1.2)*self.tag_coeff)) # self.count_elements_close_to(loc) +
L3 = self.fetch_landmarks(bbox, self.amenity_selectors['shopping'], preferences.shopping.type, score_function)
L += L3
L = self.remove_duplicates(L)
self.correct_score(L, preferences)
L_constrained = take_most_important(L, self.N_important)
self.logger.info(f'Generated {len(L)} landmarks around {center_coordinates}, and constrained to {len(L_constrained)} most important ones.')
@ -123,7 +126,7 @@ class LandmarkManager:
return L_clean
def correct_score(self, landmarks: list[Landmark], preference: Preference):
def correct_score(self, landmarks: list[Landmark], preferences: Preferences) -> None:
"""
Adjust the attractiveness score of each landmark in the list based on user preferences.
@ -132,20 +135,16 @@ class LandmarkManager:
Args:
landmarks (list[Landmark]): A list of landmarks whose scores need to be corrected.
preference (Preference): The user's preference settings that influence the attractiveness score adjustment.
Raises:
TypeError: If the type of any landmark in the list does not match the expected type in the preference.
preferences (Preferences): The user's preference settings that influence the attractiveness score adjustment.
"""
if len(landmarks) == 0:
return
if landmarks[0].type != preference.type:
raise TypeError(f"LandmarkType {preference.type} does not match the type of Landmark {landmarks[0].name}")
for elem in landmarks:
elem.attractiveness = int(elem.attractiveness*preference.score/5) # arbitrary computation
score_dict = {
preferences.sightseeing.type: preferences.sightseeing.score,
preferences.nature.type: preferences.nature.score,
preferences.shopping.type: preferences.shopping.score
}
for landmark in landmarks:
landmark.attractiveness = int(landmark.attractiveness * score_dict[landmark.type] / 5)
def count_elements_close_to(self, coordinates: tuple[float, float]) -> int:
@ -191,12 +190,13 @@ class LandmarkManager:
return 0
def create_bbox(self, coordinates: tuple[float, float]) -> tuple[float, float, float, float]:
def create_bbox(self, coordinates: tuple[float, float], reachable_bbox_side: int) -> tuple[float, float, float, float]:
"""
Create a bounding box around the given coordinates.
Args:
coordinates (tuple[float, float]): The latitude and longitude of the center of the bounding box.
reachable_bbox_side (int): The side length of the bounding box in meters.
Returns:
tuple[float, float, float, float]: The minimum latitude, minimum longitude, maximum latitude, and maximum longitude
@ -207,7 +207,7 @@ class LandmarkManager:
lon = coordinates[1]
# Half the side length in km (since it's a square bbox)
half_side_length_km = self.city_bbox_side / 2 / 1000
half_side_length_km = reachable_bbox_side / 2 / 1000
# Convert distance to degrees
lat_diff = half_side_length_km / 111 # 1 degree latitude is approximately 111 km
@ -296,21 +296,26 @@ class LandmarkManager:
break
if "wikipedia" in tag:
n_tags += 3 # wikipedia entries count more
n_tags += 1 # wikipedia entries count more
if tag == "wikidata":
Q = elem.tag('wikidata')
site = Site("wikidata", "wikidata")
item = ItemPage(site, Q)
item.get()
n_languages = len(item.labels)
n_tags += n_languages/10
# if tag == "wikidata":
# Q = elem.tag('wikidata')
# site = Site("wikidata", "wikidata")
# item = ItemPage(site, Q)
# item.get()
# n_languages = len(item.labels)
# n_tags += n_languages/10
if "viewpoint" in tag:
n_tags += 10
if elem_type != "nature":
if "leisure" in tag and elem.tag('leisure') == "park":
elem_type = "nature"
if elem_type == "nature":
n_tags += 1
if landmarktype != SHOPPING:
if landmarktype != "shopping":
if "shop" in tag:
skip = True
break
@ -318,7 +323,6 @@ class LandmarkManager:
if tag == "building" and elem.tag('building') in ['retail', 'supermarket', 'parking']:
skip = True
break
if skip:
continue

View File

@ -61,7 +61,7 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.anyway"
applicationId "com.anydev.anyway"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
// Minimum Android version for Google Maps SDK

View File

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

View File

@ -1,4 +1,4 @@
const String APP_NAME = 'AnyWay';
const String API_URL_BASE = 'https://anyway.kluster.moll.re';
String API_URL_BASE = 'https://anyway.kluster.moll.re';

View File

@ -1,3 +1,6 @@
import 'dart:collection';
import 'package:anyway/structs/landmark.dart';
import 'package:flutter/material.dart';
import 'package:anyway/constants.dart';
@ -15,12 +18,12 @@ import 'package:anyway/pages/profile.dart';
// A side drawer is used to switch between pages
class BasePage extends StatefulWidget {
final String mainScreen;
final Future<Trip>? trip;
final Trip? trip;
const BasePage({
super.key,
required this.mainScreen,
this.trip
this.trip,
});
@override
@ -53,13 +56,13 @@ class _BasePageState extends State<BasePage> {
children: [
DrawerHeader(
decoration: BoxDecoration(
gradient: LinearGradient(colors: [Colors.cyan, theme.primaryColor])
gradient: LinearGradient(colors: [Colors.red, Colors.yellow])
),
child: Center(
child: Text(
APP_NAME,
style: TextStyle(
color: Colors.white,
color: Colors.grey[800],
fontSize: 24,
fontWeight: FontWeight.bold,
),
@ -129,9 +132,71 @@ class _BasePageState extends State<BasePage> {
}
}
Future<Trip> getFirstTrip (Future<List<Trip>> trips) async {
List<Trip> tripsf = await trips;
return tripsf[0];
// This function is used to get the first trip from a list of trips
// TODO: Implement this function
Trip getFirstTrip(Future<List<Trip>> trips) {
Trip t1 = Trip(uuid: '1', landmarks: LinkedList<Landmark>());
t1.landmarks.add(
Landmark(
uuid: '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(
title: APP_NAME,
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:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
class Greeter extends StatefulWidget {
final Future<Trip> trip;
final bool standalone;
final Trip trip;
Greeter({
required this.standalone,
required this.trip
required this.trip,
});
@override
@ -16,43 +17,91 @@ class Greeter extends StatefulWidget {
}
class _GreeterState extends State<Greeter> {
Widget greeterBuild (BuildContext context, AsyncSnapshot<Trip> snapshot) {
Widget greeterBuilder (BuildContext context, Widget? child) {
ThemeData theme = Theme.of(context);
String cityName = "";
if (snapshot.hasData) {
cityName = snapshot.data?.cityName ?? '...';
} else if (snapshot.hasError) {
cityName = "error";
} else { // still awaiting the cityname
cityName = "...";
}
TextStyle greeterStyle = TextStyle(color: theme.primaryColor, fontWeight: FontWeight.bold, fontSize: 24);
Widget topGreeter = Text(
'Welcome to $cityName!',
style: TextStyle(color: theme.primaryColor, fontWeight: FontWeight.bold, fontSize: 24),
);
Widget topGreeter;
if (widget.standalone) {
return Center(
child: Padding(
padding: EdgeInsets.only(top: 24.0),
child: topGreeter,
),
if (widget.trip.uuid != 'pending') {
topGreeter = FutureBuilder(
future: widget.trip.cityName,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.hasData) {
return AutoSizeText(
maxLines: 1,
'Welcome to ${snapshot.data}!',
style: greeterStyle
);
} else if (snapshot.hasError) {
log('Error while fetching city name');
return AutoSizeText(
maxLines: 1,
'Welcome to your trip!',
style: greeterStyle
);
} else {
return AutoSizeText(
maxLines: 1,
'Welcome to ...',
style: greeterStyle
);
}
}
);
} else {
return Center(
child: Column(
children: [
Padding(padding: EdgeInsets.only(top: 24.0)),
topGreeter,
bottomGreeter,
Padding(padding: EdgeInsets.only(bottom: 24.0)),
],
)
// still awaiting the trip
// We can hopefully infer the city name from the cityName future
// Show a linear loader at the bottom and an info message above
topGreeter = Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FutureBuilder(
future: widget.trip.cityName,
builder: (BuildContext context, AsyncSnapshot<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(
@ -65,9 +114,9 @@ class _GreeterState extends State<Greeter> {
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: widget.trip,
builder: greeterBuild,
return ListenableBuilder(
listenable: widget.trip,
builder: greeterBuilder,
);
}
}

View File

@ -1,4 +1,5 @@
import 'package:anyway/structs/landmark.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
@ -31,9 +32,10 @@ class _LandmarkCardState extends State<LandmarkCard> {
height: double.infinity,
// force a fixed width
width: 160,
child: Image.network(
widget.landmark.imageURL ?? '',
errorBuilder: (context, error, stackTrace) => Icon(Icons.question_mark_outlined),
child: CachedNetworkImage(
imageUrl: widget.landmark.imageURL ?? '',
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, error, stackTrace) => Icon(Icons.question_mark_outlined),
// TODO: make this a switch statement to load a placeholder if null
// cover the whole container meaning the image will be cropped
fit: BoxFit.cover,

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/structs/landmark.dart';
import 'package:anyway/structs/trip.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class LandmarksOverview extends StatefulWidget {
final Future<Trip>? trip;
final Trip? trip;
const LandmarksOverview({super.key, this.trip});
@override
@ -19,22 +18,37 @@ class LandmarksOverview extends StatefulWidget {
}
class _LandmarksOverviewState extends State<LandmarksOverview> {
// final Future<List<Landmark>> _landmarks = fetchLandmarks();
@override
Widget build(BuildContext context) {
final Future<LinkedList<Landmark>> _landmarks = getLandmarks(widget.trip);
return DefaultTextStyle(
style: Theme.of(context).textTheme.displayMedium!,
textAlign: TextAlign.center,
child: FutureBuilder<LinkedList<Landmark>>(
future: _landmarks,
builder: (BuildContext context, AsyncSnapshot<LinkedList<Landmark>> snapshot) {
List<Widget> children;
if (snapshot.hasData) {
children = [landmarksWithSteps(snapshot.data!), saveButton()];
} else if (snapshot.hasError) {
children = <Widget>[
return ListenableBuilder(
listenable: widget.trip!,
builder: (BuildContext context, Widget? child) {
Trip trip = widget.trip!;
log("Trip ${trip.uuid} ${trip.landmarks.length} landmarks");
List<Widget> children;
if (trip.uuid != 'pending' && trip.uuid != 'error') {
log("Trip ${trip.uuid} ${trip.landmarks.length} landmarks");
if (trip.landmarks.length <= 1) {
children = [
const Text("No landmarks in this trip"),
];
} else {
children = [
landmarksWithSteps(),
saveButton(),
];
}
} else if(trip.uuid == 'pending') {
// the trip is still being fetched from the api
children = [Center(child: CircularProgressIndicator())];
} else {
// trip.uuid == 'error'
// show the error raised by the api
// String error =
children = [
const Icon(
Icons.error_outline,
color: Colors.red,
@ -42,20 +56,15 @@ class _LandmarksOverviewState extends State<LandmarksOverview> {
),
Padding(
padding: const EdgeInsets.only(top: 16),
child: Text('Error: ${snapshot.error}', style: TextStyle(fontSize: 12)),
child: Text('Error: ${trip.errorDescription}'),
),
];
} else {
children = [Center(child: CircularProgressIndicator())];
}
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: children,
),
);
},
),
}
return Column(
children: children,
);
},
);
}
Widget saveButton() => ElevatedButton(
@ -67,55 +76,56 @@ class _LandmarksOverviewState extends State<LandmarksOverview> {
child: const Text('Save'),
);
}
Widget landmarksWithSteps(LinkedList<Landmark> landmarks) {
List<Widget> children = [];
int lkey = 0;
for (Landmark landmark in landmarks) {
children.add(
Dismissible(
key: ValueKey<int>(lkey),
child: LandmarkCard(landmark),
// onDismissed: (direction) {
// // Remove the item from the data source.
// setState(() {
// landmarks.remove(landmark);
// });
// // Then show a snackbar.
// ScaffoldMessenger.of(context)
// .showSnackBar(SnackBar(content: Text("${landmark.name} dismissed")));
// },
background: Container(color: Colors.red),
secondaryBackground: Container(
color: Colors.red,
child: Icon(
Icons.delete,
color: Colors.white,
),
padding: EdgeInsets.all(15),
alignment: Alignment.centerRight,
),
)
Widget landmarksWithSteps() {
return ListenableBuilder(
listenable: widget.trip!,
builder: (BuildContext context, Widget? child) {
List<Widget> children = [];
for (Landmark landmark in widget.trip!.landmarks) {
children.add(
Dismissible(
key: ValueKey<int>(landmark.hashCode),
child: LandmarkCard(landmark),
dismissThresholds: {DismissDirection.endToStart: 0.6},
onDismissed: (direction) {
// Remove the item from the data source.
log(landmark.name);
setState(() {
widget.trip!.removeLandmark(landmark);
});
// Then show a snackbar.
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text("We won't show ${landmark.name} again")));
},
background: Container(color: Colors.red),
secondaryBackground: Container(
color: Colors.red,
child: Icon(
Icons.delete,
color: Colors.white,
),
padding: EdgeInsets.all(15),
alignment: Alignment.centerRight,
),
)
);
if (landmark.next != null) {
Widget step = stepBetweenLandmarks(landmark, landmark.next!);
children.add(step);
}
}
return Column(
children: children
);
},
);
lkey++;
if (landmark.next != null) {
Widget step = stepBetweenLandmarks(landmark, landmark.next!);
children.add(step);
}
}
return Column(
children: children
);
}
Widget stepBetweenLandmarks(Landmark before, Landmark after) {
// This is a simple widget that draws a line between landmark-cards
// It's a vertical dotted line
// Next to the line is the icon for the mode of transport (walking for now) and the estimated time
// There is also a button to open the navigation instructions as a new intent
Widget stepBetweenLandmarks(Landmark current, Landmark next) {
int timeRounded = 5 * (current.tripTime?.inMinutes ?? 0) ~/ 5;
// ~/ is integer division (rounding)
return Container(
margin: EdgeInsets.all(10),
padding: EdgeInsets.all(10),
@ -134,7 +144,7 @@ Widget stepBetweenLandmarks(Landmark before, Landmark after) {
Column(
children: [
Icon(Icons.directions_walk),
Text("5 min", style: TextStyle(fontSize: 10)),
Text("~$timeRounded min", style: TextStyle(fontSize: 10)),
],
),
Spacer(),
@ -142,15 +152,17 @@ Widget stepBetweenLandmarks(Landmark before, Landmark after) {
onPressed: () {
// Open navigation instructions
},
child: Text("Navigate"),
),
child: Row(
children: [
Icon(Icons.directions),
Text("Directions"),
],
),
)
],
),
);
}
Future<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:developer';
import 'package:flutter/material.dart';
import 'package:anyway/structs/landmark.dart';
import 'package:anyway/structs/trip.dart';
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:widget_to_marker/widget_to_marker.dart';
class MapWidget extends StatefulWidget {
final Future<Trip>? trip;
final Trip? trip;
MapWidget({
this.trip
@ -19,58 +22,130 @@ class MapWidget extends StatefulWidget {
class _MapWidgetState extends State<MapWidget> {
late GoogleMapController mapController;
// coordinates of Paris
CameraPosition _cameraPosition = CameraPosition(
target: LatLng(48.8566, 2.3522),
zoom: 11.0,
);
Set<Marker> markers = <Marker>{};
Set<Marker> mapMarkers = <Marker>{};
void _onMapCreated(GoogleMapController controller) async {
mapController = controller;
Trip? trip = await widget.trip;
List<double>? newLocation = trip?.landmarks.first.location;
List<double>? newLocation = widget.trip?.landmarks.firstOrNull?.location;
if (newLocation != null) {
CameraUpdate update = CameraUpdate.newLatLng(LatLng(newLocation[0], newLocation[1]));
controller.moveCamera(update);
}
drawLandmarks();
setMapMarkers();
}
void _onCameraIdle() {
// print(mapController.getLatLng(ScreenCoordinate(x: 0, y: 0)));
}
void drawLandmarks() async {
// (re)draws landmarks on the map
Trip? trip = await widget.trip;
LinkedList<Landmark>? landmarks = trip?.landmarks;
if (landmarks != null){
setState(() {
for (Landmark landmark in landmarks) {
markers.add(Marker(
markerId: MarkerId(landmark.name),
position: LatLng(landmark.location[0], landmark.location[1]),
infoWindow: InfoWindow(title: landmark.name, snippet: landmark.type.name),
));
}
});
void setMapMarkers() async {
List<Landmark> landmarks = widget.trip?.landmarks.toList() ?? [];
Set<Marker> newMarkers = <Marker>{};
for (int i = 0; i < landmarks.length; i++) {
Landmark landmark = landmarks[i];
List<double> location = landmark.location;
Marker marker = Marker(
markerId: MarkerId(landmark.uuid),
position: LatLng(location[0], location[1]),
icon: await CustomMarker(landmark: landmark, position: i).toBitmapDescriptor(
logicalSize: const Size(150, 150),
imageSize: const Size(150, 150)
),
);
newMarkers.add(marker);
}
setState(() {
mapMarkers = newMarkers;
});
}
@override
Widget build(BuildContext context) {
widget.trip?.addListener(setMapMarkers);
return GoogleMap(
onMapCreated: _onMapCreated,
initialCameraPosition: _cameraPosition,
onCameraIdle: _onCameraIdle,
// onLongPress: ,
markers: markers,
markers: mapMarkers,
cloudMapId: '41c21ac9b81dbfd8',
);
}
}
class CustomMarker extends StatelessWidget {
final Landmark landmark;
final int position;
CustomMarker({
super.key,
required this.landmark,
required this.position
});
@override
Widget build(BuildContext context) {
// This returns an outlined circle, with an icon corresponding to the landmark type
// As a small dot, the number of the landmark is displayed in the top right
Icon icon;
if (landmark.type == sightseeing) {
icon = Icon(Icons.church, color: Colors.black, size: 50);
} else if (landmark.type == nature) {
icon = Icon(Icons.park, color: Colors.black, size: 50);
} else if (landmark.type == shopping) {
icon = Icon(Icons.shopping_cart, color: Colors.black, size: 50);
} else if (landmark.type == start || landmark.type == finish) {
icon = Icon(Icons.flag, color: Colors.black, size: 50);
} else {
icon = Icon(Icons.location_on, color: Colors.black, size: 50);
}
Widget? positionIndicator;
if (landmark.type != start && landmark.type != finish) {
positionIndicator = Positioned(
top: 0,
right: 0,
child: Container(
padding: EdgeInsets.all(5),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
shape: BoxShape.circle,
),
child: Text('$position', style: TextStyle(color: Colors.white, fontSize: 20)),
),
);
}
return RepaintBoundary(
child: Stack(
children: [
Container(
// these are not the final sizes, since the final size is set in the toBitmapDescriptor method
// they are useful nevertheless to ensure the scale of the components are correct
width: 75,
height: 75,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Colors.red, Colors.yellow]
),
shape: BoxShape.circle,
border: Border.all(color: Colors.black, width: 5),
),
child: icon,
),
positionIndicator ?? Container(),
],
),
);
}
}

View File

@ -25,12 +25,23 @@ class _TripsOverviewState extends State<TripsOverview> {
children = List<Widget>.generate(snapshot.data!.length, (index) {
Trip trip = snapshot.data![index];
return ListTile(
title: Text("Trip to ${trip.cityName}"),
title: FutureBuilder(
future: trip.cityName,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.hasData) {
return Text("Trip to ${snapshot.data}");
} else if (snapshot.hasError) {
return Text("Error: ${snapshot.error}");
} else {
return const Text("Trip to ...");
}
},
),
leading: Icon(Icons.pin_drop),
onTap: () {
Navigator.of(context).push(
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:geocoding/geocoding.dart';
import 'package:anyway/layout.dart';
import 'package:anyway/utils/fetch_trip.dart';
import 'package:anyway/structs/preferences.dart';
import "package:anyway/structs/trip.dart";
class NewTripPage extends StatefulWidget {
const NewTripPage({Key? key}) : super(key: key);
@ -9,22 +17,77 @@ class NewTripPage extends StatefulWidget {
}
class _NewTripPageState extends State<NewTripPage> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final TextEditingController latController = TextEditingController();
final TextEditingController lonController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('New Trip'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'Create a new trip',
),
],
),
),
body: Form(
key: _formKey,
child: Padding(
padding: const EdgeInsets.all(15.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
TextFormField(
decoration: const InputDecoration(hintText: 'Lat'),
controller: latController,
validator: (String? value) {
if (value == null || value.isEmpty || double.tryParse(value) == null){
return 'Please enter a floating point number';
}
return null;
},
),
TextFormField(
decoration: const InputDecoration(hintText: 'Lon'),
controller: lonController,
validator: (String? value) {
if (value == null || value.isEmpty || double.tryParse(value) == null){
return 'Please enter a floating point number';
}
return null;
},
),
Divider(height: 15, color: Colors.transparent),
ElevatedButton(
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 {
final Future<Trip> trip;
final Trip trip;
NavigationOverview({
required this.trip
required this.trip,
});
@override
@ -27,53 +27,56 @@ class _NavigationOverviewState extends State<NavigationOverview> {
@override
Widget build(BuildContext context) {
return SlidingUpPanel(
renderPanelSheet: false,
panel: _floatingPanel(),
collapsed: _floatingCollapsed(),
body: MapWidget(trip: widget.trip)
// collapsed: _floatingCollapsed(),
body: MapWidget(trip: widget.trip),
// renderPanelSheet: false,
// backdropEnabled: true,
maxHeight: MediaQuery.of(context).size.height * 0.8,
padding: EdgeInsets.all(10),
// panelSnapping: false,
borderRadius: BorderRadius.only(topLeft: Radius.circular(25), topRight: Radius.circular(25)),
boxShadow: [
BoxShadow(
blurRadius: 20.0,
color: Colors.black,
)
],
);
}
Widget _floatingCollapsed(){
final ThemeData theme = Theme.of(context);
return Container(
decoration: BoxDecoration(
color: theme.canvasColor,
borderRadius: BorderRadius.only(topLeft: Radius.circular(24.0), topRight: Radius.circular(24.0)),
boxShadow: []
),
child: Greeter(standalone: true, trip: widget.trip)
return Greeter(
trip: widget.trip
);
}
Widget _floatingPanel(){
final ThemeData theme = Theme.of(context);
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(24.0)),
boxShadow: [
BoxShadow(
blurRadius: 20.0,
color: theme.shadowColor,
),
]
),
child: Center(
child: Padding(
padding: EdgeInsets.all(8.0),
child: SingleChildScrollView(
child: Column(
children: <Widget>[
Greeter(standalone: false, trip: widget.trip),
LandmarksOverview(trip: widget.trip),
],
),
return Column(
children: [
Padding(
padding: const EdgeInsets.all(15),
child:
Center(
child: Container(
width: 40,
height: 5,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.all(Radius.circular(12.0)),
),
),
),
),
),
),
Expanded(
child: ListView(
children: [
Greeter(trip: widget.trip),
LandmarksOverview(trip: widget.trip)
]
)
)
],
);
}
}

View File

@ -1,7 +1,9 @@
import 'package:anyway/constants.dart';
import 'package:anyway/structs/preferences.dart';
import 'package:flutter/material.dart';
bool debugMode = false;
class ProfilePage extends StatefulWidget {
@override
@ -9,6 +11,56 @@ class ProfilePage extends StatefulWidget {
}
class _ProfilePageState extends State<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
Widget build(BuildContext context) {
return ListView(
@ -24,66 +76,82 @@ class _ProfilePageState extends State<ProfilePage> {
child: Text('Curious traveler', style: TextStyle(fontSize: 24))
),
Padding(padding: EdgeInsets.all(10)),
Divider(indent: 25, endIndent: 25),
Padding(padding: EdgeInsets.all(10)),
Divider(indent: 25, endIndent: 25, height: 50),
Padding(
padding: EdgeInsets.only(left: 10, right: 10, top: 0, bottom: 10),
child: Text('Please rate your personal preferences so that we can taylor your experience.', style: TextStyle(fontSize: 18))
Center(
child: Padding(
padding: EdgeInsets.only(left: 10, right: 10, top: 0, bottom: 10),
child: Text('For a tailored experience, please rate your discovery preferences.', style: TextStyle(fontSize: 18))
),
),
// Now the sliders
ImportanceSliders()
FutureBuilder(future: _prefs, builder: futureSliders),
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
State<ImportanceSliders> createState() => _ImportanceSlidersState();
State<PreferenceSliders> createState() => _PreferenceSlidersState();
}
class _ImportanceSlidersState extends State<ImportanceSliders> {
UserPreferences _prefs = UserPreferences();
List<Card> _createSliders() {
List<Card> sliders = [];
for (SinglePreference pref in _prefs.preferences) {
sliders.add(Card(
child: ListTile(
leading: pref.icon,
title: Text(pref.name),
subtitle: Slider(
value: pref.value.toDouble(),
min: 0,
max: 10,
divisions: 10,
label: pref.value.toString(),
onChanged: (double newValue) {
setState(() {
pref.value = newValue.toInt();
_prefs.save();
});
},
)
),
margin: const EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 0),
shadowColor: Colors.grey,
));
}
return sliders;
}
class _PreferenceSlidersState extends State<PreferenceSliders> {
@override
Widget build(BuildContext context) {
List<Card> sliders = [];
for (SinglePreference pref in widget.prefs) {
sliders.add(
Card(
child: ListTile(
leading: pref.icon,
title: Text(pref.name),
subtitle: Slider(
value: pref.value.toDouble(),
min: pref.minVal.toDouble(),
max: pref.maxVal.toDouble(),
divisions: pref.maxVal - pref.minVal,
label: pref.value.toString(),
onChanged: (double newValue) {
setState(() {
pref.value = newValue.toInt();
pref.save();
});
},
)
),
margin: const EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 0),
shadowColor: Colors.grey,
)
);
}
return Column(children: _createSliders());
return Column(
children: sliders);
}
}

View File

@ -3,6 +3,15 @@ import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
const LandmarkType sightseeing = LandmarkType(name: 'sightseeing');
const LandmarkType nature = LandmarkType(name: 'nature');
const LandmarkType shopping = LandmarkType(name: 'shopping');
// const LandmarkType museum = LandmarkType(name: 'Museum');
// const LandmarkType restaurant = LandmarkType(name: 'Restaurant');
const LandmarkType start = LandmarkType(name: 'start');
const LandmarkType finish = LandmarkType(name: 'finish');
final class Landmark extends LinkedListEntry<Landmark>{
// A linked node of a list of Landmarks
final String uuid;
@ -47,7 +56,7 @@ final class Landmark extends LinkedListEntry<Landmark>{
'location': List<dynamic> location,
'type': String type,
}) {
// refine the parsing on a few
// refine the parsing on a few fields
List<double> locationFixed = List<double>.from(location);
// parse the rest separately, they could be missing
LandmarkType typeFixed = LandmarkType(name: type);
@ -55,11 +64,12 @@ final class Landmark extends LinkedListEntry<Landmark>{
final imageURL = json['image_url'] as String?;
final description = json['description'] as String?;
var duration = Duration(minutes: json['duration'] ?? 0) as Duration?;
if (duration == const Duration()) {duration = null;};
// if (duration == const Duration()) {duration = null;};
final visited = json['visited'] as bool?;
var tripTime = Duration(minutes: json['time_to_reach_next'] ?? 0) as Duration?;
return Landmark(
uuid: uuid, name: name, location: locationFixed, type: typeFixed, isSecondary: isSecondary, imageURL: imageURL, description: description, duration: duration, visited: visited);
uuid: uuid, name: name, location: locationFixed, type: typeFixed, isSecondary: isSecondary, imageURL: imageURL, description: description, duration: duration, visited: visited, tripTime: tripTime);
} else {
throw FormatException('Invalid JSON: $json');
}
@ -81,7 +91,8 @@ final class Landmark extends LinkedListEntry<Landmark>{
'image_url': imageURL,
'description': description,
'duration': duration?.inMinutes,
'visited': visited
'visited': visited,
'trip_time': tripTime?.inMinutes,
};
}
@ -96,6 +107,14 @@ class LandmarkType {
// required this.description,
// required this.icon,
});
@override
bool operator ==(Object other) {
if (other is LandmarkType) {
return name == other.name;
} else {
return false;
}
}
}

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 {
String slug;
String name;
String description;
int value;
int minVal;
int maxVal;
Icon icon;
String key;
SinglePreference({
required this.slug,
required this.name,
required this.description,
required this.value,
required this.icon,
required this.key,
this.minVal = 0,
this.maxVal = 5,
});
}
class UserPreferences {
List<SinglePreference> preferences = [
SinglePreference(
name: "Sightseeing",
description: "How much do you like sightseeing?",
value: 0,
icon: Icon(Icons.church),
key: "sightseeing",
),
SinglePreference(
name: "Shopping",
description: "How much do you like shopping?",
value: 0,
icon: Icon(Icons.shopping_bag),
key: "shopping",
),
SinglePreference(
name: "Foods & Drinks",
description: "How much do you like eating?",
value: 0,
icon: Icon(Icons.restaurant),
key: "eating",
),
SinglePreference(
name: "Nightlife",
description: "How much do you like nightlife?",
value: 0,
icon: Icon(Icons.wine_bar),
key: "nightlife",
),
SinglePreference(
name: "Nature",
description: "How much do you like nature?",
value: 0,
icon: Icon(Icons.landscape),
key: "nature",
),
SinglePreference(
name: "Culture",
description: "How much do you like culture?",
value: 0,
icon: Icon(Icons.palette),
key: "culture",
),
];
void save() async {
SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
for (SinglePreference pref in preferences) {
sharedPrefs.setInt(pref.key, pref.value);
}
sharedPrefs.setInt('pref_$slug', value);
}
void load() async {
SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
for (SinglePreference pref in preferences) {
pref.value = sharedPrefs.getInt(pref.key) ?? 0;
value = sharedPrefs.getInt('pref_$slug') ?? minVal;
}
}
class UserPreferences {
SinglePreference sightseeing = SinglePreference(
name: "Sightseeing",
slug: "sightseeing",
description: "How much do you like sightseeing?",
value: 0,
icon: Icon(Icons.church),
);
SinglePreference shopping = SinglePreference(
name: "Shopping",
slug: "shopping",
description: "How much do you like shopping?",
value: 0,
icon: Icon(Icons.shopping_bag),
);
SinglePreference nature = SinglePreference(
name: "Nature",
slug: "nature",
description: "How much do you like nature?",
value: 0,
icon: Icon(Icons.landscape),
);
SinglePreference maxTime = SinglePreference(
name: "Trip duration",
slug: "duration",
description: "How long do you want your trip to be?",
value: 30,
minVal: 30,
maxVal: 720,
icon: Icon(Icons.timer),
);
SinglePreference maxDetour = SinglePreference(
name: "Trip detours",
slug: "detours",
description: "Are you okay with roaming even if makes the trip longer?",
value: 0,
maxVal: 30,
icon: Icon(Icons.loupe_sharp),
);
Future<void> load() async {
for (SinglePreference pref in [sightseeing, shopping, nature, maxTime, maxDetour]) {
pref.load();
}
}
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 'package:anyway/structs/landmark.dart';
import 'package:flutter/foundation.dart';
import 'package:geocoding/geocoding.dart';
import 'package:shared_preferences/shared_preferences.dart';
class Trip {
final String uuid;
final String cityName;
// TODO: cityName should be inferred from coordinates of the Landmarks
final LinkedList<Landmark> landmarks;
class Trip with ChangeNotifier {
String uuid;
int totalTime;
LinkedList<Landmark> landmarks;
// 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({
required this.uuid,
required this.cityName,
required this.landmarks,
});
this.uuid = 'pending',
this.totalTime = 0,
LinkedList<Landmark>? landmarks
// a trip can be created with no landmarks, but the list should be initialized anyway
}) : landmarks = landmarks ?? LinkedList<Landmark>();
factory Trip.fromJson(Map<String, dynamic> json) {
return Trip(
Trip trip = Trip(
uuid: json['uuid'],
cityName: json['city_name'],
landmarks: LinkedList()
totalTime: json['total_time'],
);
return trip;
}
void loadFromJson(Map<String, dynamic> json) {
uuid = json['uuid'];
totalTime = json['total_time'];
notifyListeners();
}
void addLandmark(Landmark landmark) {
landmarks.add(landmark);
notifyListeners();
}
void updateUUID(String newUUID) {
uuid = newUUID;
notifyListeners();
}
void removeLandmark(Landmark landmark) {
landmarks.remove(landmark);
notifyListeners();
}
void updateError(String error) {
errorDescription = error;
notifyListeners();
}
factory Trip.fromPrefs(SharedPreferences prefs, String uuid) {
String? content = prefs.getString('trip_$uuid');
@ -43,8 +83,8 @@ class Trip {
Map<String, dynamic> toJson() => {
'uuid': uuid,
'city_name': cityName,
'entry_uuid': landmarks.first?.uuid ?? ''
'total_time': totalTime,
'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) {
Trip t1 = Trip(uuid: '1', cityName: 'Paris', landmarks: LinkedList<Landmark>());
Trip t1 = Trip(uuid: '1', landmarks: LinkedList<Landmark>());
t1.landmarks.add(
Landmark(
uuid: '1',
@ -66,7 +66,7 @@ Future<List<Trip>> loadTrips() async {
trips.add(t1);
Trip t2 = Trip(uuid: '2', cityName: 'Vienna', landmarks: LinkedList<Landmark>());
Trip t2 = Trip(uuid: '2', landmarks: LinkedList<Landmark>());
t2.landmarks.add(
Landmark(

View File

@ -5,8 +5,12 @@
import FlutterMacOS
import Foundation
import path_provider_foundation
import shared_preferences_foundation
import sqflite
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
}

View File

@ -9,6 +9,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.11.0"
auto_size_text:
dependency: "direct main"
description:
name: auto_size_text
sha256: "3f5261cd3fb5f2a9ab4e2fc3fba84fd9fcaac8821f20a1d4e71f557521b22599"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
boolean_selector:
dependency: transitive
description:
@ -17,6 +25,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.1"
cached_network_image:
dependency: "direct main"
description:
name: cached_network_image
sha256: "4a5d8d2c728b0f3d0245f69f921d7be90cae4c2fd5288f773088672c0893f819"
url: "https://pub.dev"
source: hosted
version: "3.4.0"
cached_network_image_platform_interface:
dependency: transitive
description:
name: cached_network_image_platform_interface
sha256: ff0c949e323d2a1b52be73acce5b4a7b04063e61414c8ca542dbba47281630a7
url: "https://pub.dev"
source: hosted
version: "4.1.0"
cached_network_image_web:
dependency: transitive
description:
name: cached_network_image_web
sha256: "6322dde7a5ad92202e64df659241104a43db20ed594c41ca18de1014598d7996"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
characters:
dependency: transitive
description:
@ -41,6 +73,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.18.0"
crypto:
dependency: transitive
description:
name: crypto
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
url: "https://pub.dev"
source: hosted
version: "3.0.3"
csslib:
dependency: transitive
description:
@ -97,11 +137,27 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.0"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_cache_manager:
dependency: transitive
description:
name: flutter_cache_manager
sha256: a77f77806a790eb9ba0118a5a3a936e81c4fea2b61533033b2b0c3d50bbde5ea
url: "https://pub.dev"
source: hosted
version: "3.4.0"
flutter_lints:
dependency: "direct dev"
description:
@ -114,10 +170,10 @@ packages:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: "8cf40eebf5dec866a6d1956ad7b4f7016e6c0cc69847ab946833b7d43743809f"
sha256: "9d98bd47ef9d34e803d438f17fd32b116d31009f534a6fa5ce3a1167f189a6de"
url: "https://pub.dev"
source: hosted
version: "2.0.19"
version: "2.0.21"
flutter_test:
dependency: "direct dev"
description: flutter
@ -128,54 +184,86 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
geocoding:
dependency: "direct main"
description:
name: geocoding
sha256: d580c801cba9386b4fac5047c4c785a4e19554f46be42f4f5e5b7deacd088a66
url: "https://pub.dev"
source: hosted
version: "3.0.0"
geocoding_android:
dependency: transitive
description:
name: geocoding_android
sha256: "1b13eca79b11c497c434678fed109c2be020b158cec7512c848c102bc7232603"
url: "https://pub.dev"
source: hosted
version: "3.3.1"
geocoding_ios:
dependency: transitive
description:
name: geocoding_ios
sha256: "94ddba60387501bd1c11e18dca7c5a9e8c645d6e3da9c38b9762434941870c24"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
geocoding_platform_interface:
dependency: transitive
description:
name: geocoding_platform_interface
sha256: "8c2c8226e5c276594c2e18bfe88b19110ed770aeb7c1ab50ede570be8b92229b"
url: "https://pub.dev"
source: hosted
version: "3.2.0"
google_maps:
dependency: transitive
description:
name: google_maps
sha256: "47eef3836b49bb030d5cb3afc60b8451408bf34cf753e571b645d6529eb4251a"
sha256: "463b38e5a92a05cde41220a11fd5eef3847031fef3e8cf295ac76ec453246907"
url: "https://pub.dev"
source: hosted
version: "7.1.0"
version: "8.0.0"
google_maps_flutter:
dependency: "direct main"
description:
name: google_maps_flutter
sha256: c1972cbad779bc5346c49045f26ae45550a0958b1cbca5b524dd3c8954995d28
sha256: acf0ec482d86b2ac55ade80597ce7f797a47971f5210ebfd030f0d58130e0a94
url: "https://pub.dev"
source: hosted
version: "2.6.1"
version: "2.7.0"
google_maps_flutter_android:
dependency: transitive
description:
name: google_maps_flutter_android
sha256: "0bcadb80eba39afda77dede89a6caafd3b68f2786b90491eceea4a01c3db181c"
sha256: "5d444f4135559488d7ea325eae710ae3284e6951b1b61729a0ac026456fe1548"
url: "https://pub.dev"
source: hosted
version: "2.8.0"
version: "2.12.1"
google_maps_flutter_ios:
dependency: transitive
description:
name: google_maps_flutter_ios
sha256: e5132d17f051600d90d79d9f574b177c24231da702453a036db2490f9ced4646
sha256: a6e3c6ecdda6c985053f944be13a0645ebb919da2ef0f5bc579c5e1670a5b2a8
url: "https://pub.dev"
source: hosted
version: "2.6.0"
version: "2.10.0"
google_maps_flutter_platform_interface:
dependency: transitive
description:
name: google_maps_flutter_platform_interface
sha256: "167af879da4d004cd58771f1469b91dcc3b9b0a2c5334cc6bf71fd41d4b35403"
sha256: bd60ca330e3c7763b95b477054adec338a522d982af73ecc520b232474063ac5
url: "https://pub.dev"
source: hosted
version: "2.6.0"
version: "2.8.0"
google_maps_flutter_web:
dependency: transitive
description:
name: google_maps_flutter_web
sha256: "0c0d5c723d94b295cf86dd1c45ff91d2ac1fff7c05ddca4f01bef9fa0a014690"
sha256: "8d5d0f58bfc4afac0bbe3d399f2018fcea691e3ea3d35254b7aae56df5827659"
url: "https://pub.dev"
source: hosted
version: "0.5.7"
version: "0.5.9+1"
html:
dependency: transitive
description:
@ -188,10 +276,10 @@ packages:
dependency: "direct main"
description:
name: http
sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938"
sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
url: "https://pub.dev"
source: hosted
version: "1.2.1"
version: "1.2.2"
http_parser:
dependency: transitive
description:
@ -200,22 +288,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.2"
js:
dependency: transitive
description:
name: js
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.dev"
source: hosted
version: "0.6.7"
js_wrapping:
dependency: transitive
description:
name: js_wrapping
sha256: e385980f7c76a8c1c9a560dfb623b890975841542471eade630b2871d243851c
url: "https://pub.dev"
source: hosted
version: "0.7.4"
leak_tracker:
dependency: transitive
description:
@ -272,6 +344,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.12.0"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
octo_image:
dependency: transitive
description:
name: octo_image
sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
path:
dependency: transitive
description:
@ -280,6 +368,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.9.0"
path_provider:
dependency: transitive
description:
name: path_provider
sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378
url: "https://pub.dev"
source: hosted
version: "2.1.4"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: "490539678396d4c3c0b06efdaab75ae60675c3e0c66f72bc04c2e2c1e0e2abeb"
url: "https://pub.dev"
source: hosted
version: "2.2.9"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16
url: "https://pub.dev"
source: hosted
version: "2.4.0"
path_provider_linux:
dependency: transitive
description:
@ -300,18 +412,18 @@ packages:
dependency: transitive
description:
name: path_provider_windows
sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.2.1"
version: "2.3.0"
platform:
dependency: transitive
description:
name: platform
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
url: "https://pub.dev"
source: hosted
version: "3.1.4"
version: "3.1.5"
plugin_platform_interface:
dependency: transitive
description:
@ -320,6 +432,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.8"
provider:
dependency: "direct main"
description:
name: provider
sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c
url: "https://pub.dev"
source: hosted
version: "6.1.2"
rxdart:
dependency: transitive
description:
name: rxdart
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
url: "https://pub.dev"
source: hosted
version: "0.28.0"
sanitize_html:
dependency: transitive
description:
@ -332,58 +460,58 @@ packages:
dependency: "direct main"
description:
name: shared_preferences
sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180
sha256: c3f888ba2d659f3e75f4686112cc1e71f46177f74452d40d8307edc332296ead
url: "https://pub.dev"
source: hosted
version: "2.2.3"
version: "2.3.0"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2"
sha256: "041be4d9d2dc6079cf342bc8b761b03787e3b71192d658220a56cac9c04a0294"
url: "https://pub.dev"
source: hosted
version: "2.2.2"
version: "2.3.0"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7"
sha256: "671e7a931f55a08aa45be2a13fe7247f2a41237897df434b30d2012388191833"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
version: "2.5.0"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa"
sha256: "2ba0510d3017f91655b7543e9ee46d48619de2a2af38e5c790423f7007c7ccc1"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
version: "2.4.0"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b"
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
version: "2.4.1"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a"
sha256: "3a293170d4d9403c3254ee05b84e62e8a9b3c5808ebd17de6a33fe9ea6457936"
url: "https://pub.dev"
source: hosted
version: "2.3.0"
version: "2.4.0"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59"
sha256: "398084b47b7f92110683cac45c6dc4aae853db47e470e5ddcd52cab7f7196ab2"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
version: "2.4.0"
sky_engine:
dependency: transitive
description: flutter
@ -405,6 +533,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.10.0"
sprintf:
dependency: transitive
description:
name: sprintf
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
sqflite:
dependency: transitive
description:
name: sqflite
sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d
url: "https://pub.dev"
source: hosted
version: "2.3.3+1"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
stack_trace:
dependency: transitive
description:
@ -437,6 +589,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.0"
synchronized:
dependency: transitive
description:
name: synchronized
sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558"
url: "https://pub.dev"
source: hosted
version: "3.1.0+1"
term_glyph:
dependency: transitive
description:
@ -461,6 +621,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.2"
uuid:
dependency: transitive
description:
name: uuid
sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90"
url: "https://pub.dev"
source: hosted
version: "4.4.2"
vector_math:
dependency: transitive
description:
@ -485,14 +653,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.5.1"
win32:
dependency: transitive
widget_to_marker:
dependency: "direct main"
description:
name: win32
sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4
name: widget_to_marker
sha256: badc36f23c76f3ca9d43d7780058096be774adf0f661bdb6eb6f6b893f648ab9
url: "https://pub.dev"
source: hosted
version: "5.5.1"
version: "1.0.6"
xdg_directories:
dependency: transitive
description:
@ -503,4 +671,4 @@ packages:
version: "1.0.4"
sdks:
dart: ">=3.4.0 <4.0.0"
flutter: ">=3.19.0"
flutter: ">=3.22.0"

View File

@ -36,10 +36,15 @@ dependencies:
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.6
sliding_up_panel: ^2.0.0+1
google_maps_flutter: ^2.6.1
http: ^1.2.1
shared_preferences: ^2.2.3
dio: ^5.5.0+1
google_maps_flutter: ^2.7.0
cached_network_image: ^3.4.0
geocoding: ^3.0.0
widget_to_marker: ^1.0.6
provider: ^6.1.2
auto_size_text: ^3.0.0
dev_dependencies:
flutter_test: