1 Commits

Author SHA1 Message Date
0493eba332 remove gradle again
Some checks failed
/ push-to-remote (push) Successful in 12s
Build and deploy the backend to production / Deploy to production (push) Has been cancelled
Build and deploy the backend to production / Build and push image (push) Has been cancelled
2024-09-25 17:09:21 +02:00
42 changed files with 1444 additions and 1500 deletions

View File

@@ -6,7 +6,7 @@ on:
- frontend/** - frontend/**
name: Build and release debug APK name: Build and release 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 --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 working-directory: ./frontend
- name: Upload APKs to artifacts - name: Upload APKs to artifacts

View File

@@ -32,4 +32,4 @@ jobs:
- name: Deploy to k8s - name: Deploy to k8s
run: | run: |
kubectl apply -k backend/deployment/overlays/${{ inputs.overlay }} --kubeconfig=kubeconfig 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 }}

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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") == "true": if os.getenv('DEBUG', False):
from rich.logging import RichHandler from rich.logging import RichHandler
logging.basicConfig( logging.basicConfig(
level=logging.DEBUG, level=logging.DEBUG,

View File

@@ -63,7 +63,7 @@ def new_trip(preferences: Preferences, start: tuple[float, float], end: tuple[fl
refined_tour = refiner.refine_optimization(landmarks, base_tour, preferences.max_time_minute, preferences.detour_tolerance_minute) refined_tour = refiner.refine_optimization(landmarks, base_tour, preferences.max_time_minute, preferences.detour_tolerance_minute)
linked_tour = LinkedLandmarks(refined_tour) 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) trip = Trip.from_linked_landmarks(linked_tour, cache_client)
return trip return trip
@@ -84,4 +84,4 @@ def get_landmark(landmark_uuid: str) -> Landmark:
landmark = cache_client.get(f"landmark_{landmark_uuid}") landmark = cache_client.get(f"landmark_{landmark_uuid}")
return landmark return landmark
except KeyError: except KeyError:
raise HTTPException(status_code=404, detail="Landmark not found") raise HTTPException(status_code=404, detail="Landmark not found")

View File

@@ -1,6 +1,3 @@
# Tags were picked mostly arbitrarily, based on the OSM wiki and the OSM tags page.
# See https://taginfo.openstreetmap.org for more inspiration.
nature: nature:
leisure: park leisure: park
geological: '' geological: ''
@@ -14,24 +11,7 @@ nature:
- alpine_hut - alpine_hut
- viewpoint - viewpoint
- zoo - zoo
- resort waterway: waterfall
- picnic_site
water:
- pond
- lake
- river
- basin
- stream
- lagoon
- rapids
waterway:
- waterfall
- river
- canal
- dam
- dock
- boatyard
shopping: shopping:
shop: shop:
@@ -43,48 +23,10 @@ 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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ from uuid import uuid4
# Output to frontend # Output to frontend
class Landmark(BaseModel) : class Landmark(BaseModel) :
# Properties of the landmark # Properties of the landmark
name : str name : str
type: Literal['sightseeing', 'nature', 'shopping', 'start', 'finish'] type: Literal['sightseeing', 'nature', 'shopping', 'start', 'finish']
@@ -14,39 +14,31 @@ class Landmark(BaseModel) :
osm_id : int osm_id : int
attractiveness : int attractiveness : int
n_tags : int n_tags : int
image_url : Optional[str] = None image_url : Optional[str] = None # TODO future
website_url : Optional[str] = None website_url : Optional[str] = None
wikipedia_url : Optional[str] = None
description : Optional[str] = None # TODO future description : Optional[str] = None # TODO future
duration : Optional[int] = 0 duration : Optional[int] = 0 # TODO future
name_en : Optional[str] = None name_en : Optional[str] = None
# Unique ID of a given landmark # 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 # Additional properties depending on specific tour
must_do : Optional[bool] = False must_do : Optional[bool] = False
must_avoid : Optional[bool] = False must_avoid : Optional[bool] = False
is_secondary : Optional[bool] = False # TODO future 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 def __hash__(self) -> int:
next_uuid : Optional[str] = None return self.uuid.int
def __str__(self) -> str: def __str__(self) -> str:
time_to_next_str = f", time_to_next={self.time_to_reach_next}" if self.time_to_reach_next else "" 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 "" is_secondary_str = f", secondary" if self.is_secondary else ""
type_str = '(' + self.type + ')' type_str = '(' + self.type + ')'
if self.type in ["start", "finish", "nature", "shopping"] : type_str += '\t ' 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}]' 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)

View File

@@ -22,10 +22,9 @@ 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)
# 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)
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)
return trip return trip

View File

@@ -1,5 +1,5 @@
import yaml import yaml
from math import sin, cos, sqrt, atan2, radians from geopy.distance import geodesic
import constants import constants
@@ -8,7 +8,6 @@ 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:
""" """
@@ -17,34 +16,24 @@ def get_time(p1: tuple[float, float], p2: tuple[float, float]) -> int:
Args: Args:
p1 (Tuple[float, float]): Coordinates of the starting location. p1 (Tuple[float, float]): Coordinates of the starting location.
p2 (Tuple[float, float]): Coordinates of the destination. 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. 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 return 0
else: else:
# Compute the distance in km along the surface of the Earth dist = geodesic(p1, p2).kilometers
# (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])
dlon = lon2 - lon1 # Consider the detour factor for average cityto deterline walking distance (in km)
dlat = lat2 - lat1 walk_dist = dist*DETOUR_FACTOR
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_distance / AVERAGE_WALKING_SPEED * 60 walk_time = walk_dist/AVERAGE_WALKING_SPEED*60
return round(walk_time) return round(walk_time)

View File

@@ -1,17 +1,20 @@
import math import math as m
import yaml import yaml
import logging import logging
from OSMPythonTools.overpass import Overpass, overpassQueryBuilder from OSMPythonTools.overpass import Overpass, overpassQueryBuilder
from OSMPythonTools.cachingStrategy import CachingStrategy, JSON 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 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:
@@ -43,7 +46,7 @@ class LandmarkManager:
self.viewpoint_bonus = parameters['viewpoint_bonus'] self.viewpoint_bonus = parameters['viewpoint_bonus']
self.pay_bonus = parameters['pay_bonus'] self.pay_bonus = parameters['pay_bonus']
self.N_important = parameters['N_important'] self.N_important = parameters['N_important']
with constants.OPTIMIZER_PARAMETERS_PATH.open('r') as f: with constants.OPTIMIZER_PARAMETERS_PATH.open('r') as f:
parameters = yaml.safe_load(f) parameters = yaml.safe_load(f)
self.walking_speed = parameters['average_walking_speed'] self.walking_speed = parameters['average_walking_speed']
@@ -66,42 +69,87 @@ class LandmarkManager:
preferences (Preferences): The user's preference settings that influence the landmark selection. preferences (Preferences): The user's preference settings that influence the landmark selection.
Returns: Returns:
tuple[list[Landmark], list[Landmark]]: tuple[list[Landmark], list[Landmark]]:
- A list of all existing landmarks. - A list of all existing landmarks.
- A list of the most important landmarks based on the user's preferences. - 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 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) 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 L = []
all_landmarks = set()
bbox = self.create_bbox(center_coordinates, reachable_bbox_side) bbox = self.create_bbox(center_coordinates, reachable_bbox_side)
# list for sightseeing # list for sightseeing
if preferences.sightseeing.score != 0: if preferences.sightseeing.score != 0:
score_function = lambda score: score * 10 * preferences.sightseeing.score / 5 score_function = lambda score: int(score*10*preferences.sightseeing.score/5) # self.count_elements_close_to(loc) +
current_landmarks = self.fetch_landmarks(bbox, self.amenity_selectors['sightseeing'], preferences.sightseeing.type, score_function) L1 = self.fetch_landmarks(bbox, self.amenity_selectors['sightseeing'], preferences.sightseeing.type, score_function)
all_landmarks.update(current_landmarks) L += L1
# list for nature # list for nature
if preferences.nature.score != 0: if preferences.nature.score != 0:
score_function = lambda score: score * 10 * self.nature_coeff * preferences.nature.score / 5 score_function = lambda score: int(score*10*self.nature_coeff*preferences.nature.score/5) # self.count_elements_close_to(loc) +
current_landmarks = self.fetch_landmarks(bbox, self.amenity_selectors['nature'], preferences.nature.type, score_function) L2 = self.fetch_landmarks(bbox, self.amenity_selectors['nature'], preferences.nature.type, score_function)
all_landmarks.update(current_landmarks) L += L2
# list for shopping # list for shopping
if preferences.shopping.score != 0: if preferences.shopping.score != 0:
score_function = lambda score: score * 10 * preferences.shopping.score / 5 score_function = lambda score: int(score*10*preferences.shopping.score/5) # self.count_elements_close_to(loc) +
current_landmarks = self.fetch_landmarks(bbox, self.amenity_selectors['shopping'], preferences.shopping.type, score_function) L3 = self.fetch_landmarks(bbox, self.amenity_selectors['shopping'], preferences.shopping.type, score_function)
all_landmarks.update(current_landmarks) L += L3
landmarks_constrained = take_most_important(all_landmarks, self.N_important) L = self.remove_duplicates(L)
self.logger.info(f'Generated {len(all_landmarks)} landmarks around {center_coordinates}, and constrained to {len(landmarks_constrained)} most important ones.') # 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: def count_elements_close_to(self, coordinates: tuple[float, float]) -> int:
@@ -124,7 +172,7 @@ class LandmarkManager:
radius = self.radius_close_to 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} bbox = {'latLower':lat-alpha,'lonLower':lon-alpha,'latHigher':lat+alpha,'lonHigher': lon+alpha}
# Build the query to find elements within the radius # Build the query to find elements within the radius
@@ -168,7 +216,7 @@ class LandmarkManager:
# Convert distance to degrees # Convert distance to degrees
lat_diff = half_side_length_km / 111 # 1 degree latitude is approximately 111 km 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 # Calculate bbox
min_lat = lat - lat_diff min_lat = lat - lat_diff
@@ -207,32 +255,32 @@ 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 = ['count_tags()>5'], # conditions = [],
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)
except Exception as e: except Exception as e:
self.logger.error(f"Error fetching landmarks: {e}") self.logger.error(f"Error fetching landmarks: {e}")
continue return
for elem in result.elements(): for elem in result.elements():
name = elem.tag('name') name = elem.tag('name') # Add name
location = (elem.centerLat(), elem.centerLon()) location = (elem.centerLat(), elem.centerLon()) # Add coordinates (lat, lon)
# TODO: exclude these from the get go # TODO: exclude these from the get go
# skip if unprecise location # skip if unprecise location
if name is None or location[0] is None: if name is None or location[0] is None:
continue continue
# skip if unused
# if 'disused:leisure' in elem.tags().keys():
# continue
# skip if part of another building # skip if part of another building
if 'building:part' in elem.tags().keys() and elem.tag('building:part') == 'yes': if 'building:part' in elem.tags().keys() and elem.tag('building:part') == 'yes':
continue continue
@@ -243,6 +291,7 @@ class LandmarkManager:
n_tags = len(elem.tags().keys()) # Add number of tags n_tags = len(elem.tags().keys()) # Add number of tags
score = n_tags**self.tag_exponent # Add score score = n_tags**self.tag_exponent # Add score
website_url = None website_url = None
wikpedia_url = None
image_url = None image_url = None
name_en = None name_en = None
@@ -250,17 +299,22 @@ class LandmarkManager:
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 score += self.pay_bonus # discard payment options for tags
score += self.pay_bonus
if "disused" in tag: if "disused" in tag:
# skip disused amenities skip = True # skip disused amenities
skip = True
break break
if "wiki" in tag: if "wiki" in tag:
# wikipedia entries count more score += self.wikipedia_bonus # wikipedia entries count more
score += self.wikipedia_bonus
# 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: if "viewpoint" in tag:
score += self.viewpoint_bonus score += self.viewpoint_bonus
@@ -281,43 +335,47 @@ class LandmarkManager:
if tag == "building" and elem.tag('building') in ['retail', 'supermarket', 'parking']: if tag == "building" and elem.tag('building') in ['retail', 'supermarket', 'parking']:
skip = True skip = True
break break
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) website_url = elem.tag(tag)
if tag == 'image': if tag == 'image' :
image_url = elem.tag('image') image_url = elem.tag('image')
if tag =='name:en': if tag =='name:en' :
name_en = elem.tag('name:en') name_en = elem.tag('name:en')
if skip: if skip:
continue continue
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 = int(score*self.church_coeff)
duration = 15 duration = 15
elif "museum" in elem.tags().values(): elif "museum" in elem.tags().values() :
score = score * self.church_coeff score = int(score*self.church_coeff)
duration = 60 duration = 60
else: else :
duration = 5 duration = 5
# finally create our own landmark object # Generate the landmark and append it to the list
landmark = Landmark( landmark = Landmark(
name = name, name=name,
type = elem_type, type=elem_type,
location = location, location=location,
osm_type = osm_type, osm_type=osm_type,
osm_id = osm_id, osm_id=osm_id,
attractiveness = int(score), attractiveness=score,
must_do = False, must_do=False,
n_tags = int(n_tags), n_tags=int(n_tags),
duration = int(duration), duration = duration,
name_en = name_en, name_en=name_en,
image_url = image_url, image_url=image_url,
website_url = website_url # wikipedia_url=wikpedia_url,
website_url=website_url
) )
return_list.append(landmark) return_list.append(landmark)
@@ -341,7 +399,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:

View File

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

View File

@@ -1,16 +1,38 @@
from structs.landmark import Landmark from structs.landmark import Landmark
def take_most_important(landmarks: list[Landmark], n_important) -> list[Landmark]: def take_most_important(landmarks: list[Landmark], N_important) -> list[Landmark] :
""" L = len(landmarks)
Given a list of landmarks, return the n_important most important landmarks L_copy = []
Parameters: L_clean = []
landmarks: list[Landmark] - list of landmarks scores = [0]*len(landmarks)
n_important: int - number of most important landmarks to return names = []
Returns: name_id = {}
list[Landmark] - list of the n_important most important landmarks
"""
# Sort landmarks by attractiveness (descending) for i, elem in enumerate(landmarks) :
sorted_landmarks = sorted(landmarks, key=lambda x: x.attractiveness, reverse=True) 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

View File

@@ -1,7 +1,7 @@
on: on:
push: push:
tags: branches:
- 'v*' - main
jobs: jobs:
build: build:
@@ -23,13 +23,6 @@ jobs:
- name: Setup android SDK - name: Setup android SDK
uses: android-actions/setup-android@v3 uses: android-actions/setup-android@v3
- name: Install Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
flutter-version: 3.22.0
cache: true
- name: Infer version number from git tag - name: Infer version number from git tag
id: version id: version
@@ -41,9 +34,9 @@ jobs:
- name: Load secrets from github - name: Load secrets from github
run: | run: |
echo "${{ secrets.ANDROID_SECRET_PROPERTIES_BASE64 }}" | base64 -d > secrets.properties echo "${{ secrets.ANDROID_SECRET_PROPERTIES }}" > secrets.properties
echo "${{ secrets.ANDROID_GOOGLE_PLAY_JSON_BASE64 }}" | base64 -d > google-key.json echo "${{ secrets.ANDROID_KEYSTORE }}" > release.keystore
echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > release.keystore echo "${{ secrets.ANDROID_GOOGLE_PLAY_JSON }}" > google-key.json
working-directory: android working-directory: android
- name: Install fastlane - name: Install fastlane

View File

@@ -30,19 +30,14 @@ 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, fallback.properties) not found") throw new GradleException("Secrets file secrets.properties not found")
} }

View File

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

View File

@@ -21,10 +21,6 @@ platform :android do
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
@@ -41,10 +37,6 @@ 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 KiB

View File

@@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
const String APP_NAME = 'AnyWay'; const String APP_NAME = 'AnyWay';
String API_URL_BASE = 'https://anyway.anydev.info'; String API_URL_BASE = 'https://anyway.anydev.info';
String API_URL_DEBUG = 'https://anyway.anydev.info';
String PRIVACY_URL = 'https://anydev.info/privacy'; String PRIVACY_URL = 'https://anydev.info/privacy';
const String MAP_ID = '41c21ac9b81dbfd8'; const String MAP_ID = '41c21ac9b81dbfd8';

View File

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

View File

@@ -1,50 +1,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:auto_size_text/auto_size_text.dart';
import 'package:anyway/pages/current_trip.dart'; import 'package:flutter/material.dart';
import 'package:anyway/structs/trip.dart';
class Greeter extends StatefulWidget {
class CurrentTripGreeter extends StatefulWidget {
final Trip trip; final Trip trip;
CurrentTripGreeter({ Greeter({
super.key,
required this.trip, required this.trip,
}); });
@override @override
State<CurrentTripGreeter> createState() => _CurrentTripGreeterState(); State<Greeter> createState() => _GreeterState();
} }
class _CurrentTripGreeterState extends State<CurrentTripGreeter> { class _GreeterState extends State<Greeter> {
@override
Widget build(BuildContext context) => Center( Widget greeterBuilder (BuildContext context, Widget? child) {
child: FutureBuilder( final Shader textGradient = APP_GRADIENT.createShader(Rect.fromLTWH(0.0, 0.0, 200.0, 70.0));
future: widget.trip.cityName, TextStyle greeterStyle = TextStyle(
builder: (BuildContext context, AsyncSnapshot<String> snapshot) { foreground: Paint()..shader = textGradient,
if (snapshot.hasData) { fontWeight: FontWeight.bold,
return AutoSizeText( fontSize: 26
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
);
}
}
)
);
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,
);
}
} }

View File

@@ -1,60 +0,0 @@
import 'package:flutter/material.dart';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:anyway/structs/trip.dart';
import 'package:anyway/pages/current_trip.dart';
class CurrentTripLoadingIndicator extends StatefulWidget {
final Trip trip;
const CurrentTripLoadingIndicator({
super.key,
required this.trip,
});
@override
State<CurrentTripLoadingIndicator> createState() => _CurrentTripLoadingIndicatorState();
}
class _CurrentTripLoadingIndicatorState extends State<CurrentTripLoadingIndicator> {
@override
Widget build(BuildContext context) => Center(
child: FutureBuilder(
future: widget.trip.cityName,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
Widget greeter;
Widget loadingIndicator = const Padding(
padding: EdgeInsets.only(top: 10),
child: CircularProgressIndicator()
);
if (snapshot.hasData) {
greeter = AutoSizeText(
maxLines: 1,
'Generating your trip to ${snapshot.data}...',
style: greeterStyle,
);
} else if (snapshot.hasError) {
// the exact error is shown in the central part of the trip overview. No need to show it here
greeter = AutoSizeText(
maxLines: 1,
'Error while loading trip.',
style: greeterStyle,
);
} else {
greeter = AutoSizeText(
maxLines: 1,
'Generating your trip...',
style: greeterStyle,
);
}
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
greeter,
loadingIndicator,
],
);
}
)
);
}

View File

@@ -1,6 +1,4 @@
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';
@@ -30,36 +28,16 @@ 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 == 'error') { if (widget.trip.uuid != 'pending' && 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), padding: const EdgeInsets.only(bottom: 30, left: 5, right: 5),
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: CurrentTripGreeter(trip: widget.trip), child: Greeter(trip: widget.trip),
), ),
const Padding(padding: EdgeInsets.only(top: 10)), const Padding(padding: EdgeInsets.only(top: 10)),
@@ -75,6 +53,28 @@ 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}'),
),
],
);
} }
} }
); );

View File

@@ -34,7 +34,7 @@ class _NewTripButtonState extends State<NewTripButton> {
} }
return FloatingActionButton.extended( return FloatingActionButton.extended(
onPressed: onPressed, onPressed: onPressed,
icon: const Icon(Icons.directions), icon: const Icon(Icons.add),
label: AutoSizeText('Start planning!'), label: AutoSizeText('Start planning!'),
); );
} }

View File

@@ -6,12 +6,6 @@ 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 {
@@ -41,7 +35,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.all(10.0), padding: const EdgeInsets.only(top: 10),
// 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)),

View File

@@ -63,7 +63,7 @@ class _NewTripPreferencesPageState extends State<NewTripPreferencesPage> {
margin: const EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 0), margin: const EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 0),
shadowColor: Colors.grey, shadowColor: Colors.grey,
child: ListTile( child: ListTile(
leading: preferences.maxTime.icon, leading: Icon(Icons.timer),
title: Text(preferences.maxTime.description), title: Text(preferences.maxTime.description),
subtitle: CupertinoTimerPicker( subtitle: CupertinoTimerPicker(
mode: CupertinoTimerPickerMode.hm, mode: CupertinoTimerPickerMode.hm,

View File

@@ -61,7 +61,9 @@ class _SettingsPageState extends State<SettingsPage> {
return AlertDialog( return AlertDialog(
title: Text('Debug mode - use a custom API endpoint'), title: Text('Debug mode - use a custom API endpoint'),
content: TextField( content: TextField(
controller: TextEditingController(text: API_URL_DEBUG), decoration: InputDecoration(
hintText: 'https://anyway-stg.anydev.info'
),
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
API_URL_BASE = value; API_URL_BASE = value;

View File

@@ -38,18 +38,10 @@ fetchTrip(
String dataString = jsonEncode(data); String dataString = jsonEncode(data);
log(dataString); log(dataString);
late Response response; final response = await dio.post(
try { "/trip/new",
response = await dio.post( data: data
"/trip/new", );
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) {