Compare commits
28 Commits
v0.0.24
...
e18a9c63e6
Author | SHA1 | Date | |
---|---|---|---|
e18a9c63e6 | |||
5fcadbe8d8 | |||
5afb646381 | |||
d0e837377b | |||
d94c69c545 | |||
9e595ad933 | |||
53d56f3e30 | |||
f39d02f967 | |||
94a7adac6c | |||
4d99715447 | |||
48555e7429 | |||
8b24876fd1 | |||
c832461f29 | |||
6f1a019d4f | |||
e6ccb7078b | |||
84839c5a02 | |||
9850e949c3 | |||
5fc25a3c39 | |||
f76cd603f3 | |||
67f748244f | |||
66fa55e8c3 | |||
f4729a2de7 | |||
40edd923c3 | |||
6ad749eeed | |||
c20ebf3d63 | |||
6facde6e0b | |||
55b0a1b793 | |||
39df97f4d1 |
@@ -6,7 +6,7 @@ on:
|
|||||||
- frontend/**
|
- frontend/**
|
||||||
|
|
||||||
|
|
||||||
name: Build and release APK
|
name: Build and release debug APK
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
ls -lah android
|
ls -lah android
|
||||||
working-directory: ./frontend
|
working-directory: ./frontend
|
||||||
|
|
||||||
- run: flutter build apk --release --split-per-abi --build-number=${{ gitea.run_number }}
|
- run: flutter build apk --debug --split-per-abi --build-number=${{ gitea.run_number }}
|
||||||
working-directory: ./frontend
|
working-directory: ./frontend
|
||||||
|
|
||||||
- name: Upload APKs to artifacts
|
- name: Upload APKs to artifacts
|
||||||
|
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"cmake.ignoreCMakeListsMissing": true
|
||||||
|
}
|
@@ -9,9 +9,9 @@ name = "pypi"
|
|||||||
numpy = "*"
|
numpy = "*"
|
||||||
fastapi = "*"
|
fastapi = "*"
|
||||||
pydantic = "*"
|
pydantic = "*"
|
||||||
geopy = "*"
|
|
||||||
shapely = "*"
|
shapely = "*"
|
||||||
scipy = "*"
|
scipy = "*"
|
||||||
osmpythontools = "*"
|
osmpythontools = "*"
|
||||||
pywikibot = "*"
|
pywikibot = "*"
|
||||||
pymemcache = "*"
|
pymemcache = "*"
|
||||||
|
fastapi-cli = "*"
|
||||||
|
2208
backend/Pipfile.lock
generated
@@ -1,12 +1,37 @@
|
|||||||
# Backend
|
# Backend
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
- 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`.
|
### Directory Structure
|
||||||
- Since the application is aimed to be deployed in a container, the `Dockerfile` is provided to build the image.
|
- 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/...
|
||||||
|
```
|
||||||
|
|
||||||
### Deployment
|
### 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.
|
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
|
import logging
|
||||||
# if we are in a debug session, set verbose and rich logging
|
# if we are in a debug session, set verbose and rich logging
|
||||||
if os.getenv('DEBUG', False):
|
if os.getenv('DEBUG', "false") == "true":
|
||||||
from rich.logging import RichHandler
|
from rich.logging import RichHandler
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.DEBUG,
|
level=logging.DEBUG,
|
||||||
|
@@ -1,3 +1,6 @@
|
|||||||
|
# Tags were picked mostly arbitrarily, based on the OSM wiki and the OSM tags page.
|
||||||
|
# See https://taginfo.openstreetmap.org for more inspiration.
|
||||||
|
|
||||||
nature:
|
nature:
|
||||||
leisure: park
|
leisure: park
|
||||||
geological: ''
|
geological: ''
|
||||||
@@ -11,7 +14,24 @@ nature:
|
|||||||
- alpine_hut
|
- alpine_hut
|
||||||
- viewpoint
|
- viewpoint
|
||||||
- zoo
|
- zoo
|
||||||
waterway: waterfall
|
- resort
|
||||||
|
- picnic_site
|
||||||
|
water:
|
||||||
|
- pond
|
||||||
|
- lake
|
||||||
|
- river
|
||||||
|
- basin
|
||||||
|
- stream
|
||||||
|
- lagoon
|
||||||
|
- rapids
|
||||||
|
waterway:
|
||||||
|
- waterfall
|
||||||
|
- river
|
||||||
|
- canal
|
||||||
|
- dam
|
||||||
|
- dock
|
||||||
|
- boatyard
|
||||||
|
|
||||||
|
|
||||||
shopping:
|
shopping:
|
||||||
shop:
|
shop:
|
||||||
@@ -23,10 +43,47 @@ sightseeing:
|
|||||||
- museum
|
- museum
|
||||||
- attraction
|
- attraction
|
||||||
- gallery
|
- gallery
|
||||||
|
- artwork
|
||||||
|
- aquarium
|
||||||
historic: ''
|
historic: ''
|
||||||
amenity:
|
amenity:
|
||||||
- planetarium
|
- planetarium
|
||||||
- place_of_worship
|
- place_of_worship
|
||||||
- fountain
|
- fountain
|
||||||
|
- townhall
|
||||||
water:
|
water:
|
||||||
- reflecting_pool
|
- 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
|
||||||
|
@@ -22,7 +22,8 @@ class Trip(BaseModel):
|
|||||||
|
|
||||||
# Store the trip in the cache
|
# Store the trip in the cache
|
||||||
cache_client.set(f"trip_{trip.uuid}", trip)
|
cache_client.set(f"trip_{trip.uuid}", trip)
|
||||||
cache_client.set_many({f"landmark_{landmark.uuid}": landmark for landmark in landmarks}, expire=3600)
|
# make sure to await the result (noreply=False). Otherwise the cache might not be inplace when the trip is actually requested
|
||||||
|
cache_client.set_many({f"landmark_{landmark.uuid}": landmark for landmark in landmarks}, expire=3600, noreply=False)
|
||||||
# is equivalent to:
|
# is equivalent to:
|
||||||
# for landmark in landmarks:
|
# for landmark in landmarks:
|
||||||
# cache_client.set(f"landmark_{landmark.uuid}", landmark, expire=3600)
|
# cache_client.set(f"landmark_{landmark.uuid}", landmark, expire=3600)
|
||||||
|
@@ -23,7 +23,7 @@ def test(start_coords: tuple[float, float], finish_coords: tuple[float, float] =
|
|||||||
sightseeing=Preference(type='sightseeing', score = 5),
|
sightseeing=Preference(type='sightseeing', score = 5),
|
||||||
nature=Preference(type='nature', score = 5),
|
nature=Preference(type='nature', score = 5),
|
||||||
shopping=Preference(type='shopping', score = 5),
|
shopping=Preference(type='shopping', score = 5),
|
||||||
max_time_minute=100,
|
max_time_minute=15,
|
||||||
detour_tolerance_minute=0
|
detour_tolerance_minute=0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -75,5 +75,6 @@ def test(start_coords: tuple[float, float], finish_coords: tuple[float, float] =
|
|||||||
# test(tuple((48.8375946, 2.2949904))) # Point random
|
# test(tuple((48.8375946, 2.2949904))) # Point random
|
||||||
# test(tuple((47.377859, 8.540585))) # Zurich HB
|
# test(tuple((47.377859, 8.540585))) # Zurich HB
|
||||||
# test(tuple((45.758217, 4.831814))) # Lyon Bellecour
|
# test(tuple((45.758217, 4.831814))) # Lyon Bellecour
|
||||||
test(tuple((48.5848435, 7.7332974))) # Strasbourg Gare
|
# test(tuple((48.5848435, 7.7332974))) # Strasbourg Gare
|
||||||
# test(tuple((48.2067858, 16.3692340))) # Vienne
|
# test(tuple((48.2067858, 16.3692340))) # Vienne
|
||||||
|
test(tuple((48.084588, 7.280405))) # Turckheim
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import yaml
|
import yaml
|
||||||
from geopy.distance import geodesic
|
from math import sin, cos, sqrt, atan2, radians
|
||||||
|
|
||||||
import constants
|
import constants
|
||||||
|
|
||||||
@@ -8,6 +8,7 @@ with constants.OPTIMIZER_PARAMETERS_PATH.open('r') as f:
|
|||||||
DETOUR_FACTOR = parameters['detour_factor']
|
DETOUR_FACTOR = parameters['detour_factor']
|
||||||
AVERAGE_WALKING_SPEED = parameters['average_walking_speed']
|
AVERAGE_WALKING_SPEED = parameters['average_walking_speed']
|
||||||
|
|
||||||
|
EARTH_RADIUS_KM = 6373
|
||||||
|
|
||||||
def get_time(p1: tuple[float, float], p2: tuple[float, float]) -> int:
|
def get_time(p1: tuple[float, float], p2: tuple[float, float]) -> int:
|
||||||
"""
|
"""
|
||||||
@@ -22,16 +23,28 @@ def get_time(p1: tuple[float, float], p2: tuple[float, float]) -> int:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
# Compute the straight-line distance in km
|
|
||||||
if p1 == p2:
|
if p1 == p2:
|
||||||
return 0
|
return 0
|
||||||
else:
|
else:
|
||||||
dist = geodesic(p1, p2).kilometers
|
# Compute the distance in km along the surface of the Earth
|
||||||
|
# (assume spherical Earth)
|
||||||
|
# this is the haversine formula, stolen from stackoverflow
|
||||||
|
# in order to not use any external libraries
|
||||||
|
lat1, lon1 = radians(p1[0]), radians(p1[1])
|
||||||
|
lat2, lon2 = radians(p2[0]), radians(p2[1])
|
||||||
|
|
||||||
# Consider the detour factor for average cityto deterline walking distance (in km)
|
dlon = lon2 - lon1
|
||||||
walk_dist = dist*DETOUR_FACTOR
|
dlat = lat2 - lat1
|
||||||
|
|
||||||
|
a = sin(dlat / 2)**2 + cos(lat1) * cos(lat2) * sin(dlon / 2)**2
|
||||||
|
c = 2 * atan2(sqrt(a), sqrt(1 - a))
|
||||||
|
|
||||||
|
distance = EARTH_RADIUS_KM * c
|
||||||
|
|
||||||
|
# Consider the detour factor for average an average city
|
||||||
|
walk_distance = distance * DETOUR_FACTOR
|
||||||
|
|
||||||
# Time to walk this distance (in minutes)
|
# Time to walk this distance (in minutes)
|
||||||
walk_time = walk_dist/AVERAGE_WALKING_SPEED*60
|
walk_time = walk_distance / AVERAGE_WALKING_SPEED * 60
|
||||||
|
|
||||||
return round(walk_time)
|
return round(walk_time)
|
||||||
|
@@ -10,7 +10,8 @@ from structs.landmark import Landmark
|
|||||||
from .take_most_important import take_most_important
|
from .take_most_important import take_most_important
|
||||||
import constants
|
import constants
|
||||||
|
|
||||||
|
# silence the overpass logger
|
||||||
|
logging.getLogger('OSMPythonTools').setLevel(level=logging.CRITICAL)
|
||||||
|
|
||||||
|
|
||||||
class LandmarkManager:
|
class LandmarkManager:
|
||||||
@@ -93,6 +94,8 @@ class LandmarkManager:
|
|||||||
if preferences.shopping.score != 0:
|
if preferences.shopping.score != 0:
|
||||||
score_function = lambda score: score * 10 * preferences.shopping.score / 5
|
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)
|
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)
|
all_landmarks.update(current_landmarks)
|
||||||
|
|
||||||
|
|
||||||
@@ -206,11 +209,15 @@ class LandmarkManager:
|
|||||||
query = overpassQueryBuilder(
|
query = overpassQueryBuilder(
|
||||||
bbox = bbox,
|
bbox = bbox,
|
||||||
elementType = ['way', 'relation'],
|
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,
|
selector = sel,
|
||||||
# conditions = [],
|
conditions = ['count_tags()>5'],
|
||||||
includeCenter = True,
|
includeCenter = True,
|
||||||
out = 'body'
|
out = 'body'
|
||||||
)
|
)
|
||||||
|
self.logger.debug(f"Query: {query}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = self.overpass.query(query)
|
result = self.overpass.query(query)
|
||||||
@@ -241,11 +248,11 @@ class LandmarkManager:
|
|||||||
image_url = None
|
image_url = None
|
||||||
name_en = None
|
name_en = None
|
||||||
|
|
||||||
# remove specific tags
|
# Adjust scoring
|
||||||
skip = False
|
skip = False
|
||||||
for tag in elem.tags().keys():
|
for tag in elem.tags().keys():
|
||||||
if "pay" in tag:
|
if "pay" in tag:
|
||||||
# payment options are a good sign
|
# payment options are misleading and should not count for the scoring.
|
||||||
score += self.pay_bonus
|
score += self.pay_bonus
|
||||||
|
|
||||||
if "disused" in tag:
|
if "disused" in tag:
|
||||||
@@ -258,10 +265,12 @@ class LandmarkManager:
|
|||||||
score += self.wikipedia_bonus
|
score += self.wikipedia_bonus
|
||||||
|
|
||||||
if "viewpoint" in tag:
|
if "viewpoint" in tag:
|
||||||
|
# viewpoints must count more
|
||||||
score += self.viewpoint_bonus
|
score += self.viewpoint_bonus
|
||||||
duration = 10
|
duration = 10
|
||||||
|
|
||||||
if "image" in tag:
|
if "image" in tag:
|
||||||
|
# images must count more
|
||||||
score += self.image_bonus
|
score += self.image_bonus
|
||||||
|
|
||||||
if elem_type != "nature":
|
if elem_type != "nature":
|
||||||
@@ -277,6 +286,7 @@ class LandmarkManager:
|
|||||||
skip = True
|
skip = True
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# Extract image, website and english name
|
||||||
if tag in ['website', 'contact:website']:
|
if tag in ['website', 'contact:website']:
|
||||||
website_url = elem.tag(tag)
|
website_url = elem.tag(tag)
|
||||||
if tag == 'image':
|
if tag == 'image':
|
||||||
@@ -290,10 +300,9 @@ class LandmarkManager:
|
|||||||
score = score_function(score)
|
score = score_function(score)
|
||||||
if "place_of_worship" in elem.tags().values():
|
if "place_of_worship" in elem.tags().values():
|
||||||
score = score * self.church_coeff
|
score = score * self.church_coeff
|
||||||
duration = 15
|
duration = 10
|
||||||
|
|
||||||
elif "museum" in elem.tags().values():
|
elif "museum" in elem.tags().values() or "aquarium" in elem.tags().values() or "planetarium" in elem.tags().values():
|
||||||
score = score * self.church_coeff
|
|
||||||
duration = 60
|
duration = 60
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -336,7 +345,7 @@ def dict_to_selector_list(d: dict) -> list:
|
|||||||
for key, value in d.items():
|
for key, value in d.items():
|
||||||
if type(value) == list:
|
if type(value) == list:
|
||||||
val = '|'.join(value)
|
val = '|'.join(value)
|
||||||
return_list.append(f'{key}~"{val}"')
|
return_list.append(f'{key}~"^({val})$"')
|
||||||
elif type(value) == str and len(value) == 0:
|
elif type(value) == str and len(value) == 0:
|
||||||
return_list.append(f'{key}')
|
return_list.append(f'{key}')
|
||||||
else:
|
else:
|
||||||
|
@@ -3,7 +3,6 @@ import numpy as np
|
|||||||
|
|
||||||
from scipy.optimize import linprog
|
from scipy.optimize import linprog
|
||||||
from collections import defaultdict, deque
|
from collections import defaultdict, deque
|
||||||
from geopy.distance import geodesic
|
|
||||||
|
|
||||||
from structs.landmark import Landmark
|
from structs.landmark import Landmark
|
||||||
from .get_time_separation import get_time
|
from .get_time_separation import get_time
|
||||||
@@ -488,7 +487,7 @@ class Optimizer:
|
|||||||
|
|
||||||
# Raise error if no solution is found
|
# Raise error if no solution is found
|
||||||
if not res.success :
|
if not res.success :
|
||||||
raise ArithmeticError("No solution could be found, the problem is overconstrained. Please adapt your must_dos")
|
raise ArithmeticError("No solution could be found, the problem is overconstrained. Try with a longer trip (>30 minutes).")
|
||||||
|
|
||||||
# If there is a solution, we're good to go, just check for connectiveness
|
# If there is a solution, we're good to go, just check for connectiveness
|
||||||
order, circles = self.is_connected(res.x)
|
order, circles = self.is_connected(res.x)
|
||||||
|
@@ -2,6 +2,7 @@ import yaml, logging
|
|||||||
|
|
||||||
from shapely import buffer, LineString, Point, Polygon, MultiPoint, concave_hull
|
from shapely import buffer, LineString, Point, Polygon, MultiPoint, concave_hull
|
||||||
from math import pi
|
from math import pi
|
||||||
|
from typing import List
|
||||||
|
|
||||||
from structs.landmark import Landmark
|
from structs.landmark import Landmark
|
||||||
from . import take_most_important, get_time_separation
|
from . import take_most_important, get_time_separation
|
||||||
@@ -134,6 +135,21 @@ class Refiner :
|
|||||||
|
|
||||||
return tour
|
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]:
|
def find_shortest_path_through_all_landmarks(self, landmarks: list[Landmark]) -> tuple[list[Landmark], Polygon]:
|
||||||
"""
|
"""
|
||||||
@@ -253,6 +269,11 @@ class Refiner :
|
|||||||
except :
|
except :
|
||||||
better_tour_poly = concave_hull(MultiPoint(coords)) # Create concave hull with "core" of tour leaving out start and finish
|
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
|
xs, ys = better_tour_poly.exterior.xy
|
||||||
|
"""
|
||||||
|
ERROR HERE :
|
||||||
|
Exception has occurred: AttributeError
|
||||||
|
'LineString' object has no attribute 'exterior'
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
# reverse the xs and ys
|
# reverse the xs and ys
|
||||||
@@ -315,26 +336,30 @@ class Refiner :
|
|||||||
|
|
||||||
self.logger.info(f"Using {len(minor_landmarks)} minor landmarks around the predicted path")
|
self.logger.info(f"Using {len(minor_landmarks)} minor landmarks around the predicted path")
|
||||||
|
|
||||||
# full set of visitable landmarks
|
# Full set of visitable landmarks.
|
||||||
full_set = base_tour[:-1] + minor_landmarks # create full set of possible landmarks (without finish)
|
full_set = self.integrate_landmarks(minor_landmarks, base_tour) # could probably be optimized with less overhead
|
||||||
full_set.append(base_tour[-1]) # add finish back
|
|
||||||
|
|
||||||
# get a new tour
|
# Generate a new tour with the optimizer.
|
||||||
new_tour = self.optimizer.solve_optimization(
|
new_tour = self.optimizer.solve_optimization(
|
||||||
max_time = max_time + detour,
|
max_time = max_time + detour,
|
||||||
landmarks = full_set,
|
landmarks = full_set,
|
||||||
max_landmarks = self.max_landmarks_refiner
|
max_landmarks = self.max_landmarks_refiner
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# If unsuccessful optimization, use the base_tour.
|
||||||
if new_tour is None:
|
if new_tour is None:
|
||||||
self.logger.warning("No solution found for the refined tour. Returning the initial tour.")
|
self.logger.warning("No solution found for the refined tour. Returning the initial tour.")
|
||||||
new_tour = base_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)
|
better_tour, better_poly = self.find_shortest_path_through_all_landmarks(new_tour)
|
||||||
|
|
||||||
# Fix the tour using Polygons if the path looks weird
|
# Fix the tour using Polygons if the path looks weird.
|
||||||
|
# Conditions : circular trip and invalid polygon.
|
||||||
if base_tour[0].location == base_tour[-1].location and not better_poly.is_valid :
|
if base_tour[0].location == base_tour[-1].location and not better_poly.is_valid :
|
||||||
better_tour = self.fix_using_polygon(better_tour)
|
better_tour = self.fix_using_polygon(better_tour)
|
||||||
|
|
||||||
|
@@ -37,13 +37,12 @@ jobs:
|
|||||||
REF_NAME: ${{ github.ref_name }}
|
REF_NAME: ${{ github.ref_name }}
|
||||||
run:
|
run:
|
||||||
# remove the 'v' prefix from the tag name
|
# remove the 'v' prefix from the tag name
|
||||||
echo "VERSION_NAME=${REF_NAME//v}" >> $GITHUB_ENV
|
echo "BUILD_NAME=${REF_NAME//v}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Load secrets from github
|
- name: Load secrets from github
|
||||||
run: |
|
run: |
|
||||||
echo "${{ secrets.ANDROID_SECRET_PROPERTIES }}" > secrets.properties
|
echo "${{ secrets.ANDROID_SECRET_PROPERTIES_BASE64 }}" | base64 -d > secrets.properties
|
||||||
echo "${{ secrets.ANDROID_GOOGLE_PLAY_JSON }}" > fastlane/google-key.json
|
echo "${{ secrets.ANDROID_GOOGLE_PLAY_JSON_BASE64 }}" | base64 -d > google-key.json
|
||||||
# decode the base64 encoded google key
|
|
||||||
echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > release.keystore
|
echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > release.keystore
|
||||||
working-directory: android
|
working-directory: android
|
||||||
|
|
||||||
@@ -54,4 +53,6 @@ jobs:
|
|||||||
- name: Run fastlane lane
|
- name: Run fastlane lane
|
||||||
run: bundle exec fastlane deploy_testing
|
run: bundle exec fastlane deploy_testing
|
||||||
working-directory: android
|
working-directory: android
|
||||||
# the environment variable VERSION_NAME is implicitly available
|
env:
|
||||||
|
BUILD_NUMBER: ${{ github.run_number }}
|
||||||
|
# BUILD_NAME is implicitly available
|
||||||
|
@@ -30,14 +30,19 @@ if (flutterVersionName == null) {
|
|||||||
|
|
||||||
|
|
||||||
def secretPropertiesFile = rootProject.file('secrets.properties')
|
def secretPropertiesFile = rootProject.file('secrets.properties')
|
||||||
|
def fallbackPropertiesFile = rootProject.file('fallback.properties')
|
||||||
def secretProperties = new Properties()
|
def secretProperties = new Properties()
|
||||||
|
|
||||||
if (secretPropertiesFile.exists()) {
|
if (secretPropertiesFile.exists()) {
|
||||||
secretPropertiesFile.withReader('UTF-8') { reader ->
|
secretPropertiesFile.withReader('UTF-8') { reader ->
|
||||||
secretProperties.load(reader)
|
secretProperties.load(reader)
|
||||||
}
|
}
|
||||||
|
} else if (fallbackPropertiesFile.exists()) {
|
||||||
|
fallbackPropertiesFile.withReader('UTF-8') { reader ->
|
||||||
|
secretProperties.load(reader)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new GradleException("Secrets file secrets.properties not found")
|
throw new GradleException("Secrets file (secrets.properties, fallback.properties) not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@@ -1 +1,3 @@
|
|||||||
|
# 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
|
MAPS_API_KEY=Key
|
@@ -5,22 +5,28 @@ default_platform(:android)
|
|||||||
|
|
||||||
platform :android do
|
platform :android do
|
||||||
|
|
||||||
desc "Deploy a new version as a preview version"
|
desc "Deploy a new version to closed testing"
|
||||||
lane :deploy_testing do
|
lane :deploy_testing do
|
||||||
version_name = ENV["VERSION_NAME"]
|
build_name = ENV["BUILD_NAME"]
|
||||||
|
build_number = ENV["BUILD_NUMBER"]
|
||||||
|
|
||||||
sh(
|
sh(
|
||||||
"flutter",
|
"flutter",
|
||||||
"build",
|
"build",
|
||||||
"appbundle",
|
"appbundle",
|
||||||
"--release",
|
"--release",
|
||||||
"--build-name=#{version_name}",
|
"--build-name=#{build_name}",
|
||||||
|
"--build-number=#{build_number}",
|
||||||
)
|
)
|
||||||
|
|
||||||
upload_to_play_store(
|
upload_to_play_store(
|
||||||
track: 'alpha',
|
track: 'alpha',
|
||||||
skip_upload_apk: true,
|
skip_upload_apk: true,
|
||||||
skip_upload_changelogs: 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
|
||||||
|
|
||||||
@@ -28,6 +34,7 @@ platform :android do
|
|||||||
lane :deploy_release do
|
lane :deploy_release do
|
||||||
gradle(
|
gradle(
|
||||||
task: "clean assembleRelease",
|
task: "clean assembleRelease",
|
||||||
|
# todo update to a flutter call
|
||||||
properties: {
|
properties: {
|
||||||
# loaded from environment
|
# loaded from environment
|
||||||
"android.injected.version.name" => ENV["VERSION_NAME"],
|
"android.injected.version.name" => ENV["VERSION_NAME"],
|
||||||
@@ -37,6 +44,10 @@ platform :android do
|
|||||||
track: "production",
|
track: "production",
|
||||||
skip_upload_apk: true,
|
skip_upload_apk: true,
|
||||||
skip_upload_changelogs: 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
|
||||||
end
|
end
|
||||||
|
Before Width: | Height: | Size: 106 KiB |
Before Width: | Height: | Size: 1.3 MiB |
Before Width: | Height: | Size: 637 KiB |
Before Width: | Height: | Size: 573 KiB |
Before Width: | Height: | Size: 175 KiB |
Before Width: | Height: | Size: 360 KiB |
Before Width: | Height: | Size: 106 KiB |
Before Width: | Height: | Size: 1.3 MiB |
Before Width: | Height: | Size: 637 KiB |
Before Width: | Height: | Size: 573 KiB |
Before Width: | Height: | Size: 175 KiB |
Before Width: | Height: | Size: 360 KiB |
37
frontend/lib/modules/current_trip_error_message.dart
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import 'package:anyway/structs/trip.dart';
|
||||||
|
import 'package:auto_size_text/auto_size_text.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class CurrentTripErrorMessage extends StatefulWidget {
|
||||||
|
final Trip trip;
|
||||||
|
const CurrentTripErrorMessage({
|
||||||
|
super.key,
|
||||||
|
required this.trip,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CurrentTripErrorMessage> createState() => _CurrentTripErrorMessageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CurrentTripErrorMessageState extends State<CurrentTripErrorMessage> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Center(
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.error_outline,
|
||||||
|
color: Colors.red,
|
||||||
|
size: 50,
|
||||||
|
),
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.only(left: 10),
|
||||||
|
),
|
||||||
|
AutoSizeText(
|
||||||
|
'Error: ${widget.trip.errorDescription}',
|
||||||
|
maxLines: 3,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
@@ -1,37 +1,27 @@
|
|||||||
import 'dart:developer';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:anyway/constants.dart';
|
|
||||||
import 'package:anyway/structs/trip.dart';
|
|
||||||
import 'package:auto_size_text/auto_size_text.dart';
|
import 'package:auto_size_text/auto_size_text.dart';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:anyway/pages/current_trip.dart';
|
||||||
|
import 'package:anyway/structs/trip.dart';
|
||||||
|
|
||||||
class Greeter extends StatefulWidget {
|
|
||||||
|
class CurrentTripGreeter extends StatefulWidget {
|
||||||
final Trip trip;
|
final Trip trip;
|
||||||
|
|
||||||
Greeter({
|
CurrentTripGreeter({
|
||||||
|
super.key,
|
||||||
required this.trip,
|
required this.trip,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<Greeter> createState() => _GreeterState();
|
State<CurrentTripGreeter> createState() => _CurrentTripGreeterState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class _GreeterState extends State<Greeter> {
|
class _CurrentTripGreeterState extends State<CurrentTripGreeter> {
|
||||||
|
@override
|
||||||
Widget greeterBuilder (BuildContext context, Widget? child) {
|
Widget build(BuildContext context) => Center(
|
||||||
final Shader textGradient = APP_GRADIENT.createShader(Rect.fromLTWH(0.0, 0.0, 200.0, 70.0));
|
child: FutureBuilder(
|
||||||
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,
|
future: widget.trip.cityName,
|
||||||
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
|
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
|
||||||
if (snapshot.hasData) {
|
if (snapshot.hasData) {
|
||||||
@@ -41,7 +31,6 @@ class _GreeterState extends State<Greeter> {
|
|||||||
style: greeterStyle
|
style: greeterStyle
|
||||||
);
|
);
|
||||||
} else if (snapshot.hasError) {
|
} else if (snapshot.hasError) {
|
||||||
log('Error while fetching city name');
|
|
||||||
return AutoSizeText(
|
return AutoSizeText(
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
'Welcome to your trip!',
|
'Welcome to your trip!',
|
||||||
@@ -55,57 +44,7 @@ class _GreeterState extends State<Greeter> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
|
||||||
} 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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
60
frontend/lib/modules/current_trip_loading_indicator.dart
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:auto_size_text/auto_size_text.dart';
|
||||||
|
|
||||||
|
import 'package:anyway/structs/trip.dart';
|
||||||
|
import 'package:anyway/pages/current_trip.dart';
|
||||||
|
|
||||||
|
class CurrentTripLoadingIndicator extends StatefulWidget {
|
||||||
|
final Trip trip;
|
||||||
|
const CurrentTripLoadingIndicator({
|
||||||
|
super.key,
|
||||||
|
required this.trip,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CurrentTripLoadingIndicator> createState() => _CurrentTripLoadingIndicatorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CurrentTripLoadingIndicatorState extends State<CurrentTripLoadingIndicator> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Center(
|
||||||
|
child: FutureBuilder(
|
||||||
|
future: widget.trip.cityName,
|
||||||
|
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
|
||||||
|
Widget greeter;
|
||||||
|
Widget loadingIndicator = const Padding(
|
||||||
|
padding: EdgeInsets.only(top: 10),
|
||||||
|
child: CircularProgressIndicator()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
greeter = AutoSizeText(
|
||||||
|
maxLines: 1,
|
||||||
|
'Generating your trip to ${snapshot.data}...',
|
||||||
|
style: greeterStyle,
|
||||||
|
);
|
||||||
|
} else if (snapshot.hasError) {
|
||||||
|
// the exact error is shown in the central part of the trip overview. No need to show it here
|
||||||
|
greeter = AutoSizeText(
|
||||||
|
maxLines: 1,
|
||||||
|
'Error while loading trip.',
|
||||||
|
style: greeterStyle,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
greeter = AutoSizeText(
|
||||||
|
maxLines: 1,
|
||||||
|
'Generating your trip...',
|
||||||
|
style: greeterStyle,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
greeter,
|
||||||
|
loadingIndicator,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
@@ -1,4 +1,6 @@
|
|||||||
import 'package:anyway/constants.dart';
|
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:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:anyway/structs/trip.dart';
|
import 'package:anyway/structs/trip.dart';
|
||||||
@@ -28,16 +30,36 @@ class _CurrentTripPanelState extends State<CurrentTripPanel> {
|
|||||||
return ListenableBuilder(
|
return ListenableBuilder(
|
||||||
listenable: widget.trip,
|
listenable: widget.trip,
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
if (widget.trip.uuid != 'pending' && widget.trip.uuid != 'error') {
|
if (widget.trip.uuid == 'error') {
|
||||||
|
return Align(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: SizedBox(
|
||||||
|
// reuse the exact same height as the panel has when collapsed
|
||||||
|
// this way the greeter will be centered when the panel is collapsed
|
||||||
|
height: MediaQuery.of(context).size.height * TRIP_PANEL_MIN_HEIGHT - 20,
|
||||||
|
child: CurrentTripErrorMessage(trip: widget.trip)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (widget.trip.uuid == 'pending') {
|
||||||
|
return Align(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: SizedBox(
|
||||||
|
// reuse the exact same height as the panel has when collapsed
|
||||||
|
// this way the greeter will be centered when the panel is collapsed
|
||||||
|
height: MediaQuery.of(context).size.height * TRIP_PANEL_MIN_HEIGHT - 20,
|
||||||
|
child: CurrentTripLoadingIndicator(trip: widget.trip),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
return ListView(
|
return ListView(
|
||||||
controller: widget.controller,
|
controller: widget.controller,
|
||||||
padding: const EdgeInsets.only(bottom: 30, left: 5, right: 5),
|
padding: const EdgeInsets.only(bottom: 30),
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
// reuse the exact same height as the panel has when collapsed
|
// reuse the exact same height as the panel has when collapsed
|
||||||
// this way the greeter will be centered when the panel is collapsed
|
// this way the greeter will be centered when the panel is collapsed
|
||||||
height: MediaQuery.of(context).size.height * TRIP_PANEL_MIN_HEIGHT - 20,
|
height: MediaQuery.of(context).size.height * TRIP_PANEL_MIN_HEIGHT - 20,
|
||||||
child: Greeter(trip: widget.trip),
|
child: CurrentTripGreeter(trip: widget.trip),
|
||||||
),
|
),
|
||||||
|
|
||||||
const Padding(padding: EdgeInsets.only(top: 10)),
|
const Padding(padding: EdgeInsets.only(top: 10)),
|
||||||
@@ -53,28 +75,6 @@ class _CurrentTripPanelState extends State<CurrentTripPanel> {
|
|||||||
Center(child: saveButton(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}'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@@ -6,6 +6,12 @@ import 'package:anyway/structs/trip.dart';
|
|||||||
import 'package:anyway/modules/current_trip_map.dart';
|
import 'package:anyway/modules/current_trip_map.dart';
|
||||||
import 'package:anyway/modules/current_trip_panel.dart';
|
import 'package:anyway/modules/current_trip_panel.dart';
|
||||||
|
|
||||||
|
final Shader textGradient = APP_GRADIENT.createShader(Rect.fromLTWH(0.0, 0.0, 200.0, 70.0));
|
||||||
|
TextStyle greeterStyle = TextStyle(
|
||||||
|
foreground: Paint()..shader = textGradient,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 26
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
class TripPage extends StatefulWidget {
|
class TripPage extends StatefulWidget {
|
||||||
@@ -35,7 +41,7 @@ class _TripPageState extends State<TripPage> {
|
|||||||
maxHeight: MediaQuery.of(context).size.height * TRIP_PANEL_MAX_HEIGHT,
|
maxHeight: MediaQuery.of(context).size.height * TRIP_PANEL_MAX_HEIGHT,
|
||||||
// padding in this context is annoying: it offsets the notion of vertical alignment.
|
// 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
|
// children that want to be centered vertically need to have their size adjusted by 2x the padding
|
||||||
padding: const EdgeInsets.only(top: 10),
|
padding: const EdgeInsets.all(10.0),
|
||||||
// Panel snapping should not be disabled because it significantly improves the user experience
|
// Panel snapping should not be disabled because it significantly improves the user experience
|
||||||
// panelSnapping: false
|
// panelSnapping: false
|
||||||
borderRadius: const BorderRadius.only(topLeft: Radius.circular(25), topRight: Radius.circular(25)),
|
borderRadius: const BorderRadius.only(topLeft: Radius.circular(25), topRight: Radius.circular(25)),
|
||||||
|
@@ -38,10 +38,18 @@ fetchTrip(
|
|||||||
String dataString = jsonEncode(data);
|
String dataString = jsonEncode(data);
|
||||||
log(dataString);
|
log(dataString);
|
||||||
|
|
||||||
final response = await dio.post(
|
late Response response;
|
||||||
|
try {
|
||||||
|
response = await dio.post(
|
||||||
"/trip/new",
|
"/trip/new",
|
||||||
data: data
|
data: data
|
||||||
);
|
);
|
||||||
|
} catch (e) {
|
||||||
|
trip.updateUUID("error");
|
||||||
|
trip.updateError(e.toString());
|
||||||
|
log(e.toString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// handle errors
|
// handle errors
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
|