Compare commits
1 Commits
d4de945df8
...
v0.0.21
Author | SHA1 | Date | |
---|---|---|---|
1e54ff45d5 |
@@ -6,7 +6,7 @@ on:
|
||||
- frontend/**
|
||||
|
||||
|
||||
name: Build and release debug APK
|
||||
name: Build and release APK
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
ls -lah android
|
||||
working-directory: ./frontend
|
||||
|
||||
- run: flutter build apk --debug --split-per-abi --build-number=${{ gitea.run_number }}
|
||||
- run: flutter build apk --release --split-per-abi --build-number=${{ gitea.run_number }}
|
||||
working-directory: ./frontend
|
||||
|
||||
- name: Upload APKs to artifacts
|
||||
|
@@ -32,4 +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
|
||||
kubectl -n anyway-backend rollout restart deployment/anyway-backend-${{ inputs.overlay }}
|
||||
|
3
.vscode/settings.json
vendored
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"cmake.ignoreCMakeListsMissing": true
|
||||
}
|
@@ -9,9 +9,9 @@ name = "pypi"
|
||||
numpy = "*"
|
||||
fastapi = "*"
|
||||
pydantic = "*"
|
||||
geopy = "*"
|
||||
shapely = "*"
|
||||
scipy = "*"
|
||||
osmpythontools = "*"
|
||||
pywikibot = "*"
|
||||
pymemcache = "*"
|
||||
fastapi-cli = "*"
|
||||
|
2208
backend/Pipfile.lock
generated
@@ -1,37 +1,12 @@
|
||||
# Backend
|
||||
|
||||
This repository contains the backend code for the application. It utilizes **FastAPI** to quickly create a RESTful API that exposes the endpoints of the route optimizer.
|
||||
This repository contains the backend code for the application. It utilizes FastAPI that allows to quickly create a RESTful API that exposes the endpoints of the route optimizer.
|
||||
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Directory Structure
|
||||
- The code for the Python application is located in the `src` directory.
|
||||
- Package management is handled with **pipenv**, and the dependencies are listed in the `Pipfile`.
|
||||
- Since the application is designed to be deployed in a container, the `Dockerfile` is provided to build the image.
|
||||
|
||||
### Setting Up the Development Environment
|
||||
|
||||
To set up your development environment using **pipenv**, follow these steps:
|
||||
|
||||
1. Install `pipenv` by running:
|
||||
```bash
|
||||
sudo apt install pipenv
|
||||
```
|
||||
|
||||
2. Create and activate a virtual environment:
|
||||
```bash
|
||||
pipenv shell
|
||||
```
|
||||
|
||||
3. Install the dependencies listed in the `Pipfile`:
|
||||
```bash
|
||||
pipenv install
|
||||
```
|
||||
|
||||
4. The virtual environment will be created under:
|
||||
```bash
|
||||
~/.local/share/virtualenvs/...
|
||||
```
|
||||
- The code of the python application is located in the `src` directory.
|
||||
- Package management is handled with `pipenv` and the dependencies are listed in the `Pipfile`.
|
||||
- Since the application is aimed to be deployed in a container, the `Dockerfile` is provided to build the image.
|
||||
|
||||
### Deployment
|
||||
To deploy the backend docker container, we use kubernetes. Modifications to the backend are automatically pushed to a two-stage environment through the CI pipeline. See [deployment/README](deployment/README.md] for further information.
|
||||
|
@@ -16,7 +16,7 @@ OSM_CACHE_DIR = Path(cache_dir_string)
|
||||
|
||||
import logging
|
||||
# if we are in a debug session, set verbose and rich logging
|
||||
if os.getenv('DEBUG', "false") == "true":
|
||||
if os.getenv('DEBUG', False):
|
||||
from rich.logging import RichHandler
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
|
@@ -63,7 +63,7 @@ def new_trip(preferences: Preferences, start: tuple[float, float], end: tuple[fl
|
||||
refined_tour = refiner.refine_optimization(landmarks, base_tour, preferences.max_time_minute, preferences.detour_tolerance_minute)
|
||||
|
||||
linked_tour = LinkedLandmarks(refined_tour)
|
||||
# upon creation of the trip, persistence of both the trip and its landmarks is ensured
|
||||
# upon creation of the trip, persistence of both the trip and its landmarks is ensured. Ca
|
||||
trip = Trip.from_linked_landmarks(linked_tour, cache_client)
|
||||
return trip
|
||||
|
||||
@@ -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")
|
@@ -1,6 +1,3 @@
|
||||
# Tags were picked mostly arbitrarily, based on the OSM wiki and the OSM tags page.
|
||||
# See https://taginfo.openstreetmap.org for more inspiration.
|
||||
|
||||
nature:
|
||||
leisure: park
|
||||
geological: ''
|
||||
@@ -14,24 +11,7 @@ nature:
|
||||
- alpine_hut
|
||||
- viewpoint
|
||||
- zoo
|
||||
- resort
|
||||
- picnic_site
|
||||
water:
|
||||
- pond
|
||||
- lake
|
||||
- river
|
||||
- basin
|
||||
- stream
|
||||
- lagoon
|
||||
- rapids
|
||||
waterway:
|
||||
- waterfall
|
||||
- river
|
||||
- canal
|
||||
- dam
|
||||
- dock
|
||||
- boatyard
|
||||
|
||||
waterway: waterfall
|
||||
|
||||
shopping:
|
||||
shop:
|
||||
@@ -43,47 +23,10 @@ sightseeing:
|
||||
- museum
|
||||
- attraction
|
||||
- gallery
|
||||
- artwork
|
||||
- aquarium
|
||||
historic: ''
|
||||
amenity:
|
||||
- planetarium
|
||||
- place_of_worship
|
||||
- fountain
|
||||
- townhall
|
||||
water:
|
||||
- reflecting_pool
|
||||
bridge:
|
||||
- aqueduct
|
||||
- viaduct
|
||||
- boardwalk
|
||||
- cantilever
|
||||
- abandoned
|
||||
building:
|
||||
- church
|
||||
- chapel
|
||||
- mosque
|
||||
- synagogue
|
||||
- ruins
|
||||
- temple
|
||||
- government
|
||||
- cathedral
|
||||
- castle
|
||||
- museum
|
||||
|
||||
|
||||
|
||||
|
||||
# to be used later on
|
||||
restauration:
|
||||
shop:
|
||||
- coffee
|
||||
- bakery
|
||||
- restaurant
|
||||
- pastry
|
||||
amenity:
|
||||
- restaurant
|
||||
- cafe
|
||||
- ice_cream
|
||||
- food_court
|
||||
- biergarten
|
||||
|
@@ -1,6 +1,6 @@
|
||||
city_bbox_side: 7500 #m
|
||||
radius_close_to: 50
|
||||
church_coeff: 0.5
|
||||
church_coeff: 0.75
|
||||
nature_coeff: 1.25
|
||||
overall_coeff: 10
|
||||
tag_exponent: 1.15
|
||||
|
@@ -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
|
||||
)
|
||||
|
@@ -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,39 +14,31 @@ class Landmark(BaseModel) :
|
||||
osm_id : int
|
||||
attractiveness : int
|
||||
n_tags : int
|
||||
image_url : Optional[str] = None
|
||||
image_url : Optional[str] = None # TODO future
|
||||
website_url : Optional[str] = None
|
||||
wikipedia_url : Optional[str] = None
|
||||
description : Optional[str] = None # TODO future
|
||||
duration : Optional[int] = 0
|
||||
duration : Optional[int] = 0 # TODO future
|
||||
name_en : Optional[str] = None
|
||||
|
||||
# Unique ID of a given landmark
|
||||
uuid: str = Field(default_factory=uuid4)
|
||||
|
||||
uuid: str = Field(default_factory=uuid4) # TODO implement this ASAP
|
||||
|
||||
# Additional properties depending on specific tour
|
||||
must_do : Optional[bool] = False
|
||||
must_avoid : Optional[bool] = False
|
||||
is_secondary : Optional[bool] = False # TODO future
|
||||
|
||||
time_to_reach_next : Optional[int] = 0 # 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 __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 ""
|
||||
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 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)
|
||||
|
||||
|
@@ -22,10 +22,9 @@ class Trip(BaseModel):
|
||||
|
||||
# Store the trip in the cache
|
||||
cache_client.set(f"trip_{trip.uuid}", trip)
|
||||
# make sure to await the result (noreply=False). Otherwise the cache might not be inplace when the trip is actually requested
|
||||
cache_client.set_many({f"landmark_{landmark.uuid}": landmark for landmark in landmarks}, expire=3600, noreply=False)
|
||||
cache_client.set_many({f"landmark_{landmark.uuid}": landmark for landmark in landmarks}, expire=3600)
|
||||
# is equivalent to:
|
||||
# for landmark in landmarks:
|
||||
# cache_client.set(f"landmark_{landmark.uuid}", landmark, expire=3600)
|
||||
|
||||
return trip
|
||||
return trip
|
@@ -23,7 +23,7 @@ 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=15,
|
||||
max_time_minute=100,
|
||||
detour_tolerance_minute=0
|
||||
)
|
||||
|
||||
@@ -74,7 +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.758217, 4.831814))) # 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
|
||||
test(tuple((48.084588, 7.280405))) # Turckheim
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import yaml
|
||||
from math import sin, cos, sqrt, atan2, radians
|
||||
from geopy.distance import geodesic
|
||||
|
||||
import constants
|
||||
|
||||
@@ -8,7 +8,6 @@ with constants.OPTIMIZER_PARAMETERS_PATH.open('r') as f:
|
||||
DETOUR_FACTOR = parameters['detour_factor']
|
||||
AVERAGE_WALKING_SPEED = parameters['average_walking_speed']
|
||||
|
||||
EARTH_RADIUS_KM = 6373
|
||||
|
||||
def get_time(p1: tuple[float, float], p2: tuple[float, float]) -> int:
|
||||
"""
|
||||
@@ -17,34 +16,24 @@ def get_time(p1: tuple[float, float], p2: tuple[float, float]) -> int:
|
||||
Args:
|
||||
p1 (Tuple[float, float]): Coordinates of the starting location.
|
||||
p2 (Tuple[float, float]): Coordinates of the destination.
|
||||
detour (float): Detour factor affecting the distance.
|
||||
speed (float): Walking speed in kilometers per hour.
|
||||
|
||||
Returns:
|
||||
Returns:
|
||||
int: Time to travel from p1 to p2 in minutes.
|
||||
"""
|
||||
|
||||
|
||||
if p1 == p2:
|
||||
# Compute the straight-line distance in km
|
||||
if p1 == p2 :
|
||||
return 0
|
||||
else:
|
||||
# Compute the distance in km along the surface of the Earth
|
||||
# (assume spherical Earth)
|
||||
# this is the haversine formula, stolen from stackoverflow
|
||||
# in order to not use any external libraries
|
||||
lat1, lon1 = radians(p1[0]), radians(p1[1])
|
||||
lat2, lon2 = radians(p2[0]), radians(p2[1])
|
||||
else:
|
||||
dist = geodesic(p1, p2).kilometers
|
||||
|
||||
dlon = lon2 - lon1
|
||||
dlat = lat2 - lat1
|
||||
|
||||
a = sin(dlat / 2)**2 + cos(lat1) * cos(lat2) * sin(dlon / 2)**2
|
||||
c = 2 * atan2(sqrt(a), sqrt(1 - a))
|
||||
|
||||
distance = EARTH_RADIUS_KM * c
|
||||
|
||||
# Consider the detour factor for average an average city
|
||||
walk_distance = distance * DETOUR_FACTOR
|
||||
# Consider the detour factor for average cityto deterline walking distance (in km)
|
||||
walk_dist = dist*DETOUR_FACTOR
|
||||
|
||||
# Time to walk this distance (in minutes)
|
||||
walk_time = walk_distance / AVERAGE_WALKING_SPEED * 60
|
||||
walk_time = walk_dist/AVERAGE_WALKING_SPEED*60
|
||||
|
||||
return round(walk_time)
|
||||
|
@@ -1,17 +1,20 @@
|
||||
import math
|
||||
import math as m
|
||||
import yaml
|
||||
import logging
|
||||
|
||||
from OSMPythonTools.overpass import Overpass, overpassQueryBuilder
|
||||
from OSMPythonTools.cachingStrategy import CachingStrategy, JSON
|
||||
from pywikibot import ItemPage, Site
|
||||
from pywikibot import config
|
||||
config.put_throttle = 0
|
||||
config.maxlag = 0
|
||||
|
||||
from structs.preferences import Preferences
|
||||
from structs.preferences import Preferences, Preference
|
||||
from structs.landmark import Landmark
|
||||
from .take_most_important import take_most_important
|
||||
import constants
|
||||
|
||||
# silence the overpass logger
|
||||
logging.getLogger('OSMPythonTools').setLevel(level=logging.CRITICAL)
|
||||
|
||||
|
||||
|
||||
class LandmarkManager:
|
||||
@@ -43,7 +46,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']
|
||||
@@ -66,44 +69,87 @@ class LandmarkManager:
|
||||
preferences (Preferences): The user's preference settings that influence the landmark selection.
|
||||
|
||||
Returns:
|
||||
tuple[list[Landmark], list[Landmark]]:
|
||||
- A list of all existing landmarks.
|
||||
- A list of the most important landmarks based on the user's preferences.
|
||||
tuple[list[Landmark], list[Landmark]]:
|
||||
- A list of all existing landmarks.
|
||||
- A list of the most important landmarks based on the user's preferences.
|
||||
"""
|
||||
|
||||
max_walk_dist = (preferences.max_time_minute/2)/60*self.walking_speed*1000/self.detour_factor
|
||||
reachable_bbox_side = min(max_walk_dist, self.max_bbox_side)
|
||||
|
||||
# use set to avoid duplicates, this requires some __methods__ to be set in Landmark
|
||||
all_landmarks = set()
|
||||
|
||||
L = []
|
||||
bbox = self.create_bbox(center_coordinates, reachable_bbox_side)
|
||||
# list for sightseeing
|
||||
if preferences.sightseeing.score != 0:
|
||||
score_function = lambda score: score * 10 * preferences.sightseeing.score / 5
|
||||
current_landmarks = self.fetch_landmarks(bbox, self.amenity_selectors['sightseeing'], preferences.sightseeing.type, score_function)
|
||||
all_landmarks.update(current_landmarks)
|
||||
score_function = lambda score: int(score*10*preferences.sightseeing.score/5) # self.count_elements_close_to(loc) +
|
||||
L1 = self.fetch_landmarks(bbox, self.amenity_selectors['sightseeing'], preferences.sightseeing.type, score_function)
|
||||
L += L1
|
||||
|
||||
# list for nature
|
||||
if preferences.nature.score != 0:
|
||||
score_function = lambda score: score * 10 * self.nature_coeff * preferences.nature.score / 5
|
||||
current_landmarks = self.fetch_landmarks(bbox, self.amenity_selectors['nature'], preferences.nature.type, score_function)
|
||||
all_landmarks.update(current_landmarks)
|
||||
score_function = lambda score: int(score*10*self.nature_coeff*preferences.nature.score/5) # self.count_elements_close_to(loc) +
|
||||
L2 = self.fetch_landmarks(bbox, self.amenity_selectors['nature'], preferences.nature.type, score_function)
|
||||
L += L2
|
||||
|
||||
# list for shopping
|
||||
if preferences.shopping.score != 0:
|
||||
score_function = lambda score: score * 10 * preferences.shopping.score / 5
|
||||
current_landmarks = self.fetch_landmarks(bbox, self.amenity_selectors['shopping'], preferences.shopping.type, score_function)
|
||||
# set time for all shopping activites :
|
||||
for landmark in current_landmarks : landmark.duration = 45
|
||||
all_landmarks.update(current_landmarks)
|
||||
score_function = lambda score: int(score*10*preferences.shopping.score/5) # self.count_elements_close_to(loc) +
|
||||
L3 = self.fetch_landmarks(bbox, self.amenity_selectors['shopping'], preferences.shopping.type, score_function)
|
||||
L += L3
|
||||
|
||||
|
||||
landmarks_constrained = take_most_important(all_landmarks, self.N_important)
|
||||
self.logger.info(f'Generated {len(all_landmarks)} landmarks around {center_coordinates}, and constrained to {len(landmarks_constrained)} most important ones.')
|
||||
L = self.remove_duplicates(L)
|
||||
# self.correct_score(L, preferences)
|
||||
|
||||
return all_landmarks, landmarks_constrained
|
||||
L_constrained = take_most_important(L, self.N_important)
|
||||
self.logger.info(f'Generated {len(L)} landmarks around {center_coordinates}, and constrained to {len(L_constrained)} most important ones.')
|
||||
|
||||
return L, L_constrained
|
||||
|
||||
|
||||
def remove_duplicates(self, landmarks: list[Landmark]) -> list[Landmark]:
|
||||
"""
|
||||
Removes duplicate landmarks based on their names from the given list. Only retains the landmark with highest score
|
||||
|
||||
Parameters:
|
||||
landmarks (list[Landmark]): A list of Landmark objects.
|
||||
|
||||
Returns:
|
||||
list[Landmark]: A list of unique Landmark objects based on their names.
|
||||
"""
|
||||
|
||||
L_clean = []
|
||||
names = []
|
||||
|
||||
for landmark in landmarks:
|
||||
if landmark.name in names:
|
||||
continue
|
||||
else:
|
||||
names.append(landmark.name)
|
||||
L_clean.append(landmark)
|
||||
|
||||
return L_clean
|
||||
|
||||
|
||||
def correct_score(self, landmarks: list[Landmark], preferences: Preferences) -> None:
|
||||
"""
|
||||
Adjust the attractiveness score of each landmark in the list based on user preferences.
|
||||
|
||||
This method updates the attractiveness of each landmark by scaling it according to the user's preference score.
|
||||
The score adjustment is computed using a simple linear transformation based on the preference score.
|
||||
|
||||
Args:
|
||||
landmarks (list[Landmark]): A list of landmarks whose scores need to be corrected.
|
||||
preferences (Preferences): The user's preference settings that influence the attractiveness score adjustment.
|
||||
"""
|
||||
|
||||
score_dict = {
|
||||
preferences.sightseeing.type: preferences.sightseeing.score,
|
||||
preferences.nature.type: preferences.nature.score,
|
||||
preferences.shopping.type: preferences.shopping.score
|
||||
}
|
||||
for landmark in landmarks:
|
||||
landmark.attractiveness = int(landmark.attractiveness * score_dict[landmark.type] / 5)
|
||||
|
||||
|
||||
def count_elements_close_to(self, coordinates: tuple[float, float]) -> int:
|
||||
@@ -126,7 +172,7 @@ class LandmarkManager:
|
||||
|
||||
radius = self.radius_close_to
|
||||
|
||||
alpha = (180 * radius) / (6371000 * math.pi)
|
||||
alpha = (180*radius) / (6371000*m.pi)
|
||||
bbox = {'latLower':lat-alpha,'lonLower':lon-alpha,'latHigher':lat+alpha,'lonHigher': lon+alpha}
|
||||
|
||||
# Build the query to find elements within the radius
|
||||
@@ -170,7 +216,7 @@ class LandmarkManager:
|
||||
|
||||
# Convert distance to degrees
|
||||
lat_diff = half_side_length_km / 111 # 1 degree latitude is approximately 111 km
|
||||
lon_diff = half_side_length_km / (111 * math.cos(math.radians(lat))) # Adjust for longitude based on latitude
|
||||
lon_diff = half_side_length_km / (111 * m.cos(m.radians(lat))) # Adjust for longitude based on latitude
|
||||
|
||||
# Calculate bbox
|
||||
min_lat = lat - lat_diff
|
||||
@@ -209,32 +255,32 @@ class LandmarkManager:
|
||||
query = overpassQueryBuilder(
|
||||
bbox = bbox,
|
||||
elementType = ['way', 'relation'],
|
||||
# selector can in principle be a list already,
|
||||
# but it generates the intersection of the queries
|
||||
# we want the union
|
||||
selector = sel,
|
||||
conditions = ['count_tags()>5'],
|
||||
# conditions = [],
|
||||
includeCenter = True,
|
||||
out = 'body'
|
||||
)
|
||||
self.logger.debug(f"Query: {query}")
|
||||
|
||||
try:
|
||||
result = self.overpass.query(query)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error fetching landmarks: {e}")
|
||||
continue
|
||||
|
||||
return
|
||||
|
||||
for elem in result.elements():
|
||||
|
||||
name = elem.tag('name')
|
||||
location = (elem.centerLat(), elem.centerLon())
|
||||
name = elem.tag('name') # Add name
|
||||
location = (elem.centerLat(), elem.centerLon()) # Add coordinates (lat, lon)
|
||||
|
||||
# TODO: exclude these from the get go
|
||||
# skip if unprecise location
|
||||
if name is None or location[0] is None:
|
||||
continue
|
||||
|
||||
# skip if unused
|
||||
# if 'disused:leisure' in elem.tags().keys():
|
||||
# continue
|
||||
|
||||
# skip if part of another building
|
||||
if 'building:part' in elem.tags().keys() and elem.tag('building:part') == 'yes':
|
||||
continue
|
||||
@@ -245,32 +291,36 @@ class LandmarkManager:
|
||||
n_tags = len(elem.tags().keys()) # Add number of tags
|
||||
score = n_tags**self.tag_exponent # Add score
|
||||
website_url = None
|
||||
wikpedia_url = None
|
||||
image_url = None
|
||||
name_en = None
|
||||
|
||||
# Adjust scoring
|
||||
# remove specific tags
|
||||
skip = False
|
||||
for tag in elem.tags().keys():
|
||||
if "pay" in tag:
|
||||
# payment options are misleading and should not count for the scoring.
|
||||
score += self.pay_bonus
|
||||
score += self.pay_bonus # discard payment options for tags
|
||||
|
||||
if "disused" in tag:
|
||||
# skip disused amenities
|
||||
skip = True
|
||||
skip = True # skip disused amenities
|
||||
break
|
||||
|
||||
if "wiki" in tag:
|
||||
# wikipedia entries count more
|
||||
score += self.wikipedia_bonus
|
||||
score += self.wikipedia_bonus # wikipedia entries count more
|
||||
|
||||
# if tag == "wikidata":
|
||||
# Q = elem.tag('wikidata')
|
||||
# site = Site("wikidata", "wikidata")
|
||||
# item = ItemPage(site, Q)
|
||||
# item.get()
|
||||
# n_languages = len(item.labels)
|
||||
# n_tags += n_languages/10
|
||||
|
||||
if "viewpoint" in tag:
|
||||
# viewpoints must count more
|
||||
score += self.viewpoint_bonus
|
||||
duration = 10
|
||||
|
||||
if "image" in tag:
|
||||
# images must count more
|
||||
score += self.image_bonus
|
||||
|
||||
if elem_type != "nature":
|
||||
@@ -285,43 +335,47 @@ class LandmarkManager:
|
||||
if tag == "building" and elem.tag('building') in ['retail', 'supermarket', 'parking']:
|
||||
skip = True
|
||||
break
|
||||
|
||||
# Extract image, website and english name
|
||||
if tag in ['website', 'contact:website']:
|
||||
|
||||
# Get additional information
|
||||
# if tag == 'wikipedia' :
|
||||
# wikpedia_url = elem.tag('wikipedia')
|
||||
if tag in ['website', 'contact:website'] :
|
||||
website_url = elem.tag(tag)
|
||||
if tag == 'image':
|
||||
if tag == 'image' :
|
||||
image_url = elem.tag('image')
|
||||
if tag =='name:en':
|
||||
if tag =='name:en' :
|
||||
name_en = elem.tag('name:en')
|
||||
|
||||
if skip:
|
||||
continue
|
||||
|
||||
score = score_function(score)
|
||||
if "place_of_worship" in elem.tags().values():
|
||||
score = score * self.church_coeff
|
||||
duration = 10
|
||||
if "place_of_worship" in elem.tags().values() :
|
||||
score = int(score*self.church_coeff)
|
||||
duration = 15
|
||||
|
||||
elif "museum" in elem.tags().values() or "aquarium" in elem.tags().values() or "planetarium" in elem.tags().values():
|
||||
elif "museum" in elem.tags().values() :
|
||||
score = int(score*self.church_coeff)
|
||||
duration = 60
|
||||
|
||||
else:
|
||||
else :
|
||||
duration = 5
|
||||
|
||||
# finally create our own landmark object
|
||||
# Generate the landmark and append it to the list
|
||||
landmark = Landmark(
|
||||
name = name,
|
||||
type = elem_type,
|
||||
location = location,
|
||||
osm_type = osm_type,
|
||||
osm_id = osm_id,
|
||||
attractiveness = int(score),
|
||||
must_do = False,
|
||||
n_tags = int(n_tags),
|
||||
duration = int(duration),
|
||||
name_en = name_en,
|
||||
image_url = image_url,
|
||||
website_url = website_url
|
||||
name=name,
|
||||
type=elem_type,
|
||||
location=location,
|
||||
osm_type=osm_type,
|
||||
osm_id=osm_id,
|
||||
attractiveness=score,
|
||||
must_do=False,
|
||||
n_tags=int(n_tags),
|
||||
duration = duration,
|
||||
name_en=name_en,
|
||||
image_url=image_url,
|
||||
# wikipedia_url=wikpedia_url,
|
||||
website_url=website_url
|
||||
)
|
||||
return_list.append(landmark)
|
||||
|
||||
@@ -345,7 +399,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:
|
||||
|
@@ -3,6 +3,7 @@ import numpy as np
|
||||
|
||||
from scipy.optimize import linprog
|
||||
from collections import defaultdict, deque
|
||||
from geopy.distance import geodesic
|
||||
|
||||
from structs.landmark import Landmark
|
||||
from .get_time_separation import get_time
|
||||
@@ -487,7 +488,7 @@ class Optimizer:
|
||||
|
||||
# Raise error if no solution is found
|
||||
if not res.success :
|
||||
raise ArithmeticError("No solution could be found, the problem is overconstrained. Try with a longer trip (>30 minutes).")
|
||||
raise ArithmeticError("No solution could be found, the problem is overconstrained. Please adapt your must_dos")
|
||||
|
||||
# If there is a solution, we're good to go, just check for connectiveness
|
||||
order, circles = self.is_connected(res.x)
|
||||
|
@@ -2,7 +2,6 @@ import yaml, logging
|
||||
|
||||
from shapely import buffer, LineString, Point, Polygon, MultiPoint, concave_hull
|
||||
from math import pi
|
||||
from typing import List
|
||||
|
||||
from structs.landmark import Landmark
|
||||
from . import take_most_important, get_time_separation
|
||||
@@ -134,21 +133,6 @@ class Refiner :
|
||||
i += 1
|
||||
|
||||
return tour
|
||||
|
||||
def integrate_landmarks(self, sub_list: List[Landmark], main_list: List[Landmark]) :
|
||||
"""
|
||||
Inserts 'sub_list' of Landmarks inside the 'main_list' by leaving the ends untouched.
|
||||
|
||||
Args:
|
||||
sub_list : the list of Landmarks to be inserted inside of the 'main_list'.
|
||||
main_list : the original list with start and finish.
|
||||
|
||||
Returns:
|
||||
the full list.
|
||||
"""
|
||||
sub_list.append(main_list[-1]) # add finish back
|
||||
return main_list[:-1] + sub_list # create full set of possible landmarks
|
||||
|
||||
|
||||
|
||||
def find_shortest_path_through_all_landmarks(self, landmarks: list[Landmark]) -> tuple[list[Landmark], Polygon]:
|
||||
@@ -269,11 +253,6 @@ class Refiner :
|
||||
except :
|
||||
better_tour_poly = concave_hull(MultiPoint(coords)) # Create concave hull with "core" of tour leaving out start and finish
|
||||
xs, ys = better_tour_poly.exterior.xy
|
||||
"""
|
||||
ERROR HERE :
|
||||
Exception has occurred: AttributeError
|
||||
'LineString' object has no attribute 'exterior'
|
||||
"""
|
||||
|
||||
|
||||
# reverse the xs and ys
|
||||
@@ -336,30 +315,26 @@ class Refiner :
|
||||
|
||||
self.logger.info(f"Using {len(minor_landmarks)} minor landmarks around the predicted path")
|
||||
|
||||
# Full set of visitable landmarks.
|
||||
full_set = self.integrate_landmarks(minor_landmarks, base_tour) # could probably be optimized with less overhead
|
||||
# full set of visitable landmarks
|
||||
full_set = base_tour[:-1] + minor_landmarks # create full set of possible landmarks (without finish)
|
||||
full_set.append(base_tour[-1]) # add finish back
|
||||
|
||||
# Generate a new tour with the optimizer.
|
||||
# get a new tour
|
||||
new_tour = self.optimizer.solve_optimization(
|
||||
max_time = max_time + detour,
|
||||
landmarks = full_set,
|
||||
max_landmarks = self.max_landmarks_refiner
|
||||
)
|
||||
|
||||
# If unsuccessful optimization, use the base_tour.
|
||||
if new_tour is None:
|
||||
self.logger.warning("No solution found for the refined tour. Returning the initial tour.")
|
||||
new_tour = base_tour
|
||||
|
||||
# If only one landmark, return it.
|
||||
if len(new_tour) < 4 :
|
||||
return new_tour
|
||||
|
||||
# Find shortest path using the nearest neighbor heuristic.
|
||||
# Find shortest path using the nearest neighbor heuristic
|
||||
better_tour, better_poly = self.find_shortest_path_through_all_landmarks(new_tour)
|
||||
|
||||
# Fix the tour using Polygons if the path looks weird.
|
||||
# Conditions : circular trip and invalid polygon.
|
||||
# Fix the tour using Polygons if the path looks weird
|
||||
if base_tour[0].location == base_tour[-1].location and not better_poly.is_valid :
|
||||
better_tour = self.fix_using_polygon(better_tour)
|
||||
|
||||
|
@@ -1,16 +1,38 @@
|
||||
from structs.landmark import Landmark
|
||||
|
||||
def take_most_important(landmarks: list[Landmark], n_important) -> list[Landmark]:
|
||||
"""
|
||||
Given a list of landmarks, return the n_important most important landmarks
|
||||
Parameters:
|
||||
landmarks: list[Landmark] - list of landmarks
|
||||
n_important: int - number of most important landmarks to return
|
||||
Returns:
|
||||
list[Landmark] - list of the n_important most important landmarks
|
||||
"""
|
||||
def take_most_important(landmarks: list[Landmark], N_important) -> list[Landmark] :
|
||||
L = len(landmarks)
|
||||
L_copy = []
|
||||
L_clean = []
|
||||
scores = [0]*len(landmarks)
|
||||
names = []
|
||||
name_id = {}
|
||||
|
||||
# Sort landmarks by attractiveness (descending)
|
||||
sorted_landmarks = sorted(landmarks, key=lambda x: x.attractiveness, reverse=True)
|
||||
for i, elem in enumerate(landmarks) :
|
||||
if elem.name not in names :
|
||||
names.append(elem.name)
|
||||
name_id[elem.name] = [i]
|
||||
L_copy.append(elem)
|
||||
else :
|
||||
name_id[elem.name] += [i]
|
||||
scores = []
|
||||
for j in name_id[elem.name] :
|
||||
scores.append(L[j].attractiveness)
|
||||
best_id = max(range(len(scores)), key=scores.__getitem__)
|
||||
t = name_id[elem.name][best_id]
|
||||
if t == i :
|
||||
for old in L_copy :
|
||||
if old.name == elem.name :
|
||||
old.attractiveness = L[t].attractiveness
|
||||
|
||||
scores = [0]*len(L_copy)
|
||||
for i, elem in enumerate(L_copy) :
|
||||
scores[i] = elem.attractiveness
|
||||
|
||||
return sorted_landmarks[:n_important]
|
||||
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
|
||||
|
111
backend/test.py
@@ -1,111 +0,0 @@
|
||||
import numpy as np
|
||||
|
||||
def euclidean_distance(p1, p2):
|
||||
print(p1, p2)
|
||||
return np.sqrt((p1[0] - p2[0])**2 + (p1[1] - p2[1])**2)
|
||||
|
||||
|
||||
def maximize_score(places, max_distance, fixed_entry, top_k=3):
|
||||
"""
|
||||
Maximizes the total score of visited places while staying below the maximum distance.
|
||||
|
||||
Parameters:
|
||||
places (list of tuples): Each tuple contains (score, (x, y), location).
|
||||
max_distance (float): The maximum distance that can be traveled.
|
||||
fixed_entry (tuple): The place that needs to be visited independently of its score.
|
||||
top_k (int): Number of top candidates to consider in each iteration.
|
||||
|
||||
Returns:
|
||||
list of tuples: The visited places.
|
||||
float: The total score of the visited places.
|
||||
"""
|
||||
# Initialize total distance and score
|
||||
total_distance = 0
|
||||
total_score = 0
|
||||
visited_places = []
|
||||
|
||||
# Add the fixed entry to the visited list
|
||||
score, (x, y), _ = fixed_entry
|
||||
visited_places.append(fixed_entry)
|
||||
total_score += score
|
||||
|
||||
# Remove the fixed entry from the list of places
|
||||
remaining_places = [place for place in places if place != fixed_entry]
|
||||
|
||||
# Sort remaining places by score-to-distance ratio
|
||||
|
||||
remaining_places.sort(key=lambda p: p[0] / euclidean_distance((x, y), (p[1][0], p[1][1])), reverse=True)
|
||||
|
||||
# Add places to the visited list if they don't exceed the maximum distance
|
||||
current_location = (x, y)
|
||||
while remaining_places and total_distance < max_distance:
|
||||
# Consider top_k candidates
|
||||
candidates = remaining_places[:top_k]
|
||||
best_candidate = None
|
||||
best_score_increase = -np.inf
|
||||
|
||||
for candidate in candidates:
|
||||
score, (cx, cy), location = candidate
|
||||
distance = euclidean_distance(current_location, (cx, cy))
|
||||
if total_distance + distance <= max_distance:
|
||||
score_increase = score / distance
|
||||
if score_increase > best_score_increase:
|
||||
best_score_increase = score_increase
|
||||
best_candidate = candidate
|
||||
|
||||
if best_candidate:
|
||||
visited_places.append(best_candidate)
|
||||
total_distance += euclidean_distance(current_location, best_candidate[1])
|
||||
total_score += best_candidate[0]
|
||||
current_location = best_candidate[1]
|
||||
remaining_places.remove(best_candidate)
|
||||
else:
|
||||
break
|
||||
|
||||
return visited_places, total_score
|
||||
|
||||
# Example usage
|
||||
places = [
|
||||
(10, (0, 0), 'A'),
|
||||
(8, (4, 2), 'B'),
|
||||
(15, (6, 4), 'C'),
|
||||
(7, (5, 6), 'D'),
|
||||
(12, (1, 8), 'E'),
|
||||
(14, (34, 10), 'F'),
|
||||
(15, (65, 12), 'G'),
|
||||
(12, (3, 14), 'H'),
|
||||
(12, (15, 1), 'I'),
|
||||
(7, (17, 4), 'J'),
|
||||
(12, (3, 3), 'K'),
|
||||
(4, (21, 22), 'L'),
|
||||
(12, (23, 24), 'M'),
|
||||
(4, (25, 26), 'N'),
|
||||
(2, (27, 28), 'O'),
|
||||
]
|
||||
fixed_entry = (10, (0, 0), 'A')
|
||||
max_distance = 50
|
||||
|
||||
visited_places, total_score = maximize_score(places, max_distance, fixed_entry)
|
||||
print("Visited Places:", visited_places)
|
||||
print("Total Score:", total_score)
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
# Plot the route
|
||||
def plot_route(visited_places):
|
||||
x_coords = [place[1][0] for place in visited_places]
|
||||
y_coords = [place[1][1] for place in visited_places]
|
||||
labels = [place[2] for place in visited_places]
|
||||
|
||||
plt.figure(figsize=(10, 6))
|
||||
plt.plot(x_coords, y_coords, marker='o', linestyle='-', color='b')
|
||||
for i, label in enumerate(labels):
|
||||
plt.text(x_coords[i], y_coords[i], label, fontsize=12, ha='right')
|
||||
|
||||
plt.title('Route of Visited Places')
|
||||
plt.xlabel('X Coordinate')
|
||||
plt.ylabel('Y Coordinate')
|
||||
plt.grid(True)
|
||||
plt.savefig('route.png')
|
||||
|
||||
plot_route(visited_places)
|
@@ -37,13 +37,14 @@ jobs:
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
run:
|
||||
# remove the 'v' prefix from the tag name
|
||||
echo "BUILD_NAME=${REF_NAME//v}" >> $GITHUB_ENV
|
||||
echo "VERSION_NAME=${REF_NAME//v}" >> $GITHUB_ENV
|
||||
|
||||
- name: Load secrets from github
|
||||
run: |
|
||||
echo "${{ secrets.ANDROID_SECRET_PROPERTIES_BASE64 }}" | base64 -d > secrets.properties
|
||||
echo "${{ secrets.ANDROID_GOOGLE_PLAY_JSON_BASE64 }}" | base64 -d > google-key.json
|
||||
echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > release.keystore
|
||||
echo "${{ secrets.ANDROID_SECRET_PROPERTIES }}" > secrets.properties
|
||||
echo "${{ secrets.ANDROID_GOOGLE_PLAY_JSON }}" > google-key.json
|
||||
# decode the base64 encoded google key
|
||||
base64 -d ${{ secrets.ANDROID_KEYSTORE_BASE64 }} > release.keystore
|
||||
working-directory: android
|
||||
|
||||
- name: Install fastlane
|
||||
@@ -53,6 +54,4 @@ jobs:
|
||||
- name: Run fastlane lane
|
||||
run: bundle exec fastlane deploy_testing
|
||||
working-directory: android
|
||||
env:
|
||||
BUILD_NUMBER: ${{ github.run_number }}
|
||||
# BUILD_NAME is implicitly available
|
||||
# the environment variable VERSION_NAME is implicitly available
|
||||
|
@@ -30,19 +30,14 @@ if (flutterVersionName == null) {
|
||||
|
||||
|
||||
def secretPropertiesFile = rootProject.file('secrets.properties')
|
||||
def fallbackPropertiesFile = rootProject.file('fallback.properties')
|
||||
def secretProperties = new Properties()
|
||||
|
||||
if (secretPropertiesFile.exists()) {
|
||||
secretPropertiesFile.withReader('UTF-8') { reader ->
|
||||
secretProperties.load(reader)
|
||||
}
|
||||
} else if (fallbackPropertiesFile.exists()) {
|
||||
fallbackPropertiesFile.withReader('UTF-8') { reader ->
|
||||
secretProperties.load(reader)
|
||||
}
|
||||
} else {
|
||||
throw new GradleException("Secrets file (secrets.properties, fallback.properties) not found")
|
||||
throw new GradleException("Secrets file secrets.properties not found")
|
||||
}
|
||||
|
||||
|
||||
|
@@ -1,3 +1 @@
|
||||
# This file mirrors the state of secrets.properties as a reference for the developer.
|
||||
# And as a fallback for build.gradle
|
||||
MAPS_API_KEY=Key
|
@@ -5,28 +5,22 @@ default_platform(:android)
|
||||
|
||||
platform :android do
|
||||
|
||||
desc "Deploy a new version to closed testing"
|
||||
desc "Deploy a new version as a preview version"
|
||||
lane :deploy_testing do
|
||||
build_name = ENV["BUILD_NAME"]
|
||||
build_number = ENV["BUILD_NUMBER"]
|
||||
version_name = ENV["VERSION_NAME"]
|
||||
|
||||
sh(
|
||||
"flutter",
|
||||
"build",
|
||||
"appbundle",
|
||||
"--release",
|
||||
"--build-name=#{build_name}",
|
||||
"--build-number=#{build_number}",
|
||||
"--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 this the build folder lies in the flutter root folder
|
||||
# this is the parent folder for the android folder
|
||||
)
|
||||
end
|
||||
|
||||
@@ -34,7 +28,6 @@ platform :android do
|
||||
lane :deploy_release do
|
||||
gradle(
|
||||
task: "clean assembleRelease",
|
||||
# todo update to a flutter call
|
||||
properties: {
|
||||
# loaded from environment
|
||||
"android.injected.version.name" => ENV["VERSION_NAME"],
|
||||
@@ -44,10 +37,6 @@ platform :android do
|
||||
track: "production",
|
||||
skip_upload_apk: true,
|
||||
skip_upload_changelogs: true,
|
||||
aab: "../build/app/outputs/bundle/release/app-release.aab",
|
||||
# this is the default output of flutter build ... --release
|
||||
# in particular this the build folder lies in the flutter root folder
|
||||
# this is the parent folder for the android folder
|
||||
)
|
||||
end
|
||||
end
|
||||
|
After Width: | Height: | Size: 106 KiB |
After Width: | Height: | Size: 1.3 MiB |
After Width: | Height: | Size: 637 KiB |
After Width: | Height: | Size: 573 KiB |
After Width: | Height: | Size: 175 KiB |
After Width: | Height: | Size: 360 KiB |
After Width: | Height: | Size: 106 KiB |
After Width: | Height: | Size: 1.3 MiB |
After Width: | Height: | Size: 637 KiB |
After Width: | Height: | Size: 573 KiB |
After Width: | Height: | Size: 175 KiB |
After Width: | Height: | Size: 360 KiB |
Before Width: | Height: | Size: 40 KiB |
@@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
|
||||
const String APP_NAME = 'AnyWay';
|
||||
|
||||
String API_URL_BASE = 'https://anyway.anydev.info';
|
||||
String API_URL_DEBUG = 'https://anyway.anydev.info';
|
||||
String PRIVACY_URL = 'https://anydev.info/privacy';
|
||||
|
||||
const String MAP_ID = '41c21ac9b81dbfd8';
|
||||
|
@@ -1,6 +1,3 @@
|
||||
import 'package:anyway/main.dart';
|
||||
import 'package:anyway/modules/help_dialog.dart';
|
||||
import 'package:anyway/pages/current_trip.dart';
|
||||
import 'package:anyway/pages/settings.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -11,24 +8,22 @@ import 'package:anyway/modules/trips_saved_list.dart';
|
||||
import 'package:anyway/utils/load_trips.dart';
|
||||
|
||||
import 'package:anyway/pages/new_trip_location.dart';
|
||||
import 'package:anyway/pages/current_trip.dart';
|
||||
import 'package:anyway/pages/onboarding.dart';
|
||||
|
||||
|
||||
|
||||
|
||||
// BasePage is the scaffold that holds a child page and a side drawer
|
||||
// The side drawer is the main way to switch between pages
|
||||
|
||||
// BasePage is the scaffold that holds all other pages
|
||||
// A side drawer is used to switch between pages
|
||||
class BasePage extends StatefulWidget {
|
||||
final Widget mainScreen;
|
||||
final Widget title;
|
||||
final List<String> helpTexts;
|
||||
final String mainScreen;
|
||||
final Trip? trip;
|
||||
|
||||
const BasePage({
|
||||
super.key,
|
||||
required this.mainScreen,
|
||||
this.title = const Text(APP_NAME),
|
||||
this.helpTexts = const [],
|
||||
this.trip,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -39,25 +34,53 @@ class _BasePageState extends State<BasePage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
savedTrips.loadTrips();
|
||||
|
||||
Widget currentView = const Text("loading...");
|
||||
Future<List<Trip>> trips = loadTrips();
|
||||
|
||||
|
||||
if (widget.mainScreen == "map") {
|
||||
if (widget.trip != null) {
|
||||
currentView = TripPage(trip: widget.trip!);
|
||||
} else {
|
||||
currentView = FutureBuilder(
|
||||
future: trips,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
List<Trip> availableTrips = snapshot.data!;
|
||||
if (availableTrips.isNotEmpty) {
|
||||
return TripPage(trip: availableTrips[0]);
|
||||
} else {
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: Text("Wow, so empty!"),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const NewTripPage()
|
||||
)
|
||||
);
|
||||
},
|
||||
label: Text("Plan a trip"),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return const Text("loading...");
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
} else if (widget.mainScreen == "tutorial") {
|
||||
currentView = OnboardingPage();
|
||||
} else if (widget.mainScreen == "settings") {
|
||||
currentView = SettingsPage();
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: widget.title,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.help),
|
||||
tooltip: 'Help',
|
||||
onPressed: () {
|
||||
if (widget.helpTexts.isNotEmpty) {
|
||||
helpDialog(context, widget.helpTexts[0], widget.helpTexts[1]);
|
||||
}
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Center(child: widget.mainScreen),
|
||||
appBar: AppBar(title: Text(APP_NAME)),
|
||||
body: Center(child: currentView),
|
||||
drawer: Drawer(
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -81,8 +104,7 @@ class _BasePageState extends State<BasePage> {
|
||||
ListTile(
|
||||
title: const Text('Your Trips'),
|
||||
leading: const Icon(Icons.map),
|
||||
// TODO: this is not working!
|
||||
selected: widget.mainScreen is TripPage,
|
||||
selected: widget.mainScreen == "map",
|
||||
onTap: () {},
|
||||
trailing: ElevatedButton(
|
||||
onPressed: () {
|
||||
@@ -100,11 +122,11 @@ class _BasePageState extends State<BasePage> {
|
||||
// through the options in the drawer if there isn't enough vertical
|
||||
// space to fit everything.
|
||||
Expanded(
|
||||
child: TripsOverview(trips: savedTrips),
|
||||
child: TripsOverview(trips: trips),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
savedTrips.clearTrips();
|
||||
removeAllTripsFromPrefs();
|
||||
},
|
||||
child: const Text('Clear trips'),
|
||||
),
|
||||
@@ -112,12 +134,11 @@ class _BasePageState extends State<BasePage> {
|
||||
ListTile(
|
||||
title: const Text('How to use'),
|
||||
leading: Icon(Icons.help),
|
||||
// TODO: this is not working!
|
||||
selected: widget.mainScreen is OnboardingPage,
|
||||
selected: widget.mainScreen == "tutorial",
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => OnboardingPage()
|
||||
builder: (context) => BasePage(mainScreen: "tutorial")
|
||||
)
|
||||
);
|
||||
},
|
||||
@@ -127,12 +148,11 @@ class _BasePageState extends State<BasePage> {
|
||||
ListTile(
|
||||
title: const Text('Settings'),
|
||||
leading: const Icon(Icons.settings),
|
||||
// TODO: this is not working!
|
||||
selected: widget.mainScreen is SettingsPage,
|
||||
selected: widget.mainScreen == "settings",
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => SettingsPage()
|
||||
builder: (context) => BasePage(mainScreen: "settings")
|
||||
)
|
||||
);
|
||||
},
|
@@ -1,12 +1,10 @@
|
||||
import 'package:anyway/utils/get_first_page.dart';
|
||||
import 'package:anyway/utils/load_trips.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:anyway/constants.dart';
|
||||
import 'package:anyway/layout.dart';
|
||||
|
||||
void main() => runApp(const App());
|
||||
|
||||
final GlobalKey<ScaffoldMessengerState> rootScaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
|
||||
final SavedTrips savedTrips = SavedTrips();
|
||||
|
||||
class App extends StatelessWidget {
|
||||
const App({super.key});
|
||||
@@ -16,7 +14,7 @@ class App extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: APP_NAME,
|
||||
home: getFirstPage(),
|
||||
home: BasePage(mainScreen: "map"),
|
||||
theme: APP_THEME,
|
||||
scaffoldMessengerKey: rootScaffoldMessengerKey
|
||||
);
|
||||
|
@@ -1,37 +0,0 @@
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CurrentTripErrorMessage extends StatefulWidget {
|
||||
final Trip trip;
|
||||
const CurrentTripErrorMessage({
|
||||
super.key,
|
||||
required this.trip,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CurrentTripErrorMessage> createState() => _CurrentTripErrorMessageState();
|
||||
}
|
||||
|
||||
class _CurrentTripErrorMessageState extends State<CurrentTripErrorMessage> {
|
||||
@override
|
||||
Widget build(BuildContext context) => Center(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
color: Colors.red,
|
||||
size: 50,
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(left: 10),
|
||||
),
|
||||
AutoSizeText(
|
||||
'Error: ${widget.trip.errorDescription}',
|
||||
maxLines: 3,
|
||||
),
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
@@ -1,50 +1,111 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:anyway/constants.dart';
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
|
||||
import 'package:anyway/pages/current_trip.dart';
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
class CurrentTripGreeter extends StatefulWidget {
|
||||
class Greeter extends StatefulWidget {
|
||||
final Trip trip;
|
||||
|
||||
CurrentTripGreeter({
|
||||
super.key,
|
||||
Greeter({
|
||||
required this.trip,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CurrentTripGreeter> createState() => _CurrentTripGreeterState();
|
||||
State<Greeter> createState() => _GreeterState();
|
||||
}
|
||||
|
||||
|
||||
class _CurrentTripGreeterState extends State<CurrentTripGreeter> {
|
||||
@override
|
||||
Widget build(BuildContext context) => Center(
|
||||
child: FutureBuilder(
|
||||
future: widget.trip.cityName,
|
||||
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return AutoSizeText(
|
||||
maxLines: 1,
|
||||
'Welcome to ${snapshot.data}!',
|
||||
style: greeterStyle
|
||||
);
|
||||
} else if (snapshot.hasError) {
|
||||
return AutoSizeText(
|
||||
maxLines: 1,
|
||||
'Welcome to your trip!',
|
||||
style: greeterStyle
|
||||
);
|
||||
} else {
|
||||
return AutoSizeText(
|
||||
maxLines: 1,
|
||||
'Welcome to ...',
|
||||
style: greeterStyle
|
||||
);
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
class _GreeterState extends State<Greeter> {
|
||||
|
||||
Widget greeterBuilder (BuildContext context, Widget? child) {
|
||||
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
|
||||
);
|
||||
|
||||
Widget topGreeter;
|
||||
|
||||
if (widget.trip.uuid != 'pending') {
|
||||
topGreeter = FutureBuilder(
|
||||
future: widget.trip.cityName,
|
||||
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return AutoSizeText(
|
||||
maxLines: 1,
|
||||
'Welcome to ${snapshot.data}!',
|
||||
style: greeterStyle
|
||||
);
|
||||
} else if (snapshot.hasError) {
|
||||
log('Error while fetching city name');
|
||||
return AutoSizeText(
|
||||
maxLines: 1,
|
||||
'Welcome to your trip!',
|
||||
style: greeterStyle
|
||||
);
|
||||
} else {
|
||||
return AutoSizeText(
|
||||
maxLines: 1,
|
||||
'Welcome to ...',
|
||||
style: greeterStyle
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// still awaiting the trip
|
||||
// We can hopefully infer the city name from the cityName future
|
||||
// Show a linear loader at the bottom and an info message above
|
||||
topGreeter = Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
FutureBuilder(
|
||||
future: widget.trip.cityName,
|
||||
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return AutoSizeText(
|
||||
maxLines: 1,
|
||||
'Generating your trip to ${snapshot.data}...',
|
||||
style: greeterStyle
|
||||
);
|
||||
} else if (snapshot.hasError) {
|
||||
// the exact error is shown in the central part of the trip overview. No need to show it here
|
||||
return AutoSizeText(
|
||||
maxLines: 1,
|
||||
'Error while loading trip.',
|
||||
style: greeterStyle
|
||||
);
|
||||
}
|
||||
return AutoSizeText(
|
||||
maxLines: 1,
|
||||
'Generating your trip...',
|
||||
style: greeterStyle
|
||||
);
|
||||
}
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(5),
|
||||
child: const LinearProgressIndicator()
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: topGreeter,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListenableBuilder(
|
||||
listenable: widget.trip,
|
||||
builder: greeterBuilder,
|
||||
);
|
||||
}
|
||||
}
|
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:anyway/modules/landmark_card.dart';
|
||||
import 'package:anyway/structs/landmark.dart';
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
import 'package:anyway/main.dart';
|
||||
|
||||
|
||||
|
||||
@@ -24,7 +25,30 @@ List<Widget> landmarksList(Trip trip) {
|
||||
|
||||
for (Landmark landmark in trip.landmarks) {
|
||||
children.add(
|
||||
LandmarkCard(landmark, trip),
|
||||
Dismissible(
|
||||
key: ValueKey<int>(landmark.hashCode),
|
||||
child: LandmarkCard(landmark),
|
||||
dismissThresholds: {DismissDirection.endToStart: 0.95, DismissDirection.startToEnd: 0.95},
|
||||
onDismissed: (direction) {
|
||||
log('Removing ${landmark.name}');
|
||||
trip.removeLandmark(landmark);
|
||||
|
||||
rootScaffoldMessengerKey.currentState!.showSnackBar(
|
||||
SnackBar(content: Text("We won't show ${landmark.name} again"))
|
||||
);
|
||||
},
|
||||
|
||||
background: Container(color: Colors.red),
|
||||
secondaryBackground: Container(
|
||||
color: Colors.red,
|
||||
child: Icon(
|
||||
Icons.delete,
|
||||
color: Colors.white,
|
||||
),
|
||||
padding: EdgeInsets.all(15),
|
||||
alignment: Alignment.centerRight,
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
if (landmark.next != null) {
|
||||
|
@@ -1,162 +0,0 @@
|
||||
import 'package:anyway/constants.dart';
|
||||
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';
|
||||
|
||||
|
||||
final List<String> statusTexts = [
|
||||
'Parsing your preferences...',
|
||||
'Finding the best places...',
|
||||
'Crunching the numbers...',
|
||||
'Calculating the best route...',
|
||||
'Making sure you have a great time...',
|
||||
];
|
||||
|
||||
|
||||
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) => Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
// In the very center of the panel, show the greeter which tells the user that the trip is being generated
|
||||
Center(child: loadingText(widget.trip)),
|
||||
// As a gimmick, and a way to show that the app is still working, show a few loading dots
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: statusText(),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// automatically cycle through the greeter texts
|
||||
class statusText extends StatefulWidget {
|
||||
const statusText({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_statusTextState createState() => _statusTextState();
|
||||
}
|
||||
|
||||
class _statusTextState extends State<statusText> {
|
||||
int statusIndex = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Future.delayed(Duration(seconds: 5), () {
|
||||
setState(() {
|
||||
statusIndex = (statusIndex + 1) % statusTexts.length;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AutoSizeText(
|
||||
statusTexts[statusIndex],
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Widget loadingText(Trip trip) => FutureBuilder(
|
||||
future: trip.cityName,
|
||||
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
|
||||
Widget greeter;
|
||||
|
||||
if (snapshot.hasData) {
|
||||
greeter = AnimatedGradientText(
|
||||
text: 'Creating 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 = AnimatedGradientText(
|
||||
text: 'Error while loading trip.',
|
||||
style: greeterStyle,
|
||||
);
|
||||
} else {
|
||||
greeter = AnimatedGradientText(
|
||||
text: 'Creating your trip...',
|
||||
style: greeterStyle,
|
||||
);
|
||||
}
|
||||
return greeter;
|
||||
}
|
||||
);
|
||||
|
||||
class AnimatedGradientText extends StatefulWidget {
|
||||
final String text;
|
||||
final TextStyle style;
|
||||
|
||||
const AnimatedGradientText({
|
||||
Key? key,
|
||||
required this.text,
|
||||
required this.style,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_AnimatedGradientTextState createState() => _AnimatedGradientTextState();
|
||||
}
|
||||
|
||||
class _AnimatedGradientTextState extends State<AnimatedGradientText> with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(seconds: 1),
|
||||
vsync: this,
|
||||
)..repeat();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
return ShaderMask(
|
||||
shaderCallback: (bounds) {
|
||||
return LinearGradient(
|
||||
colors: [GRADIENT_START, GRADIENT_END, GRADIENT_START],
|
||||
stops: [
|
||||
_controller.value - 1.0,
|
||||
_controller.value,
|
||||
_controller.value + 1.0,
|
||||
],
|
||||
tileMode: TileMode.mirror,
|
||||
).createShader(bounds);
|
||||
},
|
||||
child: Text(
|
||||
widget.text,
|
||||
style: widget.style,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,4 @@
|
||||
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';
|
||||
@@ -30,37 +28,16 @@ class _CurrentTripPanelState extends State<CurrentTripPanel> {
|
||||
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,
|
||||
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,
|
||||
child: CurrentTripLoadingIndicator(trip: widget.trip),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
if (widget.trip.uuid != 'pending' && widget.trip.uuid != 'error') {
|
||||
return ListView(
|
||||
controller: widget.controller,
|
||||
padding: const EdgeInsets.only(top: 10, left: 10, right: 10, bottom: 30),
|
||||
padding: const EdgeInsets.only(bottom: 30, left: 5, right: 5),
|
||||
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
|
||||
// note that we need to account for the padding above
|
||||
height: MediaQuery.of(context).size.height * TRIP_PANEL_MIN_HEIGHT - 10,
|
||||
child: CurrentTripGreeter(trip: widget.trip),
|
||||
height: MediaQuery.of(context).size.height * TRIP_PANEL_MIN_HEIGHT - 20,
|
||||
child: Greeter(trip: widget.trip),
|
||||
),
|
||||
|
||||
const Padding(padding: EdgeInsets.only(top: 10)),
|
||||
@@ -73,7 +50,29 @@ class _CurrentTripPanelState extends State<CurrentTripPanel> {
|
||||
|
||||
const Padding(padding: EdgeInsets.only(top: 10)),
|
||||
|
||||
Center(child: saveButton(trip: widget.trip)),
|
||||
Center(child: saveButton(widget.trip)),
|
||||
],
|
||||
);
|
||||
} else if(widget.trip.uuid == 'pending') {
|
||||
return 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: Greeter(trip: widget.trip)
|
||||
);
|
||||
} else {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
color: Colors.red,
|
||||
size: 50,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 10),
|
||||
child: Text('Error: ${widget.trip.errorDescription}'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@@ -3,53 +3,39 @@ 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';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
|
||||
class saveButton extends StatefulWidget {
|
||||
Trip trip;
|
||||
saveButton({super.key, required this.trip});
|
||||
|
||||
@override
|
||||
State<saveButton> createState() => _saveButtonState();
|
||||
}
|
||||
|
||||
class _saveButtonState extends State<saveButton> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ElevatedButton(
|
||||
onPressed: () async {
|
||||
savedTrips.addTrip(widget.trip);
|
||||
// SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
// setState(() => widget.trip.toPrefs(prefs));
|
||||
rootScaffoldMessengerKey.currentState!.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Trip saved'),
|
||||
duration: Duration(seconds: 2),
|
||||
dismissDirection: DismissDirection.horizontal
|
||||
)
|
||||
);
|
||||
},
|
||||
child: SizedBox(
|
||||
width: 100,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.save,
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 10, top: 5, bottom: 5, right: 5),
|
||||
child: AutoSizeText(
|
||||
'Save trip',
|
||||
maxLines: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
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,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.save,
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 10, top: 5, bottom: 5, right: 5),
|
||||
child: AutoSizeText(
|
||||
'Save trip',
|
||||
maxLines: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
|
@@ -1,25 +0,0 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Future<void> helpDialog(BuildContext context, String title, String content) {
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(title),
|
||||
content: Text(content),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
textStyle: Theme.of(context).textTheme.labelLarge,
|
||||
),
|
||||
child: const Text('Got it!'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
@@ -1,5 +1,3 @@
|
||||
import 'package:anyway/main.dart';
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
@@ -8,12 +6,8 @@ import 'package:anyway/structs/landmark.dart';
|
||||
|
||||
class LandmarkCard extends StatefulWidget {
|
||||
final Landmark landmark;
|
||||
final Trip parentTrip;
|
||||
|
||||
LandmarkCard(
|
||||
this.landmark,
|
||||
this.parentTrip,
|
||||
);
|
||||
LandmarkCard(this.landmark);
|
||||
|
||||
@override
|
||||
_LandmarkCardState createState() => _LandmarkCardState();
|
||||
@@ -23,149 +17,110 @@ class LandmarkCard extends StatefulWidget {
|
||||
class _LandmarkCardState extends State<LandmarkCard> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.landmark.type == typeStart || widget.landmark.type == typeFinish) {
|
||||
return TextButton.icon(
|
||||
onPressed: () {},
|
||||
icon: widget.landmark.type.icon,
|
||||
label: Text(widget.landmark.name),
|
||||
);
|
||||
|
||||
}
|
||||
// else:
|
||||
ThemeData theme = Theme.of(context);
|
||||
return Container(
|
||||
height: 160,
|
||||
child: Card(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
),
|
||||
elevation: 5,
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
// if the image is available, display it on the left side of the card, otherwise only display the text
|
||||
child: widget.landmark.imageURL != null ? splitLayout() : textLayout(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget splitLayout() {
|
||||
// If an image is available, display it on the left side of the card
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
// the image on the left
|
||||
width: 160,
|
||||
height: 160,
|
||||
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: widget.landmark.imageURL ?? '',
|
||||
placeholder: (context, url) => Center(child: CircularProgressIndicator()),
|
||||
errorWidget: (context, error, stackTrace) => Icon(Icons.question_mark_outlined),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: textLayout(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget textLayout() {
|
||||
return Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
widget.landmark.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 2,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
if (widget.landmark.nameEN != null)
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
widget.landmark.nameEN!,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
),
|
||||
maxLines: 1,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
Padding(padding: EdgeInsets.only(top: 10)),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: SingleChildScrollView(
|
||||
// allows the buttons to be scrolled
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Wrap(
|
||||
spacing: 10,
|
||||
// show the type, the website, and the wikipedia link as buttons/labels in a row
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: () {},
|
||||
icon: widget.landmark.type.icon,
|
||||
label: Text(widget.landmark.type.name),
|
||||
),
|
||||
if (widget.landmark.duration != null && widget.landmark.duration!.inMinutes > 0)
|
||||
TextButton.icon(
|
||||
onPressed: () {},
|
||||
icon: Icon(Icons.hourglass_bottom),
|
||||
label: Text('${widget.landmark.duration!.inMinutes} minutes'),
|
||||
),
|
||||
if (widget.landmark.websiteURL != null)
|
||||
TextButton.icon(
|
||||
onPressed: () async {
|
||||
// open a browser with the website link
|
||||
await launchUrl(Uri.parse(widget.landmark.websiteURL!));
|
||||
},
|
||||
icon: Icon(Icons.link),
|
||||
label: Text('Website'),
|
||||
),
|
||||
PopupMenuButton(
|
||||
icon: Icon(Icons.settings),
|
||||
style: TextButtonTheme.of(context).style,
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.delete),
|
||||
title: Text('Delete'),
|
||||
onTap: () async {
|
||||
widget.parentTrip.removeLandmark(widget.landmark);
|
||||
rootScaffoldMessengerKey.currentState!.showSnackBar(
|
||||
SnackBar(content: Text("We won't show ${widget.landmark.name} again"))
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.star),
|
||||
title: Text('Favorite'),
|
||||
onTap: () async {
|
||||
// delete the landmark
|
||||
// await deleteLandmark(widget.landmark);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
],
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container( // the image on the left
|
||||
// inherit the height of the parent container
|
||||
height: double.infinity,
|
||||
// force a fixed width
|
||||
width: 160,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: widget.landmark.imageURL ?? '',
|
||||
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
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
Flexible(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
widget.landmark.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 2,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
if (widget.landmark.nameEN != null)
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
widget.landmark.nameEN!,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
),
|
||||
maxLines: 1,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
SingleChildScrollView(
|
||||
// allows the buttons to be scrolled
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Wrap(
|
||||
spacing: 10,
|
||||
// show the type, the website, and the wikipedia link as buttons/labels in a row
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: () {},
|
||||
icon: widget.landmark.type.icon,
|
||||
label: Text(widget.landmark.type.name),
|
||||
),
|
||||
if (widget.landmark.duration != null && widget.landmark.duration!.inMinutes > 0)
|
||||
TextButton.icon(
|
||||
onPressed: () {},
|
||||
icon: Icon(Icons.hourglass_bottom),
|
||||
label: Text('${widget.landmark.duration!.inMinutes} minutes'),
|
||||
),
|
||||
if (widget.landmark.websiteURL != null)
|
||||
TextButton.icon(
|
||||
onPressed: () async {
|
||||
// open a browser with the website link
|
||||
await launchUrl(Uri.parse(widget.landmark.websiteURL!));
|
||||
},
|
||||
icon: Icon(Icons.link),
|
||||
label: Text('Website'),
|
||||
),
|
||||
if (widget.landmark.wikipediaURL != null)
|
||||
TextButton.icon(
|
||||
onPressed: () async {
|
||||
// open a browser with the wikipedia link
|
||||
await launchUrl(Uri.parse(widget.landmark.wikipediaURL!));
|
||||
},
|
||||
icon: Icon(Icons.book),
|
||||
label: Text('Wikipedia'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import 'package:anyway/layout.dart';
|
||||
import 'package:anyway/main.dart';
|
||||
import 'package:anyway/pages/current_trip.dart';
|
||||
import 'package:anyway/structs/preferences.dart';
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
import 'package:anyway/utils/fetch_trip.dart';
|
||||
@@ -34,7 +34,7 @@ class _NewTripButtonState extends State<NewTripButton> {
|
||||
}
|
||||
return FloatingActionButton.extended(
|
||||
onPressed: onPressed,
|
||||
icon: const Icon(Icons.directions),
|
||||
icon: const Icon(Icons.add),
|
||||
label: AutoSizeText('Start planning!'),
|
||||
);
|
||||
}
|
||||
@@ -57,7 +57,7 @@ class _NewTripButtonState extends State<NewTripButton> {
|
||||
fetchTrip(trip, widget.preferences);
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => TripPage(trip: trip)
|
||||
builder: (context) => BasePage(mainScreen: "map", trip: trip)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@@ -9,15 +9,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
const Map<String, List> debugLocations = {
|
||||
'paris': [48.8575, 2.3514],
|
||||
'london': [51.5074, -0.1278],
|
||||
'new york': [40.7128, -74.0060],
|
||||
'tokyo': [35.6895, 139.6917],
|
||||
};
|
||||
|
||||
|
||||
|
||||
class NewTripLocationSearch extends StatefulWidget {
|
||||
Future<SharedPreferences> prefs = SharedPreferences.getInstance();
|
||||
Trip trip;
|
||||
@@ -36,35 +27,26 @@ class _NewTripLocationSearchState extends State<NewTripLocationSearch> {
|
||||
|
||||
setTripLocation (String query) async {
|
||||
List<Location> locations = [];
|
||||
Location startLocation;
|
||||
log('Searching for: $query');
|
||||
if (GeocodingPlatform.instance != null) {
|
||||
locations.addAll(await locationFromAddress(query));
|
||||
|
||||
try{
|
||||
locations = await locationFromAddress(query);
|
||||
} catch (e) {
|
||||
log('No results found for: $query : $e');
|
||||
}
|
||||
|
||||
if (locations.isNotEmpty) {
|
||||
startLocation = locations.first;
|
||||
} else {
|
||||
log('No results found for: $query. Is geocoding available?');
|
||||
log('Setting Fallback location');
|
||||
List coordinates = debugLocations[query.toLowerCase()] ?? [48.8575, 2.3514];
|
||||
startLocation = Location(
|
||||
latitude: coordinates[0],
|
||||
longitude: coordinates[1],
|
||||
timestamp: DateTime.now(),
|
||||
Location location = locations.first;
|
||||
widget.trip.landmarks.clear();
|
||||
widget.trip.addLandmark(
|
||||
Landmark(
|
||||
uuid: 'pending',
|
||||
name: query,
|
||||
location: [location.latitude, location.longitude],
|
||||
type: typeStart
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
widget.trip.landmarks.clear();
|
||||
widget.trip.addLandmark(
|
||||
Landmark(
|
||||
uuid: 'pending',
|
||||
name: query,
|
||||
location: [startLocation.latitude, startLocation.longitude],
|
||||
type: typeStart
|
||||
)
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
late Widget locationSearchBar = SearchBar(
|
||||
|
@@ -26,7 +26,7 @@ class _NewTripMapState extends State<NewTripMap> {
|
||||
target: LatLng(48.8566, 2.3522),
|
||||
zoom: 11.0,
|
||||
);
|
||||
GoogleMapController? _mapController;
|
||||
late GoogleMapController _mapController;
|
||||
final Set<Marker> _markers = <Marker>{};
|
||||
|
||||
_onLongPress(LatLng location) {
|
||||
@@ -56,15 +56,11 @@ class _NewTripMapState extends State<NewTripMap> {
|
||||
),
|
||||
)
|
||||
);
|
||||
// check if the controller is ready
|
||||
|
||||
if (_mapController != null) {
|
||||
_mapController!.animateCamera(
|
||||
CameraUpdate.newLatLng(
|
||||
LatLng(landmark.location[0], landmark.location[1])
|
||||
)
|
||||
);
|
||||
}
|
||||
_mapController.moveCamera(
|
||||
CameraUpdate.newLatLng(
|
||||
LatLng(landmark.location[0], landmark.location[1])
|
||||
)
|
||||
);
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
@@ -2,11 +2,13 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
class OnboardingCard extends StatelessWidget {
|
||||
final String title;
|
||||
final String description;
|
||||
final String imagePath;
|
||||
int index;
|
||||
String title;
|
||||
String description;
|
||||
String imagePath;
|
||||
|
||||
const OnboardingCard({
|
||||
OnboardingCard({
|
||||
required this.index,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.imagePath,
|
||||
@@ -14,35 +16,41 @@ class OnboardingCard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
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)),
|
||||
SvgPicture.asset(
|
||||
imagePath,
|
||||
height: 200,
|
||||
),
|
||||
Padding(padding: EdgeInsets.only(top: 20)),
|
||||
Text(
|
||||
description,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
Padding(padding: EdgeInsets.only(top: 20)),
|
||||
SvgPicture.asset(
|
||||
imagePath,
|
||||
height: 200,
|
||||
),
|
||||
Padding(padding: EdgeInsets.only(top: 20)),
|
||||
Text(
|
||||
description,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
]
|
||||
),
|
||||
]
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,12 +1,11 @@
|
||||
import 'package:anyway/pages/current_trip.dart';
|
||||
import 'package:anyway/utils/load_trips.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:anyway/layout.dart';
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
|
||||
|
||||
class TripsOverview extends StatefulWidget {
|
||||
final SavedTrips trips;
|
||||
final Future<List<Trip>> trips;
|
||||
const TripsOverview({
|
||||
super.key,
|
||||
required this.trips,
|
||||
@@ -17,34 +16,50 @@ class TripsOverview extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _TripsOverviewState extends State<TripsOverview> {
|
||||
Widget listBuild (BuildContext context, SavedTrips trips) {
|
||||
|
||||
Widget listBuild (BuildContext context, AsyncSnapshot<List<Trip>> snapshot) {
|
||||
List<Widget> children;
|
||||
List<Trip> items = trips.trips;
|
||||
children = List<Widget>.generate(items.length, (index) {
|
||||
Trip trip = items[index];
|
||||
return ListTile(
|
||||
title: FutureBuilder(
|
||||
future: trip.cityName,
|
||||
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return Text("Trip to ${snapshot.data}");
|
||||
} else if (snapshot.hasError) {
|
||||
return Text("Error: ${snapshot.error}");
|
||||
} else {
|
||||
return const Text("Trip to ...");
|
||||
}
|
||||
if (snapshot.hasData) {
|
||||
children = List<Widget>.generate(snapshot.data!.length, (index) {
|
||||
Trip trip = snapshot.data![index];
|
||||
return ListTile(
|
||||
title: FutureBuilder(
|
||||
future: trip.cityName,
|
||||
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return Text("Trip to ${snapshot.data}");
|
||||
} else if (snapshot.hasError) {
|
||||
return Text("Error: ${snapshot.error}");
|
||||
} else {
|
||||
return const Text("Trip to ...");
|
||||
}
|
||||
},
|
||||
),
|
||||
leading: Icon(Icons.pin_drop),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => BasePage(mainScreen: "map", trip: trip)
|
||||
)
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
} else if (snapshot.hasError) {
|
||||
children = [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
color: Colors.red,
|
||||
size: 60,
|
||||
),
|
||||
leading: Icon(Icons.pin_drop),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => TripPage(trip: trip)
|
||||
)
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
child: Text('Error: ${snapshot.error}'),
|
||||
),
|
||||
];
|
||||
} else {
|
||||
children = [Center(child: CircularProgressIndicator())];
|
||||
}
|
||||
|
||||
return ListView(
|
||||
children: children,
|
||||
@@ -54,11 +69,9 @@ class _TripsOverviewState extends State<TripsOverview> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListenableBuilder(
|
||||
listenable: widget.trips,
|
||||
builder: (BuildContext context, Widget? child) {
|
||||
return listBuild(context, widget.trips);
|
||||
}
|
||||
return FutureBuilder(
|
||||
future: widget.trips,
|
||||
builder: listBuild,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,5 +1,4 @@
|
||||
import 'package:anyway/constants.dart';
|
||||
import 'package:anyway/pages/base_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:sliding_up_panel/sliding_up_panel.dart';
|
||||
|
||||
@@ -7,12 +6,6 @@ import 'package:anyway/structs/trip.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: 25
|
||||
);
|
||||
|
||||
|
||||
class TripPage extends StatefulWidget {
|
||||
@@ -32,8 +25,7 @@ class _TripPageState extends State<TripPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BasePage(
|
||||
mainScreen: SlidingUpPanel(
|
||||
return SlidingUpPanel(
|
||||
// use panelBuilder instead of panel so that we can reuse the scrollcontroller for the listview
|
||||
panelBuilder: (scrollcontroller) => CurrentTripPanel(controller: scrollcontroller, trip: widget.trip),
|
||||
// using collapsed and panelBuilder seems to show both at the same time, so we include the greeter in the panelBuilder
|
||||
@@ -43,7 +35,7 @@ class _TripPageState extends State<TripPage> {
|
||||
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),
|
||||
padding: const EdgeInsets.only(top: 10),
|
||||
// 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)),
|
||||
@@ -54,13 +46,6 @@ class _TripPageState extends State<TripPage> {
|
||||
color: Colors.black,
|
||||
)
|
||||
],
|
||||
),
|
||||
title: FutureBuilder(
|
||||
future: widget.trip.cityName,
|
||||
builder: (context, snapshot) => Text(
|
||||
'Your trip to ${snapshot.hasData ? snapshot.data! : "..."}',
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import 'package:anyway/modules/new_trip_button.dart';
|
||||
import 'package:anyway/modules/new_trip_options_button.dart';
|
||||
import 'package:anyway/pages/base_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import "package:anyway/structs/trip.dart";
|
||||
@@ -19,28 +19,23 @@ class _NewTripPageState extends State<NewTripPage> {
|
||||
final TextEditingController lonController = TextEditingController();
|
||||
Trip trip = Trip();
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// floating search bar and map as a background
|
||||
return BasePage(
|
||||
mainScreen: Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
NewTripMap(trip),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(15),
|
||||
child: NewTripLocationSearch(trip),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: NewTripOptionsButton(trip: trip),
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('New Trip'),
|
||||
),
|
||||
title: Text("New Trip"),
|
||||
helpTexts: [
|
||||
"Setting the start location",
|
||||
"To set the starting point, type a city name in the search bar. You can also navigate the map like you're used to and long press anywhere to set a starting point."
|
||||
],
|
||||
body: Stack(
|
||||
children: [
|
||||
NewTripMap(trip),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(15),
|
||||
child: NewTripLocationSearch(trip),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: NewTripOptionsButton(trip: trip),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import 'package:anyway/modules/new_trip_button.dart';
|
||||
import 'package:anyway/pages/base_page.dart';
|
||||
import 'package:anyway/structs/preferences.dart';
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
@@ -20,54 +19,41 @@ class _NewTripPreferencesPageState extends State<NewTripPreferencesPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BasePage(
|
||||
mainScreen: 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)
|
||||
// )
|
||||
// )
|
||||
// ),
|
||||
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))
|
||||
),
|
||||
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),
|
||||
Divider(indent: 25, endIndent: 25, height: 50),
|
||||
|
||||
durationPicker(preferences.maxTime),
|
||||
durationPicker(preferences.maxTime),
|
||||
|
||||
preferenceSliders([preferences.sightseeing, preferences.shopping, preferences.nature]),
|
||||
]
|
||||
),
|
||||
floatingActionButton: NewTripButton(trip: widget.trip, preferences: preferences),
|
||||
preferenceSliders([preferences.sightseeing, preferences.shopping, preferences.nature]),
|
||||
]
|
||||
),
|
||||
|
||||
title: FutureBuilder(
|
||||
future: widget.trip.cityName,
|
||||
builder: (context, snapshot) => Text(
|
||||
'Your trip to ${snapshot.hasData ? snapshot.data! : "..."}',
|
||||
)
|
||||
),
|
||||
helpTexts: [
|
||||
'Trip preferences',
|
||||
'Set your preferences for this trip. These will be used to generate a custom itinerary.'
|
||||
],
|
||||
floatingActionButton: NewTripButton(trip: widget.trip, preferences: preferences),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -77,7 +63,7 @@ class _NewTripPreferencesPageState extends State<NewTripPreferencesPage> {
|
||||
margin: const EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 0),
|
||||
shadowColor: Colors.grey,
|
||||
child: ListTile(
|
||||
leading: preferences.maxTime.icon,
|
||||
leading: Icon(Icons.timer),
|
||||
title: Text(preferences.maxTime.description),
|
||||
subtitle: CupertinoTimerPicker(
|
||||
mode: CupertinoTimerPickerMode.hm,
|
||||
|
@@ -1,33 +1,7 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:anyway/constants.dart';
|
||||
import 'package:anyway/modules/onboarding_card.dart';
|
||||
import 'package:anyway/pages/new_trip_location.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const List<Widget> onboardingCards = [
|
||||
OnboardingCard(
|
||||
title: "Welcome to anyway!",
|
||||
description: "Anyway helps you plan a city trip that suits your wishes.",
|
||||
imagePath: "assets/city.svg"
|
||||
),
|
||||
OnboardingCard(
|
||||
title: "Find your way",
|
||||
description: "Bored by churches? No problem! Hate shopping? No worries! Instead of suggesting the generic trips that bore you, anyway will try to give you recommendations that really suit you.",
|
||||
imagePath: "assets/plan.svg"
|
||||
),
|
||||
OnboardingCard(
|
||||
title: "Change your mind",
|
||||
description: "Feet get sore, the weather changes. Anyway understands that! Move or remove destinations, visit hidden gems along your journey, do your own thing. Anyway adapts to your spontaneous decisions.",
|
||||
imagePath: "assets/cat.svg"
|
||||
),
|
||||
OnboardingCard(
|
||||
title: "Feeling lost?",
|
||||
description: "Whenever you are confused or need help with the app, look out for the question mark in the top right corner. Help is just a tap away!",
|
||||
imagePath: "assets/confused.svg"
|
||||
),
|
||||
];
|
||||
|
||||
class OnboardingPage extends StatefulWidget {
|
||||
const OnboardingPage({super.key});
|
||||
|
||||
@@ -36,83 +10,37 @@ class OnboardingPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _OnboardingPageState extends State<OnboardingPage> {
|
||||
final PageController _controller = PageController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final PageController _controller = PageController();
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
return Stack(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: APP_GRADIENT.colors,
|
||||
stops: [
|
||||
(_controller.hasClients ? _controller.page ?? _controller.initialPage : _controller.initialPage) / onboardingCards.length,
|
||||
(_controller.hasClients ? _controller.page ?? _controller.initialPage + 1 : _controller.initialPage + 1) / onboardingCards.length,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 100, sigmaY: 100),
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(0),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
PageView(
|
||||
// horizontally scrollable list of pages
|
||||
controller: _controller,
|
||||
children: List.generate(
|
||||
onboardingCards.length,
|
||||
(index) {
|
||||
return Container(
|
||||
alignment: Alignment.center,
|
||||
child: onboardingCards[index],
|
||||
);
|
||||
}
|
||||
),
|
||||
|
||||
children: [
|
||||
OnboardingCard(index: 1, title: "Welcome to anyway!", description: "Anyway helps you plan a city trip that suits your wishes.", imagePath: "assets/city.svg"),
|
||||
OnboardingCard(index: 2, title: "Find your way", description: "Bored by churches? No problem! Hate shopping? No worries! More than showing you the typical 'must-sees' of a city, anyway will try to give you recommendations that really suit you.", imagePath: "assets/plan.svg"),
|
||||
OnboardingCard(index: 3, title: "Change your mind", description: "Life happens when you're busy making plans. Anyway understands that! Move or remove destinations, visit hidden gems along your journey, do your own thing. Anyway adapts to your spontaneous decisions.", imagePath: "assets/cat.svg"),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
if (_controller.page == onboardingCards.length - 1) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const NewTripPage()
|
||||
)
|
||||
);
|
||||
} else {
|
||||
_controller.nextPage(duration: Duration(milliseconds: 500), curve: Curves.ease);
|
||||
}
|
||||
},
|
||||
label: AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
if ((_controller.page ?? _controller.initialPage) == onboardingCards.length - 1) {
|
||||
return Row(
|
||||
children: [
|
||||
const Text("Start planning!"),
|
||||
Padding(padding: const EdgeInsets.only(right: 8.0)),
|
||||
const Icon(Icons.map_outlined)
|
||||
],
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {
|
||||
if (_controller.page == 2) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const NewTripPage()
|
||||
)
|
||||
);
|
||||
} else {
|
||||
return const Icon(Icons.arrow_forward);
|
||||
_controller.nextPage(duration: Duration(milliseconds: 500), curve: Curves.ease);
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
child: Icon(Icons.arrow_forward),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import 'package:anyway/constants.dart';
|
||||
import 'package:anyway/main.dart';
|
||||
import 'package:anyway/pages/base_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
@@ -17,37 +16,30 @@ class SettingsPage extends StatefulWidget {
|
||||
class _SettingsPageState extends State<SettingsPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BasePage(
|
||||
mainScreen: 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))
|
||||
),
|
||||
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),
|
||||
Divider(indent: 25, endIndent: 25, height: 50),
|
||||
|
||||
darkMode(),
|
||||
setLocationUsage(),
|
||||
setDebugMode(),
|
||||
darkMode(),
|
||||
setLocationUsage(),
|
||||
setDebugMode(),
|
||||
|
||||
Divider(indent: 25, endIndent: 25, height: 50),
|
||||
Divider(indent: 25, endIndent: 25, height: 50),
|
||||
|
||||
privacyInfo(),
|
||||
]
|
||||
),
|
||||
title: Text('Settings'),
|
||||
helpTexts: [
|
||||
'Settings',
|
||||
'Preferences set in this page are global and will affect the entire application.'
|
||||
],
|
||||
privacyInfo(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -69,7 +61,9 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
return AlertDialog(
|
||||
title: Text('Debug mode - use a custom API endpoint'),
|
||||
content: TextField(
|
||||
controller: TextEditingController(text: API_URL_DEBUG),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'https://anyway-stg.anydev.info'
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
API_URL_BASE = value;
|
||||
@@ -177,9 +171,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
return Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Text('AnyWay does not collect or store any of the data that is submitted via the app. The location of your trip is not stored. The location feature is only used to show your current location on the map, it is not transmitted to our servers.', textAlign: TextAlign.center),
|
||||
Padding(padding: EdgeInsets.only(top: 3)),
|
||||
Text('Our full privacy policy is available under:', textAlign: TextAlign.center),
|
||||
Text('Our privacy policy is available under:'),
|
||||
|
||||
TextButton.icon(
|
||||
icon: Icon(Icons.info),
|
||||
|
@@ -24,7 +24,8 @@ final class Landmark extends LinkedListEntry<Landmark>{
|
||||
// description to be shown in the overview
|
||||
final String? nameEN;
|
||||
final String? websiteURL;
|
||||
String? imageURL; // not final because it can be patched
|
||||
final String? wikipediaURL;
|
||||
final String? imageURL;
|
||||
final String? description;
|
||||
final Duration? duration;
|
||||
final bool? visited;
|
||||
@@ -43,6 +44,7 @@ final class Landmark extends LinkedListEntry<Landmark>{
|
||||
|
||||
this.nameEN,
|
||||
this.websiteURL,
|
||||
this.wikipediaURL,
|
||||
this.imageURL,
|
||||
this.description,
|
||||
this.duration,
|
||||
@@ -68,6 +70,7 @@ final class Landmark extends LinkedListEntry<Landmark>{
|
||||
final isSecondary = json['is_secondary'] as bool?;
|
||||
final nameEN = json['name_en'] as String?;
|
||||
final websiteURL = json['website_url'] as String?;
|
||||
final wikipediaURL = json['wikipedia_url'] as String?;
|
||||
final imageURL = json['image_url'] as String?;
|
||||
final description = json['description'] as String?;
|
||||
var duration = Duration(minutes: json['duration'] ?? 0) as Duration?;
|
||||
@@ -82,6 +85,7 @@ final class Landmark extends LinkedListEntry<Landmark>{
|
||||
isSecondary: isSecondary,
|
||||
nameEN: nameEN,
|
||||
websiteURL: websiteURL,
|
||||
wikipediaURL: wikipediaURL,
|
||||
imageURL: imageURL,
|
||||
description: description,
|
||||
duration: duration,
|
||||
@@ -108,6 +112,7 @@ final class Landmark extends LinkedListEntry<Landmark>{
|
||||
'is_secondary': isSecondary,
|
||||
'name_en': nameEN,
|
||||
'website_url': websiteURL,
|
||||
'wikipedia_url': wikipediaURL,
|
||||
'image_url': imageURL,
|
||||
'description': description,
|
||||
'duration': duration?.inMinutes,
|
||||
@@ -125,7 +130,7 @@ class LandmarkType {
|
||||
LandmarkType({required this.name, this.icon = const Icon(Icons.location_on)}) {
|
||||
switch (name) {
|
||||
case 'sightseeing':
|
||||
icon = const Icon(Icons.castle);
|
||||
icon = const Icon(Icons.church);
|
||||
break;
|
||||
case 'nature':
|
||||
icon = const Icon(Icons.eco);
|
||||
|
@@ -113,3 +113,10 @@ LinkedList<Landmark> readLandmarks(SharedPreferences prefs, String? firstUUID) {
|
||||
}
|
||||
return landmarks;
|
||||
}
|
||||
|
||||
|
||||
|
||||
void removeAllTripsFromPrefs () async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
prefs.clear();
|
||||
}
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import "dart:convert";
|
||||
import "dart:developer";
|
||||
import "package:anyway/utils/load_landmark_image.dart";
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import 'package:anyway/constants.dart';
|
||||
@@ -39,18 +38,10 @@ fetchTrip(
|
||||
String dataString = jsonEncode(data);
|
||||
log(dataString);
|
||||
|
||||
late Response response;
|
||||
try {
|
||||
response = await dio.post(
|
||||
"/trip/new",
|
||||
data: data
|
||||
);
|
||||
} catch (e) {
|
||||
trip.updateUUID("error");
|
||||
trip.updateError(e.toString());
|
||||
log(e.toString());
|
||||
return;
|
||||
}
|
||||
final response = await dio.post(
|
||||
"/trip/new",
|
||||
data: data
|
||||
);
|
||||
|
||||
// handle errors
|
||||
if (response.statusCode != 200) {
|
||||
@@ -86,20 +77,6 @@ fetchTrip(
|
||||
}
|
||||
|
||||
|
||||
patchLandmarkImage(Landmark landmark) async {
|
||||
// patch the landmark to include an image from an external source
|
||||
if (landmark.imageURL == null) {
|
||||
String? newUrl = await getImageUrlFromName(landmark.name);
|
||||
if (newUrl != null) {
|
||||
landmark.imageURL = newUrl;
|
||||
}
|
||||
} else if (landmark.imageURL!.contains("photos.app.goo.gl")) {
|
||||
// the image is a google photos link, we should get the image behind the link
|
||||
String? newUrl = await getImageUrlFromGooglePhotos(landmark.imageURL!);
|
||||
// also set the new url if it is null
|
||||
landmark.imageURL = newUrl;
|
||||
}
|
||||
}
|
||||
|
||||
Future<(Landmark, String?)> fetchLandmark(String uuid) async {
|
||||
final response = await dio.get(
|
||||
@@ -116,7 +93,5 @@ Future<(Landmark, String?)> fetchLandmark(String uuid) async {
|
||||
log(response.data.toString());
|
||||
Map<String, dynamic> json = response.data;
|
||||
String? nextUUID = json["next_uuid"];
|
||||
Landmark landmark = Landmark.fromJson(json);
|
||||
patchLandmarkImage(landmark);
|
||||
return (landmark, nextUUID);
|
||||
return (Landmark.fromJson(json), nextUUID);
|
||||
}
|
||||
|
@@ -1,41 +0,0 @@
|
||||
import 'package:anyway/pages/current_trip.dart';
|
||||
import 'package:anyway/pages/onboarding.dart';
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
import 'package:anyway/utils/load_trips.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Widget getFirstPage() {
|
||||
SavedTrips trips = SavedTrips();
|
||||
trips.loadTrips();
|
||||
|
||||
return ListenableBuilder(
|
||||
listenable: trips,
|
||||
builder: (BuildContext context, Widget? child) {
|
||||
List<Trip> items = trips.trips;
|
||||
if (items.isNotEmpty) {
|
||||
return TripPage(trip: items[0]);
|
||||
} else {
|
||||
return OnboardingPage();
|
||||
}
|
||||
}
|
||||
);
|
||||
// Future<List<Trip>> trips = loadTrips();
|
||||
// // test if there are any active trips
|
||||
// // if there are, return the trip list
|
||||
// // if there are not, return the onboarding page
|
||||
// return FutureBuilder(
|
||||
// future: trips,
|
||||
// builder: (context, snapshot) {
|
||||
// if (snapshot.hasData) {
|
||||
// List<Trip> availableTrips = snapshot.data!;
|
||||
// if (availableTrips.isNotEmpty) {
|
||||
// return TripPage(trip: availableTrips[0]);
|
||||
// } else {
|
||||
// return OnboardingPage();
|
||||
// }
|
||||
// } else {
|
||||
// return CircularProgressIndicator();
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
}
|
@@ -1,71 +0,0 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:fuzzywuzzy/model/extracted_result.dart';
|
||||
|
||||
const String baseUrl = "https://en.wikipedia.org/w/api.php";
|
||||
final Dio dio = Dio();
|
||||
|
||||
Future<int?> bestPageMatch(String title) async {
|
||||
final response = await dio.get(baseUrl, queryParameters: {
|
||||
"action": "query",
|
||||
"format": "json",
|
||||
"list": "prefixsearch",
|
||||
"pssearch": title,
|
||||
});
|
||||
|
||||
final data = jsonDecode(response.toString());
|
||||
log(data.toString());
|
||||
final List<dynamic> results = data["query"]["prefixsearch"] ?? {};
|
||||
final Map<String, int> titlesAndIds = {
|
||||
for (var d in results) d["title"]: d["pageid"]
|
||||
};
|
||||
if (titlesAndIds.isEmpty) {
|
||||
log("No pages found for $title");
|
||||
return null;
|
||||
}
|
||||
|
||||
// after the empty check, we can safely assume that there is a best match
|
||||
final ExtractedResult<String> bestMatch = extractOne(
|
||||
query: title,
|
||||
choices: titlesAndIds.keys.toList(),
|
||||
cutoff: 70,
|
||||
);
|
||||
return titlesAndIds[bestMatch.choice];
|
||||
}
|
||||
|
||||
Future<String?> getImageUrl(int pageId) async {
|
||||
final response = await dio.get(baseUrl, queryParameters: {
|
||||
"action": "query",
|
||||
"format": "json",
|
||||
"prop": "pageimages",
|
||||
"pageids": pageId,
|
||||
"pithumbsize": 500,
|
||||
});
|
||||
|
||||
final data = jsonDecode(response.toString());
|
||||
final pageData = data["query"]["pages"][pageId.toString()];
|
||||
return pageData["thumbnail"]?["source"];
|
||||
}
|
||||
|
||||
Future<String?> getImageUrlFromName(String title) async {
|
||||
int? pageId = await bestPageMatch(title);
|
||||
if (pageId == null) {
|
||||
return null;
|
||||
}
|
||||
return await getImageUrl(pageId);
|
||||
}
|
||||
|
||||
|
||||
Future<String?> getImageUrlFromGooglePhotos(String url) async {
|
||||
// this is a very simple implementation that just gets the image behind the link
|
||||
// it is not guaranteed to work for all google photos links
|
||||
final response = await dio.get(url);
|
||||
final data = response.toString();
|
||||
final int start = data.indexOf("https://lh3.googleusercontent.com");
|
||||
final int end = data.indexOf('"', start);
|
||||
return data.substring(start, end);
|
||||
}
|
@@ -1,39 +1,19 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
import 'package:anyway/structs/landmark.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
Future<List<Trip>> loadTrips() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
|
||||
class SavedTrips extends ChangeNotifier {
|
||||
List<Trip> _trips = [];
|
||||
|
||||
List<Trip> get trips => _trips;
|
||||
|
||||
void loadTrips() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
|
||||
List<Trip> trips = [];
|
||||
Set<String> keys = prefs.getKeys();
|
||||
for (String key in keys) {
|
||||
if (key.startsWith('trip_')) {
|
||||
String uuid = key.replaceFirst('trip_', '');
|
||||
trips.add(Trip.fromPrefs(prefs, uuid));
|
||||
}
|
||||
List<Trip> trips = [];
|
||||
Set<String> keys = prefs.getKeys();
|
||||
for (String key in keys) {
|
||||
if (key.startsWith('trip_')) {
|
||||
String uuid = key.replaceFirst('trip_', '');
|
||||
trips.add(Trip.fromPrefs(prefs, uuid));
|
||||
}
|
||||
_trips = trips;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void addTrip(Trip trip) async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
trip.toPrefs(prefs);
|
||||
_trips.add(trip);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void clearTrips () async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
prefs.clear();
|
||||
_trips = [];
|
||||
notifyListeners();
|
||||
}
|
||||
return trips;
|
||||
}
|
||||
|
@@ -101,10 +101,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
|
||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.0"
|
||||
version: "1.18.0"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -232,14 +232,6 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
fuzzywuzzy:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: fuzzywuzzy
|
||||
sha256: "3004379ffd6e7f476a0c2091f38f16588dc45f67de7adf7c41aa85dec06b432c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
geocoding:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -412,18 +404,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
|
||||
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.7"
|
||||
version: "10.0.5"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
|
||||
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.8"
|
||||
version: "3.0.5"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -708,7 +700,7 @@ packages:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
version: "0.0.99"
|
||||
sliding_up_panel:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -753,10 +745,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
|
||||
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.12.0"
|
||||
version: "1.11.1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -777,10 +769,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
|
||||
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
version: "1.2.0"
|
||||
synchronized:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -801,10 +793,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
|
||||
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.3"
|
||||
version: "0.7.2"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -921,10 +913,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
|
||||
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.3.0"
|
||||
version: "14.2.5"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@@ -51,7 +51,6 @@ dependencies:
|
||||
flutter_launcher_icons: ^0.13.1
|
||||
permission_handler: ^11.3.1
|
||||
geolocator: ^13.0.1
|
||||
fuzzywuzzy: ^1.2.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
30
frontend/test/widget_test.dart
Normal file
@@ -0,0 +1,30 @@
|
||||
// This is a basic Flutter widget test.
|
||||
//
|
||||
// To perform an interaction with a widget in your test, use the WidgetTester
|
||||
// utility in the flutter_test package. For example, you can send tap and scroll
|
||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
||||
// tree, read text, and verify that the values of widget properties are correct.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
// import 'package:anyway/main.dart';
|
||||
import 'package:anyway/layout.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||
// Build our app and trigger a frame.
|
||||
await tester.pumpWidget(BasePage(mainScreen: "map",));
|
||||
|
||||
// Verfiy that the title is displayed
|
||||
expect(find.text('City Nav'), findsOneWidget);
|
||||
|
||||
// Tap the '+' icon and trigger a frame.
|
||||
await tester.tap(find.byIcon(Icons.add));
|
||||
await tester.pump();
|
||||
|
||||
// Verify that our counter has incremented.
|
||||
expect(find.text('0'), findsNothing);
|
||||
expect(find.text('1'), findsOneWidget);
|
||||
});
|
||||
}
|