1 Commits

Author SHA1 Message Date
b50126cd3a even more fixes
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 1m44s
Build and deploy the backend to staging / Deploy to staging (pull_request) Failing after 12s
2024-09-21 14:16:02 +02:00
89 changed files with 2067 additions and 2446 deletions

View File

@@ -20,5 +20,5 @@ jobs:
with:
overlay: prod
secrets:
KUBE_CONFIG: ${{ secrets.KUBE_CONFIG }}
PACKAGE_REGISTRY_ACCESS: ${{ secrets.KUBE_CONFIG }}
needs: build-and-push

View File

@@ -22,5 +22,5 @@ jobs:
with:
overlay: stg
secrets:
KUBE_CONFIG: ${{ secrets.KUBE_CONFIG }}
PACKAGE_REGISTRY_ACCESS: ${{ secrets.KUBE_CONFIG }}
needs: build-and-push

View File

@@ -6,7 +6,7 @@ on:
- frontend/**
name: Build and release debug APK
name: Build and release APK
jobs:
build:
@@ -55,7 +55,7 @@ jobs:
ls -lah android
working-directory: ./frontend
- run: flutter build apk --debug --split-per-abi --build-number=${{ gitea.run_number }}
- run: flutter build apk --release --split-per-abi --build-number=${{ gitea.run_number }}
working-directory: ./frontend
- name: Upload APKs to artifacts

View File

@@ -31,5 +31,5 @@ jobs:
- name: Deploy to k8s
run: |
kubectl apply -k backend/deployment/overlays/${{ inputs.overlay }} --kubeconfig=kubeconfig
kubectl -n anyway-backend rollout restart deployment/anyway-backend-${{ inputs.overlay }} --kubeconfig=kubeconfig
kubectl apply -k backend/deployment/overlays/${{ inputs.overlay }} --kubeconfig=kubeconfig --validate=false
# skip validation because downloading the schema from the internet is broken anyway

View File

@@ -9,9 +9,9 @@ name = "pypi"
numpy = "*"
fastapi = "*"
pydantic = "*"
geopy = "*"
shapely = "*"
scipy = "*"
osmpythontools = "*"
pywikibot = "*"
pymemcache = "*"
fastapi-cli = "*"

2208
backend/Pipfile.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,11 +9,9 @@ This repository contains the backend code for the application. It utilizes FastA
- Since the application is aimed to be deployed in a container, the `Dockerfile` is provided to build the image.
### Deployment
To deploy the backend docker container, we use kubernetes. Modifications to the backend are automatically pushed to a two-stage environment through the CI pipeline. See [deployment/README](deployment/README.md] for further information.
The deployment configuration is included as a submodule in the `deployment` directory. The standalone repository is under [https://git.kluster.moll.re/anydev/anyway-backend-deployment/](https://git.kluster.moll.re/anydev/anyway-backend-deployment/).
To deploy the backend docker container, we use kubernetes. The deployment configuration is located under [https://git.kluster.moll.re/anydev/deployment-backend/](https://git.kluster.moll.re/anydev/deployment-backend/).
## Development
TBD
Test for pull request

View File

@@ -16,7 +16,7 @@ OSM_CACHE_DIR = Path(cache_dir_string)
import logging
# if we are in a debug session, set verbose and rich logging
if os.getenv('DEBUG', "false") == "true":
if os.getenv('DEBUG', False):
from rich.logging import RichHandler
logging.basicConfig(
level=logging.DEBUG,

View File

@@ -63,7 +63,7 @@ def new_trip(preferences: Preferences, start: tuple[float, float], end: tuple[fl
refined_tour = refiner.refine_optimization(landmarks, base_tour, preferences.max_time_minute, preferences.detour_tolerance_minute)
linked_tour = LinkedLandmarks(refined_tour)
# upon creation of the trip, persistence of both the trip and its landmarks is ensured
# 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

View File

@@ -1,6 +1,3 @@
# Tags were picked mostly arbitrarily, based on the OSM wiki and the OSM tags page.
# See https://taginfo.openstreetmap.org for more inspiration.
nature:
leisure: park
geological: ''
@@ -14,24 +11,7 @@ nature:
- alpine_hut
- viewpoint
- zoo
- resort
- picnic_site
water:
- pond
- lake
- river
- basin
- stream
- lagoon
- rapids
waterway:
- waterfall
- river
- canal
- dam
- dock
- boatyard
waterway: waterfall
shopping:
shop:
@@ -43,48 +23,10 @@ sightseeing:
- museum
- attraction
- gallery
- artwork
- aquarium
historic: ''
amenity:
- planetarium
- place_of_worship
- fountain
- townhall
water:
- reflecting_pool
bridge:
- aqueduct
- viaduct
- boardwalk
- cantilever
- abandoned
building:
- church
- chapel
- mosque
- synagogue
- ruins
- temple
- government
- cathedral
- castle
- museum
# to be used later on
restauration:
shop:
- coffee
- bakery
- restaurant
- pastry
amenity:
- restaurant
- cafe
- ice_cream
- food_court
- biergarten

View File

@@ -1,6 +1,6 @@
city_bbox_side: 7500 #m
radius_close_to: 50
church_coeff: 0.5
church_coeff: 0.75
nature_coeff: 1.25
overall_coeff: 10
tag_exponent: 1.15

View File

@@ -2,5 +2,5 @@ detour_factor: 1.4
detour_corridor_width: 300
average_walking_speed: 4.8
max_landmarks: 10
max_landmarks_refiner: 30
overshoot: 1.8
max_landmarks_refiner: 20
overshoot: 1.3

View File

@@ -21,8 +21,8 @@ if constants.MEMCACHED_HOST_PATH is None:
else:
client = Client(
constants.MEMCACHED_HOST_PATH,
timeout = 1,
allow_unicode_keys = True,
encoding = 'utf-8',
serde = serde.pickle_serde
timeout=1,
allow_unicode_keys=True,
encoding='utf-8',
serde=serde.pickle_serde
)

View File

@@ -14,39 +14,26 @@ class Landmark(BaseModel) :
osm_id : int
attractiveness : int
n_tags : int
image_url : Optional[str] = None
website_url : Optional[str] = None
image_url : Optional[str] = None # TODO future
description : Optional[str] = None # TODO future
duration : Optional[int] = 0
name_en : Optional[str] = None
duration : Optional[int] = 0 # TODO future
# Unique ID of a given landmark
uuid: str = Field(default_factory=uuid4)
uuid: str = Field(default_factory=uuid4) # TODO implement this ASAP
# Additional properties depending on specific tour
must_do : Optional[bool] = False
must_avoid : Optional[bool] = False
is_secondary : Optional[bool] = False # TODO future
time_to_reach_next : Optional[int] = 0
next_uuid : Optional[str] = None
def __str__(self) -> str:
time_to_next_str = f", time_to_next={self.time_to_reach_next}" if self.time_to_reach_next else ""
is_secondary_str = f", secondary" if self.is_secondary else ""
type_str = '(' + self.type + ')'
if self.type in ["start", "finish", "nature", "shopping"] : type_str += '\t '
return f'Landmark{type_str}: [{self.name} @{self.location}, score={self.attractiveness}{time_to_next_str}{is_secondary_str}]'
def distance(self, value: 'Landmark') -> float:
return (self.location[0] - value.location[0])**2 + (self.location[1] - value.location[1])**2
time_to_reach_next : Optional[int] = 0 # TODO fix this in existing code
next_uuid : Optional[str] = None # TODO implement this ASAP
def __hash__(self) -> int:
return hash(self.name)
return self.uuid.int
def __str__(self) -> str:
time_to_next_str = f"time_to_next={self.time_to_reach_next}" if self.time_to_reach_next else ""
# return f'Landmark({self.type}): [{self.name} @{self.location}, score={self.attractiveness}{time_to_next_str}]'
return f'({self.type[:4]}), score={self.attractiveness}\tmain:{not self.is_secondary}\tduration={self.duration}\t{time_to_next_str}\t{self.name}'
def __eq__(self, value: 'Landmark') -> bool:
# eq and hash must be consistent
# in particular, if two objects are equal, their hash must be equal
# uuid and osm_id are just shortcuts to avoid comparing all the properties
# if they are equal, we know that the name is also equal and in turn the hash is equal
return self.uuid == value.uuid or self.osm_id == value.osm_id or (self.name == value.name and self.distance(value) < 0.001)

View File

@@ -52,7 +52,7 @@ class LinkedLandmarks:
# Update 'is_secondary' for landmarks with attractiveness below the threshold score
for landmark in self._landmarks:
if landmark.attractiveness < threshold_score and landmark.type not in ["start", "finish"]:
if landmark.attractiveness < threshold_score:
landmark.is_secondary = True

View File

@@ -22,8 +22,7 @@ class Trip(BaseModel):
# Store the trip in the cache
cache_client.set(f"trip_{trip.uuid}", trip)
# make sure to await the result (noreply=False). Otherwise the cache might not be inplace when the trip is actually requested
cache_client.set_many({f"landmark_{landmark.uuid}": landmark for landmark in landmarks}, expire=3600, noreply=False)
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)

View File

@@ -23,8 +23,9 @@ def test(start_coords: tuple[float, float], finish_coords: tuple[float, float] =
sightseeing=Preference(type='sightseeing', score = 5),
nature=Preference(type='nature', score = 5),
shopping=Preference(type='shopping', score = 5),
max_time_minute=100,
detour_tolerance_minute=0
max_time_minute=300,
detour_tolerance_minute=15
)
# Create start and finish
@@ -63,7 +64,10 @@ def test(start_coords: tuple[float, float], finish_coords: tuple[float, float] =
logger.info("Optimized route : ")
for l in linked_tour :
logger.info(f"{l}")
logger.info(f"Estimated length of tour : {linked_tour.total_time} mintutes and visiting {len(linked_tour._landmarks)} landmarks.")
total_time += l.duration
total_time += l.time_to_reach_next
logger.info(f"Total time: {total_time}")
# with open('linked_tour.yaml', 'w') as f:
# yaml.dump(linked_tour.asdict(), f)
@@ -74,6 +78,6 @@ def test(start_coords: tuple[float, float], finish_coords: tuple[float, float] =
# test(tuple((48.8344400, 2.3220540))) # Café Chez César
# test(tuple((48.8375946, 2.2949904))) # Point random
# test(tuple((47.377859, 8.540585))) # Zurich HB
# test(tuple((45.758217, 4.831814))) # Lyon Bellecour
test(tuple((48.5848435, 7.7332974))) # Strasbourg Gare
test(tuple((45.7576485, 4.8330241))) # Lyon Bellecour
# test(tuple((48.5848435, 7.7332974))) # Strasbourg Gare
# test(tuple((48.2067858, 16.3692340))) # Vienne

View File

@@ -1,5 +1,5 @@
import yaml
from math import sin, cos, sqrt, atan2, radians
from geopy.distance import geodesic
import constants
@@ -8,7 +8,6 @@ with constants.OPTIMIZER_PARAMETERS_PATH.open('r') as f:
DETOUR_FACTOR = parameters['detour_factor']
AVERAGE_WALKING_SPEED = parameters['average_walking_speed']
EARTH_RADIUS_KM = 6373
def get_time(p1: tuple[float, float], p2: tuple[float, float]) -> int:
"""
@@ -17,34 +16,24 @@ def get_time(p1: tuple[float, float], p2: tuple[float, float]) -> int:
Args:
p1 (Tuple[float, float]): Coordinates of the starting location.
p2 (Tuple[float, float]): Coordinates of the destination.
detour (float): Detour factor affecting the distance.
speed (float): Walking speed in kilometers per hour.
Returns:
Returns:
int: Time to travel from p1 to p2 in minutes.
"""
if p1 == p2:
# Compute the straight-line distance in km
if p1 == p2 :
return 0
else:
# Compute the distance in km along the surface of the Earth
# (assume spherical Earth)
# this is the haversine formula, stolen from stackoverflow
# in order to not use any external libraries
lat1, lon1 = radians(p1[0]), radians(p1[1])
lat2, lon2 = radians(p2[0]), radians(p2[1])
dist = geodesic(p1, p2).kilometers
dlon = lon2 - lon1
dlat = lat2 - lat1
a = sin(dlat / 2)**2 + cos(lat1) * cos(lat2) * sin(dlon / 2)**2
c = 2 * atan2(sqrt(a), sqrt(1 - a))
distance = EARTH_RADIUS_KM * c
# Consider the detour factor for average an average city
walk_distance = distance * DETOUR_FACTOR
# Consider the detour factor for average cityto deterline walking distance (in km)
walk_dist = dist*DETOUR_FACTOR
# Time to walk this distance (in minutes)
walk_time = walk_distance / AVERAGE_WALKING_SPEED * 60
walk_time = walk_dist/AVERAGE_WALKING_SPEED*60
return round(walk_time)

View File

@@ -1,17 +1,20 @@
import math
import math as m
import yaml
import logging
from OSMPythonTools.overpass import Overpass, overpassQueryBuilder
from OSMPythonTools.cachingStrategy import CachingStrategy, JSON
from pywikibot import ItemPage, Site
from pywikibot import config
config.put_throttle = 0
config.maxlag = 0
from structs.preferences import Preferences
from structs.preferences import Preferences, Preference
from structs.landmark import Landmark
from .take_most_important import take_most_important
import constants
# silence the overpass logger
logging.getLogger('OSMPythonTools').setLevel(level=logging.CRITICAL)
class LandmarkManager:
@@ -66,42 +69,87 @@ class LandmarkManager:
preferences (Preferences): The user's preference settings that influence the landmark selection.
Returns:
tuple[list[Landmark], list[Landmark]]:
- A list of all existing landmarks.
- A list of the most important landmarks based on the user's preferences.
tuple[list[Landmark], list[Landmark]]:
- A list of all existing landmarks.
- 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)
# use set to avoid duplicates, this requires some __methods__ to be set in Landmark
all_landmarks = set()
L = []
bbox = self.create_bbox(center_coordinates, reachable_bbox_side)
# list for sightseeing
if preferences.sightseeing.score != 0:
score_function = lambda score: score * 10 * preferences.sightseeing.score / 5
current_landmarks = self.fetch_landmarks(bbox, self.amenity_selectors['sightseeing'], preferences.sightseeing.type, score_function)
all_landmarks.update(current_landmarks)
score_function = lambda score: int(score*10)*preferences.sightseeing.score/5 # 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 score: score * 10 * self.nature_coeff * preferences.nature.score / 5
current_landmarks = self.fetch_landmarks(bbox, self.amenity_selectors['nature'], preferences.nature.type, score_function)
all_landmarks.update(current_landmarks)
score_function = lambda score: int(score*10*self.nature_coeff)*preferences.nature.score/5 # 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 score: score * 10 * preferences.shopping.score / 5
current_landmarks = self.fetch_landmarks(bbox, self.amenity_selectors['shopping'], preferences.shopping.type, score_function)
all_landmarks.update(current_landmarks)
score_function = lambda score: int(score*10)*preferences.shopping.score/5 # self.count_elements_close_to(loc) +
L3 = self.fetch_landmarks(bbox, self.amenity_selectors['shopping'], preferences.shopping.type, score_function)
L += L3
landmarks_constrained = take_most_important(all_landmarks, self.N_important)
self.logger.info(f'Generated {len(all_landmarks)} landmarks around {center_coordinates}, and constrained to {len(landmarks_constrained)} most important ones.')
L = self.remove_duplicates(L)
# self.correct_score(L, preferences)
return all_landmarks, landmarks_constrained
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.')
return L, L_constrained
def remove_duplicates(self, landmarks: list[Landmark]) -> list[Landmark]:
"""
Removes duplicate landmarks based on their names from the given list. Only retains the landmark with highest score
Parameters:
landmarks (list[Landmark]): A list of Landmark objects.
Returns:
list[Landmark]: A list of unique Landmark objects based on their names.
"""
L_clean = []
names = []
for landmark in landmarks:
if landmark.name in names:
continue
else:
names.append(landmark.name)
L_clean.append(landmark)
return L_clean
def correct_score(self, landmarks: list[Landmark], preferences: Preferences) -> None:
"""
Adjust the attractiveness score of each landmark in the list based on user preferences.
This method updates the attractiveness of each landmark by scaling it according to the user's preference score.
The score adjustment is computed using a simple linear transformation based on the preference score.
Args:
landmarks (list[Landmark]): A list of landmarks whose scores need to be corrected.
preferences (Preferences): The user's preference settings that influence the attractiveness score adjustment.
"""
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:
@@ -124,7 +172,7 @@ class LandmarkManager:
radius = self.radius_close_to
alpha = (180 * radius) / (6371000 * math.pi)
alpha = (180*radius) / (6371000*m.pi)
bbox = {'latLower':lat-alpha,'lonLower':lon-alpha,'latHigher':lat+alpha,'lonHigher': lon+alpha}
# Build the query to find elements within the radius
@@ -168,7 +216,7 @@ class LandmarkManager:
# Convert distance to degrees
lat_diff = half_side_length_km / 111 # 1 degree latitude is approximately 111 km
lon_diff = half_side_length_km / (111 * math.cos(math.radians(lat))) # Adjust for longitude based on latitude
lon_diff = half_side_length_km / (111 * m.cos(m.radians(lat))) # Adjust for longitude based on latitude
# Calculate bbox
min_lat = lat - lat_diff
@@ -207,32 +255,32 @@ class LandmarkManager:
query = overpassQueryBuilder(
bbox = bbox,
elementType = ['way', 'relation'],
# selector can in principle be a list already,
# but it generates the intersection of the queries
# we want the union
selector = sel,
conditions = ['count_tags()>5'],
# conditions = [],
includeCenter = True,
out = 'body'
)
self.logger.debug(f"Query: {query}")
try:
result = self.overpass.query(query)
except Exception as e:
self.logger.error(f"Error fetching landmarks: {e}")
continue
return
for elem in result.elements():
name = elem.tag('name')
location = (elem.centerLat(), elem.centerLon())
name = elem.tag('name') # Add name
location = (elem.centerLat(), elem.centerLon()) # Add coordinates (lat, lon)
# TODO: exclude these from the get go
# skip if unprecise location
if name is None or location[0] is None:
continue
# skip if unused
# if 'disused:leisure' in elem.tags().keys():
# continue
# skip if part of another building
if 'building:part' in elem.tags().keys() and elem.tag('building:part') == 'yes':
continue
@@ -242,29 +290,31 @@ class LandmarkManager:
elem_type = landmarktype # Add the landmark type as 'sightseeing,
n_tags = len(elem.tags().keys()) # Add number of tags
score = n_tags**self.tag_exponent # Add score
website_url = None
image_url = None
name_en = None
# remove specific tags
skip = False
for tag in elem.tags().keys():
if "pay" in tag:
# payment options are a good sign
score += self.pay_bonus
score += self.pay_bonus # discard payment options for tags
if "disused" in tag:
# skip disused amenities
skip = True
skip = True # skip disused amenities
break
if "wiki" in tag:
# wikipedia entries count more
score += self.wikipedia_bonus
score += self.wikipedia_bonus # 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 "viewpoint" in tag:
score += self.viewpoint_bonus
duration = 10
if "image" in tag:
score += self.image_bonus
@@ -281,43 +331,38 @@ class LandmarkManager:
if tag == "building" and elem.tag('building') in ['retail', 'supermarket', 'parking']:
skip = True
break
if tag in ['website', 'contact:website']:
website_url = elem.tag(tag)
if tag == 'image':
image_url = elem.tag('image')
if tag =='name:en':
name_en = elem.tag('name:en')
if skip:
continue
score = score_function(score)
if "place_of_worship" in elem.tags().values():
score = score * self.church_coeff
duration = 15
if "place_of_worship" in elem.tags().values() :
score = int(score*self.church_coeff)
duration = 20
elif "museum" in elem.tags().values():
score = score * self.church_coeff
elif "museum" in elem.tags().values() :
score = int(score*self.church_coeff)
duration = 60
else:
elif "fountain" in elem.tags().values() :
duration = 5
# finally create our own landmark object
elif "park" in elem.tags().values() :
duration = 30
else :
duration = 15
# Generate the landmark and append it to the list
landmark = Landmark(
name = name,
type = elem_type,
location = location,
osm_type = osm_type,
osm_id = osm_id,
attractiveness = int(score),
must_do = False,
n_tags = int(n_tags),
duration = int(duration),
name_en = name_en,
image_url = image_url,
website_url = website_url
name=name,
type=elem_type,
location=location,
osm_type=osm_type,
osm_id=osm_id,
attractiveness=score,
must_do=False,
n_tags=int(n_tags),
duration = duration
)
return_list.append(landmark)
@@ -341,7 +386,7 @@ def dict_to_selector_list(d: dict) -> list:
for key, value in d.items():
if type(value) == list:
val = '|'.join(value)
return_list.append(f'{key}~"^({val})$"')
return_list.append(f'{key}~"{val}"')
elif type(value) == str and len(value) == 0:
return_list.append(f'{key}')
else:

View File

@@ -3,6 +3,7 @@ import numpy as np
from scipy.optimize import linprog
from collections import defaultdict, deque
from geopy.distance import geodesic
from structs.landmark import Landmark
from .get_time_separation import get_time
@@ -20,7 +21,7 @@ class Optimizer:
detour_factor: float # detour factor of straight line vs real distance in cities
average_walking_speed: float # average walking speed of adult
max_landmarks: int # max number of landmarks to visit
overshoot: float # overshoot to allow maxtime to overflow. Optimizer is a bit restrictive
overshoot: float # experimentally determined overshoot possibility to return long enough tours
def __init__(self) :
@@ -177,7 +178,7 @@ class Optimizer:
Args:
landmarks (list[Landmark]): List of landmarks.
max_time (int): Maximum time of visit allowed.
max_time (int): Maximum time allowed for tour.
Returns:
Tuple[list[float], list[float], list[int]]: Objective function coefficients, inequality constraint coefficients, and the right-hand side of the inequality constraint.
@@ -194,7 +195,7 @@ class Optimizer:
for j, spot2 in enumerate(landmarks) :
t = get_time(spot1.location, spot2.location) + spot1.duration
dist_table[j] = t
closest = sorted(dist_table)[:25]
closest = sorted(dist_table)[:22]
for i, dist in enumerate(dist_table) :
if dist not in closest :
dist_table[i] = 32700
@@ -475,7 +476,7 @@ class Optimizer:
A, b = self.respect_start_finish(L) # Force start and finish positions
A_eq = np.vstack((A_eq, A), dtype=np.int8)
b_eq += b
A, b = self.respect_order(L) # Respect order of visit (only works when max_time is limiting factor)
A, b = self.respect_order(L) # Respect order of visit (only works when max_steps is limiting factor)
A_eq = np.vstack((A_eq, A), dtype=np.int8)
b_eq += b

View File

@@ -214,7 +214,7 @@ class Refiner :
if self.is_in_area(area, landmark.location) and landmark.name not in visited_names:
second_order_landmarks.append(landmark)
return take_most_important.take_most_important(second_order_landmarks, int(self.max_landmarks_refiner*0.75))
return take_most_important.take_most_important(second_order_landmarks, len(visited_landmarks))
# Try fix the shortest path using shapely

View File

@@ -1,16 +1,38 @@
from structs.landmark import Landmark
def take_most_important(landmarks: list[Landmark], n_important) -> list[Landmark]:
"""
Given a list of landmarks, return the n_important most important landmarks
Parameters:
landmarks: list[Landmark] - list of landmarks
n_important: int - number of most important landmarks to return
Returns:
list[Landmark] - list of the n_important most important landmarks
"""
def take_most_important(landmarks: list[Landmark], N_important) -> list[Landmark] :
L = len(landmarks)
L_copy = []
L_clean = []
scores = [0]*len(landmarks)
names = []
name_id = {}
# Sort landmarks by attractiveness (descending)
sorted_landmarks = sorted(landmarks, key=lambda x: x.attractiveness, reverse=True)
for i, elem in enumerate(landmarks) :
if elem.name not in names :
names.append(elem.name)
name_id[elem.name] = [i]
L_copy.append(elem)
else :
name_id[elem.name] += [i]
scores = []
for j in name_id[elem.name] :
scores.append(L[j].attractiveness)
best_id = max(range(len(scores)), key=scores.__getitem__)
t = name_id[elem.name][best_id]
if t == i :
for old in L_copy :
if old.name == elem.name :
old.attractiveness = L[t].attractiveness
return sorted_landmarks[:n_important]
scores = [0]*len(L_copy)
for i, elem in enumerate(L_copy) :
scores[i] = elem.attractiveness
res = sorted(range(len(scores)), key = lambda sub: scores[sub])[-(N_important-L):]
for i, elem in enumerate(L_copy) :
if i in res :
L_clean.append(elem)
return L_clean

View File

@@ -1,7 +1,7 @@
on:
push:
tags:
- 'v*'
branches:
- main
jobs:
build:
@@ -24,26 +24,19 @@ jobs:
- name: Setup android SDK
uses: android-actions/setup-android@v3
- name: Install Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
flutter-version: 3.22.0
cache: true
- name: Infer version number from git tag
id: version
env:
REF_NAME: ${{ github.ref_name }}
run:
# remove the 'v' prefix from the tag name
echo "VERSION_NAME=${REF_NAME//v}" >> $GITHUB_ENV
echo "VERSION=${REF_NAME//v}" >> $GITHUB_OUTPUT
- name: Load secrets from github
run: |
echo "${{ secrets.ANDROID_SECRET_PROPERTIES_BASE64 }}" | base64 -d > secrets.properties
echo "${{ secrets.ANDROID_GOOGLE_PLAY_JSON_BASE64 }}" | base64 -d > google-key.json
echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > release.keystore
echo "${{ secrets.ANDROID_SECRET_PROPERTIES }}" > secrets.properties
echo "${{ secrets.ANDROID_KEYSTORE }}" > release.keystore
echo "${{ secrets.ANDROID_GOOGLE_PLAY_JSON }}" > google-key.json
working-directory: android
- name: Install fastlane
@@ -51,6 +44,7 @@ jobs:
working-directory: android
- name: Run fastlane lane
run: bundle exec fastlane deploy_testing
run: bundle exec fastlane deploy_release
working-directory: android
# the environment variable VERSION_NAME is implicitly available
env:
VERSION_NAME: ${{ steps.version.VERSION }}

View File

@@ -1,6 +1,4 @@
gradlew
gradlew.bat
gradle/
gradle-wrapper.jar
/.gradle
/captures/
/local.properties

View File

@@ -30,19 +30,14 @@ if (flutterVersionName == null) {
def secretPropertiesFile = rootProject.file('secrets.properties')
def fallbackPropertiesFile = rootProject.file('fallback.properties')
def secretProperties = new Properties()
if (secretPropertiesFile.exists()) {
secretPropertiesFile.withReader('UTF-8') { reader ->
secretProperties.load(reader)
}
} else if (fallbackPropertiesFile.exists()) {
fallbackPropertiesFile.withReader('UTF-8') { reader ->
secretProperties.load(reader)
}
} else {
throw new GradleException("Secrets file (secrets.properties, fallback.properties) not found")
throw new GradleException("Secrets file secrets.properties not found")
}

View File

@@ -1,9 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Required to fetch data from the internet. -->
<uses-permission android:name="android.permission.INTERNET"/>
<!-- Required to show user location -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application
android:label="anyway"

View File

@@ -1,3 +1 @@
# This file mirrors the state of secrets.properties as a reference for the developer.
# And as a fallback for build.gradle
MAPS_API_KEY=Key

View File

@@ -4,27 +4,22 @@
default_platform(:android)
platform :android do
# desc "Runs all the tests"
# lane :test do
# gradle(task: "test")
# end
desc "Deploy a new version as a preview version"
lane :deploy_testing do
version_name = ENV["VERSION_NAME"]
sh(
"flutter",
"build",
"appbundle",
"--release",
"--build-name=#{version_name}",
gradle(
task: "bundle",
# flavor: "staging",
)
upload_to_play_store(
track: 'alpha',
skip_upload_apk: true,
skip_upload_changelogs: true,
aab: "../../build/app/outputs/bundle/release/app-release.aab",
# this is the default output of flutter build ... --release
# in particular this the build folder lies in the flutter root folder
# hence the relative path
)
end
@@ -41,10 +36,6 @@ platform :android do
track: "production",
skip_upload_apk: true,
skip_upload_changelogs: true,
aab: "../../build/app/outputs/bundle/release/app-release.aab",
# this is the default output of flutter build ... --release
# in particular this the build folder lies in the flutter root folder
# hence the relative path
)
end
end

Binary file not shown.

After

Width:  |  Height:  |  Size: 638 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 893 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 876 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 638 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 893 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 876 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 638 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 893 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 876 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip

160
frontend/android/gradlew vendored Executable file
View File

@@ -0,0 +1,160 @@
#!/usr/bin/env bash
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"

View File

@@ -20,7 +20,7 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.3.0" apply false
id "org.jetbrains.kotlin.android" version "2.0.20" apply false
id "org.jetbrains.kotlin.android" version "1.7.10" apply false
}
include ":app"

View File

@@ -1,3 +0,0 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

View File

@@ -1,86 +1,6 @@
import 'package:flutter/material.dart';
const String APP_NAME = 'AnyWay';
String API_URL_BASE = 'https://anyway.anydev.info';
String API_URL_DEBUG = 'https://anyway.anydev.info';
String PRIVACY_URL = 'https://anydev.info/privacy';
const String MAP_ID = '41c21ac9b81dbfd8';
const Color GRADIENT_START = Color(0xFFF9B208);
const Color GRADIENT_END = Color(0xFFE72E77);
const Color PRIMARY_COLOR = Color(0xFFF38F1A);
const double TRIP_PANEL_MAX_HEIGHT = 0.8;
const double TRIP_PANEL_MIN_HEIGHT = 0.12;
ThemeData APP_THEME = ThemeData(
primaryColor: PRIMARY_COLOR,
scaffoldBackgroundColor: Colors.white,
cardColor: Colors.white,
useMaterial3: true,
colorScheme: ColorScheme.light(
primary: PRIMARY_COLOR,
secondary: GRADIENT_END,
surface: Colors.white,
error: Colors.red,
onPrimary: Colors.white,
onSecondary: const Color.fromARGB(255, 30, 22, 22),
onSurface: Colors.black,
onError: Colors.white,
brightness: Brightness.light,
),
textButtonTheme: const TextButtonThemeData(
style: ButtonStyle(
foregroundColor: WidgetStatePropertyAll(PRIMARY_COLOR),
side: WidgetStatePropertyAll(
BorderSide(
color: PRIMARY_COLOR,
width: 1,
),
),
)
),
elevatedButtonTheme: const ElevatedButtonThemeData(
style: ButtonStyle(
foregroundColor: WidgetStatePropertyAll(PRIMARY_COLOR),
)
),
outlinedButtonTheme: const OutlinedButtonThemeData(
style: ButtonStyle(
foregroundColor: WidgetStatePropertyAll(PRIMARY_COLOR),
)
),
cardTheme: const CardTheme(
shadowColor: Colors.grey,
elevation: 2,
margin: EdgeInsets.all(10),
),
sliderTheme: const SliderThemeData(
trackHeight: 15,
inactiveTrackColor: Colors.grey,
thumbColor: PRIMARY_COLOR,
activeTrackColor: GRADIENT_END
)
);
const Gradient APP_GRADIENT = LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [GRADIENT_START, GRADIENT_END],
);

View File

@@ -1,4 +1,3 @@
import 'package:anyway/pages/settings.dart';
import 'package:flutter/material.dart';
import 'package:anyway/constants.dart';
@@ -7,9 +6,10 @@ import 'package:anyway/structs/trip.dart';
import 'package:anyway/modules/trips_saved_list.dart';
import 'package:anyway/utils/load_trips.dart';
import 'package:anyway/pages/new_trip_location.dart';
import 'package:anyway/pages/new_trip.dart';
import 'package:anyway/pages/current_trip.dart';
import 'package:anyway/pages/onboarding.dart';
import 'package:anyway/pages/profile.dart';
@@ -62,7 +62,7 @@ class _BasePageState extends State<BasePage> {
)
);
},
label: Text("Plan a trip"),
label: Text("Plan a trip now"),
),
);
}
@@ -74,33 +74,33 @@ class _BasePageState extends State<BasePage> {
}
} else if (widget.mainScreen == "tutorial") {
currentView = OnboardingPage();
} else if (widget.mainScreen == "settings") {
currentView = SettingsPage();
} else if (widget.mainScreen == "profile") {
currentView = ProfilePage();
}
final ThemeData theme = Theme.of(context);
return Scaffold(
appBar: AppBar(title: Text(APP_NAME)),
body: Center(child: currentView),
drawer: Drawer(
child: Column(
children: [
Container(
DrawerHeader(
decoration: BoxDecoration(
gradient: APP_GRADIENT,
gradient: LinearGradient(colors: [Colors.red, Colors.yellow])
),
height: 150,
child: Center(
child: Text(
APP_NAME,
style: TextStyle(
color: Colors.white,
color: Colors.grey[800],
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
),
),
ListTile(
title: const Text('Your Trips'),
leading: const Icon(Icons.map),
@@ -130,7 +130,7 @@ class _BasePageState extends State<BasePage> {
},
child: const Text('Clear trips'),
),
const Divider(indent: 10, endIndent: 10),
const Divider(),
ListTile(
title: const Text('How to use'),
leading: Icon(Icons.help),
@@ -148,11 +148,11 @@ class _BasePageState extends State<BasePage> {
ListTile(
title: const Text('Settings'),
leading: const Icon(Icons.settings),
selected: widget.mainScreen == "settings",
selected: widget.mainScreen == "profile",
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => BasePage(mainScreen: "settings")
builder: (context) => BasePage(mainScreen: "profile")
)
);
},

View File

@@ -15,7 +15,7 @@ class App extends StatelessWidget {
return MaterialApp(
title: APP_NAME,
home: BasePage(mainScreen: "map"),
theme: APP_THEME,
theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.red[600]),
scaffoldMessengerKey: rootScaffoldMessengerKey
);
}

View File

@@ -1,37 +0,0 @@
import 'package:anyway/structs/trip.dart';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
class CurrentTripErrorMessage extends StatefulWidget {
final Trip trip;
const CurrentTripErrorMessage({
super.key,
required this.trip,
});
@override
State<CurrentTripErrorMessage> createState() => _CurrentTripErrorMessageState();
}
class _CurrentTripErrorMessageState extends State<CurrentTripErrorMessage> {
@override
Widget build(BuildContext context) => Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
color: Colors.red,
size: 50,
),
const Padding(
padding: EdgeInsets.only(left: 10),
),
AutoSizeText(
'Error: ${widget.trip.errorDescription}',
maxLines: 3,
),
],
)
);
}

View File

@@ -1,50 +1,122 @@
import 'package:flutter/material.dart';
import 'dart:developer';
import 'package:anyway/structs/trip.dart';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:anyway/pages/current_trip.dart';
import 'package:anyway/structs/trip.dart';
import 'package:flutter/material.dart';
class CurrentTripGreeter extends StatefulWidget {
class Greeter extends StatefulWidget {
final Trip trip;
CurrentTripGreeter({
super.key,
Greeter({
required this.trip,
});
@override
State<CurrentTripGreeter> createState() => _CurrentTripGreeterState();
State<Greeter> createState() => _GreeterState();
}
class _CurrentTripGreeterState extends State<CurrentTripGreeter> {
@override
Widget build(BuildContext context) => Center(
child: 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) {
return AutoSizeText(
maxLines: 1,
'Welcome to your trip!',
style: greeterStyle
);
} else {
return AutoSizeText(
maxLines: 1,
'Welcome to ...',
style: greeterStyle
);
class _GreeterState extends State<Greeter> {
Widget greeterBuilder (BuildContext context, Widget? child) {
ThemeData theme = Theme.of(context);
TextStyle greeterStyle = TextStyle(color: theme.primaryColor, fontWeight: FontWeight.bold, fontSize: 24);
Widget topGreeter;
if (widget.trip.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 {
// 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(
"Busy day ahead? Here is how to make the most of it!",
style: TextStyle(color: Colors.black, fontSize: 18),
textAlign: TextAlign.center,
);
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: widget.trip,
builder: greeterBuilder,
);
}
}

View File

@@ -16,7 +16,7 @@ List<Widget> landmarksList(Trip trip) {
log("Trip ${trip.uuid} ${trip.landmarks.length} landmarks");
if (trip.landmarks.isEmpty || trip.landmarks.length <= 1 && trip.landmarks.first.type == typeStart ) {
if (trip.landmarks.isEmpty || trip.landmarks.length <= 1 && trip.landmarks.first.type == start ) {
children.add(
const Text("No landmarks in this trip"),
);
@@ -32,6 +32,7 @@ List<Widget> landmarksList(Trip trip) {
onDismissed: (direction) {
log('Removing ${landmark.name}');
trip.removeLandmark(landmark);
// Then show a snackbar
rootScaffoldMessengerKey.currentState!.showSnackBar(
SnackBar(content: Text("We won't show ${landmark.name} again"))

View File

@@ -1,60 +0,0 @@
import 'package:flutter/material.dart';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:anyway/structs/trip.dart';
import 'package:anyway/pages/current_trip.dart';
class CurrentTripLoadingIndicator extends StatefulWidget {
final Trip trip;
const CurrentTripLoadingIndicator({
super.key,
required this.trip,
});
@override
State<CurrentTripLoadingIndicator> createState() => _CurrentTripLoadingIndicatorState();
}
class _CurrentTripLoadingIndicatorState extends State<CurrentTripLoadingIndicator> {
@override
Widget build(BuildContext context) => Center(
child: FutureBuilder(
future: widget.trip.cityName,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
Widget greeter;
Widget loadingIndicator = const Padding(
padding: EdgeInsets.only(top: 10),
child: CircularProgressIndicator()
);
if (snapshot.hasData) {
greeter = 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
greeter = AutoSizeText(
maxLines: 1,
'Error while loading trip.',
style: greeterStyle,
);
} else {
greeter = AutoSizeText(
maxLines: 1,
'Generating your trip...',
style: greeterStyle,
);
}
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
greeter,
loadingIndicator,
],
);
}
)
);
}

View File

@@ -1,28 +1,27 @@
import 'dart:collection';
import 'package:anyway/constants.dart';
import 'package:anyway/modules/landmark_map_marker.dart';
import 'package:anyway/modules/themed_marker.dart';
import 'package:flutter/material.dart';
import 'package:anyway/structs/landmark.dart';
import 'package:anyway/structs/trip.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:widget_to_marker/widget_to_marker.dart';
class CurrentTripMap extends StatefulWidget {
class MapWidget extends StatefulWidget {
final Trip? trip;
CurrentTripMap({
MapWidget({
this.trip
});
@override
State<CurrentTripMap> createState() => _CurrentTripMapState();
State<MapWidget> createState() => _MapWidgetState();
}
class _CurrentTripMapState extends State<CurrentTripMap> {
class _MapWidgetState extends State<MapWidget> {
late GoogleMapController mapController;
CameraPosition _cameraPosition = CameraPosition(
@@ -68,27 +67,9 @@ class _CurrentTripMapState extends State<CurrentTripMap> {
});
}
@override
Widget build(BuildContext context) {
widget.trip?.addListener(setMapMarkers);
Future<SharedPreferences> preferences = SharedPreferences.getInstance();
return FutureBuilder(
future: preferences,
builder: (context, snapshot) {
if (snapshot.hasData) {
SharedPreferences prefs = snapshot.data as SharedPreferences;
bool useLocation = prefs.getBool('useLocation') ?? true;
return _buildMap(useLocation);
} else {
return const CircularProgressIndicator();
}
}
);
}
Widget _buildMap(bool useLocation) {
return GoogleMap(
onMapCreated: _onMapCreated,
initialCameraPosition: _cameraPosition,
@@ -98,9 +79,7 @@ class _CurrentTripMapState extends State<CurrentTripMap> {
cloudMapId: MAP_ID,
mapToolbarEnabled: false,
zoomControlsEnabled: false,
myLocationEnabled: useLocation,
myLocationButtonEnabled: false,
);
}
}

View File

@@ -1,82 +0,0 @@
import 'package:anyway/constants.dart';
import 'package:anyway/modules/current_trip_error_message.dart';
import 'package:anyway/modules/current_trip_loading_indicator.dart';
import 'package:flutter/material.dart';
import 'package:anyway/structs/trip.dart';
import 'package:anyway/modules/current_trip_summary.dart';
import 'package:anyway/modules/current_trip_save_button.dart';
import 'package:anyway/modules/current_trip_landmarks_list.dart';
import 'package:anyway/modules/current_trip_greeter.dart';
class CurrentTripPanel extends StatefulWidget {
final ScrollController controller;
final Trip trip;
const CurrentTripPanel({
super.key,
required this.controller,
required this.trip,
});
@override
State<CurrentTripPanel> createState() => _CurrentTripPanelState();
}
class _CurrentTripPanelState extends State<CurrentTripPanel> {
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: widget.trip,
builder: (context, child) {
if (widget.trip.uuid == 'error') {
return Align(
alignment: Alignment.topCenter,
child: SizedBox(
// reuse the exact same height as the panel has when collapsed
// this way the greeter will be centered when the panel is collapsed
height: MediaQuery.of(context).size.height * TRIP_PANEL_MIN_HEIGHT - 20,
child: CurrentTripErrorMessage(trip: widget.trip)
),
);
} else if (widget.trip.uuid == 'pending') {
return Align(
alignment: Alignment.topCenter,
child: SizedBox(
// reuse the exact same height as the panel has when collapsed
// this way the greeter will be centered when the panel is collapsed
height: MediaQuery.of(context).size.height * TRIP_PANEL_MIN_HEIGHT - 20,
child: CurrentTripLoadingIndicator(trip: widget.trip),
),
);
} else {
return ListView(
controller: widget.controller,
padding: const EdgeInsets.only(bottom: 30),
children: [
SizedBox(
// reuse the exact same height as the panel has when collapsed
// this way the greeter will be centered when the panel is collapsed
height: MediaQuery.of(context).size.height * TRIP_PANEL_MIN_HEIGHT - 20,
child: CurrentTripGreeter(trip: widget.trip),
),
const Padding(padding: EdgeInsets.only(top: 10)),
// CurrentTripSummary(trip: widget.trip),
// const Divider(),
...landmarksList(widget.trip),
const Padding(padding: EdgeInsets.only(top: 10)),
Center(child: saveButton(widget.trip)),
],
);
}
}
);
}
}

View File

@@ -1,5 +1,4 @@
import 'package:anyway/main.dart';
import 'package:anyway/structs/trip.dart';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
@@ -9,13 +8,6 @@ Widget saveButton(Trip trip) => ElevatedButton(
onPressed: () async {
SharedPreferences prefs = await SharedPreferences.getInstance();
trip.toPrefs(prefs);
rootScaffoldMessengerKey.currentState!.showSnackBar(
SnackBar(
content: Text('Trip saved'),
duration: Duration(seconds: 2),
dismissDirection: DismissDirection.horizontal
)
);
},
child: SizedBox(
width: 100,

View File

@@ -1,31 +0,0 @@
import 'package:anyway/structs/trip.dart';
import 'package:flutter/material.dart';
class CurrentTripSummary extends StatefulWidget {
final Trip trip;
const CurrentTripSummary({
super.key,
required this.trip,
});
@override
State<CurrentTripSummary> createState() => _CurrentTripSummaryState();
}
class _CurrentTripSummaryState extends State<CurrentTripSummary> {
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Summary'),
// Text('Start: ${widget.trip.start}'),
// Text('End: ${widget.trip.end}'),
Text('Total duration: ${widget.trip.totalTime}'),
Text('Total distance: ${widget.trip.totalTime}'),
// Text('Fuel: ${widget.trip.fuel}'),
// Text('Cost: ${widget.trip.cost}'),
],
);
}
}

View File

@@ -18,6 +18,10 @@ class _LandmarkCardState extends State<LandmarkCard> {
@override
Widget build(BuildContext context) {
ThemeData theme = Theme.of(context);
ButtonStyle buttonStyle = TextButton.styleFrom(
backgroundColor: Colors.orange,
fixedSize: Size.fromHeight(20)
);
return Container(
height: 160,
child: Card(
@@ -36,7 +40,7 @@ class _LandmarkCardState extends State<LandmarkCard> {
width: 160,
child: CachedNetworkImage(
imageUrl: widget.landmark.imageURL ?? '',
placeholder: (context, url) => Center(child: CircularProgressIndicator()),
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
@@ -84,18 +88,21 @@ class _LandmarkCardState extends State<LandmarkCard> {
// show the type, the website, and the wikipedia link as buttons/labels in a row
children: [
TextButton.icon(
style: buttonStyle,
onPressed: () {},
icon: widget.landmark.type.icon,
label: Text(widget.landmark.type.name),
),
if (widget.landmark.duration != null && widget.landmark.duration!.inMinutes > 0)
TextButton.icon(
style: buttonStyle,
onPressed: () {},
icon: Icon(Icons.hourglass_bottom),
label: Text('${widget.landmark.duration!.inMinutes} minutes'),
),
if (widget.landmark.websiteURL != null)
TextButton.icon(
style: buttonStyle,
onPressed: () async {
// open a browser with the website link
await launchUrl(Uri.parse(widget.landmark.websiteURL!));
@@ -105,6 +112,7 @@ class _LandmarkCardState extends State<LandmarkCard> {
),
if (widget.landmark.wikipediaURL != null)
TextButton.icon(
style: buttonStyle,
onPressed: () async {
// open a browser with the wikipedia link
await launchUrl(Uri.parse(widget.landmark.wikipediaURL!));

View File

@@ -1,5 +1,4 @@
import 'package:anyway/layout.dart';
import 'package:anyway/main.dart';
import 'package:anyway/structs/preferences.dart';
import 'package:anyway/structs/trip.dart';
import 'package:anyway/utils/fetch_trip.dart';
@@ -9,12 +8,8 @@ import 'package:flutter/material.dart';
class NewTripButton extends StatefulWidget {
final Trip trip;
final UserPreferences preferences;
const NewTripButton({
required this.trip,
required this.preferences,
});
const NewTripButton({required this.trip});
@override
State<NewTripButton> createState() => _NewTripButtonState();
@@ -28,39 +23,42 @@ class _NewTripButtonState extends State<NewTripButton> {
listenable: widget.trip,
builder: (BuildContext context, Widget? child) {
if (widget.trip.landmarks.isEmpty){
// Fallback if the trip setup is lagging behind
// This should in theory never happen
return Container();
}
return FloatingActionButton.extended(
onPressed: onPressed,
icon: const Icon(Icons.directions),
label: AutoSizeText('Start planning!'),
onPressed: () async {
Future<UserPreferences> preferences = loadUserPreferences();
Trip trip = widget.trip;
fetchTrip(trip, preferences);
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => BasePage(mainScreen: "map", trip: trip)
)
);
},
icon: Icon(Icons.add),
label: FutureBuilder(
future: widget.trip.cityName,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return AutoSizeText(
'New trip to ${snapshot.data.toString()}',
style: TextStyle(fontSize: 18),
maxLines: 2,
);
} else {
return AutoSizeText(
'New trip to ...',
style: TextStyle(fontSize: 18),
maxLines: 2,
);
}
},
)
);
}
);
}
void onPressed() async {
// Check that the preferences are valid
UserPreferences preferences = widget.preferences;
if (preferences.nature.value == 0 && preferences.shopping.value == 0 && preferences.sightseeing.value == 0){
rootScaffoldMessengerKey.currentState!.showSnackBar(
SnackBar(content: Text("Please specify at least one preference"))
);
} else if (preferences.maxTime.value == 0){
rootScaffoldMessengerKey.currentState!.showSnackBar(
SnackBar(content: Text("Please choose a longer duration"))
);
} else {
Trip trip = widget.trip;
fetchTrip(trip, widget.preferences);
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => BasePage(mainScreen: "map", trip: trip)
)
);
}
}
}

View File

@@ -6,13 +6,9 @@ import 'dart:developer';
import 'package:anyway/structs/trip.dart';
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:shared_preferences/shared_preferences.dart';
class NewTripLocationSearch extends StatefulWidget {
Future<SharedPreferences> prefs = SharedPreferences.getInstance();
Trip trip;
NewTripLocationSearch(
this.trip,
);
@@ -43,66 +39,26 @@ class _NewTripLocationSearchState extends State<NewTripLocationSearch> {
uuid: 'pending',
name: query,
location: [location.latitude, location.longitude],
type: typeStart
type: start
)
);
}
}
late Widget locationSearchBar = SearchBar(
hintText: 'Enter a city name or long press on the map.',
onSubmitted: setTripLocation,
controller: _controller,
leading: Icon(Icons.search),
trailing: [
ElevatedButton(
@override
Widget build(BuildContext context) {
return SearchBar(
hintText: 'Enter a city name or long press on the map.',
onSubmitted: setTripLocation,
controller: _controller,
leading: Icon(Icons.search),
trailing: [ElevatedButton(
onPressed: () {
setTripLocation(_controller.text);
},
child: Text('Search'),
)
]
);
),]
late Widget useCurrentLocationButton = ElevatedButton(
onPressed: () async {
// this widget is only shown if the user has already granted location permissions
Position position = await Geolocator.getCurrentPosition();
widget.trip.landmarks.clear();
widget.trip.addLandmark(
Landmark(
uuid: 'pending',
name: 'start',
location: [position.latitude, position.longitude],
type: typeStart
)
);
},
child: Text('Use current location'),
);
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: widget.prefs,
builder: (context, snapshot) {
if (snapshot.hasData) {
final useLocation = snapshot.data!.getBool('useLocation') ?? false;
if (useLocation) {
return Column(
children: [
locationSearchBar,
useCurrentLocationButton,
],
);
} else {
return locationSearchBar;
}
} else {
return locationSearchBar;
}
},
);
}
}

View File

@@ -1,13 +1,13 @@
// A map that allows the user to select a location for a new trip.
import 'dart:developer';
import 'package:anyway/constants.dart';
import 'package:anyway/modules/landmark_map_marker.dart';
import 'package:anyway/modules/themed_marker.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:shared_preferences/shared_preferences.dart';
import 'package:widget_to_marker/widget_to_marker.dart';
@@ -22,7 +22,7 @@ class NewTripMap extends StatefulWidget {
}
class _NewTripMapState extends State<NewTripMap> {
final CameraPosition _cameraPosition = const CameraPosition(
final CameraPosition _cameraPosition = CameraPosition(
target: LatLng(48.8566, 2.3522),
zoom: 11.0,
);
@@ -37,7 +37,7 @@ class _NewTripMapState extends State<NewTripMap> {
uuid: 'pending',
name: 'start',
location: [location.latitude, location.longitude],
type: typeStart
type: start
)
);
}
@@ -70,26 +70,10 @@ class _NewTripMapState extends State<NewTripMap> {
}
@override
Widget build(BuildContext context) {
widget.trip.addListener(updateTripDetails);
Future<SharedPreferences> preferences = SharedPreferences.getInstance();
return FutureBuilder(
future: preferences,
builder: (context, snapshot) {
if (snapshot.hasData) {
SharedPreferences prefs = snapshot.data as SharedPreferences;
bool useLocation = prefs.getBool('useLocation') ?? true;
return _buildMap(useLocation);
} else {
return const CircularProgressIndicator();
}
}
);
}
Widget _buildMap(bool useLocation) {
return GoogleMap(
onMapCreated: _onMapCreated,
initialCameraPosition: _cameraPosition,
@@ -98,8 +82,6 @@ class _NewTripMapState extends State<NewTripMap> {
cloudMapId: MAP_ID,
mapToolbarEnabled: false,
zoomControlsEnabled: false,
myLocationButtonEnabled: false,
myLocationEnabled: useLocation,
);
}
}

View File

@@ -1,41 +0,0 @@
import 'package:anyway/pages/new_trip_preferences.dart';
import 'package:anyway/structs/trip.dart';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
class NewTripOptionsButton extends StatefulWidget {
final Trip trip;
const NewTripOptionsButton({required this.trip});
@override
State<NewTripOptionsButton> createState() => _NewTripOptionsButtonState();
}
class _NewTripOptionsButtonState extends State<NewTripOptionsButton> {
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: widget.trip,
builder: (BuildContext context, Widget? child) {
if (widget.trip.landmarks.isEmpty){
return Container();
}
return FloatingActionButton.extended(
onPressed: () async {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => NewTripPreferencesPage(trip: widget.trip)
)
);
},
icon: const Icon(Icons.add),
label: const AutoSizeText('Set preferences')
);
}
);
}
}

View File

@@ -16,23 +16,20 @@ class OnboardingCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
Color baseColor = Theme.of(context).colorScheme.secondary;
Color baseColor = Theme.of(context).primaryColor;
// have a different color for each card, incrementing the hue
Color currentColor = baseColor.withAlpha(baseColor.alpha - index * 30);
return Container(
color: currentColor,
alignment: Alignment.center,
child: Padding(
padding: EdgeInsets.all(20),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
title,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
Padding(padding: EdgeInsets.only(top: 20)),
@@ -47,8 +44,7 @@ class OnboardingCard extends StatelessWidget {
fontSize: 16,
),
),
]
],
),
)
);

View File

@@ -1,4 +1,3 @@
import 'package:anyway/constants.dart';
import 'package:anyway/structs/landmark.dart';
import 'package:flutter/material.dart';
@@ -17,9 +16,21 @@ class ThemedMarker extends StatelessWidget {
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 != typeStart && landmark.type != typeFinish) {
if (landmark.type != start && landmark.type != finish) {
positionIndicator = Positioned(
top: 0,
right: 0,
@@ -40,14 +51,14 @@ class ThemedMarker extends StatelessWidget {
children: [
Container(
decoration: BoxDecoration(
gradient: APP_GRADIENT,
gradient: LinearGradient(
colors: [Colors.red, Colors.yellow]
),
shape: BoxShape.circle,
border: Border.all(color: Colors.black, width: 5),
),
width: 70,
height: 70,
padding: const EdgeInsets.all(5),
child: Icon(landmark.type.icon.icon, size: 50),
padding: EdgeInsets.all(5),
child: icon
),
if (positionIndicator != null) positionIndicator,
],

View File

@@ -1,17 +1,12 @@
import 'package:anyway/constants.dart';
import 'package:anyway/modules/current_trip_save_button.dart';
import 'package:flutter/material.dart';
import 'package:sliding_up_panel/sliding_up_panel.dart';
import 'package:anyway/structs/trip.dart';
import 'package:anyway/modules/current_trip_landmarks_list.dart';
import 'package:anyway/modules/current_trip_greeter.dart';
import 'package:anyway/modules/current_trip_map.dart';
import 'package:anyway/modules/current_trip_panel.dart';
final Shader textGradient = APP_GRADIENT.createShader(Rect.fromLTWH(0.0, 0.0, 200.0, 70.0));
TextStyle greeterStyle = TextStyle(
foreground: Paint()..shader = textGradient,
fontWeight: FontWeight.bold,
fontSize: 26
);
class TripPage extends StatefulWidget {
@@ -32,21 +27,16 @@ class _TripPageState extends State<TripPage> {
@override
Widget build(BuildContext context) {
return SlidingUpPanel(
// use panelBuilder instead of panel so that we can reuse the scrollcontroller for the listview
panelBuilder: (scrollcontroller) => CurrentTripPanel(controller: scrollcontroller, trip: widget.trip),
// using collapsed and panelBuilder seems to show both at the same time, so we include the greeter in the panelBuilder
// collapsed: Greeter(trip: widget.trip),
body: CurrentTripMap(trip: widget.trip),
minHeight: MediaQuery.of(context).size.height * TRIP_PANEL_MIN_HEIGHT,
maxHeight: MediaQuery.of(context).size.height * TRIP_PANEL_MAX_HEIGHT,
// padding in this context is annoying: it offsets the notion of vertical alignment.
// children that want to be centered vertically need to have their size adjusted by 2x the padding
padding: const EdgeInsets.all(10.0),
// Panel snapping should not be disabled because it significantly improves the user experience
// panelSnapping: false
borderRadius: const BorderRadius.only(topLeft: Radius.circular(25), topRight: Radius.circular(25)),
parallaxEnabled: true,
boxShadow: const [
panelBuilder: (sc) => _panelFull(sc),
// collapsed: _floatingCollapsed(),
body: MapWidget(trip: widget.trip),
// renderPanelSheet: false,
// backdropEnabled: true,
maxHeight: MediaQuery.of(context).size.height * 0.8,
padding: EdgeInsets.only(left: 10, right: 10, top: 25, bottom: 10),
// panelSnapping: false,
borderRadius: BorderRadius.only(topLeft: Radius.circular(25), topRight: Radius.circular(25)),
boxShadow: [
BoxShadow(
blurRadius: 20.0,
color: Colors.black,
@@ -54,4 +44,41 @@ class _TripPageState extends State<TripPage> {
],
);
}
Widget _panelFull(ScrollController sc) {
return ListenableBuilder(
listenable: widget.trip,
builder: (context, child) {
if (widget.trip.uuid != 'pending' && widget.trip.uuid != 'error') {
return ListView(
controller: sc,
padding: EdgeInsets.only(bottom: 35),
children: [
Greeter(trip: widget.trip),
...landmarksList(widget.trip),
Padding(padding: EdgeInsets.only(top: 10)),
Center(child: saveButton(widget.trip)),
],
);
} else if(widget.trip.uuid == 'pending') {
return Greeter(trip: widget.trip);
} else {
return Column(
children: [
const Icon(
Icons.error_outline,
color: Colors.red,
size: 60,
),
Padding(
padding: const EdgeInsets.only(top: 16),
child: Text('Error: ${widget.trip.errorDescription}'),
),
],
);
}
}
);
}
}

View File

@@ -1,7 +1,11 @@
import 'package:anyway/modules/new_trip_button.dart';
import 'package:anyway/modules/new_trip_options_button.dart';
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";
import 'package:anyway/modules/new_trip_location_search.dart';
import 'package:anyway/modules/new_trip_map.dart';
@@ -15,6 +19,7 @@ class NewTripPage extends StatefulWidget {
}
class _NewTripPageState extends State<NewTripPage> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final TextEditingController latController = TextEditingController();
final TextEditingController lonController = TextEditingController();
Trip trip = Trip();
@@ -35,7 +40,7 @@ class _NewTripPageState extends State<NewTripPage> {
),
],
),
floatingActionButton: NewTripOptionsButton(trip: trip),
floatingActionButton: NewTripButton(trip: trip),
);
}
}

View File

@@ -1,113 +0,0 @@
import 'package:anyway/modules/new_trip_button.dart';
import 'package:anyway/structs/preferences.dart';
import 'package:anyway/structs/trip.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class NewTripPreferencesPage extends StatefulWidget {
final Trip trip;
const NewTripPreferencesPage({required this.trip});
@override
_NewTripPreferencesPageState createState() => _NewTripPreferencesPageState();
}
class _NewTripPreferencesPageState extends State<NewTripPreferencesPage> {
UserPreferences preferences = UserPreferences();
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [
// Center(
// child: CircleAvatar(
// radius: 100,
// child: Icon(Icons.person, size: 100),
// )
// ),
Padding(padding: EdgeInsets.only(top: 30)),
Center(
child: FutureBuilder(
future: widget.trip.cityName,
builder: (context, snapshot) => Text(
'Your trip to ${snapshot.hasData ? snapshot.data! : "..."}',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)
)
)
),
Center(
child: Padding(
padding: EdgeInsets.only(left: 10, right: 10, top: 20, bottom: 0),
child: Text('Tell us about your ideal trip.', style: TextStyle(fontSize: 18))
),
),
Divider(indent: 25, endIndent: 25, height: 50),
durationPicker(preferences.maxTime),
preferenceSliders([preferences.sightseeing, preferences.shopping, preferences.nature]),
]
),
floatingActionButton: NewTripButton(trip: widget.trip, preferences: preferences),
);
}
Widget durationPicker(SinglePreference maxTime) {
return Card(
margin: const EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 0),
shadowColor: Colors.grey,
child: ListTile(
leading: preferences.maxTime.icon,
title: Text(preferences.maxTime.description),
subtitle: CupertinoTimerPicker(
mode: CupertinoTimerPickerMode.hm,
initialTimerDuration: Duration(minutes: 90),
minuteInterval: 15,
onTimerDurationChanged: (Duration newDuration) {
setState(() {
preferences.maxTime.value = newDuration.inMinutes;
});
},
)
),
);
}
Widget preferenceSliders(List<SinglePreference> prefs) {
List<Card> sliders = [];
for (SinglePreference pref in 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();
});
},
)
),
)
);
}
return Column(
children: sliders
);
}
}

View File

@@ -1,5 +1,5 @@
import 'package:anyway/modules/onboarding_card.dart';
import 'package:anyway/pages/new_trip_location.dart';
import 'package:anyway/pages/new_trip.dart';
import 'package:flutter/material.dart';
class OnboardingPage extends StatefulWidget {

View File

@@ -0,0 +1,177 @@
import 'package:anyway/constants.dart';
import 'package:anyway/structs/preferences.dart';
import 'package:flutter/material.dart';
bool debugMode = false;
class ProfilePage extends StatefulWidget {
@override
_ProfilePageState createState() => _ProfilePageState();
}
class _ProfilePageState extends State<ProfilePage> {
Future<UserPreferences> _prefs = loadUserPreferences();
@override
Widget build(BuildContext context) {
return ListView(
children: [
// First a round, centered image
Center(
child: CircleAvatar(
radius: 100,
child: Icon(Icons.person, size: 100),
)
),
Center(
child: Text('Curious traveler', style: TextStyle(fontSize: 24))
),
Divider(indent: 25, endIndent: 25, height: 50),
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))
),
),
FutureBuilder(future: _prefs, builder: futureSliders),
Divider(indent: 25, endIndent: 25, height: 50),
privacyInfo(),
debugButton(),
]
);
}
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();
},
),
],
);
}
);
});
}
)
],
)
);
}
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();
}
}
Widget privacyInfo() {
return Padding(
padding: EdgeInsets.only(top: 20),
child: Row(
children: [
Text('Privacy policy is available at '),
TextButton.icon(
icon: Icon(Icons.info),
label: Text(PRIVACY_URL),
onPressed: () {
}
)
],
)
);
}
}
class PreferenceSliders extends StatefulWidget {
final List<SinglePreference> prefs;
PreferenceSliders({required this.prefs});
@override
State<PreferenceSliders> createState() => _PreferenceSlidersState();
}
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: sliders);
}
}

View File

@@ -1,186 +0,0 @@
import 'package:anyway/constants.dart';
import 'package:anyway/main.dart';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:url_launcher/url_launcher.dart';
bool debugMode = false;
class SettingsPage extends StatefulWidget {
@override
_SettingsPageState createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage> {
@override
Widget build(BuildContext context) {
return ListView(
padding: EdgeInsets.all(15),
children: [
// First a round, centered image
Center(
child: CircleAvatar(
radius: 75,
child: Icon(Icons.settings, size: 100),
)
),
Center(
child: Text('Global settings', style: TextStyle(fontSize: 24))
),
Divider(indent: 25, endIndent: 25, height: 50),
darkMode(),
setLocationUsage(),
setDebugMode(),
Divider(indent: 25, endIndent: 25, height: 50),
privacyInfo(),
]
);
}
Widget setDebugMode() {
return Row(
children: [
Text('Debugging: use a custom API URL'),
// white space
Spacer(),
Switch(
value: debugMode,
onChanged: (bool? newValue) {
setState(() {
debugMode = newValue!;
if (debugMode) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('Debug mode - use a custom API endpoint'),
content: TextField(
controller: TextEditingController(text: API_URL_DEBUG),
onChanged: (value) {
setState(() {
API_URL_BASE = value;
});
},
),
actions: [
TextButton(
child: Text('OK'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
}
);
}
});
}
)
],
);
}
Widget darkMode() {
return Row(
children: [
Text('Dark mode'),
Spacer(),
Switch(
value: Theme.of(context).brightness == Brightness.dark,
onChanged: (bool? newValue) {
setState(() {
rootScaffoldMessengerKey.currentState!.showSnackBar(
SnackBar(content: Text('Dark mode is not implemented yet'))
);
// if (newValue!) {
// // Dark mode
// Theme.of(context).brightness = Brightness.dark;
// } else {
// // Light mode
// Theme.of(context).brightness = Brightness.light;
// }
});
}
)
],
);
}
Widget setLocationUsage() {
Future<SharedPreferences> preferences = SharedPreferences.getInstance();
return Row(
children: [
Text('Use location services'),
// white space
Spacer(),
FutureBuilder(
future: preferences,
builder: (context, snapshot) {
if (snapshot.hasData) {
bool useLocation = snapshot.data!.getBool('useLocation') ?? false;
return Switch(
value: useLocation,
onChanged: setUseLocation,
);
} else {
return CircularProgressIndicator();
}
}
)
],
);
}
void setUseLocation(bool newValue) async {
await Permission.locationWhenInUse
.onDeniedCallback(() {
rootScaffoldMessengerKey.currentState!.showSnackBar(
SnackBar(content: Text('Location services are required for this feature'))
);
})
.onGrantedCallback(() {
rootScaffoldMessengerKey.currentState!.showSnackBar(
SnackBar(content: Text('Location services are now enabled'))
);
SharedPreferences.getInstance().then(
(SharedPreferences prefs) {
setState(() {
prefs.setBool('useLocation', newValue);
});
}
);
})
.onPermanentlyDeniedCallback(() {
rootScaffoldMessengerKey.currentState!.showSnackBar(
SnackBar(content: Text('Location services are required for this feature'))
);
})
.request();
}
Widget privacyInfo() {
return Center(
child: Column(
children: [
Text('Our privacy policy is available under:'),
TextButton.icon(
icon: Icon(Icons.info),
label: Text(PRIVACY_URL),
onPressed: () async{
await launchUrl(Uri.parse(PRIVACY_URL));
}
)
],
)
);
}
}

View File

@@ -4,13 +4,13 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
LandmarkType typeSightseeing = LandmarkType(name: 'sightseeing');
LandmarkType typeNature = LandmarkType(name: 'nature');
LandmarkType typeShopping = LandmarkType(name: 'shopping');
LandmarkType sightseeing = LandmarkType(name: 'sightseeing');
LandmarkType nature = LandmarkType(name: 'nature');
LandmarkType shopping = LandmarkType(name: 'shopping');
// LandmarkType museum = LandmarkType(name: 'Museum');
// LandmarkType restaurant = LandmarkType(name: 'Restaurant');
LandmarkType typeStart = LandmarkType(name: 'start');
LandmarkType typeFinish = LandmarkType(name: 'finish');
LandmarkType start = LandmarkType(name: 'start');
LandmarkType finish = LandmarkType(name: 'finish');
final class Landmark extends LinkedListEntry<Landmark>{
@@ -130,22 +130,22 @@ class LandmarkType {
LandmarkType({required this.name, this.icon = const Icon(Icons.location_on)}) {
switch (name) {
case 'sightseeing':
icon = const Icon(Icons.church);
icon = Icon(Icons.church);
break;
case 'nature':
icon = const Icon(Icons.eco);
icon = Icon(Icons.eco);
break;
case 'shopping':
icon = const Icon(Icons.shopping_cart);
icon = Icon(Icons.shopping_cart);
break;
case 'start':
icon = const Icon(Icons.play_arrow);
icon = Icon(Icons.play_arrow);
break;
case 'finish':
icon = const Icon(Icons.flag);
icon = Icon(Icons.flag);
break;
default:
icon = const Icon(Icons.location_on);
icon = Icon(Icons.location_on);
}
}
@override

View File

@@ -1,5 +1,5 @@
import 'package:anyway/structs/landmark.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class SinglePreference {
@@ -20,6 +20,16 @@ class SinglePreference {
this.minVal = 0,
this.maxVal = 5,
});
void save() async {
SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
sharedPrefs.setInt('pref_$slug', value);
}
void load() async {
SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
value = sharedPrefs.getInt('pref_$slug') ?? minVal;
}
}
@@ -29,41 +39,64 @@ class UserPreferences {
slug: "sightseeing",
description: "How much do you like sightseeing?",
value: 0,
icon: typeSightseeing.icon,
icon: Icon(Icons.church),
);
SinglePreference shopping = SinglePreference(
name: "Shopping",
slug: "shopping",
description: "How much do you like shopping?",
value: 0,
icon: typeShopping.icon,
icon: Icon(Icons.shopping_bag),
);
SinglePreference nature = SinglePreference(
name: "Nature",
slug: "nature",
description: "How much do you like nature?",
value: 0,
icon: typeNature.icon,
icon: Icon(Icons.landscape),
);
SinglePreference maxTime = SinglePreference(
name: "Trip duration",
slug: "duration",
description: "How long should your trip be?",
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
"max_time_minute": maxTime.value,
"detour_tolerance_minute": maxDetour.value
};
}
}
Future<UserPreferences> loadUserPreferences() async {
UserPreferences prefs = UserPreferences();
await prefs.load();
return prefs;
}

View File

@@ -29,41 +29,29 @@ Dio dio = Dio(
fetchTrip(
Trip trip,
UserPreferences preferences,
Future<UserPreferences> preferences,
) async {
UserPreferences prefs = await preferences;
Map<String, dynamic> data = {
"preferences": preferences.toJson(),
"preferences": prefs.toJson(),
"start": trip.landmarks!.first.location,
};
String dataString = jsonEncode(data);
log(dataString);
late Response response;
try {
response = await dio.post(
"/trip/new",
data: data
);
} catch (e) {
trip.updateUUID("error");
trip.updateError(e.toString());
log(e.toString());
return;
}
final response = await dio.post(
"/trip/new",
data: data
);
// handle errors
if (response.statusCode != 200) {
trip.updateUUID("error");
String errorDetail;
if (response.data.runtimeType == String) {
errorDetail = response.data;
} else {
errorDetail = response.data["detail"] ?? "Unknown error";
if (response.data["detail"] != null) {
trip.updateError(response.data["detail"]);
log(response.data["detail"]);
// throw Exception(response.data["detail"]);
}
trip.updateError(errorDetail);
log(errorDetail);
// Actualy no need to throw an exception, we can just log the error and let the user retry
// throw Exception(errorDetail);
} else {
Map<String, dynamic> json = response.data;

View File

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

View File

@@ -45,26 +45,26 @@ packages:
dependency: "direct main"
description:
name: cached_network_image
sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916"
sha256: "4a5d8d2c728b0f3d0245f69f921d7be90cae4c2fd5288f773088672c0893f819"
url: "https://pub.dev"
source: hosted
version: "3.4.1"
version: "3.4.0"
cached_network_image_platform_interface:
dependency: transitive
description:
name: cached_network_image_platform_interface
sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829"
sha256: ff0c949e323d2a1b52be73acce5b4a7b04063e61414c8ca542dbba47281630a7
url: "https://pub.dev"
source: hosted
version: "4.1.1"
version: "4.1.0"
cached_network_image_web:
dependency: transitive
description:
name: cached_network_image_web
sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062"
sha256: "6322dde7a5ad92202e64df659241104a43db20ed594c41ca18de1014598d7996"
url: "https://pub.dev"
source: hosted
version: "1.3.1"
version: "1.3.0"
characters:
dependency: transitive
description:
@@ -109,10 +109,10 @@ packages:
dependency: transitive
description:
name: crypto
sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
url: "https://pub.dev"
source: hosted
version: "3.0.5"
version: "3.0.3"
csslib:
dependency: transitive
description:
@@ -133,18 +133,18 @@ packages:
dependency: "direct main"
description:
name: dio
sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260"
sha256: e17f6b3097b8c51b72c74c9f071a605c47bcc8893839bd66732457a5ebe73714
url: "https://pub.dev"
source: hosted
version: "5.7.0"
version: "5.5.0+1"
dio_web_adapter:
dependency: transitive
description:
name: dio_web_adapter
sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8"
sha256: "36c5b2d79eb17cdae41e974b7a8284fec631651d2a6f39a8a2ff22327e90aeac"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
version: "1.0.1"
fake_async:
dependency: transitive
description:
@@ -157,10 +157,10 @@ packages:
dependency: transitive
description:
name: ffi
sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
version: "2.1.2"
file:
dependency: transitive
description:
@@ -186,10 +186,10 @@ packages:
dependency: transitive
description:
name: flutter_cache_manager
sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
sha256: a77f77806a790eb9ba0118a5a3a936e81c4fea2b61533033b2b0c3d50bbde5ea
url: "https://pub.dev"
source: hosted
version: "3.4.1"
version: "3.4.0"
flutter_launcher_icons:
dependency: "direct main"
description:
@@ -210,10 +210,10 @@ packages:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: "9ee02950848f61c4129af3d6ec84a1cfc0e47931abc746b03e7a3bc3e8ff6eda"
sha256: "9d98bd47ef9d34e803d438f17fd32b116d31009f534a6fa5ce3a1167f189a6de"
url: "https://pub.dev"
source: hosted
version: "2.0.22"
version: "2.0.21"
flutter_svg:
dependency: "direct main"
description:
@@ -264,54 +264,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.2.0"
geolocator:
dependency: "direct main"
description:
name: geolocator
sha256: "0ec58b731776bc43097fcf751f79681b6a8f6d3bc737c94779fe9f1ad73c1a81"
url: "https://pub.dev"
source: hosted
version: "13.0.1"
geolocator_android:
dependency: transitive
description:
name: geolocator_android
sha256: "7aefc530db47d90d0580b552df3242440a10fe60814496a979aa67aa98b1fd47"
url: "https://pub.dev"
source: hosted
version: "4.6.1"
geolocator_apple:
dependency: transitive
description:
name: geolocator_apple
sha256: bc2aca02423ad429cb0556121f56e60360a2b7d694c8570301d06ea0c00732fd
url: "https://pub.dev"
source: hosted
version: "2.3.7"
geolocator_platform_interface:
dependency: transitive
description:
name: geolocator_platform_interface
sha256: "386ce3d9cce47838355000070b1d0b13efb5bc430f8ecda7e9238c8409ace012"
url: "https://pub.dev"
source: hosted
version: "4.2.4"
geolocator_web:
dependency: transitive
description:
name: geolocator_web
sha256: "2ed69328e05cd94e7eb48bb0535f5fc0c0c44d1c4fa1e9737267484d05c29b5e"
url: "https://pub.dev"
source: hosted
version: "4.1.1"
geolocator_windows:
dependency: transitive
description:
name: geolocator_windows
sha256: "53da08937d07c24b0d9952eb57a3b474e29aae2abf9dd717f7e1230995f13f0e"
url: "https://pub.dev"
source: hosted
version: "0.2.3"
google_maps:
dependency: transitive
description:
@@ -324,42 +276,42 @@ packages:
dependency: "direct main"
description:
name: google_maps_flutter
sha256: "2e302fa3aaf4e2a297f0342d83ebc5e8e9f826e9a716aef473fe7f404ec630a7"
sha256: acf0ec482d86b2ac55ade80597ce7f797a47971f5210ebfd030f0d58130e0a94
url: "https://pub.dev"
source: hosted
version: "2.9.0"
version: "2.7.0"
google_maps_flutter_android:
dependency: transitive
description:
name: google_maps_flutter_android
sha256: "10cf27bee8c560f8e69992b3a0f27ddf1d7acbea622ddb13ef3f587848a73f26"
sha256: "5d444f4135559488d7ea325eae710ae3284e6951b1b61729a0ac026456fe1548"
url: "https://pub.dev"
source: hosted
version: "2.14.7"
version: "2.12.1"
google_maps_flutter_ios:
dependency: transitive
description:
name: google_maps_flutter_ios
sha256: "3a484846fc56f15e47e3de1f5ea80a7ff2b31721d2faa88f390f3b3cf580c953"
sha256: a6e3c6ecdda6c985053f944be13a0645ebb919da2ef0f5bc579c5e1670a5b2a8
url: "https://pub.dev"
source: hosted
version: "2.13.0"
version: "2.10.0"
google_maps_flutter_platform_interface:
dependency: transitive
description:
name: google_maps_flutter_platform_interface
sha256: "099874463dc4c9bff04fe4b2b8cf7284d2455c2deead8f9a59a87e1b9f028c69"
sha256: bd60ca330e3c7763b95b477054adec338a522d982af73ecc520b232474063ac5
url: "https://pub.dev"
source: hosted
version: "2.9.2"
version: "2.8.0"
google_maps_flutter_web:
dependency: transitive
description:
name: google_maps_flutter_web
sha256: ff39211bd25d7fad125d19f757eba85bd154460907cd4d135e07e3d0f98a4130
sha256: "8d5d0f58bfc4afac0bbe3d399f2018fcea691e3ea3d35254b7aae56df5827659"
url: "https://pub.dev"
source: hosted
version: "0.5.10"
version: "0.5.9+1"
html:
dependency: transitive
description:
@@ -436,10 +388,10 @@ packages:
dependency: "direct main"
description:
name: map_launcher
sha256: "7436d6ef9ae57ff15beafcedafe0a8f0604006cbecd2d26024c4cfb0158c2b9a"
sha256: af59b9f79f641022e06761c9d4217c6c57b9ef9020af2fdb23155ec87af79e61
url: "https://pub.dev"
source: hosted
version: "3.5.0"
version: "3.3.1"
matcher:
dependency: transitive
description:
@@ -508,10 +460,10 @@ packages:
dependency: transitive
description:
name: path_provider_android
sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7"
sha256: "490539678396d4c3c0b06efdaab75ae60675c3e0c66f72bc04c2e2c1e0e2abeb"
url: "https://pub.dev"
source: hosted
version: "2.2.10"
version: "2.2.9"
path_provider_foundation:
dependency: transitive
description:
@@ -544,54 +496,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.0"
permission_handler:
dependency: "direct main"
description:
name: permission_handler
sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb"
url: "https://pub.dev"
source: hosted
version: "11.3.1"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
sha256: "76e4ab092c1b240d31177bb64d2b0bea43f43d0e23541ec866151b9f7b2490fa"
url: "https://pub.dev"
source: hosted
version: "12.0.12"
permission_handler_apple:
dependency: transitive
description:
name: permission_handler_apple
sha256: e6f6d73b12438ef13e648c4ae56bd106ec60d17e90a59c4545db6781229082a0
url: "https://pub.dev"
source: hosted
version: "9.4.5"
permission_handler_html:
dependency: transitive
description:
name: permission_handler_html
sha256: af26edbbb1f2674af65a8f4b56e1a6f526156bc273d0e65dd8075fab51c78851
url: "https://pub.dev"
source: hosted
version: "0.1.3+2"
permission_handler_platform_interface:
dependency: transitive
description:
name: permission_handler_platform_interface
sha256: e9c8eadee926c4532d0305dff94b85bf961f16759c3af791486613152af4b4f9
url: "https://pub.dev"
source: hosted
version: "4.2.3"
permission_handler_windows:
dependency: transitive
description:
name: permission_handler_windows
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
url: "https://pub.dev"
source: hosted
version: "0.2.1"
petitparser:
dependency: transitive
description:
@@ -644,34 +548,34 @@ packages:
dependency: "direct main"
description:
name: shared_preferences
sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051"
sha256: c3f888ba2d659f3e75f4686112cc1e71f46177f74452d40d8307edc332296ead
url: "https://pub.dev"
source: hosted
version: "2.3.2"
version: "2.3.0"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "480ba4345773f56acda9abf5f50bd966f581dac5d514e5fc4a18c62976bbba7e"
sha256: "041be4d9d2dc6079cf342bc8b761b03787e3b71192d658220a56cac9c04a0294"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
version: "2.3.0"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f
sha256: "671e7a931f55a08aa45be2a13fe7247f2a41237897df434b30d2012388191833"
url: "https://pub.dev"
source: hosted
version: "2.5.2"
version: "2.5.0"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
sha256: "2ba0510d3017f91655b7543e9ee46d48619de2a2af38e5c790423f7007c7ccc1"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
version: "2.4.0"
shared_preferences_platform_interface:
dependency: transitive
description:
@@ -684,18 +588,18 @@ packages:
dependency: transitive
description:
name: shared_preferences_web
sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e
sha256: "3a293170d4d9403c3254ee05b84e62e8a9b3c5808ebd17de6a33fe9ea6457936"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
version: "2.4.0"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
sha256: "398084b47b7f92110683cac45c6dc4aae853db47e470e5ddcd52cab7f7196ab2"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
version: "2.4.0"
sky_engine:
dependency: transitive
description: flutter
@@ -737,10 +641,10 @@ packages:
dependency: transitive
description:
name: sqflite_common
sha256: "4058172e418eb7e7f2058dcb7657d451a8fc264afa0dea4dbd0f304a57131611"
sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4"
url: "https://pub.dev"
source: hosted
version: "2.5.4+3"
version: "2.5.4"
stack_trace:
dependency: transitive
description:
@@ -777,10 +681,10 @@ packages:
dependency: transitive
description:
name: synchronized
sha256: "51b08572b9f091f8c3eb4d9d4be253f196ff0075d5ec9b10a884026d5b55d7bc"
sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558"
url: "https://pub.dev"
source: hosted
version: "3.3.0+2"
version: "3.1.0+1"
term_glyph:
dependency: transitive
description:
@@ -841,10 +745,10 @@ packages:
dependency: transitive
description:
name: url_launcher_macos
sha256: "769549c999acdb42b8bcfa7c43d72bf79a382ca7441ab18a808e101149daf672"
sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
version: "3.2.0"
url_launcher_platform_interface:
dependency: transitive
description:
@@ -873,10 +777,10 @@ packages:
dependency: transitive
description:
name: uuid
sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77
sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90"
url: "https://pub.dev"
source: hosted
version: "4.5.0"
version: "4.4.2"
vector_graphics:
dependency: transitive
description:
@@ -921,10 +825,10 @@ packages:
dependency: transitive
description:
name: web
sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb
sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
version: "0.5.1"
widget_to_marker:
dependency: "direct main"
description:

View File

@@ -49,8 +49,6 @@ dependencies:
flutter_svg: ^2.0.10+1
url_launcher: ^6.3.0
flutter_launcher_icons: ^0.13.1
permission_handler: ^11.3.1
geolocator: ^13.0.1
dev_dependencies:
flutter_test:

View File

@@ -6,15 +6,9 @@
#include "generated_plugin_registrant.h"
#include <geolocator_windows/geolocator_windows.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
GeolocatorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("GeolocatorWindows"));
PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}

View File

@@ -3,8 +3,6 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
geolocator_windows
permission_handler_windows
url_launcher_windows
)