41 Commits

Author SHA1 Message Date
e6ccb7078b Merge pull request 'also upload aab' (#29) from fix/frontend/fastlane-config into main
Some checks are pending
Build and deploy the backend to production / Deploy to production (push) Blocked by required conditions
Build and deploy the backend to production / Build and push image (push) Waiting to run
/ push-to-remote (push) Successful in 12s
Reviewed-on: #29
2024-10-22 12:51:22 +00:00
84839c5a02 also upload aab
Some checks failed
Build and release APK / Build APK (pull_request) Has been cancelled
2024-10-22 14:50:59 +02:00
9850e949c3 Merge pull request 'remove unneeded screenshots' (#28) from frontend/fix/remove-screenshots into main
Some checks failed
Build and deploy the backend to production / Build and push image (push) Has been cancelled
Build and deploy the backend to production / Deploy to production (push) Has been cancelled
/ push-to-remote (push) Successful in 13s
Reviewed-on: #28
2024-10-22 09:54:44 +00:00
5fc25a3c39 remove unneeded screenshots
Some checks failed
Build and release APK / Build APK (pull_request) Has been cancelled
2024-10-22 11:52:59 +02:00
f76cd603f3 Merge pull request 'Better landmark finding' (#27) from fix/backend/moore-tags into main
All checks were successful
Build and deploy the backend to production / Build and push image (push) Successful in 1m38s
/ push-to-remote (push) Successful in 13s
Build and deploy the backend to production / Deploy to production (push) Successful in 14s
Reviewed-on: #27
2024-10-22 09:20:53 +00:00
67f748244f update deployment config
Some checks failed
Build and release APK / Build APK (pull_request) Has been cancelled
Build and deploy the backend to staging / Deploy to staging (pull_request) Has been cancelled
Build and deploy the backend to staging / Build and push image (pull_request) Has been cancelled
2024-10-15 10:30:20 +02:00
66fa55e8c3 remove boring bridges
Some checks failed
Build and release APK / Build APK (pull_request) Has been cancelled
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 1m40s
Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 14s
2024-10-14 19:10:31 +00:00
f4729a2de7 fix pipfile mismatch
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 2m8s
Build and release APK / Build APK (pull_request) Failing after 5m41s
Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 14s
2024-10-09 23:41:58 +02:00
40edd923c3 remove geopy dependency
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Failing after 53s
Build and deploy the backend to staging / Deploy to staging (pull_request) Has been skipped
Build and release APK / Build APK (pull_request) Failing after 6m44s
2024-10-09 16:12:41 +02:00
6ad749eeed fix? bug where landmarks are returned as none 2024-10-09 16:10:40 +02:00
c20ebf3d63 add more tags and filter more restrictively
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 1m57s
Build and release APK / Build APK (pull_request) Failing after 5m40s
Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 19s
2024-10-01 16:10:52 +02:00
6facde6e0b Merge pull request 'UI and UX fixes to make new trips (and the errors it might have less confusing)' (#26) from fix/frontend/greeter-ui into main
Some checks failed
Build and deploy the backend to production / Deploy to production (push) Has been cancelled
Build and deploy the backend to production / Build and push image (push) Has been cancelled
/ push-to-remote (push) Successful in 16s
Reviewed-on: #26
2024-09-30 16:09:10 +00:00
55b0a1b793 make ui at least usable throughout
Some checks failed
Build and release APK / Build APK (pull_request) Has been cancelled
2024-09-30 18:07:27 +02:00
39df97f4d1 fix multiline secrets by using base64 2024-09-30 18:07:17 +02:00
43aa26a107 small kubeconfig dum dum
All checks were successful
Build and deploy the backend to production / Build and push image (push) Successful in 1m43s
/ push-to-remote (push) Successful in 12s
Build and deploy the backend to production / Deploy to production (push) Successful in 13s
2024-09-27 10:41:04 +02:00
badb8ff919 Merge pull request 'ensure attractiveness is always an int' (#25) from fix/backend/pydantic-issues into main
Reviewed-on: #25
2024-09-27 08:28:37 +00:00
290baec64e associated backend fixes
Some checks failed
Build and deploy the backend to staging / Deploy to staging (pull_request) Has been cancelled
Build and deploy the backend to staging / Build and push image (pull_request) Has been cancelled
Build and release APK / Build APK (pull_request) Has been cancelled
Build and deploy the backend to production / Build and push image (push) Successful in 1m39s
/ push-to-remote (push) Successful in 12s
Build and deploy the backend to production / Deploy to production (push) Failing after 13s
2024-09-27 10:28:06 +02:00
3710a476e8 don't break when using sets
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 1m45s
Build and deploy the backend to staging / Deploy to staging (pull_request) Failing after 13s
2024-09-27 10:13:33 +02:00
cdc9b0ecd1 ensure attractiveness is always an int
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 2m4s
Build and deploy the backend to staging / Deploy to staging (pull_request) Failing after 18s
2024-09-27 09:47:10 +02:00
097abc5f29 remove gradle again
Some checks failed
Build and deploy the backend to production / Deploy to production (push) Has been cancelled
Build and deploy the backend to production / Build and push image (push) Has been cancelled
/ push-to-remote (push) Successful in 12s
2024-09-25 17:49:08 +02:00
55f8c938fb cleanup 2024-09-25 16:50:34 +02:00
59f3f0d454 update screenshots
Some checks failed
Build and deploy the backend to production / Deploy to production (push) Has been cancelled
Build and deploy the backend to production / Build and push image (push) Has been cancelled
/ push-to-remote (push) Successful in 41s
2024-09-25 16:20:33 +02:00
06dc0c7c3b Merge pull request 'Usability and styling' (#24) from feature/frontend-usability-and-styling into main
Some checks failed
Build and deploy the backend to production / Deploy to production (push) Has been cancelled
Build and deploy the backend to production / Build and push image (push) Has been cancelled
/ push-to-remote (push) Successful in 12s
Reviewed-on: #24
2024-09-25 13:26:19 +00:00
d37fb09d62 trip destination from current location
Some checks failed
Build and release APK / Build APK (pull_request) Failing after 5m54s
2024-09-25 15:10:49 +02:00
88b825ea31 better visual coherence 2024-09-25 14:48:33 +02:00
d323194ea7 Better location handling on map 2024-09-24 23:48:07 +02:00
ed60fcba06 revamp new trip flow 2024-09-24 22:58:28 +02:00
eaa2334942 ensure new images are pulled upon redeployment 2024-09-22 15:13:32 +02:00
f71aab22dc Merge branch 'feature/adding-timed-visits'
Some checks failed
Build and deploy the backend to production / Build and push image (push) Successful in 2m3s
/ push-to-remote (push) Failing after 22s
Build and deploy the backend to production / Deploy to production (push) Successful in 12s
2024-09-22 15:04:56 +02:00
1f7b006a64 Merge pull request 'Fix deployment configuration' (#23) from test/backend-deploy-pipeline into main
All checks were successful
Build and deploy the backend to production / Build and push image (push) Successful in 1m49s
/ push-to-remote (push) Successful in 12s
Build and deploy the backend to production / Deploy to production (push) Successful in 12s
Reviewed-on: #23
2024-09-22 12:31:20 +00:00
e5ea6e64e7 fix ingress
All checks were successful
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 2m13s
Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 13s
2024-09-21 16:04:35 +02:00
9deb461925 a bit of documentation
All checks were successful
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 1m43s
Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 13s
2024-09-21 15:10:48 +02:00
7414f7109d updated deployment config
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 1m38s
Build and deploy the backend to staging / Deploy to staging (pull_request) Failing after 12s
2024-09-21 14:56:40 +02:00
94d12f2983 even more fixes
All checks were successful
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 1m38s
Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 9s
2024-09-21 14:22:28 +02:00
1ade92aed3 better timing
All checks were successful
Build and push docker image / Build (pull_request) Successful in 1m49s
Build and release APK / Build APK (pull_request) Successful in 5m3s
2024-09-10 18:12:17 +02:00
d1c53e08bb improved tour length
All checks were successful
Build and push docker image / Build (pull_request) Successful in 1m39s
Build and release APK / Build APK (pull_request) Successful in 5m14s
2024-09-10 17:23:01 +02:00
797db5c1da set start and finish as primary
All checks were successful
Build and push docker image / Build (pull_request) Successful in 1m34s
Build and release APK / Build APK (pull_request) Successful in 5m3s
2024-09-10 17:11:36 +02:00
055089bf76 better landmarks log
All checks were successful
Build and push docker image / Build (pull_request) Successful in 2m5s
Build and release APK / Build APK (pull_request) Successful in 4m46s
2024-09-10 17:07:26 +02:00
3475990e5f fixed timing and optimizer speed
All checks were successful
Build and push docker image / Build (pull_request) Successful in 1m49s
Build and release APK / Build APK (pull_request) Successful in 5m40s
2024-09-10 16:53:16 +02:00
20f20da9c5 fixed int score
All checks were successful
Build and push docker image / Build (pull_request) Successful in 1m43s
Build and release APK / Build APK (pull_request) Successful in 6m36s
2024-09-10 10:16:01 +02:00
728b954643 added image and website urls
All checks were successful
Build and push docker image / Build (pull_request) Successful in 2m15s
Build and release APK / Build APK (pull_request) Successful in 6m40s
2024-08-28 18:08:09 +02:00
87 changed files with 2437 additions and 2066 deletions

View File

@@ -18,6 +18,7 @@ jobs:
name: Deploy to production
uses: ./.gitea/workflows/workflow_deploy-container.yaml
with:
environment: prod
overlay: prod
secrets:
PACKAGE_REGISTRY_ACCESS: ${{ secrets.KUBE_CONFIG }}
KUBE_CONFIG: ${{ secrets.KUBE_CONFIG }}
needs: build-and-push

View File

@@ -17,9 +17,10 @@ jobs:
PACKAGE_REGISTRY_ACCESS: ${{ secrets.PACKAGE_REGISTRY_ACCESS }}
deploy-prod:
name: Deploy to production
name: Deploy to staging
uses: ./.gitea/workflows/workflow_deploy-container.yaml
with:
environment: stg
overlay: stg
secrets:
PACKAGE_REGISTRY_ACCESS: ${{ secrets.KUBE_CONFIG }}
KUBE_CONFIG: ${{ secrets.KUBE_CONFIG }}
needs: build-and-push

View File

@@ -34,5 +34,5 @@ jobs:
uses: docker/build-push-action@v5
with:
context: backend
tags: git.kluster.moll.re/anydev/anyway-backend:${{intputs.tag}}
tags: git.kluster.moll.re/anydev/anyway-backend:${{ inputs.tag }}
push: true

View File

@@ -32,3 +32,4 @@ 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

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,9 +9,11 @@ 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. The deployment configuration is located under [https://git.kluster.moll.re/anydev/deployment-backend/](https://git.kluster.moll.re/anydev/deployment-backend/).
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/).
## 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):
if os.getenv('DEBUG', "false") == "true":
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. Ca
# upon creation of the trip, persistence of both the trip and its landmarks is ensured
trip = Trip.from_linked_landmarks(linked_tour, cache_client)
return trip
@@ -84,4 +84,4 @@ def get_landmark(landmark_uuid: str) -> Landmark:
landmark = cache_client.get(f"landmark_{landmark_uuid}")
return landmark
except KeyError:
raise HTTPException(status_code=404, detail="Landmark not found")
raise HTTPException(status_code=404, detail="Landmark not found")

View File

@@ -1,3 +1,6 @@
# 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: ''
@@ -11,7 +14,24 @@ nature:
- alpine_hut
- viewpoint
- zoo
waterway: waterfall
- resort
- picnic_site
water:
- pond
- lake
- river
- basin
- stream
- lagoon
- rapids
waterway:
- waterfall
- river
- canal
- dam
- dock
- boatyard
shopping:
shop:
@@ -23,10 +43,48 @@ 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.75
church_coeff: 0.5
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: 20
overshoot: 1.3
max_landmarks_refiner: 30
overshoot: 1.8

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

@@ -5,7 +5,7 @@ from uuid import uuid4
# Output to frontend
class Landmark(BaseModel) :
# Properties of the landmark
name : str
type: Literal['sightseeing', 'nature', 'shopping', 'start', 'finish']
@@ -14,26 +14,39 @@ class Landmark(BaseModel) :
osm_id : int
attractiveness : int
n_tags : int
image_url : Optional[str] = None # TODO future
image_url : Optional[str] = None
website_url : Optional[str] = None
description : Optional[str] = None # TODO future
duration : Optional[int] = 0 # TODO future
duration : Optional[int] = 0
name_en : Optional[str] = None
# Unique ID of a given landmark
uuid: str = Field(default_factory=uuid4) # TODO implement this ASAP
uuid: str = Field(default_factory=uuid4)
# 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 # TODO fix this in existing code
next_uuid : Optional[str] = None # TODO implement this ASAP
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
def __hash__(self) -> int:
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}'
return hash(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:
if landmark.attractiveness < threshold_score and landmark.type not in ["start", "finish"]:
landmark.is_secondary = True

View File

@@ -22,9 +22,10 @@ class Trip(BaseModel):
# Store the trip in the cache
cache_client.set(f"trip_{trip.uuid}", trip)
cache_client.set_many({f"landmark_{landmark.uuid}": landmark for landmark in landmarks}, expire=3600)
# 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)
# is equivalent to:
# for landmark in landmarks:
# cache_client.set(f"landmark_{landmark.uuid}", landmark, expire=3600)
return trip
return trip

View File

@@ -23,9 +23,8 @@ 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=300,
detour_tolerance_minute=15
max_time_minute=100,
detour_tolerance_minute=0
)
# Create start and finish
@@ -64,10 +63,7 @@ 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}")
total_time += l.duration
total_time += l.time_to_reach_next
logger.info(f"Total time: {total_time}")
logger.info(f"Estimated length of tour : {linked_tour.total_time} mintutes and visiting {len(linked_tour._landmarks)} landmarks.")
# with open('linked_tour.yaml', 'w') as f:
# yaml.dump(linked_tour.asdict(), f)
@@ -78,6 +74,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.7576485, 4.8330241))) # Lyon Bellecour
# test(tuple((48.5848435, 7.7332974))) # Strasbourg Gare
# test(tuple((45.758217, 4.831814))) # 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 geopy.distance import geodesic
from math import sin, cos, sqrt, atan2, radians
import constants
@@ -8,6 +8,7 @@ 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:
"""
@@ -16,24 +17,34 @@ 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.
"""
# Compute the straight-line distance in km
if p1 == p2 :
if p1 == p2:
return 0
else:
dist = geodesic(p1, p2).kilometers
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])
# Consider the detour factor for average cityto deterline walking distance (in km)
walk_dist = dist*DETOUR_FACTOR
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
# Time to walk this distance (in minutes)
walk_time = walk_dist/AVERAGE_WALKING_SPEED*60
walk_time = walk_distance / AVERAGE_WALKING_SPEED * 60
return round(walk_time)

