1 Commits

Author SHA1 Message Date
74b6550822 remove unneeded screenshots
Some checks failed
Build and deploy the backend to production / Build and push image (push) Successful in 1m35s
/ push-to-remote (push) Failing after 12s
Build and deploy the backend to production / Deploy to production (push) Successful in 14s
2024-10-22 11:49:11 +02:00
30 changed files with 634 additions and 2394 deletions

View File

@@ -1,34 +0,0 @@
on:
pull_request:
branches:
- main
paths:
- backend/**
name: Run linting on the backend code
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: https://gitea.com/actions/checkout@v4
- name: Install dependencies
run: |
apt-get update && apt-get install -y python3 python3-pip
pip install pipenv
- name: Install packages
run: |
ls -la
# only install dev-packages
pipenv install --categories=dev-packages
pipenv run pip freeze
working-directory: backend
- name: Run linter
run: pipenv run pylint src
working-directory: backend

View File

@@ -1,40 +0,0 @@
on:
pull_request:
branches:
- main
paths:
- backend/**
name: Run testing on the backend code
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: https://gitea.com/actions/checkout@v4
- name: Install dependencies
run: |
apt-get update && apt-get install -y python3 python3-pip
pip install pipenv
- name: Install packages
run: |
ls -la
# install all packages, including dev-packages
pipenv install --dev
pipenv run pip freeze
working-directory: backend
- name: Run Tests
run: pipenv run pytest src --html=report.html --self-contained-html
working-directory: backend
- name: Upload HTML report
if: always()
uses: https://gitea.com/actions/upload-artifact@v3
with:
name: pytest-html-report
path: backend/report.html

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

6
.vscode/launch.json vendored
View File

@@ -14,9 +14,9 @@
"DEBUG": "true" "DEBUG": "true"
}, },
"args": [ "args": [
// "--app-dir", "--app-dir",
// "src", "src",
"src.main:app", "main:app",
"--reload", "--reload",
], ],
"jinja": true, "jinja": true,

View File

@@ -1,3 +0,0 @@
{
"cmake.ignoreCMakeListsMissing": true
}

View File

@@ -1,2 +0,0 @@
[MAIN]
max-line-length=240

View File

@@ -4,14 +4,6 @@ verify_ssl = true
name = "pypi" name = "pypi"
[dev-packages] [dev-packages]
pylint = "*"
pytest = "*"
tomli = "*"
httpx = "*"
exceptiongroup = "*"
pytest-html = "*"
typing-extensions = "*"
dill = "*"
[packages] [packages]
numpy = "*" numpy = "*"

1336
backend/Pipfile.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,37 +1,12 @@
# Backend # 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 ## Getting Started
- The code of the python application is located in the `src` directory.
### Directory Structure - Package management is handled with `pipenv` and the dependencies are listed in the `Pipfile`.
- The code for the Python application is located in the `src` directory. - Since the application is aimed to be deployed in a container, the `Dockerfile` is provided to build the image.
- 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.

View File

@@ -1,47 +0,0 @@
import pytest
pytest_plugins = ["pytest_html"]
def pytest_html_report_title(report):
"""modifying the title of html report"""
report.title = "Backend Testing Report"
def pytest_html_results_table_header(cells):
cells.insert(2, "<th>Detailed trip</th>")
cells.insert(3, "<th>Trip Duration</th>")
cells.insert(4, "<th>Target Duration</th>")
cells[5] = "<th>Execution time</th>" # rename the column containing execution times to avoid confusion
def pytest_html_results_table_row(report, cells):
trip_details = getattr(report, "trip_details", "N/A") # Default to "N/A" if no trip data
trip_duration = getattr(report, "trip_duration", "N/A") # Default to "N/A" if no trip data
target_duration = getattr(report, "target_duration", "N/A") # Default to "N/A" if no trip data
cells.insert(2, f"<td>{trip_details}</td>")
cells.insert(3, f"<td>{trip_duration}</td>")
cells.insert(4, f"<td>{target_duration}</td>")
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
report.description = str(item.function.__doc__)
# Attach trip_details if it exists
if hasattr(item, "trip_details"):
report.trip_details = " - ".join(item.trip_details) # Convert list to string
else:
report.trip_details = "N/A" # Default if trip_string is not set
# Attach trip_duration if it exists
if hasattr(item, "trip_duration"):
report.trip_duration = item.trip_duration + " min"
else:
report.trip_duration = "N/A" # Default if duration is not set
# Attach target_duration if it exists
if hasattr(item, "target_duration"):
report.target_duration = item.target_duration + " min"
else:
report.target_duration = "N/A" # Default if duration is not set

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,14 @@
import logging import logging
from fastapi import FastAPI, Query, Body, HTTPException from fastapi import FastAPI, Query, Body, HTTPException
from .structs.landmark import Landmark from structs.landmark import Landmark
from .structs.preferences import Preferences from structs.preferences import Preferences
from .structs.linked_landmarks import LinkedLandmarks from structs.linked_landmarks import LinkedLandmarks
from .structs.trip import Trip from structs.trip import Trip
from .utils.landmarks_manager import LandmarkManager from utils.landmarks_manager import LandmarkManager
from .utils.optimizer import Optimizer from utils.optimizer import Optimizer
from .utils.refiner import Refiner from utils.refiner import Refiner
from .persistence import client as cache_client from persistence import client as cache_client
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@@ -45,6 +45,7 @@ sightseeing:
- gallery - gallery
- artwork - artwork
- aquarium - aquarium
historic: '' historic: ''
amenity: amenity:
- planetarium - planetarium

View File

@@ -1,12 +1,11 @@
city_bbox_side: 7500 #m city_bbox_side: 7500 #m
radius_close_to: 50 radius_close_to: 50
church_coeff: 0.9 church_coeff: 0.5
nature_coeff: 1.25 nature_coeff: 1.25
overall_coeff: 10 overall_coeff: 10
tag_exponent: 1.15 tag_exponent: 1.15
image_bonus: 10 image_bonus: 10
viewpoint_bonus: 15 viewpoint_bonus: 15
wikipedia_bonus: 4 wikipedia_bonus: 6
name_bonus: 3
N_important: 40 N_important: 40
pay_bonus: -1 pay_bonus: -1

View File

@@ -3,4 +3,4 @@ detour_corridor_width: 300
average_walking_speed: 4.8 average_walking_speed: 4.8
max_landmarks: 10 max_landmarks: 10
max_landmarks_refiner: 30 max_landmarks_refiner: 30
overshoot: 1.1 overshoot: 1.8

View File

@@ -1,6 +1,7 @@
from pymemcache.client.base import Client from pymemcache.client.base import Client
from pymemcache import serde
from .constants import MEMCACHED_HOST_PATH import constants
class DummyClient: class DummyClient:
@@ -15,12 +16,13 @@ class DummyClient:
return self._data[key] return self._data[key]
if MEMCACHED_HOST_PATH is None: if constants.MEMCACHED_HOST_PATH is None:
client = DummyClient() client = DummyClient()
else: else:
client = Client( client = Client(
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
) )

View File

@@ -1,5 +1,5 @@
from .landmark import Landmark from .landmark import Landmark
from ..utils.get_time_separation import get_time from utils.get_time_separation import get_time
class LinkedLandmarks: class LinkedLandmarks:
""" """
@@ -35,7 +35,6 @@ class LinkedLandmarks:
time_to_next = get_time(landmark.location, self._landmarks[i + 1].location) time_to_next = get_time(landmark.location, self._landmarks[i + 1].location)
landmark.time_to_reach_next = time_to_next landmark.time_to_reach_next = time_to_next
self.total_time += time_to_next self.total_time += time_to_next
self.total_time += landmark.duration
self._landmarks[-1].next_uuid = None self._landmarks[-1].next_uuid = None
self._landmarks[-1].time_to_reach_next = 0 self._landmarks[-1].time_to_reach_next = 0

View File

@@ -1,12 +1,12 @@
import logging import logging
import yaml import yaml
from .utils.landmarks_manager import LandmarkManager from utils.landmarks_manager import LandmarkManager
from .utils.optimizer import Optimizer from utils.optimizer import Optimizer
from .utils.refiner import Refiner from utils.refiner import Refiner
from .structs.landmark import Landmark from structs.landmark import Landmark
from .structs.linked_landmarks import LinkedLandmarks from structs.linked_landmarks import LinkedLandmarks
from .structs.preferences import Preferences, Preference from structs.preferences import Preferences, Preference
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -22,8 +22,8 @@ def test(start_coords: tuple[float, float], finish_coords: tuple[float, float] =
preferences = Preferences( preferences = Preferences(
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 = 0), shopping=Preference(type='shopping', score = 5),
max_time_minute=30, max_time_minute=100,
detour_tolerance_minute=0 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.8344400, 2.3220540))) # Café Chez César
# 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.2432090, 7.3892691))) # Orschwiller

View File

@@ -1,161 +0,0 @@
from fastapi.testclient import TestClient
from typing import List
import pytest
from ..main import app
from ..structs.landmark import Landmark
@pytest.fixture()
def client():
return TestClient(app)
# Base test for checking if the API returns correct error code when no preferences are specified.
def test_new_trip_invalid_prefs(client):
response = client.post(
"/trip/new",
json={
"preferences": {},
"start": [48.8566, 2.3522]
}
)
assert response.status_code == 422
# Test no. 2
def test_turckheim(client, request):
duration_minutes = 15
response = client.post(
"/trip/new",
json={
"preferences": {"sightseeing": {"type": "sightseeing", "score": 5}, "nature": {"type": "nature", "score": 5}, "shopping": {"type": "shopping", "score": 5}, "max_time_minute": duration_minutes, "detour_tolerance_minute": 0},
"start": [48.084588, 7.280405]
}
)
result = response.json()
landmarks = load_trip_landmarks(client, result['first_landmark_uuid'])
# Add details to report
log_trip_details(request, landmarks, result['total_time'], duration_minutes)
# checks :
assert response.status_code == 200 # check for successful planning
assert isinstance(landmarks, list) # check that the return type is a list
assert duration_minutes*0.8 < int(result['total_time']) < duration_minutes*1.2
assert len(landmarks) > 2 # check that there is something to visit
# Test no. 3
def test_bellecour(client, request) :
duration_minutes = 60
response = client.post(
"/trip/new",
json={
"preferences": {"sightseeing": {"type": "sightseeing", "score": 5}, "nature": {"type": "nature", "score": 5}, "shopping": {"type": "shopping", "score": 5}, "max_time_minute": duration_minutes, "detour_tolerance_minute": 0},
"start": [45.7576485, 4.8330241]
}
)
result = response.json()
landmarks = load_trip_landmarks(client, result['first_landmark_uuid'])
osm_ids = landmarks_to_osmid(landmarks)
# Add details to report
log_trip_details(request, landmarks, result['total_time'], duration_minutes)
# checks :
assert response.status_code == 200 # check for successful planning
assert duration_minutes*0.8 < int(result['total_time']) < duration_minutes*1.2
assert 136200148 in osm_ids # check for Cathédrale St. Jean in trip
def landmarks_to_osmid(landmarks: List[Landmark]) -> List[int] :
"""
Convert the list of landmarks into a list containing their osm ids for quick landmark checking.
Args :
landmarks (list): the list of landmarks
Returns :
ids (list) : the list of corresponding OSM ids
"""
ids = []
for landmark in landmarks :
ids.append(landmark.osm_id)
return ids
def fetch_landmark(client, landmark_uuid: str):
"""
Fetch landmark data from the API based on the landmark UUID.
Args:
landmark_uuid (str): The UUID of the landmark.
Returns:
dict: Landmark data fetched from the API.
"""
response = client.get(f"/landmark/{landmark_uuid}")
if response.status_code != 200:
raise Exception(f"Failed to fetch landmark with UUID {landmark_uuid}: {response.status_code}")
json_data = response.json()
if "detail" in json_data:
raise Exception(json_data["detail"])
return json_data
def load_trip_landmarks(client, first_uuid: str) -> List[Landmark]:
"""
Load all landmarks for a trip using the response from the API.
Args:
first_uuid (str) : The first UUID of the landmark.
Returns:
landmarks (list) : An list containing all landmarks for the trip.
"""
landmarks = []
next_uuid = first_uuid
while next_uuid is not None:
landmark_data = fetch_landmark(client, next_uuid)
# # Convert UUIDs to strings explicitly
# landmark_data = {
# key: str(value) if isinstance(value, UUID) else value
# for key, value in landmark_data.items()
# }
landmarks.append(Landmark(**landmark_data)) # Create Landmark objects
next_uuid = landmark_data.get('next_uuid') # Prepare for the next iteration
return landmarks
def log_trip_details(request, landmarks: List[Landmark], duration: int, target_duration: int) :
# Create the trip string
trip_string = [f"{landmark.name} ({landmark.attractiveness} | {landmark.duration}) - {landmark.time_to_reach_next}" for landmark in landmarks]
# Pass additional info to pytest for reporting
request.node.trip_details = trip_string
request.node.trip_duration = str(duration) # result['total_time']
request.node.target_duration = str(target_duration)
# def test_new_trip_single_prefs(client):
# response = client.post(
# "/trip/new",
# json={
# "preferences": {"sightseeing": {"type": "sightseeing", "score": 1}, "nature": {"type": "nature", "score": 1}, "shopping": {"type": "shopping", "score": 1}, "max_time_minute": 360, "detour_tolerance_minute": 0},
# "start": [48.8566, 2.3522]
# }
# )
# assert response.status_code == 200
# def test_new_trip_matches_prefs(client):
# # todo
# pass

View File

@@ -1,9 +1,9 @@
import yaml import yaml
from math import sin, cos, sqrt, atan2, radians from math import sin, cos, sqrt, atan2, radians
from ..constants import OPTIMIZER_PARAMETERS_PATH import constants
with 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)
DETOUR_FACTOR = parameters['detour_factor'] DETOUR_FACTOR = parameters['detour_factor']
AVERAGE_WALKING_SPEED = parameters['average_walking_speed'] AVERAGE_WALKING_SPEED = parameters['average_walking_speed']

View File

@@ -5,11 +5,10 @@ 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 ..structs.preferences import Preferences from structs.preferences import Preferences
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
from ..constants import AMENITY_SELECTORS_PATH, LANDMARK_PARAMETERS_PATH, OPTIMIZER_PARAMETERS_PATH, OSM_CACHE_DIR
# silence the overpass logger # silence the overpass logger
logging.getLogger('OSMPythonTools').setLevel(level=logging.CRITICAL) logging.getLogger('OSMPythonTools').setLevel(level=logging.CRITICAL)
@@ -28,10 +27,10 @@ class LandmarkManager:
def __init__(self) -> None: def __init__(self) -> None:
with AMENITY_SELECTORS_PATH.open('r') as f: with constants.AMENITY_SELECTORS_PATH.open('r') as f:
self.amenity_selectors = yaml.safe_load(f) self.amenity_selectors = yaml.safe_load(f)
with LANDMARK_PARAMETERS_PATH.open('r') as f: with constants.LANDMARK_PARAMETERS_PATH.open('r') as f:
parameters = yaml.safe_load(f) parameters = yaml.safe_load(f)
self.max_bbox_side = parameters['city_bbox_side'] self.max_bbox_side = parameters['city_bbox_side']
self.radius_close_to = parameters['radius_close_to'] self.radius_close_to = parameters['radius_close_to']
@@ -40,19 +39,18 @@ class LandmarkManager:
self.overall_coeff = parameters['overall_coeff'] self.overall_coeff = parameters['overall_coeff']
self.tag_exponent = parameters['tag_exponent'] self.tag_exponent = parameters['tag_exponent']
self.image_bonus = parameters['image_bonus'] self.image_bonus = parameters['image_bonus']
self.name_bonus = parameters['name_bonus']
self.wikipedia_bonus = parameters['wikipedia_bonus'] self.wikipedia_bonus = parameters['wikipedia_bonus']
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 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']
self.detour_factor = parameters['detour_factor'] self.detour_factor = parameters['detour_factor']
self.overpass = Overpass() self.overpass = Overpass()
CachingStrategy.use(JSON, cacheDir=OSM_CACHE_DIR) CachingStrategy.use(JSON, cacheDir=constants.OSM_CACHE_DIR)
def generate_landmarks_list(self, center_coordinates: tuple[float, float], preferences: Preferences) -> tuple[list[Landmark], list[Landmark]]: def generate_landmarks_list(self, center_coordinates: tuple[float, float], preferences: Preferences) -> tuple[list[Landmark], list[Landmark]]:
@@ -96,8 +94,6 @@ 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)
@@ -204,29 +200,18 @@ class LandmarkManager:
""" """
return_list = [] return_list = []
if landmarktype == 'nature' : query_conditions = []
else : query_conditions = ['count_tags()>5']
# caution, when applying a list of selectors, overpass will search for elements that match ALL selectors simultaneously # caution, when applying a list of selectors, overpass will search for elements that match ALL selectors simultaneously
# we need to split the selectors into separate queries and merge the results # we need to split the selectors into separate queries and merge the results
for sel in dict_to_selector_list(amenity_selector): for sel in dict_to_selector_list(amenity_selector):
self.logger.debug(f"Current selector: {sel}") self.logger.debug(f"Current selector: {sel}")
query_conditions = ['count_tags()>5']
element_types = ['way', 'relation']
if 'viewpoint' in sel :
query_conditions = []
element_types.append('node')
query = overpassQueryBuilder( query = overpassQueryBuilder(
bbox = bbox, bbox = bbox,
elementType = element_types, elementType = ['way', 'relation'],
# selector can in principle be a list already, # selector can in principle be a list already,
# but it generates the intersection of the queries # but it generates the intersection of the queries
# we want the union # we want the union
selector = sel, selector = sel,
conditions = query_conditions, # except for nature.... conditions = ['count_tags()>5'],
includeCenter = True, includeCenter = True,
out = 'body' out = 'body'
) )
@@ -242,23 +227,18 @@ class LandmarkManager:
name = elem.tag('name') name = elem.tag('name')
location = (elem.centerLat(), elem.centerLon()) location = (elem.centerLat(), elem.centerLon())
osm_type = elem.type() # Add type: 'way' or 'relation'
osm_id = elem.id() # Add OSM id
# TODO: exclude these from the get go # TODO: exclude these from the get go
# handle unprecise and no-name locations # skip if unprecise location
if name is None or location[0] is None: if name is None or location[0] is None:
if osm_type == 'node' and 'viewpoint' in elem.tags().values():
name = 'Viewpoint'
name_en = 'Viewpoint'
location = (elem.lat(), elem.lon())
else :
continue 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
osm_type = elem.type() # Add type: 'way' or 'relation'
osm_id = elem.id() # Add OSM id
elem_type = landmarktype # Add the landmark type as 'sightseeing, elem_type = landmarktype # Add the landmark type as 'sightseeing,
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
@@ -266,78 +246,59 @@ class LandmarkManager:
image_url = None image_url = None
name_en = None name_en = None
# Adjust scoring, browse through tag keys # remove specific tags
skip = False skip = False
for tag_key in elem.tags().keys(): for tag in elem.tags().keys():
if "pay" in tag_key: if "pay" in tag:
# payment options are misleading and should not count for the scoring. # payment options are a good sign
score += self.pay_bonus score += self.pay_bonus
if "disused" in tag_key: if "disused" in tag:
# skip disused amenities # skip disused amenities
skip = True skip = True
break break
if "boundary" in tag_key: if "wiki" in tag:
# skip "areas" like administrative boundaries and stuff
skip = True
break
if "historic" in tag_key and elem.tag('historic') in ['manor', 'optical_telegraph', 'pound', 'shieling', 'wayside_cross']:
# skip useless amenities
skip = True
break
if "name" in tag_key :
score += self.name_bonus
if "wiki" in tag_key:
# wikipedia entries count more # wikipedia entries count more
score += self.wikipedia_bonus score += self.wikipedia_bonus
if "image" in tag_key: if "viewpoint" in tag:
# images must count more score += self.viewpoint_bonus
duration = 10
if "image" in tag:
score += self.image_bonus score += self.image_bonus
if elem_type != "nature": if elem_type != "nature":
if "leisure" in tag_key and elem.tag('leisure') == "park": if "leisure" in tag and elem.tag('leisure') == "park":
elem_type = "nature" elem_type = "nature"
if landmarktype != "shopping": if landmarktype != "shopping":
if "shop" in tag_key: if "shop" in tag:
skip = True skip = True
break break
if tag_key == "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
# Extract image, website and english name if tag in ['website', 'contact:website']:
if tag_key in ['website', 'contact:website']: website_url = elem.tag(tag)
website_url = elem.tag(tag_key) if tag == 'image':
if tag_key == 'image':
image_url = elem.tag('image') image_url = elem.tag('image')
if tag_key =='name:en': if tag =='name:en':
name_en = elem.tag('name:en') name_en = elem.tag('name:en')
if skip: if skip:
continue continue
# Don't visit random apartments
if 'apartments' in elem.tags().values():
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 = score * self.church_coeff
duration = 10 duration = 15
if 'viewpoint' in elem.tags().values() : elif "museum" in elem.tags().values():
# viewpoints must count more score = score * self.church_coeff
score += self.viewpoint_bonus
duration = 10
elif "museum" in elem.tags().values() or "aquarium" in elem.tags().values() or "planetarium" in elem.tags().values():
duration = 60 duration = 60
else: else:

View File

@@ -4,9 +4,9 @@ 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 ..structs.landmark import Landmark from structs.landmark import Landmark
from .get_time_separation import get_time from .get_time_separation import get_time
from ..constants import OPTIMIZER_PARAMETERS_PATH import constants
@@ -26,7 +26,7 @@ class Optimizer:
def __init__(self) : def __init__(self) :
# load parameters from file # load parameters from file
with 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.detour_factor = parameters['detour_factor'] self.detour_factor = parameters['detour_factor']
self.average_walking_speed = parameters['average_walking_speed'] self.average_walking_speed = parameters['average_walking_speed']
@@ -487,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. 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 # 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)

View File

@@ -2,12 +2,11 @@ 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
from .optimizer import Optimizer from .optimizer import Optimizer
from ..constants import OPTIMIZER_PARAMETERS_PATH import constants
@@ -25,7 +24,7 @@ class Refiner :
self.optimizer = optimizer self.optimizer = optimizer
# load parameters from file # load parameters from file
with 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.detour_factor = parameters['detour_factor'] self.detour_factor = parameters['detour_factor']
self.detour_corridor_width = parameters['detour_corridor_width'] self.detour_corridor_width = parameters['detour_corridor_width']
@@ -135,21 +134,6 @@ 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]:
""" """
@@ -269,11 +253,6 @@ 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
@@ -336,30 +315,26 @@ 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 = self.integrate_landmarks(minor_landmarks, base_tour) # could probably be optimized with less overhead 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( 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)

View File

@@ -1,4 +1,4 @@
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]:
""" """

View File

@@ -37,7 +37,7 @@ 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 "BUILD_NAME=${REF_NAME//v}" >> $GITHUB_ENV echo "VERSION_NAME=${REF_NAME//v}" >> $GITHUB_ENV
- name: Load secrets from github - name: Load secrets from github
run: | run: |
@@ -53,6 +53,4 @@ 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
env: # the environment variable VERSION_NAME is implicitly available
BUILD_NUMBER: ${{ github.run_number }}
# BUILD_NAME is implicitly available

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

@@ -5,28 +5,22 @@ default_platform(:android)
platform :android do platform :android do
desc "Deploy a new version to closed testing" desc "Deploy a new version as a preview version"
lane :deploy_testing do lane :deploy_testing do
build_name = ENV["BUILD_NAME"] version_name = ENV["VERSION_NAME"]
build_number = ENV["BUILD_NUMBER"]
sh( sh(
"flutter", "flutter",
"build", "build",
"appbundle", "appbundle",
"--release", "--release",
"--build-name=#{build_name}", "--build-name=#{version_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
@@ -34,7 +28,6 @@ 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"],
@@ -44,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