16 Commits

Author SHA1 Message Date
881f6a901d Merge pull request 'fix/backend/veiwpoint-nodes-and-churches' (#38) from fix/backend/veiwpoint-nodes-and-churches into main
Reviewed-on: #38
2024-11-18 16:09:19 +00:00
2810d93f98 migrated tests
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 1m56s
Run linting on the backend code / Build (pull_request) Failing after 25s
Run testing on the backend code / Build (pull_request) Failing after 1m9s
Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 15s
2024-11-18 16:52:01 +01:00
4305b21329 first fixes 2024-11-18 15:39:20 +01:00
e18a9c63e6 Merge pull request 'feature/backend/better_time_management' (#34) from feature/backend/better_time_management into main
Reviewed-on: #34
2024-11-06 13:36:51 +00:00
5fcadbe8d8 extended backend readme
All checks were successful
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 2m8s
Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 14s
2024-11-05 18:05:34 +01:00
5afb646381 Update backend/README.md
All checks were successful
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 1m37s
Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 15s
2024-11-05 14:46:06 +00:00
d0e837377b Update backend/README.md
All checks were successful
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 2m12s
Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 14s
2024-11-05 14:45:47 +00:00
d94c69c545 somewhat better durations
All checks were successful
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 1m39s
Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 14s
2024-11-04 19:59:52 +01:00
9e595ad933 fixed the error
All checks were successful
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 1m40s
Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 14s
2024-11-04 17:32:38 +01:00
53d56f3e30 remove cmakelists from vscode settings
All checks were successful
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 1m41s
Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 14s
2024-11-04 16:55:23 +01:00
f39d02f967 better readme setup backend
All checks were successful
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 2m15s
Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 14s
2024-10-29 12:14:12 +01:00
94a7adac6c Merge pull request 'build id fixes' (#32) from fix/frontend/yet-another-fastlane-fix into main
Some checks failed
Build and deploy the backend to production / Deploy to production (push) Has been cancelled
Build and deploy the backend to production / Build and push image (push) Has been cancelled
/ push-to-remote (push) Successful in 13s
Reviewed-on: #32
2024-10-22 14:24:23 +00:00
4d99715447 build id fixes
Some checks failed
Build and release debug APK / Build APK (pull_request) Has been cancelled
2024-10-22 16:23:53 +02:00
48555e7429 Merge pull request 'adjust path' (#31) from fix/frontend/ci-adjustments into main
Some checks failed
Build and deploy the backend to production / Deploy to production (push) Has been cancelled
Build and deploy the backend to production / Build and push image (push) Has been cancelled
/ push-to-remote (push) Failing after 11s
Reviewed-on: #31
2024-10-22 13:52:32 +00:00
8b24876fd1 adjust path
Some checks failed
Build and release debug APK / Build APK (pull_request) Has been cancelled
2024-10-22 15:52:03 +02:00
c832461f29 Merge pull request 'fixes for fastlane and gitea actions' (#30) from fix/frontend/ci-adjustments into main
Reviewed-on: #30
2024-10-22 13:26:02 +00:00
25 changed files with 994 additions and 542 deletions

View File

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

@@ -0,0 +1,33 @@
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
working-directory: backend

6
.vscode/launch.json vendored
View File

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

3
.vscode/settings.json vendored Normal file
View File

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

2
backend/.pylintrc Normal file
View File

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

View File

@@ -4,6 +4,11 @@ verify_ssl = true
name = "pypi" name = "pypi"
[dev-packages] [dev-packages]
pylint = "*"
pytest = "*"
tomli = "*"
httpx = "*"
exceptiongroup = "*"
[packages] [packages]
numpy = "*" numpy = "*"

1038
backend/Pipfile.lock generated

File diff suppressed because it is too large Load Diff

View File

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

0
backend/src/__init__.py Normal file
View File

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,7 +45,6 @@ sightseeing:
- gallery - gallery
- artwork - artwork
- aquarium - aquarium
historic: '' historic: ''
amenity: amenity:
- planetarium - planetarium

View File

@@ -1,11 +1,12 @@
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.9
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: 6 wikipedia_bonus: 4
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.8 overshoot: 1.15

View File

@@ -1,7 +1,6 @@
from pymemcache.client.base import Client from pymemcache.client.base import Client
from pymemcache import serde
import constants from .constants import MEMCACHED_HOST_PATH
class DummyClient: class DummyClient:
@@ -16,13 +15,12 @@ class DummyClient:
return self._data[key] return self._data[key]
if constants.MEMCACHED_HOST_PATH is None: if MEMCACHED_HOST_PATH is None:
client = DummyClient() client = DummyClient()
else: else:
client = Client( client = Client(
constants.MEMCACHED_HOST_PATH, 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,6 +35,7 @@ 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 = 5), shopping=Preference(type='shopping', score = 0),
max_time_minute=100, max_time_minute=30,
detour_tolerance_minute=0 detour_tolerance_minute=0
) )
@@ -74,6 +74,7 @@ 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

View File

@@ -0,0 +1,141 @@
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. 1
def test_turckheim(client):
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'])
# 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. 2
def test_bellecour(client) :
duration_minutes = 35
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)
# 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 :
"""
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):
"""
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):
"""
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)
landmarks.append(Landmark(**landmark_data)) # Create Landmark objects
next_uuid = landmark_data.get('next_uuid') # Prepare for the next iteration
return landmarks
# 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
import constants from ..constants import OPTIMIZER_PARAMETERS_PATH
with constants.OPTIMIZER_PARAMETERS_PATH.open('r') as f: with 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,10 +5,11 @@ 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)
@@ -27,10 +28,10 @@ class LandmarkManager:
def __init__(self) -> None: def __init__(self) -> None:
with constants.AMENITY_SELECTORS_PATH.open('r') as f: with AMENITY_SELECTORS_PATH.open('r') as f:
self.amenity_selectors = yaml.safe_load(f) self.amenity_selectors = yaml.safe_load(f)
with constants.LANDMARK_PARAMETERS_PATH.open('r') as f: with 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']
@@ -39,18 +40,19 @@ 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 constants.OPTIMIZER_PARAMETERS_PATH.open('r') as f: with 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=constants.OSM_CACHE_DIR) CachingStrategy.use(JSON, cacheDir=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]]:
@@ -94,6 +96,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)
@@ -200,18 +204,29 @@ 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 = ['way', 'relation'], elementType = element_types,
# 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 = ['count_tags()>5'], conditions = query_conditions, # except for nature....
includeCenter = True, includeCenter = True,
out = 'body' out = 'body'
) )
@@ -227,18 +242,23 @@ 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
# skip if unprecise location # handle unprecise and no-name locations
if name is None or location[0] is None: if name is None or location[0] is None:
continue if osm_type == 'node' and 'viewpoint' in elem.tags().values():
name = 'Viewpoint'
name_en = 'Viewpoint'
location = (elem.lat(), elem.lon())
else :
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
@@ -246,59 +266,68 @@ class LandmarkManager:
image_url = None image_url = None
name_en = None name_en = None
# remove specific tags # Adjust scoring, browse through tag keys
skip = False skip = False
for tag in elem.tags().keys(): for tag_key in elem.tags().keys():
if "pay" in tag: if "pay" in tag_key:
# 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_key:
# skip disused amenities # skip disused amenities
skip = True skip = True
break break
if "wiki" in tag: 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 "viewpoint" in tag: if "image" in tag_key:
score += self.viewpoint_bonus # images must count more
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 and elem.tag('leisure') == "park": if "leisure" in tag_key and elem.tag('leisure') == "park":
elem_type = "nature" elem_type = "nature"
if landmarktype != "shopping": if landmarktype != "shopping":
if "shop" in tag: if "shop" in tag_key:
skip = True skip = True
break break
if tag == "building" and elem.tag('building') in ['retail', 'supermarket', 'parking']: if tag_key == "building" and elem.tag('building') in ['retail', 'supermarket', 'parking']:
skip = True skip = True
break break
if tag in ['website', 'contact:website']: # Extract image, website and english name
website_url = elem.tag(tag) if tag_key in ['website', 'contact:website']:
if tag == 'image': website_url = elem.tag(tag_key)
if tag_key == 'image':
image_url = elem.tag('image') image_url = elem.tag('image')
if tag =='name:en': if tag_key =='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 = 15 duration = 10
if 'viewpoint' in elem.tags().values() :
# viewpoints must count more
score += self.viewpoint_bonus
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:

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
import constants from ..constants import OPTIMIZER_PARAMETERS_PATH
@@ -26,7 +26,7 @@ class Optimizer:
def __init__(self) : def __init__(self) :
# load parameters from file # load parameters from file
with constants.OPTIMIZER_PARAMETERS_PATH.open('r') as f: with 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. 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)

View File

@@ -2,11 +2,12 @@ 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
import constants from ..constants import OPTIMIZER_PARAMETERS_PATH
@@ -24,7 +25,7 @@ class Refiner :
self.optimizer = optimizer self.optimizer = optimizer
# load parameters from file # load parameters from file
with constants.OPTIMIZER_PARAMETERS_PATH.open('r') as f: with 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']
@@ -133,6 +134,21 @@ class Refiner :
i += 1 i += 1
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)

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 "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: |
@@ -53,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

View File

@@ -5,26 +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", aab: "../build/app/outputs/bundle/release/app-release.aab",
# this is the default output of flutter build ... --release # this is the default output of flutter build ... --release
# in particular this the build folder lies in the flutter root folder # in particular this the build folder lies in the flutter root folder
# hence the relative path # this is the parent folder for the android folder
) )
end end
@@ -32,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"],
@@ -41,10 +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", aab: "../build/app/outputs/bundle/release/app-release.aab",
# this is the default output of flutter build ... --release # this is the default output of flutter build ... --release
# in particular this the build folder lies in the flutter root folder # in particular this the build folder lies in the flutter root folder
# hence the relative path # this is the parent folder for the android folder
) )
end end
end end