View File

@@ -1,20 +1,17 @@
import math as m
import math
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, Preference
from structs.preferences import Preferences
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:
@@ -46,7 +43,7 @@ class LandmarkManager:
self.viewpoint_bonus = parameters['viewpoint_bonus']
self.pay_bonus = parameters['pay_bonus']
self.N_important = parameters['N_important']
with constants.OPTIMIZER_PARAMETERS_PATH.open('r') as f:
parameters = yaml.safe_load(f)
self.walking_speed = parameters['average_walking_speed']
@@ -69,87 +66,42 @@ 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)
L = []
# use set to avoid duplicates, this requires some __methods__ to be set in Landmark
all_landmarks = set()
bbox = self.create_bbox(center_coordinates, reachable_bbox_side)
# list for sightseeing
if preferences.sightseeing.score != 0:
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
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)
# list for nature
if preferences.nature.score != 0:
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
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)
# list for shopping
if preferences.shopping.score != 0:
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
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)
L = self.remove_duplicates(L)
# self.correct_score(L, preferences)
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_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 all_landmarks, landmarks_constrained
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:
@@ -172,7 +124,7 @@ class LandmarkManager:
radius = self.radius_close_to
alpha = (180*radius) / (6371000*m.pi)
alpha = (180 * radius) / (6371000 * math.pi)
bbox = {'latLower':lat-alpha,'lonLower':lon-alpha,'latHigher':lat+alpha,'lonHigher': lon+alpha}
# Build the query to find elements within the radius
@@ -216,7 +168,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 * m.cos(m.radians(lat))) # Adjust for longitude based on latitude
lon_diff = half_side_length_km / (111 * math.cos(math.radians(lat))) # Adjust for longitude based on latitude
# Calculate bbox
min_lat = lat - lat_diff
@@ -255,32 +207,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 = [],
conditions = ['count_tags()>5'],
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}")
return
continue
for elem in result.elements():
name = elem.tag('name') # Add name
location = (elem.centerLat(), elem.centerLon()) # Add coordinates (lat, lon)
name = elem.tag('name')
location = (elem.centerLat(), elem.centerLon())
# 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
@@ -290,31 +242,29 @@ 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:
score += self.pay_bonus # discard payment options for tags
# payment options are a good sign
score += self.pay_bonus
if "disused" in tag:
skip = True # skip disused amenities
# skip disused amenities
skip = True
break
if "wiki" in tag:
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
# wikipedia entries count more
score += self.wikipedia_bonus
if "viewpoint" in tag:
score += self.viewpoint_bonus
duration = 10
if "image" in tag:
score += self.image_bonus
@@ -331,38 +281,43 @@ 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 = int(score*self.church_coeff)
duration = 20
if "place_of_worship" in elem.tags().values():
score = score * self.church_coeff
duration = 15
elif "museum" in elem.tags().values() :
score = int(score*self.church_coeff)
elif "museum" in elem.tags().values():
score = score * self.church_coeff
duration = 60
elif "fountain" in elem.tags().values() :
else:
duration = 5
elif "park" in elem.tags().values() :
duration = 30
else :
duration = 15
# Generate the landmark and append it to the list
# finally create our own landmark object
landmark = Landmark(
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
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
)
return_list.append(landmark)
@@ -386,7 +341,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,7 +3,6 @@ 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
@@ -21,7 +20,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 # experimentally determined overshoot possibility to return long enough tours
overshoot: float # overshoot to allow maxtime to overflow. Optimizer is a bit restrictive
def __init__(self) :
@@ -178,7 +177,7 @@ class Optimizer:
Args:
landmarks (list[Landmark]): List of landmarks.
max_time (int): Maximum time allowed for tour.
max_time (int): Maximum time of visit allowed.
Returns:
Tuple[list[float], list[float], list[int]]: Objective function coefficients, inequality constraint coefficients, and the right-hand side of the inequality constraint.
@@ -195,7 +194,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)[:22]
closest = sorted(dist_table)[:25]
for i, dist in enumerate(dist_table) :
if dist not in closest :
dist_table[i] = 32700
@@ -476,7 +475,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_steps is limiting factor)
A, b = self.respect_order(L) # Respect order of visit (only works when max_time 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, len(visited_landmarks))
return take_most_important.take_most_important(second_order_landmarks, int(self.max_landmarks_refiner*0.75))
# Try fix the shortest path using shapely

View File

@@ -1,38 +1,16 @@
from structs.landmark import Landmark
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 = {}
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
"""
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
scores = [0]*len(L_copy)
for i, elem in enumerate(L_copy) :
scores[i] = elem.attractiveness
# Sort landmarks by attractiveness (descending)
sorted_landmarks = sorted(landmarks, key=lambda x: x.attractiveness, reverse=True)
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
return sorted_landmarks[:n_important]

View File

@@ -1,7 +1,7 @@
on:
push:
branches:
- main
tags:
- 'v*'
jobs:
build:
@@ -23,6 +23,13 @@ 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
@@ -30,13 +37,13 @@ jobs:
REF_NAME: ${{ github.ref_name }}
run:
# remove the 'v' prefix from the tag name
echo "VERSION=${REF_NAME//v}" >> $GITHUB_OUTPUT
echo "VERSION_NAME=${REF_NAME//v}" >> $GITHUB_ENV
- name: Load secrets from github
run: |
echo "${{ secrets.ANDROID_SECRET_PROPERTIES }}" > secrets.properties
echo "${{ secrets.ANDROID_KEYSTORE }}" > release.keystore
echo "${{ secrets.ANDROID_GOOGLE_PLAY_JSON }}" > google-key.json
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
working-directory: android
- name: Install fastlane
@@ -44,7 +51,6 @@ jobs:
working-directory: android
- name: Run fastlane lane
run: bundle exec fastlane deploy_release
run: bundle exec fastlane deploy_testing
working-directory: android
env:
VERSION_NAME: ${{ steps.version.VERSION }}
# the environment variable VERSION_NAME is implicitly available

View File

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

View File

@@ -1,6 +1,9 @@
<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

@@ -4,22 +4,25 @@
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
gradle(
task: "bundle",
# flavor: "staging",
)
version_name = ENV["VERSION_NAME"]
sh(
"flutter",
"build",
"appbundle",
"--release",
"--build-name=#{version_name}",
)
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 it is relative)
)
end
@@ -36,6 +39,8 @@ 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 it is relative)
)
end
end

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 KiB

View File

@@ -1,5 +0,0 @@
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

View File

@@ -1,160 +0,0 @@
#!/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 "1.7.10" apply false
id "org.jetbrains.kotlin.android" version "2.0.20" apply false
}
include ":app"

View File

@@ -0,0 +1,3 @@
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,6 +1,86 @@
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,3 +1,4 @@
import 'package:anyway/pages/settings.dart';
import 'package:flutter/material.dart';
import 'package:anyway/constants.dart';
@@ -6,10 +7,9 @@ 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.dart';
import 'package:anyway/pages/new_trip_location.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 now"),
label: Text("Plan a trip"),
),
);
}
@@ -74,33 +74,33 @@ class _BasePageState extends State<BasePage> {
}
} else if (widget.mainScreen == "tutorial") {
currentView = OnboardingPage();
} else if (widget.mainScreen == "profile") {
currentView = ProfilePage();
} else if (widget.mainScreen == "settings") {
currentView = SettingsPage();
}
final ThemeData theme = Theme.of(context);
return Scaffold(
appBar: AppBar(title: Text(APP_NAME)),
body: Center(child: currentView),
drawer: Drawer(
child: Column(
children: [
DrawerHeader(
Container(
decoration: BoxDecoration(
gradient: LinearGradient(colors: [Colors.red, Colors.yellow])
gradient: APP_GRADIENT,
),
height: 150,
child: Center(
child: Text(
APP_NAME,
style: TextStyle(
color: Colors.grey[800],
color: Colors.white,
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(),
const Divider(indent: 10, endIndent: 10),
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 == "profile",
selected: widget.mainScreen == "settings",
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => BasePage(mainScreen: "profile")
builder: (context) => BasePage(mainScreen: "settings")
)
);
},

View File

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

View File

@@ -0,0 +1,37 @@
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,122 +1,50 @@
import 'dart:developer';
import 'package:anyway/structs/trip.dart';
import 'package:flutter/material.dart';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
import 'package:anyway/pages/current_trip.dart';
import 'package:anyway/structs/trip.dart';
class Greeter extends StatefulWidget {
class CurrentTripGreeter extends StatefulWidget {
final Trip trip;
Greeter({
CurrentTripGreeter({
super.key,
required this.trip,
});
@override
State<Greeter> createState() => _GreeterState();
State<CurrentTripGreeter> createState() => _CurrentTripGreeterState();
}
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
);
}
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
);
}
);
} 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 == start ) {
if (trip.landmarks.isEmpty || trip.landmarks.length <= 1 && trip.landmarks.first.type == typeStart ) {
children.add(
const Text("No landmarks in this trip"),
);
@@ -32,7 +32,6 @@ 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

@@ -0,0 +1,60 @@
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,27 +1,28 @@
import 'dart:collection';
import 'package:anyway/constants.dart';
import 'package:anyway/modules/themed_marker.dart';
import 'package:anyway/modules/landmark_map_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 MapWidget extends StatefulWidget {
class CurrentTripMap extends StatefulWidget {
final Trip? trip;
MapWidget({
CurrentTripMap({
this.trip
});
@override
State<MapWidget> createState() => _MapWidgetState();
State<CurrentTripMap> createState() => _CurrentTripMapState();
}
class _MapWidgetState extends State<MapWidget> {
class _CurrentTripMapState extends State<CurrentTripMap> {
late GoogleMapController mapController;
CameraPosition _cameraPosition = CameraPosition(
@@ -67,9 +68,27 @@ class _MapWidgetState extends State<MapWidget> {
});
}
@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,
@@ -79,7 +98,9 @@ class _MapWidgetState extends State<MapWidget> {
cloudMapId: MAP_ID,
mapToolbarEnabled: false,
zoomControlsEnabled: false,
myLocationEnabled: useLocation,
myLocationButtonEnabled: false,
);
}
}

View File

@@ -0,0 +1,82 @@
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,4 +1,5 @@
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';
@@ -8,6 +9,13 @@ 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

@@ -0,0 +1,31 @@
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,10 +18,6 @@ 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(
@@ -40,7 +36,7 @@ class _LandmarkCardState extends State<LandmarkCard> {
width: 160,
child: CachedNetworkImage(
imageUrl: widget.landmark.imageURL ?? '',
placeholder: (context, url) => CircularProgressIndicator(),
placeholder: (context, url) => Center(child: 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
@@ -88,21 +84,18 @@ 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!));
@@ -112,7 +105,6 @@ 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,3 +1,4 @@
import 'package:anyway/constants.dart';
import 'package:anyway/structs/landmark.dart';
import 'package:flutter/material.dart';
@@ -16,21 +17,9 @@ 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 != start && landmark.type != finish) {
if (landmark.type != typeStart && landmark.type != typeFinish) {
positionIndicator = Positioned(
top: 0,
right: 0,
@@ -51,14 +40,14 @@ class ThemedMarker extends StatelessWidget {
children: [
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.red, Colors.yellow]
),
gradient: APP_GRADIENT,
shape: BoxShape.circle,
border: Border.all(color: Colors.black, width: 5),
),
padding: EdgeInsets.all(5),
child: icon
width: 70,
height: 70,
padding: const EdgeInsets.all(5),
child: Icon(landmark.type.icon.icon, size: 50),
),
if (positionIndicator != null) positionIndicator,
],

View File

@@ -1,4 +1,5 @@
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';
@@ -8,8 +9,12 @@ import 'package:flutter/material.dart';
class NewTripButton extends StatefulWidget {
final Trip trip;
final UserPreferences preferences;
const NewTripButton({required this.trip});
const NewTripButton({
required this.trip,
required this.preferences,
});
@override
State<NewTripButton> createState() => _NewTripButtonState();
@@ -23,42 +28,39 @@ 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: () 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,
);
}
},
)
);
}
onPressed: onPressed,
icon: const Icon(Icons.directions),
label: AutoSizeText('Start planning!'),
);
}
);
}
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,9 +6,13 @@ 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,
);
@@ -39,26 +43,66 @@ class _NewTripLocationSearchState extends State<NewTripLocationSearch> {
uuid: 'pending',
name: query,
location: [location.latitude, location.longitude],
type: start
type: typeStart
)
);
}
}
@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(
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(
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/themed_marker.dart';
import 'package:anyway/modules/landmark_map_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 = CameraPosition(
final CameraPosition _cameraPosition = const 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: start
type: typeStart
)
);
}
@@ -70,10 +70,26 @@ 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,
@@ -82,6 +98,8 @@ class _NewTripMapState extends State<NewTripMap> {
cloudMapId: MAP_ID,
mapToolbarEnabled: false,
zoomControlsEnabled: false,
myLocationButtonEnabled: false,
myLocationEnabled: useLocation,
);
}
}

View File

@@ -0,0 +1,41 @@
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,20 +16,23 @@ class OnboardingCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
Color baseColor = Theme.of(context).primaryColor;
Color baseColor = Theme.of(context).colorScheme.secondary;
// 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)),
@@ -44,7 +47,8 @@ class OnboardingCard extends StatelessWidget {
fontSize: 16,
),
),
],
]
),
)
);

View File

@@ -1,12 +1,17 @@
import 'package:anyway/modules/current_trip_save_button.dart';
import 'package:anyway/constants.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 {
@@ -27,16 +32,21 @@ class _TripPageState extends State<TripPage> {
@override
Widget build(BuildContext context) {
return SlidingUpPanel(
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: [
// 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 [
BoxShadow(
blurRadius: 20.0,
color: Colors.black,
@@ -44,41 +54,4 @@ 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,11 +1,7 @@
import 'package:anyway/modules/new_trip_button.dart';
import 'package:anyway/structs/landmark.dart';
import 'package:anyway/modules/new_trip_options_button.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';
@@ -19,7 +15,6 @@ 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();
@@ -40,7 +35,7 @@ class _NewTripPageState extends State<NewTripPage> {
),
],
),
floatingActionButton: NewTripButton(trip: trip),
floatingActionButton: NewTripOptionsButton(trip: trip),
);
}
}

View File

@@ -0,0 +1,113 @@
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.dart';
import 'package:anyway/pages/new_trip_location.dart';
import 'package:flutter/material.dart';
class OnboardingPage extends StatefulWidget {

View File

@@ -1,177 +0,0 @@
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

@@ -0,0 +1,186 @@
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 sightseeing = LandmarkType(name: 'sightseeing');
LandmarkType nature = LandmarkType(name: 'nature');
LandmarkType shopping = LandmarkType(name: 'shopping');
LandmarkType typeSightseeing = LandmarkType(name: 'sightseeing');
LandmarkType typeNature = LandmarkType(name: 'nature');
LandmarkType typeShopping = LandmarkType(name: 'shopping');
// LandmarkType museum = LandmarkType(name: 'Museum');
// LandmarkType restaurant = LandmarkType(name: 'Restaurant');
LandmarkType start = LandmarkType(name: 'start');
LandmarkType finish = LandmarkType(name: 'finish');
LandmarkType typeStart = LandmarkType(name: 'start');
LandmarkType typeFinish = 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 = Icon(Icons.church);
icon = const Icon(Icons.church);
break;
case 'nature':
icon = Icon(Icons.eco);
icon = const Icon(Icons.eco);
break;
case 'shopping':
icon = Icon(Icons.shopping_cart);
icon = const Icon(Icons.shopping_cart);
break;
case 'start':
icon = Icon(Icons.play_arrow);
icon = const Icon(Icons.play_arrow);
break;
case 'finish':
icon = Icon(Icons.flag);
icon = const Icon(Icons.flag);
break;
default:
icon = Icon(Icons.location_on);
icon = const 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,16 +20,6 @@ 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;
}
}
@@ -39,64 +29,41 @@ class UserPreferences {
slug: "sightseeing",
description: "How much do you like sightseeing?",
value: 0,
icon: Icon(Icons.church),
icon: typeSightseeing.icon,
);
SinglePreference shopping = SinglePreference(
name: "Shopping",
slug: "shopping",
description: "How much do you like shopping?",
value: 0,
icon: Icon(Icons.shopping_bag),
icon: typeShopping.icon,
);
SinglePreference nature = SinglePreference(
name: "Nature",
slug: "nature",
description: "How much do you like nature?",
value: 0,
icon: Icon(Icons.landscape),
icon: typeNature.icon,
);
SinglePreference maxTime = SinglePreference(
name: "Trip duration",
slug: "duration",
description: "How long do you want your trip to be?",
description: "How long should your trip be?",
value: 30,
minVal: 30,
maxVal: 720,
icon: Icon(Icons.timer),
);
SinglePreference maxDetour = SinglePreference(
name: "Trip detours",
slug: "detours",
description: "Are you okay with roaming even if makes the trip longer?",
value: 0,
maxVal: 30,
icon: Icon(Icons.loupe_sharp),
);
Future<void> load() async {
for (SinglePreference pref in [sightseeing, shopping, nature, maxTime, maxDetour]) {
pref.load();
}
}
Map<String, dynamic> toJson() {
// This is "opinionated" JSON, corresponding to the backend's expectations
return {
"sightseeing": {"type": "sightseeing", "score": sightseeing.value},
"shopping": {"type": "shopping", "score": shopping.value},
"nature": {"type": "nature", "score": nature.value},
"max_time_minute": maxTime.value,
"detour_tolerance_minute": maxDetour.value
"max_time_minute": maxTime.value
};
}
}
Future<UserPreferences> loadUserPreferences() async {
UserPreferences prefs = UserPreferences();
await prefs.load();
return prefs;
}

View File

@@ -29,29 +29,41 @@ Dio dio = Dio(
fetchTrip(
Trip trip,
Future<UserPreferences> preferences,
UserPreferences preferences,
) async {
UserPreferences prefs = await preferences;
Map<String, dynamic> data = {
"preferences": prefs.toJson(),
"preferences": preferences.toJson(),
"start": trip.landmarks!.first.location,
};
String dataString = jsonEncode(data);
log(dataString);
final response = await dio.post(
"/trip/new",
data: data
);
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;
}
// handle errors
if (response.statusCode != 200) {
trip.updateUUID("error");
if (response.data["detail"] != null) {
trip.updateError(response.data["detail"]);
log(response.data["detail"]);
// throw Exception(response.data["detail"]);
String errorDetail;
if (response.data.runtimeType == String) {
errorDetail = response.data;
} else {
errorDetail = response.data["detail"] ?? "Unknown error";
}
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,12 +5,14 @@
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: "4a5d8d2c728b0f3d0245f69f921d7be90cae4c2fd5288f773088672c0893f819"
sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916"
url: "https://pub.dev"
source: hosted
version: "3.4.0"
version: "3.4.1"
cached_network_image_platform_interface:
dependency: transitive
description:
name: cached_network_image_platform_interface
sha256: ff0c949e323d2a1b52be73acce5b4a7b04063e61414c8ca542dbba47281630a7
sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829"
url: "https://pub.dev"
source: hosted
version: "4.1.0"
version: "4.1.1"
cached_network_image_web:
dependency: transitive
description:
name: cached_network_image_web
sha256: "6322dde7a5ad92202e64df659241104a43db20ed594c41ca18de1014598d7996"
sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
version: "1.3.1"
characters:
dependency: transitive
description:
@@ -109,10 +109,10 @@ packages:
dependency: transitive
description:
name: crypto
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27
url: "https://pub.dev"
source: hosted
version: "3.0.3"
version: "3.0.5"
csslib:
dependency: transitive
description:
@@ -133,18 +133,18 @@ packages:
dependency: "direct main"
description:
name: dio
sha256: e17f6b3097b8c51b72c74c9f071a605c47bcc8893839bd66732457a5ebe73714
sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260"
url: "https://pub.dev"
source: hosted
version: "5.5.0+1"
version: "5.7.0"
dio_web_adapter:
dependency: transitive
description:
name: dio_web_adapter
sha256: "36c5b2d79eb17cdae41e974b7a8284fec631651d2a6f39a8a2ff22327e90aeac"
sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
version: "2.0.0"
fake_async:
dependency: transitive
description:
@@ -157,10 +157,10 @@ packages:
dependency: transitive
description:
name: ffi
sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21"
sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "2.1.3"
file:
dependency: transitive
description:
@@ -186,10 +186,10 @@ packages:
dependency: transitive
description:
name: flutter_cache_manager
sha256: a77f77806a790eb9ba0118a5a3a936e81c4fea2b61533033b2b0c3d50bbde5ea
sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
url: "https://pub.dev"
source: hosted
version: "3.4.0"
version: "3.4.1"
flutter_launcher_icons:
dependency: "direct main"
description:
@@ -210,10 +210,10 @@ packages:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: "9d98bd47ef9d34e803d438f17fd32b116d31009f534a6fa5ce3a1167f189a6de"
sha256: "9ee02950848f61c4129af3d6ec84a1cfc0e47931abc746b03e7a3bc3e8ff6eda"
url: "https://pub.dev"
source: hosted
version: "2.0.21"
version: "2.0.22"
flutter_svg:
dependency: "direct main"
description:
@@ -264,6 +264,54 @@ 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:
@@ -276,42 +324,42 @@ packages:
dependency: "direct main"
description:
name: google_maps_flutter
sha256: acf0ec482d86b2ac55ade80597ce7f797a47971f5210ebfd030f0d58130e0a94
sha256: "2e302fa3aaf4e2a297f0342d83ebc5e8e9f826e9a716aef473fe7f404ec630a7"
url: "https://pub.dev"
source: hosted
version: "2.7.0"
version: "2.9.0"
google_maps_flutter_android:
dependency: transitive
description:
name: google_maps_flutter_android
sha256: "5d444f4135559488d7ea325eae710ae3284e6951b1b61729a0ac026456fe1548"
sha256: "10cf27bee8c560f8e69992b3a0f27ddf1d7acbea622ddb13ef3f587848a73f26"
url: "https://pub.dev"
source: hosted
version: "2.12.1"
version: "2.14.7"
google_maps_flutter_ios:
dependency: transitive
description:
name: google_maps_flutter_ios
sha256: a6e3c6ecdda6c985053f944be13a0645ebb919da2ef0f5bc579c5e1670a5b2a8
sha256: "3a484846fc56f15e47e3de1f5ea80a7ff2b31721d2faa88f390f3b3cf580c953"
url: "https://pub.dev"
source: hosted
version: "2.10.0"
version: "2.13.0"
google_maps_flutter_platform_interface:
dependency: transitive
description:
name: google_maps_flutter_platform_interface
sha256: bd60ca330e3c7763b95b477054adec338a522d982af73ecc520b232474063ac5
sha256: "099874463dc4c9bff04fe4b2b8cf7284d2455c2deead8f9a59a87e1b9f028c69"
url: "https://pub.dev"
source: hosted
version: "2.8.0"
version: "2.9.2"
google_maps_flutter_web:
dependency: transitive
description:
name: google_maps_flutter_web
sha256: "8d5d0f58bfc4afac0bbe3d399f2018fcea691e3ea3d35254b7aae56df5827659"
sha256: ff39211bd25d7fad125d19f757eba85bd154460907cd4d135e07e3d0f98a4130
url: "https://pub.dev"
source: hosted
version: "0.5.9+1"
version: "0.5.10"
html:
dependency: transitive
description:
@@ -388,10 +436,10 @@ packages:
dependency: "direct main"
description:
name: map_launcher
sha256: af59b9f79f641022e06761c9d4217c6c57b9ef9020af2fdb23155ec87af79e61
sha256: "7436d6ef9ae57ff15beafcedafe0a8f0604006cbecd2d26024c4cfb0158c2b9a"
url: "https://pub.dev"
source: hosted
version: "3.3.1"
version: "3.5.0"
matcher:
dependency: transitive
description:
@@ -460,10 +508,10 @@ packages:
dependency: transitive
description:
name: path_provider_android
sha256: "490539678396d4c3c0b06efdaab75ae60675c3e0c66f72bc04c2e2c1e0e2abeb"
sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7"
url: "https://pub.dev"
source: hosted
version: "2.2.9"
version: "2.2.10"
path_provider_foundation:
dependency: transitive
description:
@@ -496,6 +544,54 @@ 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:
@@ -548,34 +644,34 @@ packages:
dependency: "direct main"
description:
name: shared_preferences
sha256: c3f888ba2d659f3e75f4686112cc1e71f46177f74452d40d8307edc332296ead
sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051"
url: "https://pub.dev"
source: hosted
version: "2.3.0"
version: "2.3.2"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "041be4d9d2dc6079cf342bc8b761b03787e3b71192d658220a56cac9c04a0294"
sha256: "480ba4345773f56acda9abf5f50bd966f581dac5d514e5fc4a18c62976bbba7e"
url: "https://pub.dev"
source: hosted
version: "2.3.0"
version: "2.3.2"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "671e7a931f55a08aa45be2a13fe7247f2a41237897df434b30d2012388191833"
sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f
url: "https://pub.dev"
source: hosted
version: "2.5.0"
version: "2.5.2"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "2ba0510d3017f91655b7543e9ee46d48619de2a2af38e5c790423f7007c7ccc1"
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
version: "2.4.1"
shared_preferences_platform_interface:
dependency: transitive
description:
@@ -588,18 +684,18 @@ packages:
dependency: transitive
description:
name: shared_preferences_web
sha256: "3a293170d4d9403c3254ee05b84e62e8a9b3c5808ebd17de6a33fe9ea6457936"
sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e
url: "https://pub.dev"
source: hosted
version: "2.4.0"
version: "2.4.2"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "398084b47b7f92110683cac45c6dc4aae853db47e470e5ddcd52cab7f7196ab2"
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
version: "2.4.1"
sky_engine:
dependency: transitive
description: flutter
@@ -641,10 +737,10 @@ packages:
dependency: transitive
description:
name: sqflite_common
sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4"
sha256: "4058172e418eb7e7f2058dcb7657d451a8fc264afa0dea4dbd0f304a57131611"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
version: "2.5.4+3"
stack_trace:
dependency: transitive
description:
@@ -681,10 +777,10 @@ packages:
dependency: transitive
description:
name: synchronized
sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558"
sha256: "51b08572b9f091f8c3eb4d9d4be253f196ff0075d5ec9b10a884026d5b55d7bc"
url: "https://pub.dev"
source: hosted
version: "3.1.0+1"
version: "3.3.0+2"
term_glyph:
dependency: transitive
description:
@@ -745,10 +841,10 @@ packages:
dependency: transitive
description:
name: url_launcher_macos
sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de"
sha256: "769549c999acdb42b8bcfa7c43d72bf79a382ca7441ab18a808e101149daf672"
url: "https://pub.dev"
source: hosted
version: "3.2.0"
version: "3.2.1"
url_launcher_platform_interface:
dependency: transitive
description:
@@ -777,10 +873,10 @@ packages:
dependency: transitive
description:
name: uuid
sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90"
sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77
url: "https://pub.dev"
source: hosted
version: "4.4.2"
version: "4.5.0"
vector_graphics:
dependency: transitive
description:
@@ -825,10 +921,10 @@ packages:
dependency: transitive
description:
name: web
sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb
url: "https://pub.dev"
source: hosted
version: "0.5.1"
version: "1.1.0"
widget_to_marker:
dependency: "direct main"
description:

View File

@@ -49,6 +49,8 @@ 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,9 +6,15 @@
#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,6 +3,8 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
geolocator_windows
permission_handler_windows
url_launcher_windows
)