Compare commits
64 Commits
v0.1.0
...
a676af3a67
Author | SHA1 | Date | |
---|---|---|---|
a676af3a67 | |||
e148c851e1 | |||
4ad867e609 | |||
6f2f86f936 | |||
56c55883ea | |||
8f6dfd404d | |||
aed407e2d0 | |||
f6d0cd5360 | |||
7a18830e99 | |||
ba14a0279e | |||
5a2c61d343 | |||
5e27dd9d79 | |||
d92001faaf | |||
73f0dc8361 | |||
05092e55f1 | |||
83be4b7616 | |||
8a9ec6b4d8 | |||
8c3145dfc9 | |||
2bf38119d6 | |||
ca711c614f | |||
357edf3000 | |||
444c47e3a4 | |||
da6ab207d9 | |||
c15e257dea | |||
5a698dd02c | |||
7e4a4b3dc7 | |||
84e5902436 | |||
81330e5eb3 | |||
9002483036 | |||
0271c3d7a7 | |||
4fd1272ea4 | |||
6bedd04a57 | |||
d31ca9f81f | |||
f6e396e54b | |||
d4de945df8 | |||
6f54522b8c | |||
080ecd28ae | |||
21706ea7e6 | |||
83c1533e78 | |||
1f4815c991 | |||
699737bc40 | |||
1240f86d6e | |||
2a5023df4b | |||
581644a108 | |||
f48dcf80c2 | |||
757773f433 | |||
25c2b6b0d1 | |||
b527318eec | |||
f2943eb3ad | |||
2ac8499dfb | |||
4a904c3d3c | |||
978cae290b | |||
bab6cfe74e | |||
71abeabbd2 | |||
f64e60ddf6 | |||
d6f723bee1 | |||
a3243431e0 | |||
3605408ebb | |||
d992b62533 | |||
e78bee4597 | |||
d186a51a87 | |||
4baf045c8c | |||
3f1fe463bf | |||
d58ef2562d |
@@ -28,7 +28,7 @@ jobs:
|
||||
working-directory: backend
|
||||
|
||||
- name: Run Tests
|
||||
run: pipenv run pytest src --html=report.html --self-contained-html --log-cli-level=INFO
|
||||
run: pipenv run pytest src --html=report.html --self-contained-html --log-cli-level=DEBUG
|
||||
working-directory: backend
|
||||
|
||||
- name: Upload HTML report
|
||||
|
7
.vscode/launch.json
vendored
@@ -36,7 +36,10 @@
|
||||
"type": "dart",
|
||||
"request": "launch",
|
||||
"program": "lib/main.dart",
|
||||
"cwd": "${workspaceFolder}/frontend"
|
||||
"cwd": "${workspaceFolder}/frontend",
|
||||
"env": {
|
||||
"GOOGLE_MAPS_API_KEY": "testing"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Frontend - profile",
|
||||
@@ -47,4 +50,4 @@
|
||||
"cwd": "${workspaceFolder}/frontend"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
30
LICENSE.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# License
|
||||
|
||||
## Proprietary License
|
||||
|
||||
All code and resources in this repository are the property of AnyDev. The software and related documentation are provided solely for use with services provided by AnyDev. Redistribution, modification, or use of this software outside of its intended service is strictly prohibited without explicit permission.
|
||||
|
||||
### Copyright © 2024 AnyDev
|
||||
|
||||
All rights reserved.
|
||||
|
||||
### Restrictions
|
||||
|
||||
- You may not modify, distribute, copy, or reverse engineer any part of this codebase.
|
||||
- This software is licensed for use solely in conjunction with services provided by AnyDev.
|
||||
- Any commercial use of this software is strictly prohibited without explicit written consent from AnyDev.
|
||||
|
||||
## Third-Party Dependencies
|
||||
|
||||
This project uses third-party dependencies, which are subject to their respective licenses.
|
||||
|
||||
- Python backend dependencies: fastapi, pydantic, numpy, shapely, etc. – Licensed under their respective licenses.
|
||||
- Flutter frontend dependencies: Cupertino Icons, sliding_up_panel, http, etc. – Licensed under their respective licenses.
|
||||
|
||||
Please refer to each project's documentation for the specific terms and conditions.
|
||||
|
||||
## OpenStreetMap Data Usage
|
||||
|
||||
This project uses data derived from **OpenStreetMap**. OpenStreetMap data is available under the [Open Database License (ODbL)](https://www.openstreetmap.org/copyright). We comply with the ODbL license, and some of the data displayed in the service may be derived from OpenStreetMap sources. We do not redistribute raw OpenStreetMap data; instead, it is processed and transformed before being used in our services.
|
||||
|
||||
More information about OpenStreetMap data usage can be found [here](https://www.openstreetmap.org/copyright).
|
3
backend/.gitignore
vendored
@@ -1,6 +1,9 @@
|
||||
# osm-cache
|
||||
cache_XML/
|
||||
|
||||
# secrets
|
||||
*secrets.yaml
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
@@ -445,7 +445,9 @@ disable=raw-checker-failed,
|
||||
logging-fstring-interpolation,
|
||||
duplicate-code,
|
||||
relative-beyond-top-level,
|
||||
invalid-name
|
||||
invalid-name,
|
||||
too-many-arguments,
|
||||
too-many-positional-arguments
|
||||
|
||||
# Enable the message, report, category or checker with the given id(s). You can
|
||||
# either give multiple identifier separated by comma (,) or put this option
|
||||
|
363
backend/landmarks.json
Normal file
@@ -0,0 +1,363 @@
|
||||
[
|
||||
{
|
||||
"name": "Chinatown",
|
||||
"type": "shopping",
|
||||
"location": [
|
||||
45.7554934,
|
||||
4.8444852
|
||||
],
|
||||
"osm_type": "way",
|
||||
"osm_id": 996515596,
|
||||
"attractiveness": 129,
|
||||
"n_tags": 0,
|
||||
"image_url": null,
|
||||
"website_url": null,
|
||||
"wiki_url": null,
|
||||
"keywords": {},
|
||||
"description": null,
|
||||
"duration": 30,
|
||||
"name_en": null,
|
||||
"uuid": "285d159c-68ee-4b37-8d71-f27ee3d38b02",
|
||||
"must_do": false,
|
||||
"must_avoid": false,
|
||||
"is_secondary": false,
|
||||
"time_to_reach_next": 0,
|
||||
"next_uuid": null,
|
||||
"is_viewpoint": false,
|
||||
"is_place_of_worship": false
|
||||
},
|
||||
{
|
||||
"name": "Galeries Lafayette",
|
||||
"type": "shopping",
|
||||
"location": [
|
||||
45.7627107,
|
||||
4.8556833
|
||||
],
|
||||
"osm_type": "way",
|
||||
"osm_id": 1069872743,
|
||||
"attractiveness": 197,
|
||||
"n_tags": 11,
|
||||
"image_url": null,
|
||||
"website_url": "http://www.galerieslafayette.com/",
|
||||
"wiki_url": null,
|
||||
"keywords": null,
|
||||
"description": null,
|
||||
"duration": 30,
|
||||
"name_en": null,
|
||||
"uuid": "28f1bc30-10d3-4944-8861-0ed9abca012d",
|
||||
"must_do": false,
|
||||
"must_avoid": false,
|
||||
"is_secondary": false,
|
||||
"time_to_reach_next": 0,
|
||||
"next_uuid": null,
|
||||
"is_viewpoint": false,
|
||||
"is_place_of_worship": false
|
||||
},
|
||||
{
|
||||
"name": "Muji",
|
||||
"type": "shopping",
|
||||
"location": [
|
||||
45.7615971,
|
||||
4.8543781
|
||||
],
|
||||
"osm_type": "way",
|
||||
"osm_id": 1044165817,
|
||||
"attractiveness": 259,
|
||||
"n_tags": 14,
|
||||
"image_url": null,
|
||||
"website_url": "https://www.muji.com/fr/",
|
||||
"wiki_url": null,
|
||||
"keywords": null,
|
||||
"description": null,
|
||||
"duration": 30,
|
||||
"name_en": "Muji",
|
||||
"uuid": "957f86a5-6c00-41a2-815d-d6f739052be4",
|
||||
"must_do": false,
|
||||
"must_avoid": false,
|
||||
"is_secondary": false,
|
||||
"time_to_reach_next": 0,
|
||||
"next_uuid": null,
|
||||
"is_viewpoint": false,
|
||||
"is_place_of_worship": false
|
||||
},
|
||||
{
|
||||
"name": "HEMA",
|
||||
"type": "shopping",
|
||||
"location": [
|
||||
45.7619133,
|
||||
4.8565239
|
||||
],
|
||||
"osm_type": "way",
|
||||
"osm_id": 1069872750,
|
||||
"attractiveness": 156,
|
||||
"n_tags": 9,
|
||||
"image_url": null,
|
||||
"website_url": "https://fr.westfield.com/lapartdieu/store/HEMA/www.hema.fr",
|
||||
"wiki_url": null,
|
||||
"keywords": null,
|
||||
"description": null,
|
||||
"duration": 30,
|
||||
"name_en": null,
|
||||
"uuid": "8dae9d3e-e4c4-4e80-941d-0b106e22c85b",
|
||||
"must_do": false,
|
||||
"must_avoid": false,
|
||||
"is_secondary": false,
|
||||
"time_to_reach_next": 0,
|
||||
"next_uuid": null,
|
||||
"is_viewpoint": false,
|
||||
"is_place_of_worship": false
|
||||
},
|
||||
{
|
||||
"name": "Cordeliers",
|
||||
"type": "shopping",
|
||||
"location": [
|
||||
45.7622752,
|
||||
4.8337998
|
||||
],
|
||||
"osm_type": "node",
|
||||
"osm_id": 5545183519,
|
||||
"attractiveness": 813,
|
||||
"n_tags": 0,
|
||||
"image_url": null,
|
||||
"website_url": null,
|
||||
"wiki_url": null,
|
||||
"keywords": {},
|
||||
"description": null,
|
||||
"duration": 30,
|
||||
"name_en": null,
|
||||
"uuid": "ba02adb5-e28f-4645-8c2d-25ead6232379",
|
||||
"must_do": false,
|
||||
"must_avoid": false,
|
||||
"is_secondary": false,
|
||||
"time_to_reach_next": 0,
|
||||
"next_uuid": null,
|
||||
"is_viewpoint": false,
|
||||
"is_place_of_worship": false
|
||||
},
|
||||
{
|
||||
"name": "Halles de Lyon Paul Bocuse",
|
||||
"type": "shopping",
|
||||
"location": [
|
||||
45.7628282,
|
||||
4.8505601
|
||||
],
|
||||
"osm_type": "relation",
|
||||
"osm_id": 971529,
|
||||
"attractiveness": 272,
|
||||
"n_tags": 12,
|
||||
"image_url": null,
|
||||
"website_url": "https://www.halles-de-lyon-paulbocuse.com/",
|
||||
"wiki_url": "fr:Halles de Lyon-Paul Bocuse",
|
||||
"keywords": {
|
||||
"importance": "national",
|
||||
"height": null,
|
||||
"place_type": "marketplace",
|
||||
"date": null
|
||||
},
|
||||
"description": "Halles de Lyon Paul Bocuse is a marketplace of national importance.",
|
||||
"duration": 30,
|
||||
"name_en": null,
|
||||
"uuid": "bbd50de3-aa91-425d-90c2-d4abfd1b4abe",
|
||||
"must_do": false,
|
||||
"must_avoid": false,
|
||||
"is_secondary": false,
|
||||
"time_to_reach_next": 0,
|
||||
"next_uuid": null,
|
||||
"is_viewpoint": false,
|
||||
"is_place_of_worship": false
|
||||
},
|
||||
{
|
||||
"name": "Grand Bazar",
|
||||
"type": "shopping",
|
||||
"location": [
|
||||
45.7632141,
|
||||
4.8361975
|
||||
],
|
||||
"osm_type": "way",
|
||||
"osm_id": 82399951,
|
||||
"attractiveness": 93,
|
||||
"n_tags": 7,
|
||||
"image_url": null,
|
||||
"website_url": null,
|
||||
"wiki_url": null,
|
||||
"keywords": null,
|
||||
"description": null,
|
||||
"duration": 30,
|
||||
"name_en": null,
|
||||
"uuid": "3de9131c-87c5-4efb-9fa8-064896fb8b29",
|
||||
"must_do": false,
|
||||
"must_avoid": false,
|
||||
"is_secondary": false,
|
||||
"time_to_reach_next": 0,
|
||||
"next_uuid": null,
|
||||
"is_viewpoint": false,
|
||||
"is_place_of_worship": false
|
||||
},
|
||||
{
|
||||
"name": "Shopping Area",
|
||||
"type": "shopping",
|
||||
"location": [
|
||||
45.7673452,
|
||||
4.8438683
|
||||
],
|
||||
"osm_type": "node",
|
||||
"osm_id": 0,
|
||||
"attractiveness": 156,
|
||||
"n_tags": 0,
|
||||
"image_url": null,
|
||||
"website_url": null,
|
||||
"wiki_url": null,
|
||||
"keywords": {},
|
||||
"description": null,
|
||||
"duration": 30,
|
||||
"name_en": null,
|
||||
"uuid": "df2482a8-7e2e-4536-aad3-564899b2fa65",
|
||||
"must_do": false,
|
||||
"must_avoid": false,
|
||||
"is_secondary": false,
|
||||
"time_to_reach_next": 0,
|
||||
"next_uuid": null,
|
||||
"is_viewpoint": false,
|
||||
"is_place_of_worship": false
|
||||
},
|
||||
{
|
||||
"name": "Cour Oxyg\u00e8ne",
|
||||
"type": "shopping",
|
||||
"location": [
|
||||
45.7620905,
|
||||
4.8568873
|
||||
],
|
||||
"osm_type": "way",
|
||||
"osm_id": 132673030,
|
||||
"attractiveness": 63,
|
||||
"n_tags": 5,
|
||||
"image_url": null,
|
||||
"website_url": null,
|
||||
"wiki_url": null,
|
||||
"keywords": null,
|
||||
"description": null,
|
||||
"duration": 30,
|
||||
"name_en": null,
|
||||
"uuid": "ed134f76-9a02-4bee-9c10-78454f7bc4ce",
|
||||
"must_do": false,
|
||||
"must_avoid": false,
|
||||
"is_secondary": false,
|
||||
"time_to_reach_next": 0,
|
||||
"next_uuid": null,
|
||||
"is_viewpoint": false,
|
||||
"is_place_of_worship": false
|
||||
},
|
||||
{
|
||||
"name": "P\u00f4le de Commerces et de Loisirs Confluence",
|
||||
"type": "shopping",
|
||||
"location": [
|
||||
45.7410414,
|
||||
4.8171031
|
||||
],
|
||||
"osm_type": "way",
|
||||
"osm_id": 440270633,
|
||||
"attractiveness": 259,
|
||||
"n_tags": 14,
|
||||
"image_url": null,
|
||||
"website_url": "https://www.confluence.fr/",
|
||||
"wiki_url": null,
|
||||
"keywords": null,
|
||||
"description": null,
|
||||
"duration": 30,
|
||||
"name_en": null,
|
||||
"uuid": "dd7e2f5f-0e60-4560-b903-e5ded4b6e36a",
|
||||
"must_do": false,
|
||||
"must_avoid": false,
|
||||
"is_secondary": false,
|
||||
"time_to_reach_next": 0,
|
||||
"next_uuid": null,
|
||||
"is_viewpoint": false,
|
||||
"is_place_of_worship": false
|
||||
},
|
||||
{
|
||||
"name": "Grand H\u00f4tel-Dieu",
|
||||
"type": "shopping",
|
||||
"location": [
|
||||
45.7586955,
|
||||
4.8364597
|
||||
],
|
||||
"osm_type": "relation",
|
||||
"osm_id": 300128,
|
||||
"attractiveness": 546,
|
||||
"n_tags": 22,
|
||||
"image_url": null,
|
||||
"website_url": "https://grand-hotel-dieu.com",
|
||||
"wiki_url": "fr:H\u00f4tel-Dieu de Lyon",
|
||||
"keywords": {
|
||||
"importance": "international",
|
||||
"height": null,
|
||||
"place_type": "building",
|
||||
"date": "C17"
|
||||
},
|
||||
"description": "Grand H\u00f4tel-Dieu is an internationally famous building. It was constructed in C17.",
|
||||
"duration": 30,
|
||||
"name_en": null,
|
||||
"uuid": "a91265a8-ffbd-44f7-a7ab-3ff75f08fbab",
|
||||
"must_do": false,
|
||||
"must_avoid": false,
|
||||
"is_secondary": false,
|
||||
"time_to_reach_next": 0,
|
||||
"next_uuid": null,
|
||||
"is_viewpoint": false,
|
||||
"is_place_of_worship": false
|
||||
},
|
||||
{
|
||||
"name": "Westfield La Part-Dieu",
|
||||
"type": "shopping",
|
||||
"location": [
|
||||
45.761331,
|
||||
4.855676
|
||||
],
|
||||
"osm_type": "way",
|
||||
"osm_id": 62338376,
|
||||
"attractiveness": 546,
|
||||
"n_tags": 22,
|
||||
"image_url": null,
|
||||
"website_url": "https://fr.westfield.com/lapartdieu",
|
||||
"wiki_url": "fr:La Part-Dieu (centre commercial)",
|
||||
"keywords": null,
|
||||
"description": null,
|
||||
"duration": 30,
|
||||
"name_en": null,
|
||||
"uuid": "7d60316f-d689-4fcf-be68-ffc09353b826",
|
||||
"must_do": false,
|
||||
"must_avoid": false,
|
||||
"is_secondary": false,
|
||||
"time_to_reach_next": 0,
|
||||
"next_uuid": null,
|
||||
"is_viewpoint": false,
|
||||
"is_place_of_worship": false
|
||||
},
|
||||
{
|
||||
"name": "Ainay",
|
||||
"type": "shopping",
|
||||
"location": [
|
||||
45.7553105,
|
||||
4.8312084
|
||||
],
|
||||
"osm_type": "node",
|
||||
"osm_id": 5545126047,
|
||||
"attractiveness": 132,
|
||||
"n_tags": 0,
|
||||
"image_url": null,
|
||||
"website_url": null,
|
||||
"wiki_url": null,
|
||||
"keywords": {},
|
||||
"description": null,
|
||||
"duration": 30,
|
||||
"name_en": null,
|
||||
"uuid": "ad214f3d-a4b9-4078-876a-446caa7ab01c",
|
||||
"must_do": false,
|
||||
"must_avoid": false,
|
||||
"is_secondary": false,
|
||||
"time_to_reach_next": 0,
|
||||
"next_uuid": null,
|
||||
"is_viewpoint": false,
|
||||
"is_place_of_worship": false
|
||||
}
|
||||
]
|
@@ -2,6 +2,7 @@
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import List, Literal, Tuple
|
||||
|
||||
|
||||
LOCATION_PREFIX = Path('src')
|
||||
@@ -14,6 +15,8 @@ OPTIMIZER_PARAMETERS_PATH = PARAMETERS_DIR / 'optimizer_parameters.yaml'
|
||||
cache_dir_string = os.getenv('OSM_CACHE_DIR', './cache')
|
||||
OSM_CACHE_DIR = Path(cache_dir_string)
|
||||
|
||||
OSM_TYPES = List[Literal['way', 'node', 'relation']]
|
||||
BBOX = Tuple[float, float, float, float]
|
||||
|
||||
MEMCACHED_HOST_PATH = os.getenv('MEMCACHED_HOST_PATH', None)
|
||||
if MEMCACHED_HOST_PATH == "none":
|
||||
|
0
backend/src/landmarks/__init__.py
Normal file
@@ -1,6 +1,6 @@
|
||||
"""Find clusters of interest to add more general areas of visit to the tour."""
|
||||
import logging
|
||||
from typing import Literal
|
||||
from typing import Literal, Tuple
|
||||
|
||||
import numpy as np
|
||||
from sklearn.cluster import DBSCAN
|
||||
@@ -8,8 +8,9 @@ from pydantic import BaseModel
|
||||
|
||||
from ..overpass.overpass import Overpass, get_base_info
|
||||
from ..structs.landmark import Landmark
|
||||
from .get_time_distance import get_distance
|
||||
from ..constants import OSM_CACHE_DIR
|
||||
from ..utils.get_time_distance import get_distance
|
||||
from ..utils.bbox import create_bbox
|
||||
|
||||
|
||||
|
||||
# silence the overpass logger
|
||||
@@ -32,7 +33,7 @@ class Cluster(BaseModel):
|
||||
"""
|
||||
type: Literal['street', 'area']
|
||||
importance: int
|
||||
centroid: tuple
|
||||
centroid: Tuple[float, float]
|
||||
# start: Optional[list] = None # for later use if we want to have streets as well
|
||||
# end: Optional[list] = None
|
||||
|
||||
@@ -79,8 +80,7 @@ class ClusterManager:
|
||||
bbox: The bounding box coordinates (around:radius, center_lat, center_lon).
|
||||
"""
|
||||
# Setup the caching in the Overpass class.
|
||||
self.overpass = Overpass(caching_strategy='XML', cache_dir=OSM_CACHE_DIR)
|
||||
|
||||
self.overpass = Overpass()
|
||||
|
||||
self.cluster_type = cluster_type
|
||||
if cluster_type == 'shopping' :
|
||||
@@ -95,32 +95,29 @@ class ClusterManager:
|
||||
raise NotImplementedError("Please choose only an available option for cluster detection")
|
||||
|
||||
# Initialize the points for cluster detection
|
||||
query = self.overpass.build_query(
|
||||
area = bbox,
|
||||
try:
|
||||
result = self.overpass.send_query(
|
||||
bbox = bbox,
|
||||
osm_types = osm_types,
|
||||
selector = sel,
|
||||
out = out
|
||||
)
|
||||
self.logger.debug(f"Cluster query: {query}")
|
||||
|
||||
try:
|
||||
result = self.overpass.send_query(query)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error fetching landmarks: {e}")
|
||||
self.logger.warning(f"Error fetching clusters: {e}")
|
||||
|
||||
if result is None :
|
||||
self.logger.error(f"Error fetching {cluster_type} clusters, overpass query returned None.")
|
||||
self.logger.debug(f"Found no {cluster_type} clusters, overpass query returned no datapoints.")
|
||||
self.valid = False
|
||||
|
||||
else :
|
||||
points = []
|
||||
for osm_type in osm_types :
|
||||
for elem in result.findall(osm_type):
|
||||
|
||||
# Get coordinates and append them to the points list
|
||||
_, coords = get_base_info(elem, osm_type)
|
||||
if coords is not None :
|
||||
points.append(coords)
|
||||
for elem in result:
|
||||
osm_type = elem.get('type')
|
||||
|
||||
# Get coordinates and append them to the points list
|
||||
_, coords = get_base_info(elem, osm_type)
|
||||
if coords is not None :
|
||||
points.append(coords)
|
||||
|
||||
if points :
|
||||
self.all_points = np.array(points)
|
||||
@@ -137,7 +134,7 @@ class ClusterManager:
|
||||
|
||||
# Check that there are is least 1 cluster
|
||||
if len(set(labels)) > 1 :
|
||||
self.logger.debug(f"Found {len(set(labels))} different clusters.")
|
||||
self.logger.info(f"Found {len(set(labels))} different {cluster_type} clusters.")
|
||||
# Separate clustered points and noise points
|
||||
self.cluster_points = self.all_points[labels != -1]
|
||||
self.cluster_labels = labels[labels != -1]
|
||||
@@ -145,7 +142,7 @@ class ClusterManager:
|
||||
self.valid = True
|
||||
|
||||
else :
|
||||
self.logger.debug(f"Detected 0 {cluster_type} clusters.")
|
||||
self.logger.info(f"Found 0 {cluster_type} clusters.")
|
||||
self.valid = False
|
||||
|
||||
else :
|
||||
@@ -181,11 +178,12 @@ class ClusterManager:
|
||||
|
||||
# Calculate the centroid as the mean of the points
|
||||
centroid = np.mean(current_cluster, axis=0)
|
||||
centroid = tuple((round(centroid[0], 7), round(centroid[1], 7)))
|
||||
|
||||
if self.cluster_type == 'shopping' :
|
||||
score = len(current_cluster)*2
|
||||
score = len(current_cluster)*3
|
||||
else :
|
||||
score = len(current_cluster)*8
|
||||
score = len(current_cluster)*15
|
||||
locations.append(Cluster(
|
||||
type='area',
|
||||
centroid=centroid,
|
||||
@@ -218,8 +216,7 @@ class ClusterManager:
|
||||
"""
|
||||
|
||||
# Define the bounding box for a given radius around the coordinates
|
||||
lat, lon = cluster.centroid
|
||||
bbox = (1000, lat, lon)
|
||||
bbox = create_bbox(cluster.centroid, 300)
|
||||
|
||||
# Query neighborhoods and shopping malls
|
||||
selectors = ['"place"~"^(suburb|neighborhood|neighbourhood|quarter|city_block)$"']
|
||||
@@ -227,10 +224,10 @@ class ClusterManager:
|
||||
if self.cluster_type == 'shopping' :
|
||||
selectors.append('"shop"="mall"')
|
||||
new_name = 'Shopping Area'
|
||||
t = 40
|
||||
t = 30
|
||||
else :
|
||||
new_name = 'Neighborhood'
|
||||
t = 15
|
||||
t = 20
|
||||
|
||||
min_dist = float('inf')
|
||||
osm_id = 0
|
||||
@@ -238,37 +235,32 @@ class ClusterManager:
|
||||
osm_types = ['node', 'way', 'relation']
|
||||
|
||||
for sel in selectors :
|
||||
query = self.overpass.build_query(
|
||||
area = bbox,
|
||||
osm_types = osm_types,
|
||||
selector = sel,
|
||||
out = 'ids center'
|
||||
)
|
||||
|
||||
try:
|
||||
result = self.overpass.send_query(query)
|
||||
result = self.overpass.send_query(bbox = bbox,
|
||||
osm_types = osm_types,
|
||||
selector = sel,
|
||||
out = 'ids center tags'
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error fetching landmarks: {e}")
|
||||
self.logger.warning(f"Error fetching clusters: {e}")
|
||||
continue
|
||||
|
||||
if result is None :
|
||||
self.logger.error(f"Error fetching landmarks: {e}")
|
||||
self.logger.warning(f"Error fetching clusters: query result is None")
|
||||
continue
|
||||
|
||||
for osm_type in osm_types :
|
||||
for elem in result.findall(osm_type):
|
||||
for elem in result:
|
||||
# Get basic info
|
||||
id, coords, name = get_base_info(elem, elem.get('type'), with_name=True)
|
||||
if name is None or coords is None :
|
||||
continue
|
||||
|
||||
id, coords, name = get_base_info(elem, osm_type, with_name=True)
|
||||
|
||||
if name is None or coords is None :
|
||||
continue
|
||||
|
||||
d = get_distance(cluster.centroid, coords)
|
||||
if d < min_dist :
|
||||
min_dist = d
|
||||
new_name = name
|
||||
osm_type = osm_type # Add type: 'way' or 'relation'
|
||||
osm_id = id # Add OSM id
|
||||
d = get_distance(cluster.centroid, coords)
|
||||
if d < min_dist :
|
||||
min_dist = d
|
||||
new_name = name # add name
|
||||
osm_type = elem.get('type') # add type: 'way' or 'relation'
|
||||
osm_id = id # add OSM id
|
||||
|
||||
return Landmark(
|
||||
name=new_name,
|
@@ -1,19 +1,15 @@
|
||||
"""Module used to import data from OSM and arrange them in categories."""
|
||||
import logging
|
||||
import xml.etree.ElementTree as ET
|
||||
import yaml
|
||||
|
||||
|
||||
from ..structs.preferences import Preferences
|
||||
from ..structs.landmark import Landmark
|
||||
from .take_most_important import take_most_important
|
||||
from ..utils.take_most_important import take_most_important
|
||||
from .cluster_manager import ClusterManager
|
||||
from ..overpass.overpass import Overpass, get_base_info
|
||||
from ..utils.bbox import create_bbox
|
||||
|
||||
from ..constants import AMENITY_SELECTORS_PATH, LANDMARK_PARAMETERS_PATH, OPTIMIZER_PARAMETERS_PATH, OSM_CACHE_DIR
|
||||
|
||||
# silence the overpass logger
|
||||
logging.getLogger('Overpass').setLevel(level=logging.CRITICAL)
|
||||
from ..constants import AMENITY_SELECTORS_PATH, LANDMARK_PARAMETERS_PATH, OPTIMIZER_PARAMETERS_PATH
|
||||
|
||||
|
||||
class LandmarkManager:
|
||||
@@ -37,14 +33,12 @@ class LandmarkManager:
|
||||
|
||||
with LANDMARK_PARAMETERS_PATH.open('r') as f:
|
||||
parameters = yaml.safe_load(f)
|
||||
self.max_bbox_side = parameters['city_bbox_side']
|
||||
self.radius_close_to = parameters['radius_close_to']
|
||||
self.max_bbox_side = parameters['max_bbox_side']
|
||||
self.church_coeff = parameters['church_coeff']
|
||||
self.nature_coeff = parameters['nature_coeff']
|
||||
self.overall_coeff = parameters['overall_coeff']
|
||||
self.tag_exponent = parameters['tag_exponent']
|
||||
self.image_bonus = parameters['image_bonus']
|
||||
self.name_bonus = parameters['name_bonus']
|
||||
self.wikipedia_bonus = parameters['wikipedia_bonus']
|
||||
self.viewpoint_bonus = parameters['viewpoint_bonus']
|
||||
self.pay_bonus = parameters['pay_bonus']
|
||||
@@ -56,7 +50,7 @@ class LandmarkManager:
|
||||
self.detour_factor = parameters['detour_factor']
|
||||
|
||||
# Setup the caching in the Overpass class.
|
||||
self.overpass = Overpass(caching_strategy='XML', cache_dir=OSM_CACHE_DIR)
|
||||
self.overpass = Overpass()
|
||||
|
||||
self.logger.info('LandmakManager successfully initialized.')
|
||||
|
||||
@@ -80,39 +74,39 @@ class LandmarkManager:
|
||||
"""
|
||||
self.logger.debug('Starting to fetch landmarks...')
|
||||
max_walk_dist = int((preferences.max_time_minute/2)/60*self.walking_speed*1000/self.detour_factor)
|
||||
reachable_bbox_side = min(max_walk_dist, self.max_bbox_side)
|
||||
radius = min(max_walk_dist, int(self.max_bbox_side/2))
|
||||
|
||||
# use set to avoid duplicates, this requires some __methods__ to be set in Landmark
|
||||
all_landmarks = set()
|
||||
|
||||
# Create a bbox using the around technique, tuple of strings
|
||||
bbox = tuple((min(2000, reachable_bbox_side/2), center_coordinates[0], center_coordinates[1]))
|
||||
bbox = create_bbox(center_coordinates, radius)
|
||||
|
||||
# list for sightseeing
|
||||
if preferences.sightseeing.score != 0:
|
||||
self.logger.debug('Fetching sightseeing landmarks...')
|
||||
current_landmarks = self.fetch_landmarks(bbox, self.amenity_selectors['sightseeing'], preferences.sightseeing.type, preferences.sightseeing.score)
|
||||
all_landmarks.update(current_landmarks)
|
||||
self.logger.debug('Fetching sightseeing clusters...')
|
||||
self.logger.info(f'Found {len(current_landmarks)} sightseeing landmarks')
|
||||
|
||||
# special pipeline for historic neighborhoods
|
||||
neighborhood_manager = ClusterManager(bbox, 'sightseeing')
|
||||
historic_clusters = neighborhood_manager.generate_clusters()
|
||||
all_landmarks.update(historic_clusters)
|
||||
self.logger.debug('Sightseeing clusters done')
|
||||
|
||||
# list for nature
|
||||
if preferences.nature.score != 0:
|
||||
self.logger.debug('Fetching nature landmarks...')
|
||||
current_landmarks = self.fetch_landmarks(bbox, self.amenity_selectors['nature'], preferences.nature.type, preferences.nature.score)
|
||||
all_landmarks.update(current_landmarks)
|
||||
self.logger.info(f'Found {len(current_landmarks)} nature landmarks')
|
||||
|
||||
|
||||
# list for shopping
|
||||
if preferences.shopping.score != 0:
|
||||
self.logger.debug('Fetching shopping landmarks...')
|
||||
current_landmarks = self.fetch_landmarks(bbox, self.amenity_selectors['shopping'], preferences.shopping.type, preferences.shopping.score)
|
||||
self.logger.debug('Fetching shopping clusters...')
|
||||
self.logger.info(f'Found {len(current_landmarks)} shopping landmarks')
|
||||
|
||||
# set time for all shopping activites :
|
||||
for landmark in current_landmarks :
|
||||
@@ -123,8 +117,6 @@ class LandmarkManager:
|
||||
shopping_manager = ClusterManager(bbox, 'shopping')
|
||||
shopping_clusters = shopping_manager.generate_clusters()
|
||||
all_landmarks.update(shopping_clusters)
|
||||
self.logger.debug('Shopping clusters done')
|
||||
|
||||
|
||||
|
||||
landmarks_constrained = take_most_important(all_landmarks, self.n_important)
|
||||
@@ -154,6 +146,8 @@ class LandmarkManager:
|
||||
score *= self.wikipedia_bonus
|
||||
if landmark.is_place_of_worship :
|
||||
score *= self.church_coeff
|
||||
if landmark.is_viewpoint :
|
||||
score *= self.viewpoint_bonus
|
||||
if landmarktype == 'nature' :
|
||||
score *= self.nature_coeff
|
||||
|
||||
@@ -179,7 +173,7 @@ class LandmarkManager:
|
||||
"""
|
||||
return_list = []
|
||||
|
||||
if landmarktype == 'nature' : query_conditions = []
|
||||
if landmarktype == 'nature' : query_conditions = None
|
||||
else : query_conditions = ['count_tags()>5']
|
||||
|
||||
# caution, when applying a list of selectors, overpass will search for elements that match ALL selectors simultaneously
|
||||
@@ -190,119 +184,240 @@ class LandmarkManager:
|
||||
osm_types = ['way', 'relation']
|
||||
|
||||
if 'viewpoint' in sel :
|
||||
query_conditions = []
|
||||
query_conditions = None
|
||||
osm_types.append('node')
|
||||
|
||||
query = self.overpass.build_query(
|
||||
area = bbox,
|
||||
osm_types = osm_types,
|
||||
selector = sel,
|
||||
conditions = query_conditions, # except for nature....
|
||||
out = 'center'
|
||||
)
|
||||
self.logger.debug(f"Query: {query}")
|
||||
|
||||
# Send the overpass query
|
||||
try:
|
||||
result = self.overpass.send_query(query)
|
||||
result = self.overpass.send_query(
|
||||
bbox = bbox,
|
||||
osm_types = osm_types,
|
||||
selector = sel,
|
||||
conditions = query_conditions, # except for nature....
|
||||
out = 'ids center tags'
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error fetching landmarks: {e}")
|
||||
self.logger.debug(f"Failed to fetch landmarks, proceeding without: {str(e)}")
|
||||
continue
|
||||
|
||||
return_list += self.xml_to_landmarks(result, landmarktype, preference_level)
|
||||
return_list += self._to_landmarks(result, landmarktype, preference_level)
|
||||
|
||||
self.logger.debug(f"Fetched {len(return_list)} landmarks of type {landmarktype} in {bbox}")
|
||||
# self.logger.debug(f"Fetched {len(return_list)} landmarks of type {landmarktype} in {bbox}")
|
||||
|
||||
return return_list
|
||||
|
||||
|
||||
def xml_to_landmarks(self, root: ET.Element, landmarktype, preference_level) -> list[Landmark]:
|
||||
def _to_landmarks(self, elements: list, landmarktype, preference_level) -> list[Landmark]:
|
||||
"""
|
||||
Parse the Overpass API result and extract landmarks.
|
||||
|
||||
This method processes the XML root element returned by the Overpass API and
|
||||
This method processes the JSON elements returned by the Overpass API and
|
||||
extracts landmarks of types 'node', 'way', and 'relation'. It retrieves
|
||||
relevant information such as name, coordinates, and tags, and converts them
|
||||
into Landmark objects.
|
||||
|
||||
Args:
|
||||
root (ET.Element): The root element of the XML response from Overpass API.
|
||||
elements (list): The elements of json response from Overpass API.
|
||||
elem_type (str): The type of landmark (e.g., node, way, relation).
|
||||
|
||||
Returns:
|
||||
list[Landmark]: A list of Landmark objects extracted from the XML data.
|
||||
list[Landmark]: A list of Landmark objects extracted from the JSON data.
|
||||
"""
|
||||
if root is None :
|
||||
if elements is None :
|
||||
return []
|
||||
|
||||
landmarks = []
|
||||
for osm_type in ['node', 'way', 'relation'] :
|
||||
for elem in root.findall(osm_type):
|
||||
for elem in elements:
|
||||
osm_type = elem.get('type')
|
||||
|
||||
id, coords, name = get_base_info(elem, osm_type, with_name=True)
|
||||
|
||||
if name is None or coords is None :
|
||||
continue
|
||||
|
||||
tags = elem.findall('tag')
|
||||
|
||||
# Convert this to Landmark object
|
||||
landmark = Landmark(name=name,
|
||||
type=landmarktype,
|
||||
location=coords,
|
||||
osm_id=id,
|
||||
osm_type=osm_type,
|
||||
attractiveness=0,
|
||||
n_tags=len(tags))
|
||||
|
||||
# Browse through tags to add information to landmark.
|
||||
for tag in tags:
|
||||
key = tag.get('k')
|
||||
value = tag.get('v')
|
||||
|
||||
# Skip this landmark if not suitable.
|
||||
if key == 'building:part' and value == 'yes' :
|
||||
break
|
||||
if 'disused:' in key :
|
||||
break
|
||||
if 'boundary:' in key :
|
||||
break
|
||||
if 'shop' in key and landmarktype != 'shopping' :
|
||||
break
|
||||
# if value == 'apartments' :
|
||||
# break
|
||||
|
||||
# Fill in the other attributes.
|
||||
if key == 'image' :
|
||||
landmark.image_url = value
|
||||
if key == 'website' :
|
||||
landmark.website_url = value
|
||||
if key == 'place_of_worship' :
|
||||
landmark.is_place_of_worship = True
|
||||
if key == 'wikipedia' :
|
||||
landmark.wiki_url = value
|
||||
if key == 'name:en' :
|
||||
landmark.name_en = value
|
||||
if 'building:' in key or 'pay' in key :
|
||||
landmark.n_tags -= 1
|
||||
|
||||
# Set the duration.
|
||||
if value in ['museum', 'aquarium', 'planetarium'] :
|
||||
landmark.duration = 60
|
||||
elif value == 'viewpoint' :
|
||||
landmark.is_viewpoint = True
|
||||
landmark.duration = 10
|
||||
elif value == 'cathedral' :
|
||||
landmark.is_place_of_worship = False
|
||||
landmark.duration = 10
|
||||
|
||||
else:
|
||||
self.set_landmark_score(landmark, landmarktype, preference_level)
|
||||
landmarks.append(landmark)
|
||||
id, coords, name = get_base_info(elem, osm_type, with_name=True)
|
||||
|
||||
if name is None or coords is None :
|
||||
continue
|
||||
|
||||
tags = elem.get('tags')
|
||||
|
||||
# Convert this to Landmark object
|
||||
landmark = Landmark(name=name,
|
||||
type=landmarktype,
|
||||
location=coords,
|
||||
osm_id=id,
|
||||
osm_type=osm_type,
|
||||
attractiveness=0,
|
||||
n_tags=len(tags))
|
||||
|
||||
# Browse through tags to add information to landmark.
|
||||
for key, value in tags.items():
|
||||
|
||||
# Skip this landmark if not suitable.
|
||||
if key == 'building:part' and value == 'yes' :
|
||||
break
|
||||
if 'disused:' in key :
|
||||
break
|
||||
if 'boundary:' in key :
|
||||
break
|
||||
if 'shop' in key and landmarktype != 'shopping' :
|
||||
break
|
||||
# if value == 'apartments' :
|
||||
# break
|
||||
|
||||
# Fill in the other attributes.
|
||||
if key == 'image' :
|
||||
landmark.image_url = value
|
||||
if key == 'website' :
|
||||
landmark.website_url = value
|
||||
if value == 'place_of_worship' :
|
||||
landmark.is_place_of_worship = True
|
||||
if key == 'wikipedia' :
|
||||
landmark.wiki_url = value
|
||||
if key == 'name:en' :
|
||||
landmark.name_en = value
|
||||
if 'building:' in key or 'pay' in key :
|
||||
landmark.n_tags -= 1
|
||||
|
||||
|
||||
# Set the duration.
|
||||
if value in ['museum', 'aquarium', 'planetarium'] :
|
||||
landmark.duration = 60
|
||||
elif value == 'viewpoint' :
|
||||
landmark.is_viewpoint = True
|
||||
landmark.duration = 10
|
||||
elif value == 'cathedral' :
|
||||
landmark.is_place_of_worship = False
|
||||
landmark.duration = 10
|
||||
|
||||
landmark.description, landmark.keywords = self.description_and_keywords(tags)
|
||||
self.set_landmark_score(landmark, landmarktype, preference_level)
|
||||
landmarks.append(landmark)
|
||||
|
||||
continue
|
||||
|
||||
|
||||
return landmarks
|
||||
|
||||
|
||||
def description_and_keywords(self, tags: dict):
|
||||
"""
|
||||
Generates a description and a set of keywords for a given landmark based on its tags.
|
||||
|
||||
Params:
|
||||
tags (dict): A dictionary containing metadata about the landmark, including its name,
|
||||
importance, height, date of construction, and visitor information.
|
||||
|
||||
Returns:
|
||||
description (str): A string description of the landmark.
|
||||
keywords (dict): A dictionary of keywords with fields such as 'importance', 'height',
|
||||
'place_type', and 'date'.
|
||||
"""
|
||||
# Extract relevant fields
|
||||
name = tags.get('name')
|
||||
importance = tags.get('importance', None)
|
||||
n_visitors = tags.get('tourism:visitors', None)
|
||||
height = tags.get('height')
|
||||
place_type = self.get_place_type(tags)
|
||||
date = self.get_date(tags)
|
||||
|
||||
if place_type is None :
|
||||
return None, None
|
||||
|
||||
# Start the description.
|
||||
if importance is None :
|
||||
if len(tags.keys()) < 5 :
|
||||
return None, None
|
||||
if len(tags.keys()) < 10 :
|
||||
description = f"{name} is a well known {place_type}."
|
||||
elif len(tags.keys()) < 17 :
|
||||
importance = 'national'
|
||||
description = f"{name} is a {place_type} of national importance."
|
||||
else :
|
||||
importance = 'international'
|
||||
description = f"{name} is an internationally famous {place_type}."
|
||||
else :
|
||||
description = f"{name} is a {place_type} of {importance} importance."
|
||||
|
||||
if height is not None and date is not None :
|
||||
description += f" This {place_type} was constructed in {date} and is ca. {height} meters high."
|
||||
elif height is not None :
|
||||
description += f" This {place_type} stands ca. {height} meters tall."
|
||||
elif date is not None:
|
||||
description += f" It was constructed in {date}."
|
||||
|
||||
# Format the visitor number
|
||||
if n_visitors is not None :
|
||||
n_visitors = int(n_visitors)
|
||||
if n_visitors < 1000000 :
|
||||
description += f" It welcomes {int(n_visitors/1000)} thousand visitors every year."
|
||||
else :
|
||||
description += f" It welcomes {round(n_visitors/1000000, 1)} million visitors every year."
|
||||
|
||||
# Set the keywords.
|
||||
keywords = {"importance": importance,
|
||||
"height": height,
|
||||
"place_type": place_type,
|
||||
"date": date}
|
||||
|
||||
return description, keywords
|
||||
|
||||
|
||||
def get_place_type(self, data):
|
||||
"""
|
||||
Determines the type of the place based on available tags such as 'amenity', 'building',
|
||||
'historic', and 'leisure'. The priority order is: 'historic' > 'building' (if not generic) >
|
||||
'amenity' > 'leisure'.
|
||||
|
||||
Params:
|
||||
data (dict): A dictionary containing metadata about the place.
|
||||
|
||||
Returns:
|
||||
place_type (str): The determined type of the place, or None if no relevant type is found.
|
||||
"""
|
||||
amenity = data.get('amenity', None)
|
||||
building = data.get('building', None)
|
||||
historic = data.get('historic', None)
|
||||
leisure = data.get('leisure')
|
||||
|
||||
if historic and historic != "yes":
|
||||
return historic
|
||||
if building and building not in ["yes", "civic", "government", "apartments", "residential", "commericial", "industrial", "retail", "religious", "public", "service"]:
|
||||
return building
|
||||
if amenity:
|
||||
return amenity
|
||||
if leisure:
|
||||
return leisure
|
||||
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_date(self, data):
|
||||
"""
|
||||
Extracts the most relevant date from the available tags, prioritizing 'construction_date',
|
||||
'start_date', 'year_of_construction', and 'opening_date' in that order.
|
||||
|
||||
Params:
|
||||
data (dict): A dictionary containing metadata about the place.
|
||||
|
||||
Returns:
|
||||
date (str): The most relevant date found, or None if no date is available.
|
||||
"""
|
||||
construction_date = data.get('construction_date', None)
|
||||
opening_date = data.get('opening_date', None)
|
||||
start_date = data.get('start_date', None)
|
||||
year_of_construction = data.get('year_of_construction', None)
|
||||
|
||||
# Prioritize based on availability
|
||||
if construction_date:
|
||||
return construction_date
|
||||
if start_date:
|
||||
return start_date
|
||||
if year_of_construction:
|
||||
return year_of_construction
|
||||
if opening_date:
|
||||
return opening_date
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def dict_to_selector_list(d: dict) -> list:
|
||||
"""
|
||||
Convert a dictionary of key-value pairs to a list of Overpass query strings.
|
@@ -1,21 +1,22 @@
|
||||
"""Main app for backend api"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
from contextlib import asynccontextmanager
|
||||
from fastapi import FastAPI, HTTPException, Query
|
||||
from fastapi import FastAPI, HTTPException, BackgroundTasks
|
||||
|
||||
from .logging_config import configure_logging
|
||||
from .structs.landmark import Landmark, Toilets
|
||||
from .structs.landmark import Landmark
|
||||
from .structs.preferences import Preferences
|
||||
from .structs.linked_landmarks import LinkedLandmarks
|
||||
from .structs.trip import Trip
|
||||
from .utils.landmarks_manager import LandmarkManager
|
||||
from .utils.toilets_manager import ToiletsManager
|
||||
from .landmarks.landmarks_manager import LandmarkManager
|
||||
from .toilets.toilet_routes import router as toilets_router
|
||||
from .optimization.optimizer import Optimizer
|
||||
from .optimization.refiner import Refiner
|
||||
from .overpass.overpass import fill_cache
|
||||
from .cache import client as cache_client
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
manager = LandmarkManager()
|
||||
@@ -36,10 +37,14 @@ app = FastAPI(lifespan=lifespan)
|
||||
|
||||
|
||||
|
||||
app.include_router(toilets_router)
|
||||
|
||||
|
||||
@app.post("/trip/new")
|
||||
def new_trip(preferences: Preferences,
|
||||
start: tuple[float, float],
|
||||
end: tuple[float, float] | None = None) -> Trip:
|
||||
end: tuple[float, float] | None = None,
|
||||
background_tasks: BackgroundTasks = None) -> Trip:
|
||||
"""
|
||||
Main function to call the optimizer.
|
||||
|
||||
@@ -64,6 +69,8 @@ def new_trip(preferences: Preferences,
|
||||
end = start
|
||||
logger.info("No end coordinates provided. Using start=end.")
|
||||
|
||||
logger.info(f"Requested new trip generation. Details:\n\tCoordinates: {start}\n\tTime: {preferences.max_time_minute}\n\tSightseeing: {preferences.sightseeing.score}\n\tNature: {preferences.nature.score}\n\tShopping: {preferences.shopping.score}")
|
||||
|
||||
start_landmark = Landmark(name='start',
|
||||
type='start',
|
||||
location=(start[0], start[1]),
|
||||
@@ -85,12 +92,16 @@ def new_trip(preferences: Preferences,
|
||||
n_tags=0)
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
# Generate the landmarks from the start location
|
||||
landmarks, landmarks_short = manager.generate_landmarks_list(
|
||||
center_coordinates = start,
|
||||
preferences = preferences
|
||||
)
|
||||
|
||||
if len(landmarks) == 0 :
|
||||
raise HTTPException(status_code=500, detail="No landmarks were found.")
|
||||
|
||||
# insert start and finish to the landmarks list
|
||||
landmarks_short.insert(0, start_landmark)
|
||||
landmarks_short.append(end_landmark)
|
||||
@@ -103,6 +114,7 @@ def new_trip(preferences: Preferences,
|
||||
try:
|
||||
base_tour = optimizer.solve_optimization(preferences.max_time_minute, landmarks_short)
|
||||
except Exception as exc:
|
||||
logger.error(f"Trip generation failed: {str(exc)}")
|
||||
raise HTTPException(status_code=500, detail=f"Optimization failed: {str(exc)}") from exc
|
||||
|
||||
t_first_stage = time.time() - start_time
|
||||
@@ -115,18 +127,23 @@ def new_trip(preferences: Preferences,
|
||||
preferences.max_time_minute,
|
||||
preferences.detour_tolerance_minute)
|
||||
except Exception as exc :
|
||||
raise HTTPException(status_code=500, detail=f"An unexpected error occurred: {str(exc)}") from exc
|
||||
logger.warning(f"Refiner failed. Proceeding with base trip {str(exc)}")
|
||||
refined_tour = base_tour
|
||||
|
||||
t_second_stage = time.time() - start_time
|
||||
|
||||
logger.debug(f'First stage optimization\t: {round(t_first_stage,3)} seconds')
|
||||
logger.debug(f'Second stage optimization\t: {round(t_second_stage,3)} seconds')
|
||||
logger.info(f'Total computation time\t: {round(t_first_stage + t_second_stage,3)} seconds')
|
||||
|
||||
linked_tour = LinkedLandmarks(refined_tour)
|
||||
|
||||
# upon creation of the trip, persistence of both the trip and its landmarks is ensured.
|
||||
trip = Trip.from_linked_landmarks(linked_tour, cache_client)
|
||||
logger.info(f'Generated a trip of {trip.total_time} minutes with {len(refined_tour)} landmarks in {round(t_generate_landmarks + t_first_stage + t_second_stage,3)} seconds.')
|
||||
logger.debug('Detailed trip :\n\t' + '\n\t'.join(f'{landmark}' for landmark in refined_tour))
|
||||
|
||||
background_tasks.add_task(fill_cache)
|
||||
|
||||
return trip
|
||||
|
||||
|
||||
@@ -146,6 +163,7 @@ def get_trip(trip_uuid: str) -> Trip:
|
||||
trip = cache_client.get(f"trip_{trip_uuid}")
|
||||
return trip
|
||||
except KeyError as exc:
|
||||
logger.error(f"Failed to fetch trip with UUID {trip_uuid}: {str(exc)}")
|
||||
raise HTTPException(status_code=404, detail="Trip not found") from exc
|
||||
|
||||
|
||||
@@ -164,32 +182,45 @@ def get_landmark(landmark_uuid: str) -> Landmark:
|
||||
landmark = cache_client.get(f"landmark_{landmark_uuid}")
|
||||
return landmark
|
||||
except KeyError as exc:
|
||||
logger.error(f"Failed to fetch landmark with UUID {landmark_uuid}: {str(exc)}")
|
||||
raise HTTPException(status_code=404, detail="Landmark not found") from exc
|
||||
|
||||
|
||||
@app.post("/toilets/new")
|
||||
def get_toilets(location: tuple[float, float] = Query(...), radius: int = 500) -> list[Toilets] :
|
||||
@app.post("/trip/recompute-time/{trip_uuid}/{removed_landmark_uuid}")
|
||||
def update_trip_time(trip_uuid: str, removed_landmark_uuid: str) -> Trip:
|
||||
"""
|
||||
Endpoint to find toilets within a specified radius from a given location.
|
||||
|
||||
This endpoint expects the `location` and `radius` as **query parameters**, not in the request body.
|
||||
Updates the reaching times of a given trip when removing a landmark.
|
||||
|
||||
Args:
|
||||
location (tuple[float, float]): The latitude and longitude of the location to search from.
|
||||
radius (int, optional): The radius (in meters) within which to search for toilets. Defaults to 500 meters.
|
||||
landmark_uuid (str) : unique identifier for a Landmark.
|
||||
|
||||
Returns:
|
||||
list[Toilets]: A list of Toilets objects that meet the criteria.
|
||||
(Landmark) : the corresponding Landmark.
|
||||
"""
|
||||
if location is None:
|
||||
raise HTTPException(status_code=406, detail="Coordinates not provided or invalid")
|
||||
if not (-90 <= location[0] <= 90 or -180 <= location[1] <= 180):
|
||||
raise HTTPException(status_code=422, detail="Start coordinates not in range")
|
||||
|
||||
toilets_manager = ToiletsManager(location, radius)
|
||||
|
||||
try :
|
||||
toilets_list = toilets_manager.generate_toilet_list()
|
||||
return toilets_list
|
||||
# First, fetch the trip in the cache.
|
||||
try:
|
||||
trip = cache_client.get(f'trip_{trip_uuid}')
|
||||
except KeyError as exc:
|
||||
raise HTTPException(status_code=404, detail="No toilets found") from exc
|
||||
logger.error(f"Failed to update trip with UUID {trip_uuid} (trip not found): {str(exc)}")
|
||||
raise HTTPException(status_code=404, detail='Trip not found') from exc
|
||||
|
||||
landmarks = []
|
||||
next_uuid = trip.first_landmark_uuid
|
||||
|
||||
# Extract landmarks
|
||||
try :
|
||||
while next_uuid is not None:
|
||||
landmark = cache_client.get(f'landmark_{next_uuid}')
|
||||
# Filter out the removed landmark.
|
||||
if next_uuid != removed_landmark_uuid :
|
||||
landmarks.append(landmark)
|
||||
next_uuid = landmark.next_uuid # Prepare for the next iteration
|
||||
except KeyError as exc:
|
||||
logger.error(f"Failed to update trip with UUID {trip_uuid} : {str(exc)}")
|
||||
raise HTTPException(status_code=404, detail=f'landmark {next_uuid} not found') from exc
|
||||
|
||||
# Re-link every thing and compute times again
|
||||
linked_tour = LinkedLandmarks(landmarks)
|
||||
trip = Trip.from_linked_landmarks(linked_tour, cache_client)
|
||||
|
||||
return trip
|
||||
|
@@ -55,6 +55,9 @@ class Optimizer:
|
||||
self.average_walking_speed = parameters['average_walking_speed']
|
||||
self.max_landmarks = parameters['max_landmarks']
|
||||
self.overshoot = parameters['overshoot']
|
||||
self.time_limit = parameters['time_limit']
|
||||
self.gap_rel = parameters['gap_rel']
|
||||
self.max_iter = parameters['max_iter']
|
||||
|
||||
|
||||
def init_ub_time(self, prob: pl.LpProblem, x: pl.LpVariable, L: int, landmarks: list[Landmark], max_time: int):
|
||||
@@ -254,7 +257,6 @@ class Optimizer:
|
||||
Returns:
|
||||
None: This function modifies the `prob` object by adding L-2 equality constraints in-place.
|
||||
"""
|
||||
# FIXME: weird 0 artifact in the coefficients popping up
|
||||
# Loop through rows 1 to L-2 to prevent stacked ones
|
||||
for i in range(1, L-1):
|
||||
# Add the constraint that sums across each "row" or "block" in the decision variables
|
||||
@@ -490,10 +492,21 @@ class Optimizer:
|
||||
|
||||
|
||||
def warm_start(self, x: list[pl.LpVariable], L: int) :
|
||||
"""
|
||||
This function sets the initial values of the decision variables to a feasible solution.
|
||||
This can help the solver start with a feasible or heuristic solution,
|
||||
potentially speeding up convergence.
|
||||
|
||||
Args:
|
||||
x (list[pl.LpVariable]): A list of PuLP decision variables (binary variables).
|
||||
L (int): The size parameter, representing a dimension (likely related to a grid or matrix).
|
||||
|
||||
Returns:
|
||||
list[pl.LpVariable]: The modified list of PuLP decision variables with initial values set.
|
||||
"""
|
||||
for i in range(L*L) :
|
||||
x[i].setInitialValue(0)
|
||||
|
||||
|
||||
x[1].setInitialValue(1)
|
||||
x[2*L-1].setInitialValue(1)
|
||||
|
||||
@@ -573,37 +586,44 @@ class Optimizer:
|
||||
prob, x = self.pre_processing(L, landmarks, max_time, max_landmarks)
|
||||
|
||||
# Solve the problem and extract results.
|
||||
prob.solve(pl.PULP_CBC_CMD(msg=False, gapRel=0.1, timeLimit=10, warmStart=False))
|
||||
try :
|
||||
prob.solve(pl.PULP_CBC_CMD(msg=False, timeLimit=self.time_limit+1, gapRel=self.gap_rel))
|
||||
except Exception as exc :
|
||||
raise Exception(f"No solution found: {str(exc)}") from exc
|
||||
status = pl.LpStatus[prob.status]
|
||||
solution = [pl.value(var) for var in x] # The values of the decision variables (will be 0 or 1)
|
||||
|
||||
self.logger.debug("First results are out. Looking out for circles and correcting.")
|
||||
self.logger.debug("First results are out. Looking out for circles and correcting...")
|
||||
|
||||
# Raise error if no solution is found. FIXME: for now this throws the internal server error
|
||||
if status != 'Optimal' :
|
||||
self.logger.error("The problem is overconstrained, no solution on first try.")
|
||||
self.logger.warning("The problem is overconstrained, no solution on first try.")
|
||||
raise ArithmeticError("No solution could be found. Please try again with more time or different preferences.")
|
||||
|
||||
# If there is a solution, we're good to go, just check for connectiveness
|
||||
circles = self.is_connected(solution)
|
||||
|
||||
i = 0
|
||||
timeout = 40
|
||||
while circles is not None :
|
||||
i += 1
|
||||
if i == timeout :
|
||||
self.logger.error(f'Timeout: No solution found after {timeout} iterations.')
|
||||
raise TimeoutError(f"Optimization took too long. No solution found after {timeout} iterations.")
|
||||
if i == self.max_iter :
|
||||
self.logger.warning(f'Timeout: No solution found after {self.max_iter} iterations.')
|
||||
raise TimeoutError(f"Optimization took too long. No solution found after {self.max_iter} iterations.")
|
||||
|
||||
for circle in circles :
|
||||
self.prevent_circle(prob, x, circle, L)
|
||||
|
||||
# Solve the problem again
|
||||
prob.solve(pl.PULP_CBC_CMD(msg=False))
|
||||
try :
|
||||
prob.solve(pl.PULP_CBC_CMD(msg=False, timeLimit=self.time_limit, gapRel=self.gap_rel))
|
||||
except Exception as exc :
|
||||
self.logger.warning("No solution found: {str(exc)")
|
||||
raise Exception(f"No solution found: {str(exc)}") from exc
|
||||
|
||||
solution = [pl.value(var) for var in x]
|
||||
|
||||
if pl.LpStatus[prob.status] != 'Optimal' :
|
||||
self.logger.error("The problem is overconstrained, no solution after {i} cycles.")
|
||||
self.logger.warning("The problem is overconstrained, no solution after {i} cycles.")
|
||||
raise ArithmeticError("No solution could be found. Please try again with more time or different preferences.")
|
||||
|
||||
circles = self.is_connected(solution)
|
||||
@@ -614,5 +634,5 @@ class Optimizer:
|
||||
order = self.get_order(solution)
|
||||
tour = [landmarks[i] for i in order]
|
||||
|
||||
self.logger.debug(f"Re-optimized {i} times, objective value : {int(pl.value(prob.objective))}")
|
||||
self.logger.info(f"Re-optimized {i} times, objective value : {int(pl.value(prob.objective))}")
|
||||
return tour
|
||||
|
@@ -278,7 +278,7 @@ class Refiner :
|
||||
better_tour_poly = concave_hull(MultiPoint(coords)) # Create concave hull with "core" of tour leaving out start and finish
|
||||
xs, ys = better_tour_poly.exterior.xy
|
||||
"""
|
||||
ERROR HERE :
|
||||
FIXED : ERROR HERE :
|
||||
Exception has occurred: AttributeError
|
||||
'LineString' object has no attribute 'exterior'
|
||||
"""
|
||||
@@ -356,7 +356,7 @@ class Refiner :
|
||||
|
||||
# If unsuccessful optimization, use the base_tour.
|
||||
if new_tour is None:
|
||||
self.logger.warning("No solution found for the refined tour. Returning the initial tour.")
|
||||
self.logger.warning("Refiner failed: No solution found during second stage optimization.")
|
||||
new_tour = base_tour
|
||||
|
||||
# If only one landmark, return it.
|
||||
@@ -369,6 +369,7 @@ class Refiner :
|
||||
# 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 :
|
||||
self.logger.debug("Tours might be funky, attempting to correct with polygons")
|
||||
better_tour = self.fix_using_polygon(better_tour)
|
||||
|
||||
return better_tour
|
||||
|
@@ -1,9 +1,9 @@
|
||||
"""Module defining the caching strategy for overpass requests."""
|
||||
"""Module defining the handling of cache data from Overpass requests."""
|
||||
import os
|
||||
import xml.etree.ElementTree as ET
|
||||
import json
|
||||
import hashlib
|
||||
|
||||
from ..constants import OSM_CACHE_DIR
|
||||
from ..constants import OSM_CACHE_DIR, OSM_TYPES
|
||||
|
||||
|
||||
def get_cache_key(query: str) -> str:
|
||||
@@ -17,10 +17,6 @@ def get_cache_key(query: str) -> str:
|
||||
class CachingStrategyBase:
|
||||
"""
|
||||
Base class for implementing caching strategies.
|
||||
|
||||
This class defines the structure for a caching strategy with basic methods
|
||||
that must be implemented by subclasses. Subclasses should define how to
|
||||
retrieve, store, and close the cache.
|
||||
"""
|
||||
def get(self, key):
|
||||
"""Retrieve the cached data associated with the provided key."""
|
||||
@@ -30,111 +26,111 @@ class CachingStrategyBase:
|
||||
"""Store data in the cache with the specified key."""
|
||||
raise NotImplementedError('Subclass should implement set')
|
||||
|
||||
def set_hollow(self, key, **kwargs):
|
||||
"""Create a hollow (empty) cache entry with a specific key."""
|
||||
raise NotImplementedError('Subclass should implement set_hollow')
|
||||
|
||||
def close(self):
|
||||
"""Clean up or close any resources used by the caching strategy."""
|
||||
|
||||
|
||||
class XMLCache(CachingStrategyBase):
|
||||
class JSONCache(CachingStrategyBase):
|
||||
"""
|
||||
A caching strategy that stores and retrieves data in XML format.
|
||||
|
||||
This class provides methods to cache data as XML files in a specified directory.
|
||||
The directory is automatically suffixed with '_XML' to distinguish it from other
|
||||
caching strategies. The data is stored and retrieved using XML serialization.
|
||||
|
||||
Args:
|
||||
cache_dir (str): The base directory where XML cache files will be stored.
|
||||
Defaults to 'OSM_CACHE_DIR' with a '_XML' suffix.
|
||||
|
||||
Methods:
|
||||
get(key): Retrieve cached data from a XML file associated with the given key.
|
||||
set(key, value): Store data in a XML file with the specified key.
|
||||
A caching strategy that stores and retrieves data in JSON format.
|
||||
"""
|
||||
def __init__(self, cache_dir=OSM_CACHE_DIR):
|
||||
# Add the class name as a suffix to the directory
|
||||
self._cache_dir = f'{cache_dir}_XML'
|
||||
self._cache_dir = f'{cache_dir}'
|
||||
if not os.path.exists(self._cache_dir):
|
||||
os.makedirs(self._cache_dir)
|
||||
|
||||
def _filename(self, key):
|
||||
return os.path.join(self._cache_dir, f'{key}.xml')
|
||||
return os.path.join(self._cache_dir, f'{key}.json')
|
||||
|
||||
def get(self, key):
|
||||
"""Retrieve XML data from the cache and parse it as an ElementTree."""
|
||||
"""Retrieve JSON data from the cache and parse it as an ElementTree."""
|
||||
filename = self._filename(key)
|
||||
if os.path.exists(filename):
|
||||
try:
|
||||
# Parse and return the cached XML data
|
||||
tree = ET.parse(filename)
|
||||
return tree.getroot() # Return the root element of the parsed XML
|
||||
except ET.ParseError:
|
||||
# print(f"Error parsing cached XML file: {filename}")
|
||||
return None
|
||||
# Open and parse the cached JSON data
|
||||
with open(filename, 'r', encoding='utf-8') as file:
|
||||
data = json.load(file)
|
||||
# Return the data as a list of dicts.
|
||||
return data
|
||||
except json.JSONDecodeError:
|
||||
return None # Return None if parsing fails
|
||||
return None
|
||||
|
||||
def set(self, key, value):
|
||||
"""Save the XML data as an ElementTree to the cache."""
|
||||
"""Save the JSON data in the cache."""
|
||||
filename = self._filename(key)
|
||||
tree = ET.ElementTree(value) # value is expected to be an ElementTree root element
|
||||
try:
|
||||
# Write the XML data to a file
|
||||
with open(filename, 'wb') as file:
|
||||
tree.write(file, encoding='utf-8', xml_declaration=True)
|
||||
# Write the JSON data to the cache file
|
||||
with open(filename, 'w', encoding='utf-8') as file:
|
||||
json.dump(value, file, ensure_ascii=False, indent=4)
|
||||
except IOError as e:
|
||||
raise IOError(f"Error writing to cache file: {filename} - {e}") from e
|
||||
|
||||
def set_hollow(self, key, cell: tuple, osm_types: list,
|
||||
selector: str, conditions: list=None, out='center'):
|
||||
"""Create an empty placeholder cache entry for a future fill."""
|
||||
hollow_key = f'hollow_{key}'
|
||||
filename = self._filename(hollow_key)
|
||||
|
||||
# Create the hollow JSON structure
|
||||
hollow_data = {
|
||||
"key": key,
|
||||
"cell": list(cell),
|
||||
"osm_types": list(osm_types),
|
||||
"selector": selector,
|
||||
"conditions": conditions,
|
||||
"out": out
|
||||
}
|
||||
# Write the hollow data to the cache file
|
||||
try:
|
||||
with open(filename, 'w', encoding='utf-8') as file:
|
||||
json.dump(hollow_data, file, ensure_ascii=False, indent=4)
|
||||
except IOError as e:
|
||||
raise IOError(f"Error writing hollow cache to file: {filename} - {e}") from e
|
||||
|
||||
def close(self):
|
||||
"""Cleanup method, if needed."""
|
||||
|
||||
|
||||
class CachingStrategy:
|
||||
"""
|
||||
A class to manage different caching strategies.
|
||||
|
||||
This class provides an interface to switch between different caching strategies
|
||||
(e.g., XMLCache, JSONCache) dynamically. It allows caching data in different formats,
|
||||
depending on the strategy being used. By default, it uses the XMLCache strategy.
|
||||
|
||||
Attributes:
|
||||
__strategy (CachingStrategyBase): The currently active caching strategy.
|
||||
__strategies (dict): A mapping between strategy names (as strings) and their corresponding
|
||||
classes, allowing dynamic selection of caching strategies.
|
||||
"""
|
||||
__strategy = XMLCache() # Default caching strategy
|
||||
__strategy = JSONCache() # Default caching strategy
|
||||
__strategies = {
|
||||
'XML': XMLCache,
|
||||
'JSON': JSONCache,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def use(cls, strategy_name='XML', **kwargs):
|
||||
"""
|
||||
Set the caching strategy based on the strategy_name provided.
|
||||
|
||||
Args:
|
||||
strategy_name (str): The name of the caching strategy (e.g., 'XML').
|
||||
**kwargs: Additional keyword arguments to pass when initializing the strategy.
|
||||
"""
|
||||
# If a previous strategy exists, close it
|
||||
def use(cls, strategy_name='JSON', **kwargs):
|
||||
"""Define the caching strategy to use."""
|
||||
if cls.__strategy:
|
||||
cls.__strategy.close()
|
||||
|
||||
# Retrieve the strategy class based on the strategy name
|
||||
strategy_class = cls.__strategies.get(strategy_name)
|
||||
|
||||
if not strategy_class:
|
||||
raise ValueError(f"Unknown caching strategy: {strategy_name}")
|
||||
|
||||
# Instantiate the new strategy with the provided arguments
|
||||
cls.__strategy = strategy_class(**kwargs)
|
||||
return cls.__strategy
|
||||
|
||||
@classmethod
|
||||
def get(cls, key):
|
||||
"""Get data from the current strategy's cache."""
|
||||
if not cls.__strategy:
|
||||
raise RuntimeError("Caching strategy has not been set.")
|
||||
"""Get the data from the cache."""
|
||||
return cls.__strategy.get(key)
|
||||
|
||||
@classmethod
|
||||
def set(cls, key, value):
|
||||
"""Set data in the current strategy's cache."""
|
||||
if not cls.__strategy:
|
||||
raise RuntimeError("Caching strategy has not been set.")
|
||||
"""Save the data in the cache."""
|
||||
cls.__strategy.set(key, value)
|
||||
|
||||
@classmethod
|
||||
def set_hollow(cls, key, cell: tuple, osm_types: OSM_TYPES,
|
||||
selector: str, conditions: list=None, out='center'):
|
||||
"""Create a hollow cache entry."""
|
||||
cls.__strategy.set_hollow(key, cell, osm_types, selector, conditions, out)
|
||||
|
@@ -1,14 +1,18 @@
|
||||
"""Module allowing connexion to overpass api and fectch data from OSM."""
|
||||
from typing import Literal, List
|
||||
import os
|
||||
import time
|
||||
import urllib
|
||||
import math
|
||||
import logging
|
||||
import xml.etree.ElementTree as ET
|
||||
import json
|
||||
from typing import List, Tuple
|
||||
|
||||
from .caching_strategy import get_cache_key, CachingStrategy
|
||||
from ..constants import OSM_CACHE_DIR
|
||||
from ..constants import OSM_CACHE_DIR, OSM_TYPES, BBOX
|
||||
|
||||
logger = logging.getLogger('Overpass')
|
||||
osm_types = List[Literal['way', 'node', 'relation']]
|
||||
|
||||
RESOLUTION = 0.05
|
||||
CELL = Tuple[int, int]
|
||||
|
||||
|
||||
class Overpass :
|
||||
@@ -16,7 +20,10 @@ class Overpass :
|
||||
Overpass class to manage the query building and sending to overpass api.
|
||||
The caching strategy is a part of this class and initialized upon creation of the Overpass object.
|
||||
"""
|
||||
def __init__(self, caching_strategy: str = 'XML', cache_dir: str = OSM_CACHE_DIR) :
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def __init__(self, caching_strategy: str = 'JSON', cache_dir: str = OSM_CACHE_DIR) :
|
||||
"""
|
||||
Initialize the Overpass instance with the url, headers and caching strategy.
|
||||
"""
|
||||
@@ -25,17 +32,110 @@ class Overpass :
|
||||
self.caching_strategy = CachingStrategy.use(caching_strategy, cache_dir=cache_dir)
|
||||
|
||||
|
||||
@classmethod
|
||||
def build_query(self, area: tuple, osm_types: osm_types,
|
||||
selector: str, conditions=[], out='center') -> str:
|
||||
def send_query(self, bbox: BBOX, osm_types: OSM_TYPES,
|
||||
selector: str, conditions: list=None, out='center') -> List[dict]:
|
||||
"""
|
||||
Sends the Overpass QL query to the Overpass API and returns the parsed json response.
|
||||
|
||||
Args:
|
||||
bbox (tuple): Bounding box for the query.
|
||||
osm_types (list[str]): List of OSM element types (e.g., 'node', 'way').
|
||||
selector (str): Key or tag to filter OSM elements (e.g., 'highway').
|
||||
conditions (list): Optional list of additional filter conditions in Overpass QL format.
|
||||
out (str): Output format ('center', 'body', etc.). Defaults to 'center'.
|
||||
|
||||
Returns:
|
||||
list: Parsed json response from the Overpass API, or cached data if available.
|
||||
"""
|
||||
# Determine which grid cells overlap with this bounding box.
|
||||
overlapping_cells = Overpass._get_overlapping_cells(bbox)
|
||||
|
||||
# Retrieve cached data and identify missing cache entries
|
||||
cached_responses, non_cached_cells = self._retrieve_cached_data(overlapping_cells, osm_types, selector, conditions, out)
|
||||
|
||||
self.logger.debug(f'Cache hit for {len(overlapping_cells)-len(non_cached_cells)}/{len(overlapping_cells)} quadrants.')
|
||||
|
||||
# If there is no missing data, return the cached responses after filtering.
|
||||
if not non_cached_cells :
|
||||
return Overpass._filter_landmarks(cached_responses, bbox)
|
||||
|
||||
# If there is no cached data, fetch all from Overpass.
|
||||
if not cached_responses :
|
||||
query_str = Overpass.build_query(bbox, osm_types, selector, conditions, out)
|
||||
self.logger.debug(f'Query string: {query_str}')
|
||||
return self.fetch_data_from_api(query_str)
|
||||
|
||||
# Resize the bbox for smaller search area and build new query string.
|
||||
non_cached_bbox = Overpass._get_non_cached_bbox(non_cached_cells, bbox)
|
||||
query_str = Overpass.build_query(non_cached_bbox, osm_types, selector, conditions, out)
|
||||
self.logger.debug(f'Query string: {query_str}')
|
||||
non_cached_responses = self.fetch_data_from_api(query_str)
|
||||
return Overpass._filter_landmarks(cached_responses, bbox) + non_cached_responses
|
||||
|
||||
|
||||
def fetch_data_from_api(self, query_str: str) -> List[dict]:
|
||||
"""
|
||||
Fetch data from the Overpass API and return the json data.
|
||||
|
||||
Args:
|
||||
query_str (str): The Overpass query string.
|
||||
|
||||
Returns:
|
||||
dict: Combined cached and fetched data.
|
||||
"""
|
||||
try:
|
||||
data = urllib.parse.urlencode({'data': query_str}).encode('utf-8')
|
||||
request = urllib.request.Request(self.overpass_url, data=data, headers=self.headers)
|
||||
|
||||
with urllib.request.urlopen(request) as response:
|
||||
response_data = response.read().decode('utf-8') # Convert the HTTPResponse to a string
|
||||
data = json.loads(response_data) # Load the JSON from the string
|
||||
elements = data.get('elements', [])
|
||||
# self.logger.debug(f'Query = {query_str}')
|
||||
return elements
|
||||
|
||||
except urllib.error.URLError as e:
|
||||
self.logger.error(f"Error connecting to Overpass API: {str(e)}")
|
||||
raise ConnectionError(f"Error connecting to Overpass API: {str(e)}") from e
|
||||
except Exception as exc :
|
||||
self.logger.error(f"unexpected error while fetching data from Overpass: {str(exc)}")
|
||||
raise Exception(f'An unexpected error occured: {str(exc)}') from exc
|
||||
|
||||
|
||||
def fill_cache(self, json_data: dict) :
|
||||
"""
|
||||
Fill cache with data by using a hollow cache entry's information.
|
||||
"""
|
||||
query_str, cache_key = Overpass._build_query_from_hollow(json_data)
|
||||
try:
|
||||
data = urllib.parse.urlencode({'data': query_str}).encode('utf-8')
|
||||
request = urllib.request.Request(self.overpass_url, data=data, headers=self.headers)
|
||||
|
||||
with urllib.request.urlopen(request) as response:
|
||||
|
||||
# Convert the HTTPResponse to a string and load data
|
||||
response_data = response.read().decode('utf-8')
|
||||
data = json.loads(response_data)
|
||||
|
||||
# Get elements and set cache
|
||||
elements = data.get('elements', [])
|
||||
self.caching_strategy.set(cache_key, elements)
|
||||
self.logger.debug(f'Cache set for {cache_key}')
|
||||
except urllib.error.URLError as e:
|
||||
raise ConnectionError(f"Error connecting to Overpass API: {str(e)}") from e
|
||||
except Exception as exc :
|
||||
raise Exception(f'An unexpected error occured: {str(exc)}') from exc
|
||||
|
||||
|
||||
@staticmethod
|
||||
def build_query(bbox: BBOX, osm_types: OSM_TYPES,
|
||||
selector: str, conditions: list=None, out='center') -> str:
|
||||
"""
|
||||
Constructs a query string for the Overpass API to retrieve OpenStreetMap (OSM) data.
|
||||
|
||||
Args:
|
||||
area (tuple): A tuple representing the geographical search area, typically in the format
|
||||
(radius, latitude, longitude). The first element is a string like "around:2000"
|
||||
specifying the search radius, and the second and third elements represent
|
||||
the latitude and longitude as floats or strings.
|
||||
bbox (tuple): A tuple representing the geographical search area, typically in the format
|
||||
(lat_min, lon_min, lat_max, lon_max).
|
||||
osm_types (list[str]): A list of OSM element types to search for. Must be one or more of
|
||||
'Way', 'Node', or 'Relation'.
|
||||
selector (str): The key or tag to filter the OSM elements (e.g., 'amenity', 'highway', etc.).
|
||||
@@ -52,82 +152,203 @@ class Overpass :
|
||||
Notes:
|
||||
- If no conditions are provided, the query will just use the `selector` to filter the OSM
|
||||
elements without additional constraints.
|
||||
- The search area must always formatted as "(radius, lat, lon)".
|
||||
"""
|
||||
if not isinstance(conditions, list) :
|
||||
conditions = [conditions]
|
||||
if not isinstance(osm_types, list) :
|
||||
osm_types = [osm_types]
|
||||
query = '[out:json][timeout:20];('
|
||||
|
||||
query = '('
|
||||
# convert the bbox to string.
|
||||
bbox_str = f"({','.join(map(str, bbox))})"
|
||||
|
||||
# Round the radius to nearest 50 and coordinates to generate less queries
|
||||
if area[0] > 500 :
|
||||
search_radius = round(area[0] / 50) * 50
|
||||
loc = tuple((round(area[1], 2), round(area[2], 2)))
|
||||
else :
|
||||
search_radius = round(area[0] / 25) * 25
|
||||
loc = tuple((round(area[1], 3), round(area[2], 3)))
|
||||
|
||||
search_area = f"(around:{search_radius}, {str(loc[0])}, {str(loc[1])})"
|
||||
|
||||
if conditions :
|
||||
if conditions is not None and len(conditions) > 0:
|
||||
conditions = '(if: ' + ' && '.join(conditions) + ')'
|
||||
else :
|
||||
conditions = ''
|
||||
|
||||
for elem in osm_types :
|
||||
query += elem + '[' + selector + ']' + conditions + search_area + ';'
|
||||
query += elem + '[' + selector + ']' + conditions + bbox_str + ';'
|
||||
|
||||
query += ');' + f'out {out};'
|
||||
|
||||
return query
|
||||
|
||||
|
||||
def send_query(self, query: str) -> ET:
|
||||
def _retrieve_cached_data(self, overlapping_cells: CELL, osm_types: OSM_TYPES,
|
||||
selector: str, conditions: list, out: str) -> Tuple[List[dict], list[CELL]]:
|
||||
"""
|
||||
Sends the Overpass QL query to the Overpass API and returns the parsed JSON response.
|
||||
Retrieve cached data and identify missing cache quadrants.
|
||||
|
||||
Args:
|
||||
query (str): The Overpass QL query to be sent to the Overpass API.
|
||||
overlapping_cells (list): Cells to check for cached data.
|
||||
osm_types (list): OSM types (e.g., 'node', 'way').
|
||||
selector (str): Key or tag to filter OSM elements.
|
||||
conditions (list): Additional conditions to apply.
|
||||
out (str): Output format.
|
||||
|
||||
Returns:
|
||||
dict: The parsed JSON response from the Overpass API, or None if the request fails.
|
||||
tuple: A tuple containing:
|
||||
- cached_responses (list): List of cached data found.
|
||||
- non_cached_cells (list(tuple)): List of cells with missing data.
|
||||
"""
|
||||
cell_key_dict = {}
|
||||
for cell in overlapping_cells :
|
||||
for elem in osm_types :
|
||||
key_str = f"{elem}[{selector}]{conditions}({','.join(map(str, cell))})"
|
||||
|
||||
cell_key_dict[cell] = get_cache_key(key_str)
|
||||
|
||||
cached_responses = []
|
||||
non_cached_cells = []
|
||||
|
||||
# Retrieve the cached data and mark the missing entries as hollow
|
||||
for cell, key in cell_key_dict.items():
|
||||
cached_data = self.caching_strategy.get(key)
|
||||
if cached_data is not None :
|
||||
cached_responses += cached_data
|
||||
else:
|
||||
self.caching_strategy.set_hollow(key, cell, osm_types, selector, conditions, out)
|
||||
non_cached_cells.append(cell)
|
||||
|
||||
return cached_responses, non_cached_cells
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _build_query_from_hollow(json_data: dict) -> Tuple[str, str]:
|
||||
"""
|
||||
Build query string using information from a hollow cache entry.
|
||||
"""
|
||||
# Extract values from the JSON object
|
||||
key = json_data.get('key')
|
||||
cell = tuple(json_data.get('cell'))
|
||||
bbox = Overpass._get_bbox_from_grid_cell(cell)
|
||||
osm_types = json_data.get('osm_types')
|
||||
selector = json_data.get('selector')
|
||||
conditions = json_data.get('conditions')
|
||||
out = json_data.get('out')
|
||||
|
||||
|
||||
query_str = Overpass.build_query(bbox, osm_types, selector, conditions, out)
|
||||
return query_str, key
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _get_overlapping_cells(query_bbox: tuple) -> List[CELL]:
|
||||
"""
|
||||
Returns a set of all grid cells that overlap with the given bounding box.
|
||||
"""
|
||||
# Extract location from the query bbox
|
||||
lat_min, lon_min, lat_max, lon_max = query_bbox
|
||||
|
||||
min_lat_cell, min_lon_cell = Overpass._get_grid_cell(lat_min, lon_min)
|
||||
max_lat_cell, max_lon_cell = Overpass._get_grid_cell(lat_max, lon_max)
|
||||
|
||||
overlapping_cells = set()
|
||||
for lat_idx in range(min_lat_cell, max_lat_cell + 1):
|
||||
for lon_idx in range(min_lon_cell, max_lon_cell + 1):
|
||||
overlapping_cells.add((lat_idx, lon_idx))
|
||||
|
||||
return overlapping_cells
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _get_grid_cell(lat: float, lon: float) -> CELL:
|
||||
"""
|
||||
Returns the grid cell coordinates for a given latitude and longitude.
|
||||
Each grid cell is 0.05°lat x 0.05°lon resolution in size.
|
||||
"""
|
||||
lat_index = math.floor(lat / RESOLUTION)
|
||||
lon_index = math.floor(lon / RESOLUTION)
|
||||
return (lat_index, lon_index)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _get_bbox_from_grid_cell(cell: CELL) -> BBOX:
|
||||
"""
|
||||
Returns the bounding box for a given grid cell index.
|
||||
Each grid cell is resolution x resolution in size.
|
||||
|
||||
The bounding box is returned as (min_lat, min_lon, max_lat, max_lon).
|
||||
"""
|
||||
# Calculate the southwest (min_lat, min_lon) corner of the bounding box
|
||||
min_lat = round(cell[0] * RESOLUTION, 2)
|
||||
min_lon = round(cell[1] * RESOLUTION, 2)
|
||||
|
||||
# Calculate the northeast (max_lat, max_lon) corner of the bounding box
|
||||
max_lat = round((cell[0] + 1) * RESOLUTION, 2)
|
||||
max_lon = round((cell[1] + 1) * RESOLUTION, 2)
|
||||
|
||||
return (min_lat, min_lon, max_lat, max_lon)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _get_non_cached_bbox(non_cached_cells: List[CELL], original_bbox: BBOX):
|
||||
"""
|
||||
Calculate the non-cached bounding box by excluding cached cells.
|
||||
|
||||
Args:
|
||||
non_cached_cells (list): The list of cells that were not found in the cache.
|
||||
original_bbox (tuple): The original bounding box (min_lat, min_lon, max_lat, max_lon).
|
||||
|
||||
Returns:
|
||||
tuple: The new bounding box that excludes cached cells, or None if all cells are cached.
|
||||
"""
|
||||
if not non_cached_cells:
|
||||
return None # All cells were cached
|
||||
|
||||
# Initialize the non-cached bounding box with extreme values
|
||||
min_lat, min_lon, max_lat, max_lon = float('inf'), float('inf'), float('-inf'), float('-inf')
|
||||
|
||||
# Iterate over non-cached cells to find the new bounding box
|
||||
for cell in non_cached_cells:
|
||||
cell_min_lat, cell_min_lon, cell_max_lat, cell_max_lon = Overpass._get_bbox_from_grid_cell(cell)
|
||||
|
||||
min_lat = min(min_lat, cell_min_lat)
|
||||
min_lon = min(min_lon, cell_min_lon)
|
||||
max_lat = max(max_lat, cell_max_lat)
|
||||
max_lon = max(max_lon, cell_max_lon)
|
||||
|
||||
# If no update to bounding box, return the original
|
||||
if min_lat == float('inf') or min_lon == float('inf'):
|
||||
return None
|
||||
|
||||
return (max(min_lat, original_bbox[0]),
|
||||
max(min_lon, original_bbox[1]),
|
||||
min(max_lat, original_bbox[2]),
|
||||
min(max_lon, original_bbox[3]))
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _filter_landmarks(elements: List[dict], bbox: BBOX) -> List[dict]:
|
||||
"""
|
||||
Filters elements based on whether their coordinates are inside the given bbox.
|
||||
|
||||
Args:
|
||||
- elements (list of dict): List of elements containing coordinates.
|
||||
- bbox (tuple): A bounding box defined as (min_lat, min_lon, max_lat, max_lon).
|
||||
|
||||
Returns:
|
||||
- list: A list of elements whose coordinates are inside the bounding box.
|
||||
"""
|
||||
|
||||
# Generate a cache key for the current query
|
||||
cache_key = get_cache_key(query)
|
||||
filtered_elements = []
|
||||
min_lat, min_lon, max_lat, max_lon = bbox
|
||||
|
||||
# Try to fetch the result from the cache
|
||||
cached_response = self.caching_strategy.get(cache_key)
|
||||
if cached_response is not None :
|
||||
logger.debug("Cache hit.")
|
||||
return cached_response
|
||||
for elem in elements:
|
||||
# Extract coordinates based on the 'type' of element
|
||||
if elem.get('type') != 'node':
|
||||
center = elem.get('center', {})
|
||||
lat = float(center.get('lat', 0))
|
||||
lon = float(center.get('lon', 0))
|
||||
else:
|
||||
lat = float(elem.get('lat', 0))
|
||||
lon = float(elem.get('lon', 0))
|
||||
|
||||
# Prepare the data to be sent as POST request, encoded as bytes
|
||||
data = urllib.parse.urlencode({'data': query}).encode('utf-8')
|
||||
# Check if the coordinates fall within the given bounding box
|
||||
if min_lat <= lat <= max_lat and min_lon <= lon <= max_lon:
|
||||
filtered_elements.append(elem)
|
||||
|
||||
try:
|
||||
# Create a Request object with the specified URL, data, and headers
|
||||
request = urllib.request.Request(self.overpass_url, data=data, headers=self.headers)
|
||||
|
||||
# Send the request and read the response
|
||||
with urllib.request.urlopen(request) as response:
|
||||
# Read and decode the response
|
||||
response_data = response.read().decode('utf-8')
|
||||
root = ET.fromstring(response_data)
|
||||
|
||||
# Cache the response data as an ElementTree root
|
||||
self.caching_strategy.set(cache_key, root)
|
||||
logger.debug("Response data added to cache.")
|
||||
|
||||
return root
|
||||
|
||||
except urllib.error.URLError as e:
|
||||
raise ConnectionError(f"Error connecting to Overpass API: {e}") from e
|
||||
return filtered_elements
|
||||
|
||||
|
||||
def get_base_info(elem: ET.Element, osm_type: osm_types, with_name=False) :
|
||||
def get_base_info(elem: dict, osm_type: OSM_TYPES, with_name=False) :
|
||||
"""
|
||||
Extracts base information (coordinates, OSM ID, and optionally a name) from an OSM element.
|
||||
|
||||
@@ -136,7 +357,7 @@ def get_base_info(elem: ET.Element, osm_type: osm_types, with_name=False) :
|
||||
extracting coordinates either directly or from a center tag, depending on the element type.
|
||||
|
||||
Args:
|
||||
elem (ET.Element): The XML element representing the OSM entity.
|
||||
elem (dict): The JSON element representing the OSM entity.
|
||||
osm_type (str): The type of the OSM entity (e.g., 'node', 'way'). If 'node', the coordinates
|
||||
are extracted directly from the element; otherwise, from the 'center' tag.
|
||||
with_name (bool): Whether to extract and return the name of the element. If True, it attempts
|
||||
@@ -150,7 +371,7 @@ def get_base_info(elem: ET.Element, osm_type: osm_types, with_name=False) :
|
||||
"""
|
||||
# 1. extract coordinates
|
||||
if osm_type != 'node' :
|
||||
center = elem.find('center')
|
||||
center = elem.get('center')
|
||||
lat = float(center.get('lat'))
|
||||
lon = float(center.get('lon'))
|
||||
|
||||
@@ -165,7 +386,38 @@ def get_base_info(elem: ET.Element, osm_type: osm_types, with_name=False) :
|
||||
|
||||
# 3. Extract name if specified and return
|
||||
if with_name :
|
||||
name = elem.find("tag[@k='name']").get('v') if elem.find("tag[@k='name']") is not None else None
|
||||
name = elem.get('tags', {}).get('name')
|
||||
return osm_id, coords, name
|
||||
else :
|
||||
return osm_id, coords
|
||||
|
||||
return osm_id, coords
|
||||
|
||||
|
||||
def fill_cache():
|
||||
"""
|
||||
Scans the specified cache directory for files starting with 'hollow_' and attempts to load
|
||||
their contents as JSON to fill the cache of the Overpass system.
|
||||
"""
|
||||
overpass = Overpass()
|
||||
|
||||
n_files = 0
|
||||
total = 0
|
||||
|
||||
with os.scandir(OSM_CACHE_DIR) as it:
|
||||
for entry in it:
|
||||
if entry.is_file() and entry.name.startswith('hollow_'):
|
||||
total += 1
|
||||
try :
|
||||
# Read the whole file content as a string
|
||||
with open(entry.path, 'r', encoding='utf-8') as f:
|
||||
# load data and fill the cache with the query and key
|
||||
json_data = json.load(f)
|
||||
overpass.fill_cache(json_data)
|
||||
n_files += 1
|
||||
time.sleep(1)
|
||||
# Now delete the file as the cache is filled
|
||||
os.remove(entry.path)
|
||||
|
||||
except Exception as exc :
|
||||
overpass.logger.error(f'An error occured while parsing file {entry.path} as .json file: {str(exc)}')
|
||||
|
||||
overpass.logger.info(f"Successfully filled {n_files}/{total} cache files.")
|
||||
|
@@ -72,6 +72,7 @@ sightseeing:
|
||||
# - castle
|
||||
# - museum
|
||||
|
||||
|
||||
museums:
|
||||
tourism:
|
||||
- museum
|
||||
|
@@ -1,12 +1,11 @@
|
||||
city_bbox_side: 7500 #m
|
||||
max_bbox_side: 4000 #m
|
||||
radius_close_to: 50
|
||||
church_coeff: 0.55
|
||||
nature_coeff: 1.4
|
||||
church_coeff: 0.75
|
||||
nature_coeff: 1.6
|
||||
overall_coeff: 10
|
||||
tag_exponent: 1.15
|
||||
image_bonus: 1.1
|
||||
viewpoint_bonus: 5
|
||||
viewpoint_bonus: 10
|
||||
wikipedia_bonus: 1.25
|
||||
name_bonus: 3
|
||||
N_important: 40
|
||||
N_important: 60
|
||||
pay_bonus: -1
|
||||
|
@@ -4,3 +4,6 @@ average_walking_speed: 4.8
|
||||
max_landmarks: 10
|
||||
max_landmarks_refiner: 20
|
||||
overshoot: 0.0016
|
||||
time_limit: 1
|
||||
gap_rel: 0.025
|
||||
max_iter: 80
|
@@ -1,5 +1,4 @@
|
||||
"""Definition of the Landmark class to handle visitable objects across the world."""
|
||||
|
||||
from typing import Optional, Literal
|
||||
from uuid import uuid4, UUID
|
||||
from pydantic import BaseModel, Field
|
||||
@@ -50,7 +49,8 @@ class Landmark(BaseModel) :
|
||||
image_url : Optional[str] = None
|
||||
website_url : Optional[str] = None
|
||||
wiki_url : Optional[str] = None
|
||||
description : Optional[str] = None # TODO future
|
||||
keywords: Optional[dict] = {}
|
||||
description : Optional[str] = None
|
||||
duration : Optional[int] = 5
|
||||
name_en : Optional[str] = None
|
||||
|
||||
@@ -69,6 +69,7 @@ class Landmark(BaseModel) :
|
||||
is_viewpoint : Optional[bool] = False
|
||||
is_place_of_worship : Optional[bool] = False
|
||||
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""
|
||||
String representation of the Landmark object.
|
||||
@@ -122,30 +123,3 @@ class Landmark(BaseModel) :
|
||||
return (self.uuid == value.uuid or
|
||||
self.osm_id == value.osm_id or
|
||||
(self.name == value.name and self.distance(value) < 0.001))
|
||||
|
||||
|
||||
class Toilets(BaseModel) :
|
||||
"""
|
||||
Model for toilets. When false/empty the information is either false either not known.
|
||||
"""
|
||||
location : tuple
|
||||
wheelchair : Optional[bool] = False
|
||||
changing_table : Optional[bool] = False
|
||||
fee : Optional[bool] = False
|
||||
opening_hours : Optional[str] = ""
|
||||
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""
|
||||
String representation of the Toilets object.
|
||||
|
||||
Returns:
|
||||
str: A formatted string with the toilets location.
|
||||
"""
|
||||
return f'Toilets @{self.location}'
|
||||
|
||||
class Config:
|
||||
"""
|
||||
This allows us to easily convert the model to and from dictionaries
|
||||
"""
|
||||
from_attributes = True
|
||||
|
26
backend/src/structs/toilets.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""Definition of the Toilets class."""
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
|
||||
class Toilets(BaseModel) :
|
||||
"""
|
||||
Model for toilets. When false/empty the information is either false either not known.
|
||||
"""
|
||||
location : tuple
|
||||
wheelchair : Optional[bool] = False
|
||||
changing_table : Optional[bool] = False
|
||||
fee : Optional[bool] = False
|
||||
opening_hours : Optional[str] = ""
|
||||
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""
|
||||
String representation of the Toilets object.
|
||||
|
||||
Returns:
|
||||
str: A formatted string with the toilets location.
|
||||
"""
|
||||
return f'Toilets @{self.location}'
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
@@ -27,11 +27,13 @@ def test_turckheim(client, request): # pylint: disable=redefined-outer-name
|
||||
"/trip/new",
|
||||
json={
|
||||
"preferences": {"sightseeing": {"type": "sightseeing", "score": 5},
|
||||
"nature": {"type": "nature", "score": 5},
|
||||
"shopping": {"type": "shopping", "score": 5},
|
||||
"nature": {"type": "nature", "score": 0},
|
||||
"shopping": {"type": "shopping", "score": 0},
|
||||
"max_time_minute": duration_minutes,
|
||||
"detour_tolerance_minute": 0},
|
||||
"start": [48.084588, 7.280405]
|
||||
# "start": [45.74445023349939, 4.8222687890538865]
|
||||
# "start": [45.75156398104873, 4.827154464827647]
|
||||
}
|
||||
)
|
||||
result = response.json()
|
||||
@@ -44,18 +46,16 @@ def test_turckheim(client, request): # pylint: disable=redefined-outer-name
|
||||
# Add details to report
|
||||
log_trip_details(request, landmarks, result['total_time'], duration_minutes)
|
||||
|
||||
# for elem in landmarks :
|
||||
# print(elem)
|
||||
|
||||
# checks :
|
||||
assert response.status_code == 200 # check for successful planning
|
||||
assert isinstance(landmarks, list) # check that the return type is a list
|
||||
assert len(landmarks) > 2 # check that there is something to visit
|
||||
assert comp_time < 30, f"Computation time exceeded 30 seconds: {comp_time:.2f} seconds"
|
||||
assert duration_minutes*0.8 < result['total_time'], f"Trip too short: {result['total_time']} instead of {duration_minutes}"
|
||||
assert duration_minutes*1.2 > result['total_time'], f"Trip too long: {result['total_time']} instead of {duration_minutes}"
|
||||
# assert 2!= 3
|
||||
|
||||
|
||||
def test_bellecour(client, request) : # pylint: disable=redefined-outer-name
|
||||
"""
|
||||
Test n°2 : Custom test in Lyon centre to ensure proper decision making in crowded area.
|
||||
@@ -97,10 +97,9 @@ def test_bellecour(client, request) : # pylint: disable=redefined-outer-name
|
||||
assert duration_minutes*0.8 < result['total_time'], f"Trip too short: {result['total_time']} instead of {duration_minutes}"
|
||||
assert duration_minutes*1.2 > result['total_time'], f"Trip too long: {result['total_time']} instead of {duration_minutes}"
|
||||
|
||||
|
||||
def test_cologne(client, request) : # pylint: disable=redefined-outer-name
|
||||
"""
|
||||
Test n°2 : Custom test in Lyon centre to ensure proper decision making in crowded area.
|
||||
Test n°3 : Custom test in Cologne to ensure proper decision making in crowded area.
|
||||
|
||||
Args:
|
||||
client:
|
||||
@@ -141,7 +140,7 @@ def test_cologne(client, request) : # pylint: disable=redefined-outer-name
|
||||
|
||||
def test_strasbourg(client, request) : # pylint: disable=redefined-outer-name
|
||||
"""
|
||||
Test n°2 : Custom test in Lyon centre to ensure proper decision making in crowded area.
|
||||
Test n°4 : Custom test in Strasbourg to ensure proper decision making in crowded area.
|
||||
|
||||
Args:
|
||||
client:
|
||||
@@ -182,7 +181,7 @@ def test_strasbourg(client, request) : # pylint: disable=redefined-outer-name
|
||||
|
||||
def test_zurich(client, request) : # pylint: disable=redefined-outer-name
|
||||
"""
|
||||
Test n°2 : Custom test in Lyon centre to ensure proper decision making in crowded area.
|
||||
Test n°5 : Custom test in Zurich to ensure proper decision making in crowded area.
|
||||
|
||||
Args:
|
||||
client:
|
||||
@@ -223,24 +222,24 @@ def test_zurich(client, request) : # pylint: disable=redefined-outer-name
|
||||
|
||||
def test_paris(client, request) : # pylint: disable=redefined-outer-name
|
||||
"""
|
||||
Test n°2 : Custom test in Paris (les Halles) centre to ensure proper decision making in crowded area.
|
||||
Test n°6 : Custom test in Paris (les Halles) centre to ensure proper decision making in crowded area.
|
||||
|
||||
Args:
|
||||
client:
|
||||
request:
|
||||
"""
|
||||
start_time = time.time() # Start timer
|
||||
duration_minutes = 300
|
||||
duration_minutes = 200
|
||||
|
||||
response = client.post(
|
||||
"/trip/new",
|
||||
json={
|
||||
"preferences": {"sightseeing": {"type": "sightseeing", "score": 5},
|
||||
"nature": {"type": "nature", "score": 5},
|
||||
"nature": {"type": "nature", "score": 0},
|
||||
"shopping": {"type": "shopping", "score": 5},
|
||||
"max_time_minute": duration_minutes,
|
||||
"detour_tolerance_minute": 0},
|
||||
"start": [48.86248803298562, 2.346451131285925]
|
||||
"start": [48.85468881798671, 2.3423925755998374]
|
||||
}
|
||||
)
|
||||
result = response.json()
|
||||
@@ -264,7 +263,7 @@ def test_paris(client, request) : # pylint: disable=redefined-outer-name
|
||||
|
||||
def test_new_york(client, request) : # pylint: disable=redefined-outer-name
|
||||
"""
|
||||
Test n°2 : Custom test in New York (les Halles) centre to ensure proper decision making in crowded area.
|
||||
Test n°7 : Custom test in New York to ensure proper decision making in crowded area.
|
||||
|
||||
Args:
|
||||
client:
|
||||
@@ -305,7 +304,7 @@ def test_new_york(client, request) : # pylint: disable=redefined-outer-name
|
||||
|
||||
def test_shopping(client, request) : # pylint: disable=redefined-outer-name
|
||||
"""
|
||||
Test n°3 : Custom test in Lyon centre to ensure shopping clusters are found.
|
||||
Test n°8 : Custom test in Lyon centre to ensure shopping clusters are found.
|
||||
|
||||
Args:
|
||||
client:
|
||||
@@ -334,8 +333,8 @@ def test_shopping(client, request) : # pylint: disable=redefined-outer-name
|
||||
# Add details to report
|
||||
log_trip_details(request, landmarks, result['total_time'], duration_minutes)
|
||||
|
||||
for elem in landmarks :
|
||||
print(elem)
|
||||
# for elem in landmarks :
|
||||
# print(elem)
|
||||
|
||||
# checks :
|
||||
assert response.status_code == 200 # check for successful planning
|
||||
|
@@ -3,7 +3,7 @@
|
||||
from fastapi.testclient import TestClient
|
||||
import pytest
|
||||
|
||||
from ..structs.landmark import Toilets
|
||||
from ..structs.toilets import Toilets
|
||||
from ..main import app
|
||||
|
||||
|
||||
|
@@ -1,7 +1,6 @@
|
||||
"""Helper methods for testing."""
|
||||
import logging
|
||||
from fastapi import HTTPException
|
||||
from pydantic import ValidationError
|
||||
|
||||
from ..structs.landmark import Landmark
|
||||
from ..cache import client as cache_client
|
||||
@@ -39,7 +38,7 @@ def fetch_landmark(landmark_uuid: str):
|
||||
try:
|
||||
landmark = cache_client.get(f'landmark_{landmark_uuid}')
|
||||
if not landmark :
|
||||
logger.warning(f'Cache miss for landmark UUID: {landmark_uuid}')
|
||||
logger.error(f'Cache miss for landmark UUID: {landmark_uuid}')
|
||||
raise HTTPException(status_code=404, detail=f'Landmark with UUID {landmark_uuid} not found in cache.')
|
||||
|
||||
# Validate that the fetched data is a dictionary
|
||||
|
0
backend/src/toilets/__init__.py
Normal file
38
backend/src/toilets/toilet_routes.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""Defines the endpoint for fetching toilet locations."""
|
||||
from fastapi import HTTPException, APIRouter, Query
|
||||
|
||||
from ..structs.toilets import Toilets
|
||||
from .toilets_manager import ToiletsManager
|
||||
|
||||
|
||||
# Define the API router
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/toilets/new")
|
||||
def get_toilets(location: tuple[float, float] = Query(...), radius: int = 500) -> list[Toilets] :
|
||||
"""
|
||||
Endpoint to find toilets within a specified radius from a given location.
|
||||
|
||||
This endpoint expects the `location` and `radius` as **query parameters**, not in the request body.
|
||||
|
||||
Args:
|
||||
location (tuple[float, float]): The latitude and longitude of the location to search from.
|
||||
radius (int, optional): The radius (in meters) within which to search for toilets. Defaults to 500 meters.
|
||||
|
||||
Returns:
|
||||
list[Toilets]: A list of Toilets objects that meet the criteria.
|
||||
"""
|
||||
if location is None:
|
||||
raise HTTPException(status_code=406, detail="Coordinates not provided or invalid")
|
||||
if not (-90 <= location[0] <= 90 or -180 <= location[1] <= 180):
|
||||
raise HTTPException(status_code=422, detail="Start coordinates not in range")
|
||||
|
||||
toilets_manager = ToiletsManager(location, radius)
|
||||
|
||||
try :
|
||||
toilets_list = toilets_manager.generate_toilet_list()
|
||||
except KeyError as exc:
|
||||
raise HTTPException(status_code=404, detail="No toilets found") from exc
|
||||
|
||||
return toilets_list
|
@@ -1,10 +1,9 @@
|
||||
"""Module for finding public toilets around given coordinates."""
|
||||
import logging
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from ..overpass.overpass import Overpass, get_base_info
|
||||
from ..structs.landmark import Toilets
|
||||
from ..constants import OSM_CACHE_DIR
|
||||
from ..structs.toilets import Toilets
|
||||
from ..utils.bbox import create_bbox
|
||||
|
||||
|
||||
# silence the overpass logger
|
||||
@@ -41,7 +40,7 @@ class ToiletsManager:
|
||||
self.location = location
|
||||
|
||||
# Setup the caching in the Overpass class.
|
||||
self.overpass = Overpass(caching_strategy='XML', cache_dir=OSM_CACHE_DIR)
|
||||
self.overpass = Overpass()
|
||||
|
||||
|
||||
def generate_toilet_list(self) -> list[Toilets] :
|
||||
@@ -53,73 +52,71 @@ class ToiletsManager:
|
||||
list[Toilets]: A list of `Toilets` objects containing detailed information
|
||||
about the toilets found around the given coordinates.
|
||||
"""
|
||||
bbox = tuple((self.radius, self.location[0], self.location[1]))
|
||||
bbox = create_bbox(self.location, self.radius)
|
||||
osm_types = ['node', 'way', 'relation']
|
||||
toilets_list = []
|
||||
|
||||
query = self.overpass.build_query(
|
||||
area = bbox,
|
||||
osm_types = osm_types,
|
||||
selector = '"amenity"="toilets"',
|
||||
out = 'ids center tags'
|
||||
)
|
||||
self.logger.debug(f"Query: {query}")
|
||||
|
||||
query = Overpass.build_query(
|
||||
bbox = bbox,
|
||||
osm_types = osm_types,
|
||||
selector = '"amenity"="toilets"',
|
||||
out = 'ids center tags'
|
||||
)
|
||||
try:
|
||||
result = self.overpass.send_query(query)
|
||||
result = self.overpass.fetch_data_from_api(query_str=query)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error fetching landmarks: {e}")
|
||||
self.logger.error(f"Error fetching toilets: {e}")
|
||||
return None
|
||||
|
||||
toilets_list = self.xml_to_toilets(result)
|
||||
toilets_list = self.to_toilets(result)
|
||||
|
||||
return toilets_list
|
||||
|
||||
|
||||
def xml_to_toilets(self, root: ET.Element) -> list[Toilets]:
|
||||
def to_toilets(self, elements: list) -> list[Toilets]:
|
||||
"""
|
||||
Parse the Overpass API result and extract landmarks.
|
||||
|
||||
This method processes the XML root element returned by the Overpass API and
|
||||
This method processes the JSON elements returned by the Overpass API and
|
||||
extracts landmarks of types 'node', 'way', and 'relation'. It retrieves
|
||||
relevant information such as name, coordinates, and tags, and converts them
|
||||
into Landmark objects.
|
||||
|
||||
Args:
|
||||
root (ET.Element): The root element of the XML response from Overpass API.
|
||||
list (osm elements): The root element of the JSON response from Overpass API.
|
||||
elem_type (str): The type of landmark (e.g., node, way, relation).
|
||||
|
||||
Returns:
|
||||
list[Landmark]: A list of Landmark objects extracted from the XML data.
|
||||
list[Landmark]: A list of Landmark objects extracted from the JSON data.
|
||||
"""
|
||||
if root is None :
|
||||
if elements is None :
|
||||
return []
|
||||
|
||||
toilets_list = []
|
||||
for osm_type in ['node', 'way', 'relation'] :
|
||||
for elem in root.findall(osm_type):
|
||||
# Get coordinates and append them to the points list
|
||||
_, coords = get_base_info(elem, osm_type)
|
||||
if coords is None :
|
||||
continue
|
||||
for elem in elements:
|
||||
osm_type = elem.get('type')
|
||||
# Get coordinates and append them to the points list
|
||||
_, coords = get_base_info(elem, osm_type)
|
||||
if coords is None :
|
||||
continue
|
||||
|
||||
toilets = Toilets(location=coords)
|
||||
toilets = Toilets(location=coords)
|
||||
|
||||
# Extract tags as a dictionary
|
||||
tags = {tag.get('k'): tag.get('v') for tag in elem.findall('tag')}
|
||||
# Extract tags as a dictionary
|
||||
tags = elem.get('tags')
|
||||
|
||||
if 'wheelchair' in tags.keys() and tags['wheelchair'] == 'yes':
|
||||
toilets.wheelchair = True
|
||||
if 'wheelchair' in tags.keys() and tags['wheelchair'] == 'yes':
|
||||
toilets.wheelchair = True
|
||||
|
||||
if 'changing_table' in tags.keys() and tags['changing_table'] == 'yes':
|
||||
toilets.changing_table = True
|
||||
if 'changing_table' in tags.keys() and tags['changing_table'] == 'yes':
|
||||
toilets.changing_table = True
|
||||
|
||||
if 'fee' in tags.keys() and tags['fee'] == 'yes':
|
||||
toilets.fee = True
|
||||
if 'fee' in tags.keys() and tags['fee'] == 'yes':
|
||||
toilets.fee = True
|
||||
|
||||
if 'opening_hours' in tags.keys() :
|
||||
toilets.opening_hours = tags['opening_hours']
|
||||
if 'opening_hours' in tags.keys() :
|
||||
toilets.opening_hours = tags['opening_hours']
|
||||
|
||||
toilets_list.append(toilets)
|
||||
toilets_list.append(toilets)
|
||||
|
||||
return toilets_list
|
27
backend/src/utils/bbox.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""Various helper functions"""
|
||||
import math as m
|
||||
|
||||
def create_bbox(coords: tuple[float, float], radius: int):
|
||||
"""
|
||||
Create a bounding box around the given coordinates.
|
||||
|
||||
Args:
|
||||
coords (tuple[float, float]): The latitude and longitude of the center of the bounding box.
|
||||
radius (int): The half-side length of the bounding box in meters.
|
||||
|
||||
Returns:
|
||||
tuple[float, float, float, float]: The minimum latitude, minimum longitude, maximum latitude, and maximum longitude
|
||||
defining the bounding box.
|
||||
"""
|
||||
# Earth's radius in meters
|
||||
R = 6378137
|
||||
lat, lon = coords
|
||||
d_lat = radius / R
|
||||
d_lon = radius / (R * m.cos(m.pi * lat / 180))
|
||||
|
||||
lat_min = lat - d_lat * 180 / m.pi
|
||||
lat_max = lat + d_lat * 180 / m.pi
|
||||
lon_min = lon - d_lon * 180 / m.pi
|
||||
lon_max = lon + d_lon * 180 / m.pi
|
||||
|
||||
return (lat_min, lon_min, lat_max, lon_max)
|
@@ -19,7 +19,7 @@ pluginManagement {
|
||||
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version "7.3.0" apply false
|
||||
id "com.android.application" version "8.1.0" apply false
|
||||
id "org.jetbrains.kotlin.android" version "2.0.20" apply false
|
||||
}
|
||||
|
||||
|
2
frontend/assets/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
## Vector assets
|
||||
As per https://www.svgrepo.com/collection/pixellove-bordered-vectors/ these icons are licensed under CC0.
|
@@ -1,107 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 27.5.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 500 500" style="enable-background:new 0 0 500 500;" xml:space="preserve">
|
||||
<g id="OBJECTS">
|
||||
<g>
|
||||
<path style="fill:#F2DBDE;" d="M381.005,363.01c-53.963,8.445-84.441,11.1-138.832,6.101
|
||||
c-54.388-4.998-109.48-25.844-144.743-67.555c-23.468-27.759-36.728-62.943-43.732-98.613c-3.745-19.07-5.754-39.21,0.433-57.635
|
||||
c7.513-22.378,26.565-39.569,48.136-49.156c21.572-9.589,45.552-12.365,69.151-12.944c47.753-1.172,95.706,6.26,140.863,21.831
|
||||
c35.603,12.277,69.954,29.937,96.972,56.171c27.019,26.233,46.213,61.723,47.963,99.341
|
||||
C458.967,298.17,438.434,354.022,381.005,363.01z"/>
|
||||
<g>
|
||||
<path style="fill:#F2BFC6;" d="M314.479,248.209c-22.398,36.41-29.246,81.831-19.597,123.401
|
||||
c27.302-0.242,52.026-3.263,86.124-8.6c57.429-8.989,77.961-64.84,76.211-102.458c-1.503-32.308-15.881-63.041-37.024-87.694
|
||||
C375.546,184.337,337.241,211.21,314.479,248.209z"/>
|
||||
<path style="fill:#F2BFC6;" d="M60.074,229.111c2.232,7.566,4.802,15.029,7.749,22.32c40.138-5.931,78.066-26.379,104.834-56.907
|
||||
c26.459-30.176,41.716-69.876,42.677-109.969c-14.6-1.246-29.267-1.705-43.916-1.345c-11.908,0.292-23.911,1.147-35.655,3.151
|
||||
C136.569,142.478,107.155,198.423,60.074,229.111z"/>
|
||||
<path style="fill:#F2BFC6;" d="M365.131,128.557c-16.748-9.529-34.631-17.233-52.85-23.516
|
||||
c-6.45-2.224-12.962-4.262-19.517-6.153c-1.712,23.304-4.543,46.555-11.914,68.659c-9.236,27.692-26.464,53.808-52.01,67.931
|
||||
c-22.973,12.7-50.376,14.689-74.443,25.169c-21.624,9.417-39.587,25.305-54.36,43.893c8.346,9.381,17.714,17.663,27.862,24.902
|
||||
c16.736-21.461,41.874-37.166,67.161-48.559c35.578-16.03,74.129-26.682,105.739-49.566
|
||||
C334.357,207.023,357.577,169.22,365.131,128.557z"/>
|
||||
</g>
|
||||
</g>
|
||||
<ellipse style="opacity:0.15;fill:#2D3038;" cx="250.223" cy="394.224" rx="109.236" ry="18.917"/>
|
||||
<g>
|
||||
<path style="fill:#2D3038;" d="M305.132,388.442c-0.168,1.158-0.626,2.243-1.458,3.061c-1.863,1.832-4.823,1.724-7.427,1.538
|
||||
c-17.939-1.285-36.017-0.625-53.815,1.965c-7.053,3.155-16.423,3.233-25.275,2.004c-8.853-1.231-17.514-3.684-26.397-4.661
|
||||
c-8.885-0.976-21.867-0.33-26.499,2.758c0,0-7.266,3.996-12.907,12.021c-3.367,4.789-4.105,11.306-2.377,16.899
|
||||
c2.452,7.945,10.312,13.334,18.475,14.912c8.163,1.579,16.603-0.053,24.6-2.327c22.82-6.49,43.805-18.134,66.018-26.468
|
||||
c22.213-8.334,47.017-13.282,69.546-5.844c3.96,1.306,7.879,3.033,10.941,5.866c3.062,2.832,5.173,6.927,4.813,11.081
|
||||
c-0.464,5.356-4.97,9.719-10.061,11.444c-5.092,1.726-10.658,1.275-15.953,0.346c-5.296-0.93-10.554-2.17-15.926-2.414
|
||||
c-20.08-0.909-38.455,4.247-56.124,10.857c-17.669,6.608-35.096,14.21-53.56,18.085c-18.463,3.874-35.807,8.106-51.682-4.186
|
||||
c-20.345-15.753-19.603-41.137-8.091-63.296c5.521-10.629,12.589-18.637,19.416-27.732c-1.72-12.542-6.898-24.945-9.467-37.525
|
||||
c-4.135-20.25-1.309-41.854,7.666-61.314c5.614-15.439,11.257-30.942,19.093-45.38c7.835-14.438,18.007-27.88,31.297-37.536
|
||||
c13.289-9.656,29.927-15.279,46.993-13.222c7.787-8.403,16.038-16.377,24.703-23.871c-1.319-7.29-1.183-14.637,0.584-20.961
|
||||
c-4.077-8.872-8.2-17.907-9.54-27.579c-0.835-6.027-0.441-12.408,1.577-17.991c1.878-5.198,8.452-6.799,12.542-3.08
|
||||
c6.673,6.07,12.683,12.869,17.891,20.235c18.398-4.802,38.164-4.231,56.264,1.583c6.473-8.017,14.398-14.861,23.286-20.075
|
||||
c2.366-1.388,5.533-2.613,7.657-0.875c1.683,1.377,1.736,3.89,1.592,6.059c-0.815,12.217-3.418,24.313-8.016,36.577
|
||||
c4.862,15.779,0.82,33.862-9.812,46.412c-2.168,11.956,1.193,24.438,2.504,36.665c2.294,21.385-1.98,43.411-12.271,62.744
|
||||
c-2.4,4.508-5.754,8.444-9.863,11.477c-1.71,1.263-3.38,2.581-5.006,3.951c-5.172,20.881-10.139,41.311-15.351,62.281
|
||||
c2.061,7.78,4.487,15.496,7.272,23.126c3.209-0.899,6.478-1.696,9.816-1.809c3.896-0.132,7.942,0.744,11.024,3.131
|
||||
c2.308,1.785,3.979,4.375,4.658,7.212c0.484,2.028,0.445,4.26-0.563,6.086c-0.663,1.203-1.81,2.171-3.102,2.583
|
||||
c0.454,1.78,0.565,3.616,0.106,5.385c-0.778,3.004-3.622,5.6-6.675,5.375c-0.047,0.112-0.097,0.223-0.151,0.333
|
||||
c-0.979,1.985-3.08,3.228-5.239,3.714c-2.063,0.464-4.207,0.333-6.319,0.174c-0.138,0.225-0.3,0.437-0.489,0.633
|
||||
c-1.556,1.603-4.16,1.338-6.346,0.87c-3.015-0.645-6.04-1.471-8.688-3.051c-2.647-1.583-4.906-4.013-5.707-6.991
|
||||
c-1.237-4.607,2.111-10.097,0.151-14.313c-3.538-7.609-7.733-14.893-12.004-22.126c-8.712,7.077-18.162,13.242-28.147,18.367
|
||||
c6.95-0.974,14.248-1.345,21.476-0.293c3.273,0.475,6.596,1.283,9.285,3.208c2.689,1.924,4.631,5.173,4.214,8.453
|
||||
c-0.34,2.664-2.596,5.054-5.156,5.449"/>
|
||||
<path style="fill:none;stroke:#FFFFFF;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;" d="M151.465,379.089
|
||||
c0.578-3.877,0.614-7.729,0.28-11.566"/>
|
||||
<path style="fill:none;stroke:#FFFFFF;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;" d="M296.431,98.602
|
||||
c1.739,2.591,3.381,5.247,4.918,7.962"/>
|
||||
<path style="fill:none;stroke:#FFFFFF;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;" d="M273.736,153.553
|
||||
c-0.645-1.929-1.188-3.891-1.625-5.865"/>
|
||||
<path style="fill:none;stroke:#FFFFFF;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;" d="M295.23,106.811
|
||||
c-4.87-7.904-10.55-15.309-16.923-22.061c-1.834-1.943-4.156-3.987-6.799-3.598c-2.928,0.431-4.574,3.626-5.147,6.53
|
||||
c-1.629,8.254,1.474,16.627,4.521,24.47"/>
|
||||
<path style="fill:none;stroke:#FFFFFF;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;" d="M352.846,98.327
|
||||
c1.084,0.372,2.162,0.763,3.232,1.174"/>
|
||||
<path style="fill:none;stroke:#FFFFFF;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;" d="M363.545,168.179
|
||||
c-1.077,1.107-2.211,2.161-3.399,3.155"/>
|
||||
<path style="fill:none;stroke:#FFFFFF;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;" d="M295.583,130.136
|
||||
c3.86-4.907,10.772-7.181,16.791-5.521c6.019,1.659,10.791,7.151,11.446,13.054c-4.594,3.601-11.6,3.717-16.311,0.268
|
||||
c-3.162-2.315-5.105-6.101-5.423-9.993"/>
|
||||
<path style="fill:none;stroke:#FFFFFF;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;" d="M363.109,126.785
|
||||
c-1.79-2.631-5.159-4.002-8.321-3.646c-3.162,0.356-6.042,2.317-7.787,4.979c-1.743,2.662-2.395,5.96-1.828,9.854
|
||||
c4.738,1.952,10.727,0.164,13.621-4.066c1.462-2.137,2.057-4.785,1.832-7.36"/>
|
||||
<path style="fill:none;stroke:#FFFFFF;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;" d="M350.957,171.048
|
||||
c-4.278,4.378-10.749,6.497-16.787,5.499"/>
|
||||
<path style="fill:none;stroke:#FFFFFF;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;" d="M338.68,282.717
|
||||
c-5.42,4.867-10.31,10.327-14.541,16.258"/>
|
||||
<path style="fill:none;stroke:#FFFFFF;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;" d="M333.834,368.351
|
||||
c0.757,2.017,1.54,4.028,2.348,6.032c2.26-0.589,4.541-1.183,6.876-1.268c2.333-0.084,4.757,0.381,6.656,1.74
|
||||
c1.559,1.116,2.664,2.753,3.552,4.452c0.261,0.499,0.505,1.013,0.727,1.536"/>
|
||||
<path style="fill:none;stroke:#FFFFFF;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;" d="M317.138,283.315
|
||||
c0.476,18.805,3.038,37.553,7.633,55.961"/>
|
||||
<path style="fill:none;stroke:#FFFFFF;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;" d="M337.823,376.837
|
||||
c2.877-0.595,5.878,0.99,7.67,3.316c1.791,2.327,2.567,5.273,3.025,8.174c0.191,1.214,0.327,2.48,0.209,3.695"/>
|
||||
<path style="fill:none;stroke:#FFFFFF;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;" d="M327.236,380.633
|
||||
c3.086-0.38,6.102,1.606,7.733,4.252c1.632,2.645,2.112,5.835,2.285,8.939c0.04,0.721,0.054,1.476-0.027,2.204"/>
|
||||
<path style="fill:none;stroke:#FFFFFF;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;" d="M305.059,385.808
|
||||
c-0.036-0.193-0.079-0.385-0.128-0.573c-1.058-4.111-4.728-7.422-8.927-8.052"/>
|
||||
<g>
|
||||
<path style="fill:none;stroke:#FFFFFF;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;" d="M250.442,264.812
|
||||
c-1.67-3.125-3.183-6.325-4.488-9.622c-5.098-12.883-6.92-27.047-5.248-40.801"/>
|
||||
<path style="fill:none;stroke:#FFFFFF;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;" d="M302.266,351.248
|
||||
c-7.667-12.944-15.022-25.405-19.496-39.762"/>
|
||||
<path style="fill:none;stroke:#FFFFFF;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;" d="M272.435,372.065
|
||||
c-3.368,0.554-6.637,1.226-9.757,1.918c10.852-22.715,21.971-46.794,19.913-71.883c-0.826-10.055-4.036-20.316-11.156-27.463
|
||||
c-8.522-8.553-21.576-11.406-33.547-9.827c-22.022,2.903-41.327,20.57-46.167,42.248"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:none;stroke:#FFFFFF;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;" d="M328.579,152.076
|
||||
c1.379-0.341,2.796,0.501,3.736,1.565c0.942,1.065,1.588,2.366,2.551,3.41c0.963,1.044,2.43,1.826,3.784,1.398
|
||||
c1.002-0.317,1.702-1.217,2.207-2.139c0.504-0.921,0.888-1.923,1.572-2.721c1.237-1.447,3.432-1.978,5.192-1.258"/>
|
||||
<path style="fill:none;stroke:#FFFFFF;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;" d="M360.735,158.173
|
||||
c-2.16,5.007-7.325,8.57-12.773,8.812c-1.946,0.086-3.967-0.245-5.593-1.317c-1.872-1.234-2.979-3.253-3.85-5.361
|
||||
c-0.089,1.146-0.496,2.29-1.133,3.25c-1.229,1.854-3.175,3.116-5.189,4.059c-3.3,1.546-7.007,2.373-10.616,1.879
|
||||
c-3.611-0.495-7.099-2.413-9.07-5.477"/>
|
||||
<path style="fill:none;stroke:#FFFFFF;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;" d="M338.276,158.534
|
||||
c0,0,0.176,1.073,0.244,1.773"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 9.5 KiB |
79
frontend/assets/cel-snow-globe.svg
Normal file
@@ -0,0 +1,79 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
|
||||
<title>cel-snow-globe</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
|
||||
</defs>
|
||||
<g id="General" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="SLICES-64px" transform="translate(-450.000000, 0.000000)">
|
||||
|
||||
</g>
|
||||
<g id="ICONS" transform="translate(-445.000000, 5.000000)">
|
||||
<g id="cel-snow-globe" transform="translate(450.000000, 2.000000)">
|
||||
<path d="M46,44 C48.209,44 50,45.791 50,48 L50,52 L2,52 L2,48 C2,45.791 3.791,44 6,44 L46,44 Z" id="Fill-1055" fill="#EEC261">
|
||||
|
||||
</path>
|
||||
<path d="M7.2402,44.002 C2.7562,39.33 0.0002,32.987 0.0002,26 C0.0002,11.641 11.6402,0 26.0002,0 C40.3592,0 52.0002,11.641 52.0002,26 C52.0002,32.986 49.2442,39.33 44.7602,44.001 L7.2402,44.002 Z" id="Fill-1056" fill="#B6E0F2">
|
||||
|
||||
</path>
|
||||
<path d="M38,37 C38,33.134 34.866,30 31,30 C27.134,30 24,33.134 24,37 C24,40.866 27.134,44 31,44 C34.866,44 38,40.866 38,37" id="Fill-1057" fill="#E9EFFA">
|
||||
|
||||
</path>
|
||||
<path d="M26,25 C26,22.238 28.239,20 31,20 C33.761,20 36,22.238 36,25 C36,27.762 33.761,30 31,30 C28.239,30 26,27.762 26,25" id="Fill-1058" fill="#E9EFFA">
|
||||
|
||||
</path>
|
||||
<path d="M38,37 C38,33.134 34.866,30 31,30 C27.134,30 24,33.134 24,37 C24,40.866 27.134,44 31,44 C34.866,44 38,40.866 38,37 Z" id="Stroke-1059" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M26,25 C26,22.238 28.239,20 31,20 C33.761,20 36,22.238 36,25 C36,27.762 33.761,30 31,30 C28.239,30 26,27.762 26,25 Z" id="Stroke-1060" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M46,44 C48.209,44 50,45.791 50,48 L50,52 L2,52 L2,48 C2,45.791 3.791,44 6,44 L46,44 Z" id="Stroke-1061" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M7.2402,44.002 C2.7562,39.33 0.0002,32.987 0.0002,26 C0.0002,11.641 11.6402,0 26.0002,0 C40.3592,0 52.0002,11.641 52.0002,26 C52.0002,32.986 49.2442,39.33 44.7602,44.001" id="Stroke-1062" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M8,24 C8,14.059 16.059,6 26,6" id="Stroke-1063" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M20,28 L26.061,32.04" id="Stroke-1064" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M42,28 L35.939,32.04" id="Stroke-1065" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M42,25 L42,28" id="Stroke-1066" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M45,28 L42,28" id="Stroke-1067" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M20,25 L20,28" id="Stroke-1068" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M17,28 L20,28" id="Stroke-1069" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M10.02,31.0098 C10.02,31.5688 9.568,32.0208 9.01,32.0208 C8.452,32.0208 8,31.5688 8,31.0098 C8,30.4518 8.452,29.9998 9.01,29.9998 C9.568,29.9998 10.02,30.4518 10.02,31.0098 Z" id="Stroke-1070" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M22.02,15.0098 C22.02,15.5688 21.568,16.0208 21.01,16.0208 C20.452,16.0208 20,15.5688 20,15.0098 C20,14.4518 20.452,13.9998 21.01,13.9998 C21.568,13.9998 22.02,14.4518 22.02,15.0098 Z" id="Stroke-1071" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M44.02,17.0098 C44.02,17.5688 43.568,18.0208 43.01,18.0208 C42.452,18.0208 42,17.5688 42,17.0098 C42,16.4518 42.452,15.9998 43.01,15.9998 C43.568,15.9998 44.02,16.4518 44.02,17.0098 Z" id="Stroke-1072" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M18.02,37.0098 C18.02,37.5688 17.568,38.0208 17.01,38.0208 C16.452,38.0208 16,37.5688 16,37.0098 C16,36.4518 16.452,35.9998 17.01,35.9998 C17.568,35.9998 18.02,36.4518 18.02,37.0098 Z" id="Stroke-1073" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M36.02,9.0098 C36.02,9.5688 35.568,10.0208 35.01,10.0208 C34.452,10.0208 34,9.5688 34,9.0098 C34,8.4518 34.452,7.9998 35.01,7.9998 C35.568,7.9998 36.02,8.4518 36.02,9.0098 Z" id="Stroke-1074" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 5.1 KiB |
@@ -1,273 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 27.5.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 1000 700" style="enable-background:new 0 0 1000 700;" xml:space="preserve">
|
||||
<g id="Shadow">
|
||||
<g style="opacity:0.1;">
|
||||
<path style="fill:#38415C;" d="M186.919,556.734c0,0.331,0.541,0.599,1.208,0.599c0.667,0,1.208-0.268,1.208-0.599
|
||||
c0-0.331-0.541-0.599-1.208-0.599C187.46,556.135,186.919,556.403,186.919,556.734z"/>
|
||||
<path style="fill:#38415C;" d="M957.699,446.328h-12.196h-37.267h-8.131h-4.525h-22.106h-29.729h-8.01h-8.921h-7.462H777.69h-5.38
|
||||
h-8.517h-13.921h-24.898h-0.367h-35.201h-33.29h-13.405h-8.642h-18.387h-20.084H584.19h-2.542h-5.421h-37.944h-7.453h-2.757
|
||||
h-2.757h-1.428h-8.748h-5.514h-10.175h-0.905h-4.609h-10.175h-5.514h-10.175h-2.757h-2.757h-27.938h-8.05h-29.96h-6.713h-18.964
|
||||
h-11.234h-48.644h-12.099h-10.229h-20.764h-12.382h-3.512h-23.242h-5.943h-13.266h-10.795h-35.413h-16.467h-4.656h-8.696h-25.877
|
||||
H89.054h-4.763H72.026h-7.508H53.821H42.302v9.376h11.519v6.41h10.696v6.835h19.774v2.177h20.658v9.405h17.084v-5.919h11.557
|
||||
v10.475h3.789v11.69h11.18v9.823h-4.017v1.763h7.066v4.785h23.433v28.254h-1.845v1.897h4.028v2.429h4.636v0.913v2.777v2.41h5.594
|
||||
v4.306h0.673l0,0h0.673v-4.306h3.015l1.823-2.41h12.206v-3.69h31.948V543.3h4.028v-1.897h-1.845V484.37h15.302v40.617h1.509v3.023
|
||||
h2.811v10.012h1.016v-10.012h2.287v7.997h1.017v-7.997h4.828v-3.023h6.098v-10.569h11.445v-0.743h-1.492v-2.116h7.56v32.974
|
||||
h-1.078v0.849h7.678v5.101h-0.992v0.817h7.047v2.933h-1.099v0.627h3.502v2.77h3.348v5.513h0.402h0.398h0.314v-5.513h1.354v9.889
|
||||
h0.402h0.399h0.314v-9.889h0.451h2.897v-2.77h3.034v-0.627h-0.632v-2.933h7.047v-0.817h-0.992v-5.101h7.678v-0.849h-1.078v-43.096
|
||||
h23.505v-5.982h8.399v31.88h-4.954v0.806h4.954v1.443h6.279v2.37h30.344v27.318h7.165v21.803h13.871V593h1.952v-11.927h4.298
|
||||
v9.528h1.952v-9.528h21.941v-12.964h3.982v-8.839h11.148v-32.999h4.318v-7.629l6.342,0.769v1.742h5.514v-1.073l10.175,1.234v1.969
|
||||
h5.514v-1.3l9.332,1.131v9.523h2.491v16.539h11.982v6.297h6.46v5.29h2.267v9.068h0.586v-9.068h2.267v-5.29h6.46v-6.297h12.075
|
||||
v-16.539h2.399v-45.67l5.467,13.925h5.729v12.219h-2.645v0.527h31.278v6.75h-3.52v0.763h-1.791v2.08h8.284v2.313h-1.087v0.668
|
||||
h18.198v-0.668h-1.087v-2.313h23.966c0.802,1.935,2.023,3.811,3.668,5.596l-3.992,0.913c-0.688-0.732-2.184-1.239-3.92-1.239
|
||||
c-2.388,0-4.324,0.96-4.324,2.143c0,1.183,1.936,2.143,4.324,2.143c2.388,0,4.324-0.96,4.324-2.143
|
||||
c0-0.239-0.08-0.468-0.225-0.683l4.015-0.919c2.595,2.749,6.165,5.281,10.623,7.491c0.352,0.174,0.709,0.346,1.069,0.515
|
||||
l-3.154,1.668c-0.76-0.329-1.753-0.528-2.841-0.528c-2.388,0-4.324,0.959-4.324,2.143s1.936,2.143,4.324,2.143
|
||||
c2.388,0,4.324-0.959,4.324-2.143c0-0.559-0.432-1.068-1.139-1.449l3.16-1.671c5.36,2.471,11.576,4.337,18.308,5.527l-1.744,2.453
|
||||
c-0.378-0.054-0.777-0.083-1.19-0.083c-2.388,0-4.324,0.96-4.324,2.143c0,1.183,1.936,2.143,4.324,2.143
|
||||
c2.388,0,4.324-0.96,4.324-2.143c0-0.895-1.107-1.662-2.68-1.982l1.743-2.451c5.551,0.953,11.445,1.449,17.493,1.449
|
||||
c0.498,0,0.995-0.003,1.491-0.01l0.198,3.017c-2.096,0.148-3.707,1.041-3.707,2.121c0,1.183,1.936,2.143,4.324,2.143
|
||||
c2.388,0,4.324-0.96,4.324-2.143c0-1.184-1.936-2.143-4.324-2.143c-0.046,0-0.091,0.001-0.137,0.002l-0.197-3.004
|
||||
c2.456-0.044,4.881-0.173,7.265-0.378l-2.223,24.735l79.948-8.225v-43.336h13.883v22.309h24.985v8.902h1.355v-8.902h2.795v16.446
|
||||
h1.355v-16.446h3.219v11.855h1.355v-11.855h4.235v-54.059h12.874V506.6h2.033v3.715h2.033v2.582h14.483v-2.582h2.033V506.6h7.369
|
||||
v1.594h3.557V506.6h1.259v4.262h5.082V506.6h1.452l3.161-11.593h11.528v5.526h0.762v-5.526h4.32v6.746h0.762v-6.746h6.25v-1.567
|
||||
h-1.592v-17.317h12.874l1.507-4.997h10.931v-9.012h11.954v-6.86h12.196V446.328z M653.829,518.335l-11.117,0.179v-7.937h2.055
|
||||
v-0.76h-2.055v-0.593l13.295,2.426c-1.417,1.94-2.19,4.031-2.19,6.21C653.816,518.019,653.821,518.177,653.829,518.335z
|
||||
M689.289,499.58c-4.354,0.083-8.516,0.542-12.36,1.312l-5.314-6.414v-0.42h5.082v4.786h5.082v-4.786h7.148L689.289,499.58z
|
||||
M702.329,517.554l-8.713,0.14c-0.026-0.114-0.079-0.224-0.155-0.328l9.073-2.076L702.329,517.554z M666.025,494.058v0.401
|
||||
c-0.325,0.085-0.657,0.163-0.979,0.251l-0.713-0.651H666.025z M666.025,495.263v0.341l-0.291-0.266
|
||||
C665.83,495.311,665.929,495.289,666.025,495.263z M666.025,496.603v2.241h2.454l3.554,3.247c-2.98,0.871-5.693,1.943-8.062,3.179
|
||||
l-10.904-5.064c0.33-0.173,0.666-0.344,1.007-0.513c3.276-1.624,6.914-3.003,10.823-4.12L666.025,496.603z M672.377,502.405
|
||||
l15.07,13.768l-22.95-10.659C666.813,504.306,669.465,503.258,672.377,502.405z M669.572,498.844h2.043v-3.739l4.87,5.877
|
||||
c-1.242,0.259-2.449,0.55-3.618,0.872L669.572,498.844z M691.664,494.058v4.786h12.332l-1.224,1.721
|
||||
c-3.776-0.648-7.828-1.001-12.044-1.001c-0.32,0-0.64,0.002-0.959,0.006l-0.361-5.512H691.664z M703.939,499.641l-0.101,1.127
|
||||
c-0.206-0.039-0.404-0.087-0.612-0.124L703.939,499.641z M702.896,511.25l-8.307,4.394l8.619-7.874L702.896,511.25z
|
||||
M702.863,511.616l-0.306,3.407l-9.299,2.127c-0.053-0.046-0.11-0.09-0.172-0.133l0.598-0.547L702.863,511.616z M693.364,518.468
|
||||
l8.74,1.595l-0.252,2.801l-8.846-4.108C693.147,518.667,693.268,518.571,693.364,518.468z M693.53,518.245
|
||||
c0.056-0.1,0.091-0.205,0.102-0.312l8.676-0.14l-0.182,2.021L693.53,518.245z M656.116,486.551l-11.415-10.428h11.415V486.551z
|
||||
M656.116,487.55v4.746h-1.779v1.763h8.903l0.969,0.885c-4.029,1.15-7.778,2.571-11.154,4.243
|
||||
c-0.352,0.175-0.698,0.352-1.039,0.53l-3.311-1.538c0.63-0.372,1.01-0.852,1.01-1.376c0-1.184-1.936-2.143-4.324-2.143
|
||||
c-1.018,0-1.941,0.182-2.68,0.473v-5.035h2.055v-0.76h-2.055v-9.479h2.055v-0.76h-2.055v-2.977h0.897L656.116,487.55z
|
||||
M642.711,500.338h2.055v-0.76h-2.055v-1.104c0.739,0.292,1.662,0.473,2.68,0.473c1.158,0,2.209-0.226,2.985-0.593l3.31,1.537
|
||||
c-3.677,1.959-6.684,4.15-8.975,6.503V500.338z M642.711,508.163c2.337-2.844,5.703-5.479,10.027-7.784l10.906,5.065
|
||||
c-3.215,1.722-5.771,3.749-7.47,5.983l-13.463-2.457V508.163z M664.17,505.688l24.004,11.148l0.198,0.181
|
||||
c-0.107,0.074-0.2,0.152-0.279,0.235l-31.242-5.701C658.515,509.362,661.02,507.375,664.17,505.688z M673.21,502.168
|
||||
c1.146-0.316,2.33-0.602,3.548-0.855l12.647,15.264c-0.111,0.028-0.218,0.06-0.32,0.094L673.21,502.168z M677.203,501.222
|
||||
c3.766-0.755,7.844-1.204,12.11-1.286l1.082,16.492c-0.187,0.011-0.369,0.03-0.544,0.057L677.203,501.222z M689.793,499.928
|
||||
c0.311-0.004,0.623-0.006,0.935-0.006c4.131,0,8.103,0.346,11.804,0.981l-11.067,15.563c-0.19-0.025-0.388-0.04-0.591-0.045
|
||||
L689.793,499.928z M702.985,500.982c0.278,0.05,0.543,0.112,0.818,0.165l-0.497,5.535l-10.935,9.99
|
||||
c-0.142-0.048-0.294-0.09-0.453-0.126L702.985,500.982z M692.678,518.929l9.146,4.247l-0.629,7.002l-9.143-11.035
|
||||
C692.279,519.085,692.49,519.013,692.678,518.929z M656.683,511.774l31.244,5.702c-0.056,0.1-0.091,0.204-0.102,0.312
|
||||
l-33.276,0.536c-0.008-0.154-0.012-0.309-0.012-0.464C654.537,515.724,655.295,513.675,656.683,511.774z M687.841,518.026
|
||||
c0.026,0.114,0.079,0.224,0.155,0.328l-30.225,6.916c-1.892-2.059-3.018-4.325-3.204-6.708L687.841,518.026z M688.199,518.57
|
||||
c0.106,0.092,0.231,0.178,0.373,0.257l-22.753,12.035c-3.243-1.527-5.915-3.348-7.845-5.376L688.199,518.57z M688.923,518.989
|
||||
c0.188,0.074,0.394,0.137,0.616,0.186l-11.067,15.563c-4.604-0.824-8.776-2.098-12.301-3.714L688.923,518.989z M689.992,519.254
|
||||
c0.19,0.024,0.388,0.04,0.591,0.045l1.082,16.493c-0.311,0.004-0.623,0.006-0.936,0.006c-4.131,0-8.102-0.346-11.804-0.98
|
||||
L689.992,519.254z M691.063,519.291c0.187-0.011,0.369-0.03,0.544-0.057l9.537,11.51l-0.39,4.342
|
||||
c-2.753,0.394-5.632,0.641-8.61,0.697L691.063,519.291z M640.035,523.229h7.987v-2.08h-1.791v-0.763h-3.52v-1.635l11.136-0.179
|
||||
c0.189,2.432,1.339,4.745,3.27,6.846l-13.578,3.106C641.982,526.835,640.816,525.059,640.035,523.229z M654.074,536.027
|
||||
c-4.336-2.149-7.809-4.612-10.333-7.285l13.579-3.107c1.969,2.07,4.697,3.929,8.007,5.487l-10.218,5.404
|
||||
C654.761,536.362,654.415,536.196,654.074,536.027z M655.459,536.689l10.219-5.405c3.597,1.65,7.855,2.95,12.553,3.791
|
||||
l-4.97,6.989C666.716,540.907,660.672,539.092,655.459,536.689z M690.728,543.552c-5.882,0-11.614-0.483-17.013-1.409l4.97-6.989
|
||||
c3.776,0.648,7.828,1.001,12.043,1.001c0.321,0,0.64-0.002,0.959-0.006l0.485,7.393
|
||||
C691.692,543.549,691.211,543.552,690.728,543.552z M692.653,543.535l-0.485-7.395c2.956-0.057,5.813-0.299,8.553-0.681
|
||||
l-0.69,7.679C697.611,543.354,695.148,543.49,692.653,543.535z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Object">
|
||||
<g style="opacity:0.3;">
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="207.5072" y1="393.376" x2="207.5072" y2="229.7061">
|
||||
<stop offset="0" style="stop-color:#403E40"/>
|
||||
<stop offset="0.1275" style="stop-color:#4E4D4E"/>
|
||||
<stop offset="0.3124" style="stop-color:#5A5A5A"/>
|
||||
<stop offset="0.5479" style="stop-color:#626262"/>
|
||||
<stop offset="1" style="stop-color:#646464"/>
|
||||
</linearGradient>
|
||||
<polygon style="fill:url(#SVGID_1_);" points="175.04,393.376 239.974,393.376 239.974,259.43 241.819,259.43 241.819,255.601
|
||||
237.792,255.601 237.792,250.701 205.844,250.701 205.844,243.255 193.638,243.255 191.815,238.393 188.799,238.393
|
||||
188.799,229.706 188.126,229.706 187.454,229.706 187.454,238.393 181.859,238.393 181.859,243.255 181.859,248.859
|
||||
181.859,250.701 177.223,250.701 177.223,255.601 173.195,255.601 173.195,259.43 175.04,259.43 "/>
|
||||
|
||||
<linearGradient id="SVGID_00000000931258187104496080000017865145222397382034_" gradientUnits="userSpaceOnUse" x1="188.1266" y1="229.7061" x2="188.1266" y2="227.2891">
|
||||
<stop offset="0" style="stop-color:#403E40"/>
|
||||
<stop offset="0.1275" style="stop-color:#4E4D4E"/>
|
||||
<stop offset="0.3124" style="stop-color:#5A5A5A"/>
|
||||
<stop offset="0.5479" style="stop-color:#626262"/>
|
||||
<stop offset="1" style="stop-color:#646464"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#SVGID_00000000931258187104496080000017865145222397382034_);" d="M189.335,228.498
|
||||
c0-0.668-0.541-1.209-1.209-1.209c-0.667,0-1.208,0.541-1.208,1.209c0,0.667,0.541,1.208,1.208,1.208
|
||||
C188.794,229.706,189.335,229.165,189.335,228.498z"/>
|
||||
|
||||
<linearGradient id="SVGID_00000036247364810958532620000001993945857249512106_" gradientUnits="userSpaceOnUse" x1="508.9194" y1="421.4165" x2="508.9194" y2="155.3276">
|
||||
<stop offset="0" style="stop-color:#403E40"/>
|
||||
<stop offset="0.1275" style="stop-color:#4E4D4E"/>
|
||||
<stop offset="0.3124" style="stop-color:#5A5A5A"/>
|
||||
<stop offset="0.5479" style="stop-color:#626262"/>
|
||||
<stop offset="1" style="stop-color:#646464"/>
|
||||
</linearGradient>
|
||||
<polygon style="fill:url(#SVGID_00000036247364810958532620000001993945857249512106_);" points="777.689,401.218 777.689,221.14
|
||||
697.741,204.545 706.501,401.218 471.904,401.218 471.904,289.958 467.586,289.958 467.586,223.38 456.438,223.38
|
||||
456.438,205.547 452.456,205.547 452.456,179.391 430.514,179.391 430.514,160.168 428.562,160.168 428.562,179.391
|
||||
424.264,179.391 424.264,155.328 422.311,155.328 422.311,179.391 408.44,179.391 408.44,223.38 401.275,223.38 401.275,289.958
|
||||
395.133,289.958 395.133,401.218 332.748,401.218 332.748,253.114 333.825,253.114 333.825,251.402 326.147,251.402
|
||||
326.147,241.111 327.14,241.111 327.14,239.463 320.092,239.463 320.092,233.547 320.725,233.547 320.725,232.282 317.69,232.282
|
||||
317.69,226.693 314.794,226.693 314.343,226.693 314.343,206.742 314.029,206.742 313.63,206.742 313.228,206.742
|
||||
313.228,226.693 311.874,226.693 311.874,215.571 311.56,215.571 311.161,215.571 310.759,215.571 310.759,226.693
|
||||
307.411,226.693 307.411,232.282 303.909,232.282 303.909,233.547 305.009,233.547 305.009,239.463 297.962,239.463
|
||||
297.962,241.111 298.954,241.111 298.954,251.402 291.276,251.402 291.276,253.114 292.354,253.114 292.354,401.218
|
||||
84.29,401.218 84.29,421.417 933.548,421.417 933.548,401.218 "/>
|
||||
</g>
|
||||
|
||||
<linearGradient id="SVGID_00000121963338060960119620000016097684000583641491_" gradientUnits="userSpaceOnUse" x1="499.6613" y1="451.2495" x2="499.6613" y2="202.0752">
|
||||
<stop offset="0.0815" style="stop-color:#403E40"/>
|
||||
<stop offset="0.4715" style="stop-color:#444244"/>
|
||||
<stop offset="0.8768" style="stop-color:#504F50"/>
|
||||
<stop offset="1" style="stop-color:#555455"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#SVGID_00000121963338060960119620000016097684000583641491_);" d="M918.278,419.4v-18.183h-25.56
|
||||
l-9.674-71.571h-1.452v-8.598h-5.082v8.598h-1.259v-3.216h-3.557v3.216h-7.369v-7.496h-2.033v-5.209h-14.483v5.209h-2.033v7.496
|
||||
h-2.033v42.986h-12.874V263.564h-4.235v-23.92h-1.355v23.92h-3.219v-33.181h-1.355v33.181h-2.795v-17.961h-1.355v17.961h-24.985
|
||||
v77.418h-27.78v50.154h-25.944l-20.601-37.972c3.473-2,6.738-4.405,9.735-7.193l4.225,4.508c-0.907,0.793-1.481,1.957-1.481,3.256
|
||||
c0,2.388,1.936,4.324,4.324,4.324c2.388,0,4.324-1.936,4.324-4.324c0-2.388-1.936-4.324-4.324-4.324
|
||||
c-0.916,0-1.764,0.285-2.463,0.771l-4.255-4.54c0.36-0.341,0.717-0.687,1.069-1.039c4.458-4.459,8.028-9.568,10.623-15.114
|
||||
l4.705,2.172c-0.086,0.339-0.131,0.693-0.131,1.059c0,2.388,1.936,4.324,4.324,4.324c2.388,0,4.324-1.936,4.324-4.324
|
||||
c0-2.388-1.936-4.324-4.324-4.324c-1.852,0-3.432,1.165-4.048,2.803l-4.648-2.146c2.866-6.273,4.489-13.092,4.744-20.153
|
||||
l5.065,0.165c0.062,2.334,1.972,4.207,4.321,4.207c2.388,0,4.324-1.936,4.324-4.324c0-2.388-1.936-4.324-4.324-4.324
|
||||
c-2.266,0-4.123,1.743-4.308,3.961l-5.064-0.165c0.013-0.496,0.021-0.993,0.021-1.491c0-6.303-1.088-12.438-3.173-18.192
|
||||
l4.231-1.558c0.664,1.533,2.191,2.606,3.968,2.606c2.388,0,4.324-1.936,4.324-4.324c0-2.388-1.936-4.324-4.324-4.324
|
||||
c-2.388,0-4.324,1.936-4.324,4.324c0,0.44,0.066,0.866,0.189,1.267l-4.229,1.557c-2.41-6.464-6.085-12.437-10.898-17.611
|
||||
l3.31-3.102c0.776,0.741,1.827,1.197,2.985,1.197c2.388,0,4.324-1.935,4.324-4.324c0-2.388-1.936-4.324-4.324-4.324
|
||||
c-2.388,0-4.324,1.936-4.324,4.324c0,1.057,0.38,2.025,1.01,2.776l-3.31,3.102c-0.341-0.36-0.687-0.717-1.039-1.069
|
||||
c-5.012-5.013-10.848-8.903-17.201-11.546l1.821-4.434c0.413,0.131,0.853,0.203,1.309,0.203c2.388,0,4.324-1.936,4.324-4.324
|
||||
c0-2.388-1.936-4.324-4.324-4.324c-2.388,0-4.324,1.936-4.324,4.324c0,1.762,1.054,3.276,2.566,3.95l-1.816,4.423
|
||||
c-5.685-2.304-11.775-3.615-18.057-3.842l0.197-6.06c0.046,0.001,0.091,0.003,0.137,0.003c2.388,0,4.324-1.936,4.324-4.324
|
||||
c0-2.388-1.936-4.324-4.324-4.324c-2.388,0-4.324,1.936-4.324,4.324c0,2.179,1.611,3.98,3.707,4.279l-0.198,6.086
|
||||
c-0.496-0.014-0.993-0.021-1.491-0.021c-6.048,0-11.942,1.001-17.493,2.925l-1.743-4.946c1.573-0.647,2.68-2.193,2.68-4
|
||||
c0-2.388-1.936-4.324-4.324-4.324c-2.388,0-4.324,1.936-4.324,4.324c0,2.388,1.936,4.324,4.324,4.324
|
||||
c0.413,0,0.812-0.059,1.19-0.167l1.744,4.948c-6.732,2.401-12.948,6.166-18.308,11.152l-3.16-3.372
|
||||
c0.707-0.77,1.139-1.796,1.139-2.923c0-2.388-1.936-4.324-4.324-4.324c-2.388,0-4.324,1.935-4.324,4.324
|
||||
c0,2.388,1.936,4.324,4.324,4.324c1.088,0,2.081-0.402,2.841-1.065l3.154,3.366c-0.36,0.341-0.717,0.688-1.069,1.04
|
||||
c-4.458,4.458-8.028,9.568-10.623,15.114l-4.015-1.854c0.146-0.433,0.225-0.896,0.225-1.377c0-2.388-1.936-4.324-4.324-4.324
|
||||
c-2.388,0-4.324,1.936-4.324,4.324c0,2.388,1.936,4.324,4.324,4.324c1.736,0,3.232-1.023,3.92-2.5l3.992,1.843
|
||||
c-2.865,6.273-4.489,13.093-4.744,20.153l-5.154-0.167c-0.064-2.333-1.973-4.204-4.321-4.204c-2.388,0-4.324,1.936-4.324,4.324
|
||||
c0,2.388,1.936,4.324,4.324,4.324c2.267,0,4.125-1.744,4.308-3.963l5.153,0.167c-0.013,0.496-0.021,0.993-0.021,1.491
|
||||
c0,6.302,1.088,12.438,3.173,18.191l-4.231,1.558c-0.664-1.533-2.191-2.606-3.969-2.606c-2.388,0-4.324,1.936-4.324,4.324
|
||||
c0,2.388,1.936,4.324,4.324,4.324c2.388,0,4.324-1.936,4.324-4.324c0-0.44-0.066-0.866-0.189-1.266l4.229-1.557
|
||||
c2.409,6.464,6.084,12.436,10.898,17.611l-3.31,3.102c-0.776-0.741-1.827-1.197-2.985-1.197c-2.388,0-4.324,1.935-4.324,4.324
|
||||
c0,2.388,1.936,4.324,4.324,4.324c2.388,0,4.324-1.936,4.324-4.324c0-1.057-0.38-2.025-1.01-2.776l3.311-3.102
|
||||
c0.341,0.36,0.687,0.717,1.039,1.069c3.376,3.375,7.125,6.242,11.154,8.561l-20.601,37.972h-16.685v-25.743h-6.801v-48.882h2.645
|
||||
v-1.063H564.32v1.063h2.645v24.652h-5.729l-5.467,28.096v-92.143h-2.399v-33.37h-12.075v-12.705h-6.46V220.37h-2.267v-18.294
|
||||
h-0.586v18.294h-2.267v10.672h-6.46v12.705h-11.982v33.37h-2.491V337.7h-18.579v53.436h-61.639V287.814h4.954v-1.626h-4.954v-2.911
|
||||
h-6.279v-4.781h-51.354v4.781h-6.279v2.911h-4.954v1.626h4.954v64.319h-8.4v-12.069h-51.819v14.737h-11.298v27.442h-18.294V292.55
|
||||
h-6.098v-6.099h-4.828v-16.134h-1.017v16.134h-2.287v-20.2h-1.016v20.2h-2.811v6.099h-1.509v89.693h-37.859v-52.597h1.779v-3.557
|
||||
h-11.688v-9.655h-5.59v9.655h-5.082v-9.655h-5.082v9.655h-9.885v-9.655h-30.261v9.655h-7.066v3.557h4.017v19.819h-11.18v44.72
|
||||
h-15.346v-11.942h-17.084v26.044H84.29V419.4H53.82v31.849h891.682V419.4H918.278z M716.559,351.896l-7.135-13.152
|
||||
c2.287-1.349,4.417-2.938,6.354-4.731l10.219,10.906C723.091,347.622,719.925,349.955,716.559,351.896z M689.85,309.7
|
||||
c0.175,0.055,0.357,0.094,0.544,0.116l-1.082,33.274c-4.266-0.165-8.344-1.071-12.11-2.594L689.85,309.7z M676.758,340.314
|
||||
c-1.218-0.512-2.402-1.089-3.548-1.726l15.875-29.261c0.102,0.07,0.209,0.133,0.32,0.19L676.758,340.314z M690.874,309.832
|
||||
c0.203-0.01,0.401-0.041,0.591-0.091l11.067,31.4c-3.701,1.281-7.673,1.978-11.804,1.978c-0.313,0-0.625-0.004-0.936-0.012
|
||||
L690.874,309.832z M691.917,309.581c0.159-0.072,0.311-0.157,0.453-0.254l15.875,29.261c-1.677,0.932-3.435,1.734-5.261,2.394
|
||||
L691.917,309.581z M694.589,311.399l20.697,22.088c-1.893,1.751-3.973,3.303-6.206,4.623L694.589,311.399z M693.683,309.731
|
||||
l-0.598-1.103c0.062-0.086,0.119-0.176,0.172-0.268l30.224,13.952c-1.93,4.091-4.602,7.766-7.844,10.847L693.683,309.731z
|
||||
M693.46,307.925c0.077-0.21,0.129-0.432,0.156-0.662l33.274,1.082c-0.186,4.809-1.313,9.38-3.204,13.534L693.46,307.925z
|
||||
M693.631,306.783c-0.011-0.217-0.046-0.428-0.102-0.63l31.244-11.503c1.388,3.836,2.146,7.971,2.146,12.279
|
||||
c0,0.313-0.004,0.624-0.012,0.936L693.631,306.783z M693.364,305.702c-0.097-0.207-0.217-0.401-0.358-0.579l24.281-22.752
|
||||
c3.15,3.404,5.654,7.411,7.319,11.828L693.364,305.702z M692.678,304.772c-0.188-0.17-0.399-0.315-0.627-0.433l12.647-30.797
|
||||
c4.661,1.958,8.83,4.864,12.261,8.477L692.678,304.772z M691.606,304.157c-0.175-0.055-0.357-0.094-0.544-0.116l1.082-33.273
|
||||
c4.265,0.164,8.344,1.071,12.11,2.594L691.606,304.157z M690.582,304.025c-0.203,0.01-0.401,0.041-0.591,0.09l-11.067-31.4
|
||||
c3.701-1.281,7.672-1.978,11.804-1.978c0.313,0,0.625,0.004,0.936,0.012L690.582,304.025z M689.539,304.276
|
||||
c-0.221,0.099-0.428,0.226-0.616,0.375L666.17,280.37c3.525-3.261,7.697-5.832,12.301-7.494L689.539,304.276z M688.572,304.978
|
||||
c-0.143,0.158-0.268,0.332-0.373,0.518l-30.224-13.953c1.929-4.091,4.602-7.766,7.845-10.847L688.572,304.978z M687.996,305.932
|
||||
c-0.077,0.211-0.129,0.432-0.156,0.662l-33.274-1.082c0.186-4.809,1.313-9.38,3.204-13.534L687.996,305.932z M687.825,307.075
|
||||
c0.011,0.217,0.046,0.427,0.102,0.629l-31.244,11.503c-1.388-3.836-2.146-7.97-2.146-12.279c0-0.313,0.004-0.625,0.012-0.936
|
||||
L687.825,307.075z M688.092,308.155c0.078,0.168,0.171,0.326,0.279,0.474l-0.198,0.365l-24.004,22.492
|
||||
c-3.15-3.404-5.654-7.411-7.319-11.828L688.092,308.155z M687.446,310.332l-15.07,27.777c-2.912-1.72-5.564-3.835-7.88-6.272
|
||||
L687.446,310.332z M672.866,339.221c1.169,0.649,2.376,1.238,3.618,1.759l-5.681,13.833c-1.732-0.721-3.424-1.537-5.07-2.445
|
||||
L672.866,339.221z M676.929,341.163c3.843,1.555,8.006,2.479,12.36,2.647l-0.485,14.92c-6.107-0.221-12.028-1.496-17.555-3.735
|
||||
L676.929,341.163z M689.769,343.828c0.319,0.008,0.638,0.013,0.959,0.013c4.215,0,8.267-0.712,12.043-2.02l4.97,14.101
|
||||
c-5.399,1.87-11.131,2.844-17.013,2.844c-0.482,0-0.964-0.007-1.444-0.021L689.769,343.828z M703.225,341.661
|
||||
c1.862-0.672,3.655-1.49,5.365-2.44l7.133,13.148c-2.417,1.333-4.933,2.468-7.528,3.394L703.225,341.661z M727.382,343.582
|
||||
c-0.341,0.341-0.686,0.676-1.035,1.007l-10.217-10.904c3.31-3.144,6.038-6.895,8.007-11.07l13.579,6.269
|
||||
C735.191,334.277,731.718,339.247,727.382,343.582z M737.917,328.448l-13.578-6.268c1.931-4.239,3.081-8.904,3.27-13.813
|
||||
l14.921,0.485C742.281,315.718,740.702,322.349,737.917,328.448z M742.565,306.929c0,0.482-0.007,0.963-0.02,1.444l-14.917-0.485
|
||||
c0.008-0.319,0.012-0.639,0.012-0.959c0-4.397-0.774-8.615-2.19-12.528l14.03-5.165
|
||||
C741.507,294.832,742.565,300.799,742.565,306.929z M728.718,271.66c4.68,5.032,8.253,10.839,10.596,17.124l-14.032,5.167
|
||||
c-1.699-4.508-4.255-8.598-7.47-12.072L728.718,271.66z M727.382,270.274c0.341,0.341,0.676,0.686,1.007,1.035l-10.904,10.217
|
||||
c-3.502-3.687-7.756-6.653-12.513-8.65l5.681-13.833C716.831,261.615,722.507,265.399,727.382,270.274z M692.652,255.126
|
||||
c6.107,0.221,12.028,1.496,17.555,3.735l-5.681,13.833c-3.843-1.555-8.006-2.479-12.36-2.647L692.652,255.126z M690.728,255.092
|
||||
c0.482,0,0.964,0.007,1.444,0.02l-0.485,14.917c-0.319-0.008-0.638-0.012-0.959-0.012c-4.215,0-8.267,0.712-12.043,2.019
|
||||
l-4.97-14.101C679.114,256.065,684.846,255.092,690.728,255.092z M673.261,258.094l4.97,14.102
|
||||
c-4.698,1.696-8.956,4.319-12.553,7.648l-10.219-10.906C660.671,264.091,666.716,260.43,673.261,258.094z M654.074,270.274
|
||||
c0.341-0.341,0.687-0.676,1.035-1.007l10.218,10.904c-3.31,3.144-6.038,6.895-8.007,11.071l-13.579-6.269
|
||||
C646.265,279.58,649.738,274.61,654.074,270.274z M643.539,285.409l13.578,6.268c-1.931,4.239-3.081,8.905-3.27,13.813
|
||||
l-14.921-0.485C639.175,298.14,640.754,291.509,643.539,285.409z M638.891,306.929c0-0.482,0.007-0.964,0.02-1.444l14.917,0.485
|
||||
c-0.008,0.318-0.012,0.638-0.012,0.959c0,4.396,0.774,8.614,2.19,12.528l-14.03,5.166
|
||||
C639.949,319.025,638.891,313.058,638.891,306.929z M652.738,342.197c-4.68-5.032-8.253-10.839-10.597-17.124l14.032-5.167
|
||||
c1.699,4.508,4.255,8.598,7.47,12.072L652.738,342.197z M654.074,343.582c-0.341-0.341-0.676-0.686-1.007-1.035l10.904-10.217
|
||||
c2.368,2.494,5.082,4.656,8.062,6.414l-7.135,13.152C660.988,349.642,657.35,346.859,654.074,343.582z M665.046,353.636
|
||||
c1.692,0.934,3.431,1.771,5.21,2.512l-1.821,4.434c-0.413-0.131-0.853-0.202-1.309-0.202c-2.388,0-4.324,1.936-4.324,4.324
|
||||
c0,2.388,1.936,4.324,4.324,4.324c2.388,0,4.324-1.936,4.324-4.324c0-1.762-1.054-3.276-2.566-3.95l1.816-4.423
|
||||
c5.685,2.304,11.775,3.615,18.057,3.842l-0.148,4.534c-2.341,0.053-4.224,1.967-4.224,4.321c0,2.388,1.936,4.324,4.324,4.324
|
||||
c2.388,0,4.324-1.936,4.324-4.324c0-2.26-1.734-4.113-3.944-4.306l0.148-4.535c0.496,0.014,0.993,0.021,1.491,0.021
|
||||
c6.048,0,11.942-1.001,17.493-2.925l1.554,4.408c-1.471,0.69-2.491,2.184-2.491,3.916c0,2.388,1.936,4.324,4.324,4.324
|
||||
c2.388,0,4.324-1.936,4.324-4.324c0-2.388-1.936-4.324-4.324-4.324c-0.485,0-0.951,0.081-1.387,0.229l-1.547-4.388
|
||||
c2.667-0.952,5.252-2.117,7.736-3.487l20.345,37.5h-92.055L665.046,353.636z"/>
|
||||
<g>
|
||||
|
||||
<linearGradient id="SVGID_00000121273610027325662480000007068999652675512506_" gradientUnits="userSpaceOnUse" x1="815.83" y1="285.1626" x2="815.83" y2="287.5796">
|
||||
<stop offset="0" style="stop-color:#403E40"/>
|
||||
<stop offset="1" style="stop-color:#161F21"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#SVGID_00000121273610027325662480000007068999652675512506_);" d="M816.844,286.371
|
||||
c0-0.667-0.454-1.208-1.014-1.208c-0.56,0-1.014,0.541-1.014,1.208c0,0.668,0.454,1.208,1.014,1.208
|
||||
C816.39,287.58,816.844,287.039,816.844,286.371z"/>
|
||||
|
||||
<linearGradient id="SVGID_00000011738580747612097720000010840228285618223286_" gradientUnits="userSpaceOnUse" x1="500" y1="287.5796" x2="500" y2="451.2495">
|
||||
<stop offset="0" style="stop-color:#403E40"/>
|
||||
<stop offset="1" style="stop-color:#161F21"/>
|
||||
</linearGradient>
|
||||
<polygon style="fill:url(#SVGID_00000011738580747612097720000010840228285618223286_);" points="927.404,433.241 921.11,391.136
|
||||
908.236,391.136 908.236,356.197 909.828,356.197 909.828,353.036 903.578,353.036 903.578,339.427 902.815,339.427
|
||||
902.815,353.036 898.496,353.036 898.496,341.887 897.734,341.887 897.734,353.036 871.695,353.036 871.695,356.197
|
||||
873.474,356.197 873.474,427.088 843.745,427.088 843.745,395.334 826.815,395.334 826.815,317.303 828.363,317.303
|
||||
828.363,313.475 824.982,313.475 824.982,308.574 821.091,308.574 821.091,306.732 821.091,301.129 821.091,296.267
|
||||
816.395,296.267 816.395,287.58 815.83,287.58 815.265,287.58 815.265,296.267 812.734,296.267 811.204,301.129 800.958,301.129
|
||||
800.958,308.574 774.141,308.574 774.141,313.475 770.76,313.475 770.76,317.303 772.309,317.303 772.309,368.073
|
||||
749.871,368.073 749.871,417.957 724.973,406.925 724.973,358.507 728.991,358.507 728.991,354.95 721.924,354.95
|
||||
721.924,345.294 691.664,345.294 691.664,354.95 681.778,354.95 681.778,345.294 676.696,345.294 676.696,354.95 671.615,354.95
|
||||
671.615,345.294 666.025,345.294 666.025,354.95 654.337,354.95 654.337,358.507 656.115,358.507 656.115,425.617
|
||||
644.765,425.617 644.765,424.913 642.711,424.913 642.711,405.789 644.765,405.789 644.765,404.255 642.711,404.255
|
||||
642.711,385.13 644.765,385.13 644.765,383.597 642.711,383.597 642.711,364.472 644.765,364.472 644.765,362.939
|
||||
642.711,362.939 642.711,343.814 644.765,343.814 644.765,342.28 642.711,342.28 642.711,323.156 644.765,323.156
|
||||
644.765,321.622 642.711,321.622 642.711,301.83 646.231,301.83 646.231,300.291 648.021,300.291 648.021,296.095
|
||||
614.595,296.095 614.595,291.429 615.682,291.429 615.682,290.081 597.484,290.081 597.484,291.429 598.571,291.429
|
||||
598.571,296.095 590.287,296.095 590.287,300.291 592.078,300.291 592.078,301.83 595.598,301.83 595.598,321.622
|
||||
593.543,321.622 593.543,323.156 595.598,323.156 595.598,342.28 593.543,342.28 593.543,343.814 595.598,343.814
|
||||
595.598,362.939 593.543,362.939 593.543,364.472 595.598,364.472 595.598,383.597 593.543,383.597 593.543,385.13
|
||||
595.598,385.13 595.598,404.255 593.543,404.255 593.543,405.789 595.598,405.789 595.598,424.913 593.543,424.913
|
||||
593.543,426.447 595.598,426.447 595.598,442.075 584.189,442.075 584.189,381.685 538.283,381.685 538.283,425.617
|
||||
530.83,425.617 530.83,288.763 525.315,288.763 525.315,292.286 515.14,294.775 515.14,292.487 509.625,292.487 509.625,296.124
|
||||
499.45,298.612 499.45,295.989 493.936,295.989 493.936,299.961 483.76,302.45 483.76,300.286 478.246,300.286 478.246,303.799
|
||||
468.071,306.288 468.071,304.423 462.557,304.423 462.557,438.816 454.799,438.816 454.799,367.168 426.065,367.168
|
||||
426.065,411.227 396.608,411.227 396.608,373.655 392.979,373.655 392.979,361.316 395.133,361.316 395.133,360.077
|
||||
385.601,360.077 385.601,356.197 384.584,356.197 384.584,360.077 381.535,360.077 381.535,340.162 380.773,340.162
|
||||
380.773,360.077 376.802,360.077 376.802,361.316 379.138,361.316 379.138,373.655 359.697,373.655 359.697,402.316
|
||||
340.771,402.316 330.886,438.816 311.053,438.816 311.053,319.642 284.794,319.642 284.794,315.373 286.286,315.373
|
||||
286.286,313.875 266.397,313.875 266.397,315.373 267.961,315.373 267.961,319.642 267.961,326.508 267.961,408.127
|
||||
255.579,408.127 255.579,377.289 259.98,377.289 259.98,374.497 228.825,374.497 228.825,355.867 222.882,355.867
|
||||
222.882,352.039 209.616,352.039 209.616,355.867 198.821,355.867 198.821,422.389 163.408,422.389 163.408,373.052
|
||||
133.589,373.052 133.589,412.021 107.712,412.021 107.712,436.954 89.054,436.954 89.054,405.609 64.517,405.609 64.517,432.333
|
||||
42.301,432.333 42.301,451.25 64.517,451.25 72.025,451.25 84.29,451.25 89.054,451.25 107.712,451.25 133.589,451.25
|
||||
142.285,451.25 146.941,451.25 163.408,451.25 198.821,451.25 209.616,451.25 222.882,451.25 228.825,451.25 252.067,451.25
|
||||
255.579,451.25 267.961,451.25 288.725,451.25 298.954,451.25 311.053,451.25 359.697,451.25 370.931,451.25 389.895,451.25
|
||||
396.608,451.25 426.568,451.25 434.618,451.25 462.557,451.25 465.314,451.25 468.071,451.25 478.246,451.25 483.76,451.25
|
||||
493.936,451.25 498.545,451.25 499.45,451.25 509.625,451.25 515.14,451.25 523.887,451.25 525.315,451.25 528.072,451.25
|
||||
530.83,451.25 538.283,451.25 576.227,451.25 581.647,451.25 584.189,451.25 595.598,451.25 615.682,451.25 634.069,451.25
|
||||
642.711,451.25 656.115,451.25 689.405,451.25 724.606,451.25 724.973,451.25 749.871,451.25 763.792,451.25 772.309,451.25
|
||||
777.689,451.25 819.353,451.25 826.815,451.25 835.736,451.25 843.745,451.25 873.474,451.25 895.58,451.25 900.105,451.25
|
||||
908.236,451.25 957.698,451.25 957.698,433.241 "/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 28 KiB |
64
frontend/assets/cld-server.svg
Normal file
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
|
||||
<title>cld-server</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
|
||||
</defs>
|
||||
<g id="General" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="SLICES-64px" transform="translate(-810.000000, -200.000000)">
|
||||
|
||||
</g>
|
||||
<g id="ICONS" transform="translate(-805.000000, -195.000000)">
|
||||
<g id="cld-server" transform="translate(810.000000, 204.000000)">
|
||||
<path d="M48,12 C51.313,12 54,9.313 54,6 C54,2.687 51.313,0 48,0 L6,0 C2.687,0 0,2.687 0,6 C0,9.313 2.687,12 6,12 L48,12 Z" id="Fill-424" fill="#969CE3">
|
||||
|
||||
</path>
|
||||
<path d="M10,6 C10,7.104 9.104,8 8,8 C6.896,8 6,7.104 6,6 C6,4.896 6.896,4 8,4 C9.104,4 10,4.896 10,6" id="Fill-425" fill="#7BBDEC">
|
||||
|
||||
</path>
|
||||
<path d="M48,30 C51.313,30 54,27.313 54,24 C54,20.687 51.313,18 48,18 L6,18 C2.687,18 0,20.687 0,24 C0,27.313 2.687,30 6,30 L48,30 Z" id="Fill-426" fill="#969CE3">
|
||||
|
||||
</path>
|
||||
<path d="M10,24 C10,25.104 9.104,26 8,26 C6.896,26 6,25.104 6,24 C6,22.896 6.896,22 8,22 C9.104,22 10,22.896 10,24" id="Fill-427" fill="#7BBDEC">
|
||||
|
||||
</path>
|
||||
<path d="M48,48 C51.313,48 54,45.313 54,42 C54,38.687 51.313,36 48,36 L6,36 C2.687,36 0,38.687 0,42 C0,45.313 2.687,48 6,48 L48,48 Z" id="Fill-428" fill="#969CE3">
|
||||
|
||||
</path>
|
||||
<path d="M10,42 C10,43.104 9.104,44 8,44 C6.896,44 6,43.104 6,42 C6,40.896 6.896,40 8,40 C9.104,40 10,40.896 10,42" id="Fill-429" fill="#7BBDEC">
|
||||
|
||||
</path>
|
||||
<path d="M48,12 C51.313,12 54,9.313 54,6 C54,2.687 51.313,0 48,0 L6,0 C2.687,0 0,2.687 0,6 C0,9.313 2.687,12 6,12 L48,12 Z" id="Stroke-430" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M10,6 C10,7.104 9.104,8 8,8 C6.896,8 6,7.104 6,6 C6,4.896 6.896,4 8,4 C9.104,4 10,4.896 10,6 Z" id="Stroke-431" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M48,6 L36,6" id="Stroke-432" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M48,30 C51.313,30 54,27.313 54,24 C54,20.687 51.313,18 48,18 L6,18 C2.687,18 0,20.687 0,24 C0,27.313 2.687,30 6,30 L48,30 Z" id="Stroke-433" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M10,24 C10,25.104 9.104,26 8,26 C6.896,26 6,25.104 6,24 C6,22.896 6.896,22 8,22 C9.104,22 10,22.896 10,24 Z" id="Stroke-434" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M48,24 L36,24" id="Stroke-435" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M48,48 C51.313,48 54,45.313 54,42 C54,38.687 51.313,36 48,36 L6,36 C2.687,36 0,38.687 0,42 C0,45.313 2.687,48 6,48 L48,48 Z" id="Stroke-436" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M10,42 C10,43.104 9.104,44 8,44 C6.896,44 6,43.104 6,42 C6,40.896 6.896,40 8,40 C9.104,40 10,40.896 10,42 Z" id="Stroke-437" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M48,42 L36,42" id="Stroke-438" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.7 KiB |
64
frontend/assets/con-drill.svg
Normal file
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
|
||||
<title>con-drill</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
|
||||
</defs>
|
||||
<g id="General" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="SLICES-64px" transform="translate(-450.000000, -300.000000)">
|
||||
|
||||
</g>
|
||||
<g id="ICONS" transform="translate(-445.000000, -295.000000)">
|
||||
<g id="con-drill" transform="translate(452.000000, 306.000000)">
|
||||
<path d="M4,46 L20,46 C21.104,46 22,45.104 22,44 L22,36 C22,34.896 21.104,34 20,34 L13.375,34 L2,34 L2,44 C2,45.104 2.896,46 4,46" id="Fill-680" fill="#99A5B7">
|
||||
|
||||
</path>
|
||||
<path d="M40,4 L34,4 L34,12 L40,12 C41.104,12 42,11.104 42,10 L42,6 C42,4.896 41.104,4 40,4" id="Fill-681" fill="#E9EFFA">
|
||||
|
||||
</path>
|
||||
<path d="M30,16 C32.209,16 34,14.209 34,12 L34,4 C34,1.791 32.209,0 30,0 L4,0 C1.791,0 0,1.791 0,4 L0,12 C0,14.209 1.791,16 4,16 L30,16 Z" id="Fill-682" fill="#D3D873">
|
||||
|
||||
</path>
|
||||
<path d="M12.71,22 L18,22 C16.354,20.354 17.87,17.918 19,16 L14,16 L12.71,22 Z" id="Fill-683" fill="#F16963">
|
||||
|
||||
</path>
|
||||
<path d="M13.375,34 C11.926,34 10.75,32.824 10.75,31.375 C10.75,31.12 10.786,30.874 10.854,30.641 L14,16 L6,16 L2,34 L13.375,34 Z" id="Fill-684" fill="#AEC14A">
|
||||
|
||||
</path>
|
||||
<path d="M12.71,22 L18,22 C16.354,20.354 17.87,17.918 19,16" id="Stroke-685" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M42,8 L54,8" id="Stroke-686" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M34,4 L40,4 C41.104,4 42,4.896 42,6 L42,10 C42,11.104 41.104,12 40,12 L34,12" id="Stroke-687" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M2,34 L6,16" id="Stroke-688" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M6,8 L14,8" id="Stroke-689" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M6,4 L14,4" id="Stroke-690" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M2,34 L2,44 C2,45.104 2.896,46 4,46 L20,46 C21.104,46 22,45.104 22,44 L22,36 C22,34.896 21.104,34 20,34 L13.375,34" id="Stroke-691" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M13.375,34 C11.926,34 10.75,32.824 10.75,31.375 C10.75,31.12 10.786,30.874 10.854,30.641 L14,16" id="Stroke-692" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M2,34 L14,34" id="Stroke-693" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M30,16 C32.209,16 34,14.209 34,12 L34,4 C34,1.791 32.209,0 30,0 L4,0 C1.791,0 0,1.791 0,4 L0,12 C0,14.209 1.791,16 4,16 L30,16 Z" id="Stroke-694" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.4 KiB |
37
frontend/assets/con-warning.svg
Normal file
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
|
||||
<title>con-warning</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
|
||||
</defs>
|
||||
<g id="General" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="SLICES-64px" transform="translate(-720.000000, -300.000000)">
|
||||
|
||||
</g>
|
||||
<g id="ICONS" transform="translate(-715.000000, -295.000000)">
|
||||
<g id="con-warning" transform="translate(718.000000, 302.000000)">
|
||||
<path d="M50,46 C53.313,46 56,43.313 56,40 C56,38.751 55.358,37.299 55.358,37.299 L32.878,2.51 L32.88,2.509 C31.791,0.99 30.011,1.13686838e-13 28,1.13686838e-13 C25.989,1.13686838e-13 24.209,0.99 23.12,2.509 L23.122,2.51 L0.642,37.299 C0.642,37.299 0,38.751 0,40 C0,43.313 2.687,46 6,46 L50,46 Z" id="Fill-390" fill="#F3E777">
|
||||
|
||||
</path>
|
||||
<path d="M26,36 C26,34.896 26.896,34 28,34 C29.104,34 30,34.896 30,36 C30,37.104 29.104,38 28,38 C26.896,38 26,37.104 26,36" id="Fill-391" fill="#F16963">
|
||||
|
||||
</path>
|
||||
<path d="M32,16 C32,13.791 30.209,12 28,12 C25.791,12 24,13.791 24,16 L26,28 C26,29.104 26.896,30 28,30 C29.104,30 30,29.104 30,28 L32,16 Z" id="Fill-392" fill="#F16963">
|
||||
|
||||
</path>
|
||||
<path d="M26,36 C26,34.896 26.896,34 28,34 C29.104,34 30,34.896 30,36 C30,37.104 29.104,38 28,38 C26.896,38 26,37.104 26,36 Z" id="Stroke-393" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M32,16 C32,13.791 30.209,12 28,12 C25.791,12 24,13.791 24,16 L26,28 C26,29.104 26.896,30 28,30 C29.104,30 30,29.104 30,28 L32,16 Z" id="Stroke-394" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M50,46 C53.313,46 56,43.313 56,40 C56,38.751 55.358,37.299 55.358,37.299 L32.878,2.51 L32.88,2.509 C31.791,0.99 30.011,1.13686838e-13 28,1.13686838e-13 C25.989,1.13686838e-13 24.209,0.99 23.12,2.509 L23.122,2.51 L0.642,37.299 C0.642,37.299 0,38.751 0,40 C0,43.313 2.687,46 6,46 L50,46 Z" id="Stroke-395" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
76
frontend/assets/gen-lifebelt.svg
Normal file
@@ -0,0 +1,76 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
|
||||
<title>gen-lifebelt</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
|
||||
</defs>
|
||||
<g id="General" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="SLICES-64px">
|
||||
|
||||
</g>
|
||||
<g id="ICONS" transform="translate(5.000000, 5.000000)">
|
||||
<g id="gen-lifebelt" transform="translate(0.000000, 2.000000)">
|
||||
<path d="M26.0001,40 C18.2681,40 12.0001,33.732 12.0001,26 C12.0001,18.267 18.2681,12 26.0001,12 C33.7321,12 40.0001,18.267 40.0001,26 C40.0001,33.732 33.7321,40 26.0001,40 M26.0001,0 C11.6411,0 0.0001,11.64 0.0001,26 C0.0001,40.359 11.6411,52 26.0001,52 C40.3591,52 52.0001,40.359 52.0001,26 C52.0001,11.64 40.3591,0 26.0001,0" id="Fill-464" fill="#F16963">
|
||||
|
||||
</path>
|
||||
<path d="M3.0025,13.8716 L13.6385,19.4216 L13.6485,19.4116 C14.9905,16.9016 17.0765,14.8566 19.6105,13.5526 L19.6385,13.5256 L13.8725,3.0026 C9.2455,5.4476 5.4475,9.2456 3.0025,13.8716" id="Fill-465" fill="#F1F0E2">
|
||||
|
||||
</path>
|
||||
<path d="M38.128,3.0022 L32.361,13.5252 L32.39,13.5532 C34.923,14.8562 37.01,16.9012 38.352,19.4122 L38.361,19.4212 L48.998,13.8712 C46.553,9.2452 42.754,5.4472 38.128,3.0022" id="Fill-466" fill="#F1F0E2">
|
||||
|
||||
</path>
|
||||
<path d="M13.648,32.5872 L13.639,32.5782 L3.002,38.1282 C5.447,42.7542 9.246,46.5532 13.872,48.9972 L19.639,38.4742 L19.611,38.4472 C17.077,37.1442 14.99,35.0982 13.648,32.5872" id="Fill-467" fill="#F1F0E2">
|
||||
|
||||
</path>
|
||||
<path d="M48.9976,38.1284 L38.3616,32.5774 L38.3516,32.5864 C37.0106,35.0974 34.9236,37.1434 32.3896,38.4474 L32.3616,38.4744 L38.1276,48.9974 C42.7546,46.5524 46.5526,42.7544 48.9976,38.1284" id="Fill-468" fill="#F1F0E2">
|
||||
|
||||
</path>
|
||||
<path d="M2.9971,13.8689 C2.3621,12.7229 2.0001,11.4029 2.0001,9.9999 C2.0001,5.5819 5.5821,1.9999 10.0001,1.9999 C11.4031,1.9999 12.7231,2.3619 13.8691,2.9969" id="Stroke-469" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M49.003,13.8689 C49.638,12.7229 50,11.4029 50,9.9999 C50,5.5819 46.418,1.9999 42,1.9999 C40.597,1.9999 39.277,2.3619 38.131,2.9969" id="Stroke-470" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M2.9971,38.1311 C2.3621,39.2771 2.0001,40.5961 2.0001,42.0001 C2.0001,46.4171 5.5821,50.0001 10.0001,50.0001 C11.4031,50.0001 12.7231,49.6381 13.8691,49.0031" id="Stroke-471" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M49.003,38.1311 C49.638,39.2771 50,40.5961 50,42.0001 C50,46.4171 46.418,50.0001 42,50.0001 C40.597,50.0001 39.277,49.6381 38.131,49.0031" id="Stroke-472" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M52.0001,26 C52.0001,11.64 40.3591,0 26.0001,0 C11.6411,0 0.0001,11.64 0.0001,26 C0.0001,40.359 11.6411,52 26.0001,52 C40.3591,52 52.0001,40.359 52.0001,26 Z" id="Stroke-473" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M40.0001,26 C40.0001,33.732 33.7321,40 26.0001,40 C18.2681,40 12.0001,33.732 12.0001,26 C12.0001,18.267 18.2681,12 26.0001,12 C33.7321,12 40.0001,18.267 40.0001,26 Z" id="Stroke-474" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M13.8692,2.9968 L19.6392,13.5248" id="Stroke-475" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M13.6387,19.4216 L2.9967,13.8686" id="Stroke-476" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M38.1309,2.9968 L32.3609,13.5248" id="Stroke-477" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M38.3614,19.4216 L49.0034,13.8686" id="Stroke-478" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M13.8692,49.0027 L19.6392,38.4747" id="Stroke-479" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M13.6387,32.5779 L2.9967,38.1309" id="Stroke-480" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M38.1309,49.0027 L32.3609,38.4747" id="Stroke-481" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
<path d="M38.3614,32.5779 L49.0034,38.1309" id="Stroke-482" stroke="#414547" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
</path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 5.0 KiB |
@@ -1,161 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 27.5.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 2200 2200" style="enable-background:new 0 0 2200 2200;" xml:space="preserve">
|
||||
|
||||
<g id="Objects">
|
||||
<g>
|
||||
<path style="fill:#788D8E;" d="M1202.178,2002.073c-5.328,0-9.648-4.319-9.649-9.647c-0.001-5.328,4.319-9.649,9.647-9.649
|
||||
c9.63-0.001,19.271-0.006,28.918-0.014c0.003,0,0.006,0,0.009,0c5.325,0,9.643,4.314,9.647,9.639
|
||||
c0.005,5.328-4.311,9.651-9.639,9.656C1221.458,2002.068,1211.813,2002.072,1202.178,2002.073z M1144.298,2002.03
|
||||
c-0.006,0-0.01,0-0.016,0c-9.658-0.015-19.305-0.036-28.94-0.061c-5.328-0.014-9.636-4.345-9.622-9.673
|
||||
c0.014-5.319,4.331-9.622,9.648-9.622c0.008,0,0.017,0,0.025,0c9.628,0.025,19.269,0.046,28.919,0.061
|
||||
c5.328,0.009,9.641,4.335,9.632,9.663C1153.937,1997.721,1149.619,2002.03,1144.298,2002.03z M1288.979,2001.966
|
||||
c-5.317,0-9.634-4.306-9.647-9.626c-0.012-5.328,4.298-9.657,9.626-9.669c9.634-0.022,19.274-0.047,28.923-0.075
|
||||
c5.297,0.018,9.66,4.292,9.676,9.619c0.015,5.328-4.291,9.66-9.619,9.676c-9.652,0.028-19.299,0.054-28.936,0.075
|
||||
C1288.994,2001.966,1288.986,2001.966,1288.979,2001.966z M1057.498,2001.759c-0.015,0-0.03,0-0.045,0
|
||||
c-9.659-0.044-19.306-0.095-28.939-0.152c-5.328-0.031-9.622-4.376-9.591-9.704c0.031-5.309,4.344-9.591,9.646-9.591
|
||||
c0.02,0,0.04,0,0.058,0c9.625,0.057,19.263,0.107,28.914,0.152c5.328,0.025,9.628,4.364,9.603,9.692
|
||||
C1067.12,1997.468,1062.805,2001.759,1057.498,2001.759z M1375.787,2001.691c-5.31,0-9.625-4.293-9.647-9.609
|
||||
c-0.022-5.328,4.281-9.665,9.609-9.686c9.636-0.039,19.28-0.079,28.928-0.123c0.015,0,0.029,0,0.044,0
|
||||
c5.308,0,9.623,4.291,9.647,9.604c0.024,5.328-4.276,9.666-9.604,9.691c-9.651,0.043-19.297,0.084-28.937,0.122
|
||||
C1375.813,2001.691,1375.8,2001.691,1375.787,2001.691z M1462.601,2001.29c-5.305,0-9.619-4.287-9.647-9.598
|
||||
c-0.027-5.328,4.27-9.67,9.598-9.698c9.641-0.05,19.285-0.102,28.932-0.156c0.018,0,0.037,0,0.056,0
|
||||
c5.302,0,9.616,4.284,9.646,9.594c0.029,5.328-4.266,9.671-9.594,9.701c-9.649,0.054-19.297,0.105-28.94,0.156
|
||||
C1462.635,2001.29,1462.618,2001.29,1462.601,2001.29z M970.707,2001.198c-0.027,0-0.054,0-0.081,0
|
||||
c-9.662-0.079-19.308-0.166-28.939-0.259c-5.328-0.052-9.605-4.413-9.554-9.741c0.052-5.296,4.361-9.554,9.646-9.554
|
||||
c0.032,0,0.063,0,0.095,0.001c9.621,0.093,19.258,0.179,28.911,0.258c5.328,0.044,9.612,4.399,9.568,9.727
|
||||
C980.31,1996.93,975.998,2001.198,970.707,2001.198z M1549.42,2000.802c-5.301,0-9.614-4.281-9.646-9.59
|
||||
c-0.032-5.328,4.262-9.673,9.59-9.705l28.936-0.176c0.021,0,0.042,0,0.061,0c5.301,0,9.614,4.279,9.647,9.587
|
||||
c0.033,5.328-4.259,9.674-9.587,9.708l-28.942,0.176C1549.459,2000.802,1549.439,2000.802,1549.42,2000.802z M883.923,2000.297
|
||||
c-0.041,0-0.083,0-0.124-0.001c-9.663-0.122-19.308-0.25-28.935-0.385c-5.328-0.075-9.586-4.454-9.511-9.782
|
||||
c0.075-5.328,4.464-9.604,9.782-9.511c9.617,0.136,19.254,0.264,28.907,0.385c5.328,0.067,9.592,4.44,9.525,9.768
|
||||
C893.501,1996.057,889.195,2000.297,883.923,2000.297z M1636.244,2000.264c-5.3,0-9.612-4.279-9.646-9.586
|
||||
c-0.034-5.328,4.258-9.675,9.586-9.709l28.943-0.184c0.021,0,0.042,0,0.062,0c5.3,0,9.613,4.279,9.647,9.586
|
||||
c0.034,5.328-4.259,9.675-9.586,9.709l-28.943,0.184C1636.286,2000.264,1636.265,2000.264,1636.244,2000.264z M1723.075,1999.719
|
||||
c-5.3,0-9.614-4.28-9.647-9.588c-0.033-5.328,4.26-9.674,9.588-9.707l28.946-0.177c0.02,0,0.04,0,0.059,0
|
||||
c5.301,0,9.615,4.28,9.647,9.589c0.032,5.328-4.261,9.674-9.589,9.706l-28.945,0.177
|
||||
C1723.115,1999.719,1723.095,1999.719,1723.075,1999.719z M1809.912,1999.201c-5.303,0-9.616-4.283-9.647-9.593
|
||||
c-0.03-5.328,4.265-9.672,9.593-9.702l28.952-0.16c0.018,0,0.036,0,0.053,0c5.304,0,9.618,4.285,9.647,9.596
|
||||
c0.028,5.328-4.268,9.67-9.596,9.699l-28.947,0.16C1809.949,1999.201,1809.931,1999.201,1809.912,1999.201z M797.15,1998.997
|
||||
c-0.057,0-0.115-0.001-0.172-0.002c-9.665-0.169-19.31-0.346-28.935-0.531c-5.327-0.103-9.562-4.504-9.459-9.832
|
||||
c0.104-5.327,4.48-9.533,9.832-9.459c9.612,0.186,19.245,0.363,28.899,0.531c5.327,0.092,9.57,4.487,9.477,9.814
|
||||
C806.701,1994.788,802.399,1998.997,797.15,1998.997z M1896.756,1998.75c-5.307,0-9.621-4.29-9.647-9.602
|
||||
c-0.025-5.328,4.274-9.667,9.602-9.693c9.658-0.045,19.31-0.089,28.958-0.129c0.014,0,0.027,0,0.041,0
|
||||
c5.309,0,9.624,4.292,9.647,9.607c0.023,5.328-4.279,9.666-9.607,9.688c-9.644,0.041-19.294,0.084-28.948,0.129
|
||||
C1896.787,1998.75,1896.771,1998.75,1896.756,1998.75z M710.388,1997.237c-0.076,0-0.152-0.001-0.228-0.003
|
||||
c-9.667-0.223-19.311-0.457-28.932-0.7c-5.326-0.135-9.535-4.562-9.401-9.889c0.133-5.243,4.425-9.404,9.64-9.404
|
||||
c0.083,0,0.166,0.001,0.249,0.003c9.607,0.243,19.237,0.477,28.891,0.7c5.327,0.122,9.545,4.541,9.421,9.868
|
||||
C719.907,1993.064,715.612,1997.237,710.388,1997.237z M623.642,1994.948c-0.096,0-0.193-0.001-0.29-0.004
|
||||
c-9.67-0.287-19.314-0.585-28.929-0.894c-5.326-0.172-9.503-4.628-9.333-9.953c0.171-5.325,4.65-9.465,9.953-9.333
|
||||
c9.6,0.309,19.227,0.607,28.88,0.892c5.326,0.158,9.516,4.604,9.357,9.93C633.126,1990.815,628.838,1994.948,623.642,1994.948z
|
||||
M536.937,1991.683c-0.226,0-0.453-0.007-0.682-0.024c-10.955-0.765-20.624-1.8-29.559-3.167
|
||||
c-5.267-0.806-8.883-5.728-8.078-10.995c0.805-5.268,5.725-8.888,10.995-8.078c8.408,1.286,17.563,2.264,27.985,2.992
|
||||
c5.316,0.371,9.323,4.981,8.952,10.296C546.196,1987.794,541.959,1991.683,536.937,1991.683z M452.936,1972.044
|
||||
c-1.444,0-2.91-0.325-4.291-1.012c-5.129-2.553-10.134-5.398-14.875-8.458c-3.821-2.466-7.597-5.17-11.224-8.036
|
||||
c-4.179-3.305-4.889-9.372-1.585-13.552c3.305-4.18,9.371-4.889,13.552-1.585c3.144,2.486,6.415,4.828,9.719,6.961
|
||||
c4.142,2.673,8.519,5.162,13.01,7.397c4.77,2.373,6.713,8.165,4.34,12.935C459.893,1970.083,456.481,1972.044,452.936,1972.044z
|
||||
M390.843,1913.094c-3.257,0-6.436-1.65-8.252-4.636c-5.088-8.366-9.753-17.406-13.866-26.869
|
||||
c-2.125-4.886,0.115-10.57,5.002-12.693c4.884-2.125,10.569,0.114,12.694,5.002c3.765,8.661,8.023,16.915,12.657,24.534
|
||||
c2.768,4.552,1.323,10.487-3.23,13.256C394.28,1912.639,392.55,1913.094,390.843,1913.094z M360.798,1832.103
|
||||
c-4.545,0-8.593-3.227-9.469-7.857c-1.832-9.681-3.208-19.642-4.092-29.605c-0.471-5.307,3.45-9.991,8.757-10.463
|
||||
c5.305-0.466,9.991,3.449,10.462,8.757c0.828,9.335,2.117,18.663,3.831,27.724c0.99,5.236-2.451,10.282-7.686,11.272
|
||||
C361.996,1832.047,361.393,1832.103,360.798,1832.103z M357.3,1745.636c-0.337,0-0.677-0.018-1.02-0.054
|
||||
c-5.299-0.557-9.143-5.304-8.586-10.603c1.051-10.004,2.612-19.95,4.636-29.561c1.098-5.214,6.217-8.55,11.429-7.451
|
||||
c5.214,1.099,8.55,6.215,7.451,11.429c-1.889,8.965-3.345,18.252-4.327,27.6C366.362,1741.952,362.175,1745.636,357.3,1745.636z
|
||||
M379.592,1662.099c-1.292,0-2.604-0.261-3.863-0.812c-4.881-2.136-7.107-7.824-4.97-12.706
|
||||
c3.967-9.067,8.429-18.064,13.26-26.742c2.591-4.655,8.466-6.329,13.122-3.737c4.656,2.592,6.329,8.466,3.737,13.122
|
||||
c-4.533,8.143-8.72,16.585-12.442,25.091C386.85,1659.939,383.308,1662.099,379.592,1662.099z M425.149,1588.527
|
||||
c-2.174,0-4.36-0.73-6.162-2.228c-4.097-3.407-4.658-9.489-1.252-13.587c6.291-7.567,13.033-14.977,20.039-22.024
|
||||
c3.756-3.78,9.865-3.796,13.644-0.039c3.778,3.756,3.796,9.865,0.039,13.643c-6.604,6.642-12.958,13.624-18.884,20.754
|
||||
C430.666,1587.342,427.917,1588.527,425.149,1588.527z M488.757,1529.81c-3.021,0-5.994-1.414-7.875-4.063
|
||||
c-3.085-4.345-2.062-10.367,2.282-13.452c8.036-5.705,16.424-11.148,24.935-16.182c4.587-2.711,10.502-1.194,13.215,3.393
|
||||
c2.712,4.586,1.194,10.503-3.393,13.215c-8.051,4.762-15.987,9.912-23.588,15.308
|
||||
C492.639,1529.232,490.688,1529.81,488.757,1529.81z M565.026,1488.849c-3.858,0-7.5-2.33-8.989-6.14
|
||||
c-1.939-4.962,0.513-10.558,5.476-12.497c8.996-3.515,18.386-6.814,27.91-9.805c5.085-1.593,10.499,1.23,12.094,6.314
|
||||
c1.597,5.083-1.229,10.498-6.313,12.095c-9.107,2.86-18.081,6.012-26.67,9.368
|
||||
C567.381,1488.636,566.194,1488.849,565.026,1488.849z M648.419,1465.236c-4.529,0-8.569-3.204-9.462-7.816
|
||||
c-1.012-5.232,2.408-10.293,7.639-11.306c9.243-1.788,18.981-3.446,28.945-4.926c5.268-0.786,10.177,2.855,10.961,8.125
|
||||
c0.783,5.27-2.855,10.178-8.125,10.961c-9.686,1.439-19.145,3.049-28.114,4.784
|
||||
C649.644,1465.178,649.027,1465.236,648.419,1465.236z M734.448,1453.776c-4.949,0-9.161-3.786-9.599-8.809
|
||||
c-0.464-5.308,3.463-9.987,8.771-10.45c8.927-0.779,18.419-1.521,29.019-2.265c5.285-0.369,9.926,3.633,10.3,8.948
|
||||
c0.373,5.315-3.633,9.926-8.948,10.299c-10.489,0.737-19.875,1.469-28.691,2.239
|
||||
C735.013,1453.764,734.729,1453.776,734.448,1453.776z M821.074,1447.866c-5.059,0-9.308-3.941-9.621-9.059
|
||||
c-0.325-5.318,3.722-9.893,9.041-10.219l28.889-1.767c5.341-0.318,9.893,3.723,10.219,9.041c0.325,5.318-3.722,9.893-9.041,10.219
|
||||
l-28.889,1.766C821.471,1447.86,821.272,1447.866,821.074,1447.866z M907.74,1442.568c-5.059,0-9.308-3.941-9.621-9.059
|
||||
c-0.325-5.318,3.722-9.893,9.041-10.219l28.889-1.766c5.344-0.326,9.893,3.723,10.219,9.041c0.325,5.318-3.722,9.893-9.041,10.218
|
||||
l-28.889,1.767C908.137,1442.562,907.938,1442.568,907.74,1442.568z M994.406,1437.269c-5.059,0-9.307-3.941-9.62-9.059
|
||||
c-0.325-5.318,3.722-9.893,9.041-10.219l28.889-1.766c5.345-0.309,9.893,3.722,10.219,9.041c0.325,5.318-3.722,9.893-9.041,10.219
|
||||
l-28.889,1.767C994.805,1437.263,994.604,1437.269,994.406,1437.269z M1081.072,1431.969c-5.059,0-9.307-3.941-9.62-9.059
|
||||
c-0.325-5.318,3.722-9.893,9.041-10.219l28.889-1.766c5.322-0.318,9.894,3.723,10.219,9.041c0.325,5.318-3.722,9.893-9.041,10.219
|
||||
l-28.889,1.767C1081.47,1431.964,1081.27,1431.969,1081.072,1431.969z M1167.738,1426.671c-5.059,0-9.307-3.941-9.62-9.059
|
||||
c-0.325-5.318,3.722-9.893,9.041-10.219l28.889-1.767c5.349-0.326,9.894,3.723,10.219,9.041c0.325,5.318-3.722,9.893-9.041,10.219
|
||||
l-28.889,1.766C1168.135,1426.665,1167.936,1426.671,1167.738,1426.671z M1254.358,1420.693c-4.914,0-9.115-3.738-9.592-8.73
|
||||
c-0.508-5.304,3.38-10.015,8.685-10.523c10.205-0.975,19.481-2.043,28.357-3.264c5.276-0.72,10.146,2.966,10.872,8.244
|
||||
c0.725,5.279-2.966,10.146-8.244,10.872c-9.142,1.257-18.676,2.354-29.148,3.356
|
||||
C1254.976,1420.678,1254.665,1420.693,1254.358,1420.693z M1339.835,1406.05c-4.233,0-8.115-2.807-9.295-7.086
|
||||
c-1.416-5.136,1.6-10.448,6.735-11.864c9.242-2.549,18.223-5.456,26.694-8.639c4.994-1.875,10.551,0.65,12.425,5.636
|
||||
c1.875,4.988-0.649,10.55-5.636,12.425c-9.014,3.388-18.553,6.476-28.353,9.178
|
||||
C1341.548,1405.937,1340.684,1406.05,1339.835,1406.05z M1418.99,1371.212c-3.131,0-6.201-1.522-8.057-4.329
|
||||
c-2.938-4.445-1.716-10.43,2.729-13.368c7.542-4.985,14.948-10.622,22.014-16.754c4.022-3.493,10.115-3.063,13.609,0.962
|
||||
c3.493,4.024,3.062,10.117-0.962,13.609c-7.698,6.683-15.781,12.832-24.022,18.28
|
||||
C1422.663,1370.694,1420.817,1371.212,1418.99,1371.212z M1480.743,1310.868c-1.931,0-3.882-0.578-5.577-1.782
|
||||
c-4.344-3.086-5.366-9.108-2.281-13.452c5.366-7.558,10.318-15.487,14.721-23.567c2.549-4.679,8.41-6.406,13.087-3.856
|
||||
c4.679,2.549,6.406,8.409,3.856,13.087c-4.765,8.747-10.126,17.328-15.932,25.506
|
||||
C1486.737,1309.454,1483.762,1310.868,1480.743,1310.868z M1517.216,1232.602c-0.778,0-1.568-0.094-2.356-0.292
|
||||
c-5.169-1.297-8.306-6.538-7.009-11.707c2.256-8.984,3.898-18.121,4.878-27.155c0.575-5.299,5.342-9.13,10.632-8.55
|
||||
c5.298,0.575,9.126,5.335,8.55,10.633c-1.076,9.911-2.875,19.928-5.346,29.771
|
||||
C1525.467,1229.681,1521.535,1232.602,1517.216,1232.602z M1520.169,1146.498c-4.554,0-8.606-3.239-9.472-7.879
|
||||
c-1.672-8.957-4.032-17.905-7.015-26.593c-1.731-5.04,0.953-10.527,5.992-12.257c5.036-1.729,10.528,0.953,12.257,5.992
|
||||
c3.287,9.575,5.889,19.438,7.733,29.318c0.978,5.237-2.475,10.276-7.713,11.254
|
||||
C1521.352,1146.444,1520.757,1146.498,1520.169,1146.498z M1486.631,1067.128c-3.053,0-6.055-1.445-7.93-4.142
|
||||
c-5.223-7.514-11.025-14.791-17.243-21.63c-3.585-3.942-3.295-10.044,0.647-13.629c3.942-3.584,10.044-3.295,13.628,0.648
|
||||
c6.781,7.457,13.11,15.396,18.811,23.597c3.041,4.375,1.961,10.387-2.415,13.428
|
||||
C1490.452,1066.568,1488.532,1067.128,1486.631,1067.128z M1424.593,1007.152c-1.794,0-3.608-0.499-5.227-1.546
|
||||
c-7.728-4.995-15.75-9.441-23.845-13.217l-0.314-0.146c-4.827-2.257-6.911-7.999-4.653-12.826c2.257-4.826,8-6.91,12.825-4.653
|
||||
l0.276,0.129c8.915,4.158,17.716,9.036,26.183,14.508c4.475,2.892,5.758,8.864,2.866,13.339
|
||||
C1430.859,1005.596,1427.759,1007.152,1424.593,1007.152z M1344.533,974.803c-0.7,0-1.41-0.076-2.123-0.236
|
||||
c-8.759-1.966-18.077-3.643-27.693-4.985c-5.277-0.736-8.958-5.611-8.222-10.888c0.737-5.277,5.615-8.957,10.888-8.222
|
||||
c10.137,1.415,19.98,3.187,29.254,5.268c5.199,1.167,8.467,6.327,7.3,11.527C1352.93,971.754,1348.947,974.803,1344.533,974.803z
|
||||
M274.324,966.773c-5.318,0-9.634-4.305-9.647-9.625c-0.012-5.328,4.297-9.657,9.625-9.67l28.943-0.066c0.008,0,0.016,0,0.023,0
|
||||
c5.317,0,9.634,4.305,9.647,9.625c0.012,5.328-4.297,9.657-9.625,9.67l-28.943,0.066
|
||||
C274.339,966.773,274.331,966.773,274.324,966.773z M361.152,966.573c-5.318,0-9.635-4.305-9.647-9.625
|
||||
c-0.012-5.328,4.297-9.657,9.625-9.67l28.943-0.066c5.338-0.002,9.657,4.297,9.67,9.625s-4.297,9.657-9.625,9.67l-28.943,0.066
|
||||
C361.167,966.573,361.16,966.573,361.152,966.573z M447.979,966.373c-5.318,0-9.634-4.305-9.647-9.625
|
||||
c-0.012-5.328,4.297-9.657,9.625-9.67l28.942-0.066c0.008,0,0.015,0,0.023,0c5.318,0,9.635,4.305,9.648,9.625
|
||||
c0.012,5.328-4.297,9.657-9.626,9.67l-28.942,0.066C447.995,966.373,447.987,966.373,447.979,966.373z M534.808,966.174
|
||||
c-5.318,0-9.635-4.305-9.648-9.625c-0.012-5.328,4.297-9.657,9.625-9.67l28.943-0.066c0.008,0,0.015,0,0.023,0
|
||||
c5.318,0,9.635,4.305,9.648,9.625c0.012,5.328-4.297,9.657-9.626,9.67l-28.943,0.066
|
||||
C534.823,966.174,534.815,966.174,534.808,966.174z M621.636,965.973c-5.318,0-9.635-4.305-9.648-9.625
|
||||
c-0.012-5.328,4.297-9.657,9.625-9.67l28.943-0.066c0.008,0,0.015,0,0.023,0c5.318,0,9.635,4.305,9.648,9.625
|
||||
c0.012,5.328-4.297,9.657-9.626,9.67l-28.943,0.066C621.651,965.973,621.643,965.973,621.636,965.973z M708.463,965.774
|
||||
c-5.318,0-9.635-4.305-9.648-9.625c-0.012-5.328,4.297-9.657,9.625-9.67l28.943-0.066c0.008,0,0.015,0,0.023,0
|
||||
c5.318,0,9.635,4.305,9.648,9.625c0.012,5.328-4.297,9.657-9.626,9.67l-28.943,0.066
|
||||
C708.478,965.774,708.471,965.774,708.463,965.774z M795.291,965.574c-5.318,0-9.635-4.305-9.648-9.625
|
||||
c-0.012-5.328,4.297-9.657,9.625-9.67l28.943-0.066c5.292,0.018,9.658,4.297,9.67,9.625c0.012,5.328-4.297,9.657-9.626,9.67
|
||||
l-28.943,0.066C795.306,965.574,795.299,965.574,795.291,965.574z M882.12,965.374c-5.318,0-9.635-4.305-9.648-9.625
|
||||
c-0.012-5.328,4.297-9.657,9.625-9.67l28.943-0.066c0.008,0,0.016,0,0.024,0c5.317,0,9.634,4.305,9.647,9.625
|
||||
c0.012,5.328-4.297,9.657-9.626,9.67l-28.943,0.066C882.135,965.374,882.127,965.374,882.12,965.374z M968.947,965.174
|
||||
c-5.318,0-9.635-4.305-9.648-9.625c-0.012-5.328,4.297-9.657,9.625-9.67l28.943-0.066c0.008,0,0.015,0,0.023,0
|
||||
c5.318,0,9.635,4.305,9.648,9.625c0.012,5.328-4.297,9.657-9.626,9.67l-28.943,0.066
|
||||
C968.962,965.174,968.955,965.174,968.947,965.174z M1258.376,965.13c-0.103,0-0.204-0.001-0.307-0.005
|
||||
c-8.629-0.27-17.745-0.424-28.69-0.483c-5.328-0.029-9.623-4.372-9.595-9.7c0.029-5.31,4.342-9.595,9.647-9.595
|
||||
c0.018,0,0.036,0,0.054,0c11.114,0.06,20.389,0.216,29.188,0.492c5.326,0.167,9.508,4.619,9.341,9.945
|
||||
C1267.849,961.007,1263.564,965.13,1258.376,965.13z M1055.775,964.974c-5.318,0-9.635-4.305-9.648-9.625
|
||||
c-0.012-5.328,4.297-9.657,9.625-9.67l28.943-0.066c0.008,0,0.016,0,0.024,0c5.318,0,9.634,4.305,9.647,9.625
|
||||
c0.012,5.328-4.297,9.657-9.626,9.67l-28.943,0.066C1055.79,964.974,1055.783,964.974,1055.775,964.974z M1142.603,964.775
|
||||
c-5.318,0-9.635-4.305-9.648-9.625c-0.012-5.328,4.297-9.657,9.625-9.67l28.943-0.066c0.007,0,0.015,0,0.023,0
|
||||
c5.318,0,9.635,4.305,9.648,9.625c0.012,5.328-4.297,9.657-9.626,9.67l-28.943,0.066
|
||||
C1142.619,964.775,1142.611,964.775,1142.603,964.775z"/>
|
||||
<path style="fill:#D13737;" d="M462.452,197.927c-156.647,0-283.638,126.991-283.638,283.638
|
||||
c0,251.801,283.638,464.048,283.638,464.048S746.09,733.366,746.09,481.565C746.09,324.918,619.099,197.927,462.452,197.927z
|
||||
M462.316,679.485c-109.374,0-198.045-88.671-198.045-198.055c0-109.374,88.671-198.045,198.045-198.045
|
||||
c109.384,0,198.055,88.671,198.055,198.045C660.371,590.814,571.701,679.485,462.316,679.485z"/>
|
||||
<path style="fill:#18ACB7;" d="M1737.548,1228.212c-156.647,0-283.638,126.991-283.638,283.638
|
||||
c0,251.801,283.638,464.048,283.638,464.048s283.638-212.246,283.638-464.048
|
||||
C2021.187,1355.203,1894.196,1228.212,1737.548,1228.212z M1737.413,1709.77c-109.374,0-198.045-88.671-198.045-198.055
|
||||
c0-109.374,88.671-198.045,198.045-198.045c109.384,0,198.055,88.671,198.055,198.045
|
||||
C1935.468,1621.1,1846.797,1709.77,1737.413,1709.77z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 16 KiB |
126
frontend/assets/terms_and_conditions.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# Terms and Conditions
|
||||
|
||||
> Last updated: January 09, 2025
|
||||
>
|
||||
> Also see: https://anydev.info/terms-and-conditions/
|
||||
|
||||
Please read these terms and conditions carefully before using our Service.
|
||||
|
||||
## Interpretation and Definitions
|
||||
### Interpretation
|
||||
|
||||
The words of which the initial letter is capitalized have meanings defined under the following conditions. The following definitions shall have the same meaning regardless of whether they appear in singular or in plural.
|
||||
|
||||
### Definitions
|
||||
|
||||
For the purposes of these Terms and Conditions:
|
||||
- **Application** means the software program provided by the Company downloaded by You on any electronic device, named AnyWay
|
||||
- **Application Store** means the digital distribution service operated and developed by Apple Inc. (Apple App Store) or Google Inc. (Google Play Store) in which the Application has been downloaded.
|
||||
- **Affiliate** means an entity that controls, is controlled by or is under common control with a party, where "control" means ownership of 50% or more of the shares, equity interest or other securities entitled to vote for election of directors or other managing authority.
|
||||
- **Country** refers to: Switzerland
|
||||
- **Company** (referred to as either "the Company", "We", "Us" or "Our" in this Agreement) refers to AnyDev.
|
||||
- **Device** means any device that can access the Service such as a computer, a cellphone or a digital tablet.
|
||||
- **Service** refers to the Application.
|
||||
- **Terms and Conditions** (also referred as "Terms") mean these Terms and Conditions that form the entire agreement between You and the Company regarding the use of the Service. This Terms and Conditions agreement has been created with the help of the Terms and Conditions Generator.
|
||||
- **Third-party Social Media Service** means any services or content (including data, information, products or services) provided by a third-party that may be displayed, included or made available by the Service.
|
||||
- **You** means the individual accessing or using the Service, or the company, or other legal entity on behalf of which such individual is accessing or using the Service, as applicable.
|
||||
|
||||
## Acknowledgment
|
||||
|
||||
These are the Terms and Conditions governing the use of this Service and the agreement that operates between You and the Company. These Terms and Conditions set out the rights and obligations of all users regarding the use of the Service.
|
||||
|
||||
Your access to and use of the Service is conditioned on Your acceptance of and compliance with these Terms and Conditions. These Terms and Conditions apply to all visitors, users and others who access or use the Service.
|
||||
|
||||
By accessing or using the Service You agree to be bound by these Terms and Conditions. If You disagree with any part of these Terms and Conditions then You may not access the Service.
|
||||
|
||||
You represent that you are over the age of 18. The Company does not permit those under 18 to use the Service.
|
||||
|
||||
Your access to and use of the Service is also conditioned on Your acceptance of and compliance with the Privacy Policy of the Company. Our Privacy Policy describes Our policies and procedures on the collection, use and disclosure of Your personal information when You use the Application or the Website and tells You about Your privacy rights and how the law protects You. Please read Our Privacy Policy carefully before using Our Service.
|
||||
|
||||
|
||||
## Links to Other Websites
|
||||
|
||||
Our Service may contain links to third-party web sites or services that are not owned or controlled by the Company.
|
||||
|
||||
The Company has no control over, and assumes no responsibility for, the content, privacy policies, or practices of any third party web sites or services. You further acknowledge and agree that the Company shall not be responsible or liable, directly or indirectly, for any damage or loss caused or alleged to be caused by or in connection with the use of or reliance on any such content, goods or services available on or through any such web sites or services.
|
||||
|
||||
We strongly advise You to read the terms and conditions and privacy policies of any third-party web sites or services that You visit.
|
||||
|
||||
|
||||
## Termination
|
||||
|
||||
We may terminate or suspend Your access immediately, without prior notice or liability, for any reason whatsoever, including without limitation if You breach these Terms and Conditions.
|
||||
|
||||
Upon termination, Your right to use the Service will cease immediately.
|
||||
|
||||
|
||||
## Limitation of Liability
|
||||
|
||||
Notwithstanding any damages that You might incur, the entire liability of the Company and any of its suppliers under any provision of this Terms and Your exclusive remedy for all of the foregoing shall be limited to the amount actually paid by You through the Service or 100 USD if You haven't purchased anything through the Service.
|
||||
|
||||
To the maximum extent permitted by applicable law, in no event shall the Company or its suppliers be liable for any special, incidental, indirect, punitive, or consequential damages whatsoever (including, but not limited to, damages for loss of profits, loss of data, loss of use, loss of goodwill, business interruption, personal injury, loss of privacy, or any other pecuniary or non-pecuniary loss or damage) arising out of or in any way related to the use of or inability to use the Service, third-party software and/or third-party hardware used with the Service, or otherwise in connection with any provision of this Terms, even if the Company or any supplier has been advised of the possibility of such damages and even if the remedy fails of its essential purpose.
|
||||
|
||||
In particular, the Company and its suppliers are not liable for any damages or losses that may arise from:
|
||||
- Your reliance on any content provided through the Service;
|
||||
- Errors, mistakes, or inaccuracies of content;
|
||||
- Any unauthorized access to or use of our servers and/or any personal information stored therein;
|
||||
- Any interruption or cessation of transmission to or from the Service;
|
||||
- Any bugs, viruses, trojan horses, or the like which may be transmitted to or through the Service by any third party;
|
||||
- Any errors or omissions in any content or for any loss or damage of any kind incurred as a result of Your use of any content posted, emailed, transmitted, or otherwise made available via the Service.
|
||||
|
||||
The Company shall not be liable for any loss or damage resulting from failure to meet any of Your expectations related to the use or performance of the Service, including but not limited to inaccuracies in GPS location services, suggested itineraries, or other location-based services.
|
||||
|
||||
Some jurisdictions do not allow the exclusion or limitation of certain types of liability, such as incidental or consequential damages or implied warranties. Therefore, the above limitations or exclusions may not apply to You. In such jurisdictions, each party's liability will be limited to the greatest extent permitted by law.
|
||||
|
||||
To the extent permitted by applicable law, the Company and its suppliers’ aggregate liability to You for any claims arising from or related to the use of the Service shall in no event exceed the greater of (a) the amount You paid, if any, for accessing the Service during the twelve (12) month period preceding the claim or (b) one hundred (100) USD.
|
||||
|
||||
You agree that the limitations of liability set forth in this section will survive any termination or expiration of these Terms and apply even if any limited remedy specified in these Terms is found to have failed its essential purpose.
|
||||
"AS IS" and "AS AVAILABLE" Disclaimer
|
||||
|
||||
The Service is provided to You "AS IS" and "AS AVAILABLE" and with all faults and defects without warranty of any kind. To the maximum extent permitted under applicable law, the Company, on its own behalf and on behalf of its Affiliates and its and their respective licensors and service providers, expressly disclaims all warranties, whether express, implied, statutory or otherwise, with respect to the Service, including all implied warranties of merchantability, fitness for a particular purpose, title and non-infringement, and warranties that may arise out of course of dealing, course of performance, usage or trade practice. Without limitation to the foregoing, the Company provides no warranty or undertaking, and makes no representation of any kind that the Service will meet Your requirements, achieve any intended results, be compatible or work with any other software, applications, systems or services, operate without interruption, meet any performance or reliability standards or be error free or that any errors or defects can or will be corrected.
|
||||
|
||||
Without limiting the foregoing, neither the Company nor any of the company's provider makes any representation or warranty of any kind, express or implied: (i) as to the operation or availability of the Service, or the information, content, and materials or products included thereon; (ii) that the Service will be uninterrupted or error-free; (iii) as to the accuracy, reliability, or currency of any information or content provided through the Service; or (iv) that the Service, its servers, the content, or e-mails sent from or on behalf of the Company are free of viruses, scripts, trojan horses, worms, malware, timebombs or other harmful components.
|
||||
|
||||
Some jurisdictions do not allow the exclusion of certain types of warranties or limitations on applicable statutory rights of a consumer, so some or all of the above exclusions and limitations may not apply to You. But in such a case the exclusions and limitations set forth in this section shall be applied to the greatest extent enforceable under applicable law.
|
||||
|
||||
|
||||
## Governing Law
|
||||
|
||||
The laws of the Country, excluding its conflicts of law rules, shall govern this Terms and Your use of the Service. Your use of the Application may also be subject to other local, state, national, or international laws.
|
||||
|
||||
|
||||
## Disputes Resolution
|
||||
|
||||
If You have any concern or dispute about the Service, You agree to first try to resolve the dispute informally by contacting the Company.
|
||||
For European Union (EU) Users
|
||||
|
||||
If You are a European Union consumer, you will benefit from any mandatory provisions of the law of the country in which You are resident.
|
||||
|
||||
|
||||
## United States Legal Compliance
|
||||
|
||||
You represent and warrant that (i) You are not located in a country that is subject to the United States government embargo, or that has been designated by the United States government as a "terrorist supporting" country, and (ii) You are not listed on any United States government list of prohibited or restricted parties.
|
||||
|
||||
|
||||
## Severability and Waiver
|
||||
### Severability
|
||||
|
||||
If any provision of these Terms is held to be unenforceable or invalid, such provision will be changed and interpreted to accomplish the objectives of such provision to the greatest extent possible under applicable law and the remaining provisions will continue in full force and effect.
|
||||
|
||||
### Waiver
|
||||
|
||||
Except as provided herein, the failure to exercise a right or to require performance of an obligation under these Terms shall not affect a party's ability to exercise such right or require such performance at any time thereafter nor shall the waiver of a breach constitute a waiver of any subsequent breach.
|
||||
Translation Interpretation
|
||||
|
||||
These Terms and Conditions may have been translated if We have made them available to You on our Service. You agree that the original English text shall prevail in the case of a dispute.
|
||||
Changes to These Terms and Conditions
|
||||
|
||||
We reserve the right, at Our sole discretion, to modify or replace these Terms at any time. If a revision is material We will make reasonable efforts to provide at least 30 days' notice prior to any new terms taking effect. What constitutes a material change will be determined at Our sole discretion.
|
||||
|
||||
By continuing to access or use Our Service after those revisions become effective, You agree to be bound by the revised terms. If You do not agree to the new terms, in whole or in part, please stop using the website and the Service.
|
||||
|
||||
|
||||
## Contact Us
|
||||
|
||||
If you have any questions about these Terms and Conditions, You can contact us:
|
||||
- By visiting this page on our website: https://anydev.info
|
@@ -71,7 +71,6 @@ platform :ios do
|
||||
"",
|
||||
"s/IOS_GOOGLE_MAPS_API_KEY/#{ENV["IOS_GOOGLE_MAPS_API_KEY"]}/g",
|
||||
"../Runner/AppDelegate.swift"
|
||||
|
||||
)
|
||||
|
||||
sh(
|
||||
@@ -93,7 +92,7 @@ platform :ios do
|
||||
skip_screenshots: true,
|
||||
skip_metadata: true,
|
||||
precheck_include_in_app_purchases: false,
|
||||
|
||||
|
||||
submit_for_review: true,
|
||||
automatic_release: true,
|
||||
# automatically release the app after review
|
||||
|
@@ -1,165 +0,0 @@
|
||||
import 'package:anyway/pages/settings.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:anyway/constants.dart';
|
||||
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
import 'package:anyway/modules/trips_saved_list.dart';
|
||||
import 'package:anyway/utils/load_trips.dart';
|
||||
|
||||
import 'package:anyway/pages/new_trip_location.dart';
|
||||
import 'package:anyway/pages/current_trip.dart';
|
||||
import 'package:anyway/pages/onboarding.dart';
|
||||
|
||||
|
||||
|
||||
|
||||
// BasePage is the scaffold that holds all other pages
|
||||
// A side drawer is used to switch between pages
|
||||
class BasePage extends StatefulWidget {
|
||||
final String mainScreen;
|
||||
final Trip? trip;
|
||||
|
||||
const BasePage({
|
||||
super.key,
|
||||
required this.mainScreen,
|
||||
this.trip,
|
||||
});
|
||||
|
||||
@override
|
||||
State<BasePage> createState() => _BasePageState();
|
||||
}
|
||||
|
||||
class _BasePageState extends State<BasePage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget currentView = const Text("loading...");
|
||||
Future<List<Trip>> trips = loadTrips();
|
||||
|
||||
|
||||
if (widget.mainScreen == "map") {
|
||||
if (widget.trip != null) {
|
||||
currentView = TripPage(trip: widget.trip!);
|
||||
} else {
|
||||
currentView = FutureBuilder(
|
||||
future: trips,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
List<Trip> availableTrips = snapshot.data!;
|
||||
if (availableTrips.isNotEmpty) {
|
||||
return TripPage(trip: availableTrips[0]);
|
||||
} else {
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: Text("Wow, so empty!"),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const NewTripPage()
|
||||
)
|
||||
);
|
||||
},
|
||||
label: Text("Plan a trip"),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return const Text("loading...");
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
} else if (widget.mainScreen == "tutorial") {
|
||||
currentView = OnboardingPage();
|
||||
} else if (widget.mainScreen == "settings") {
|
||||
currentView = SettingsPage();
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(APP_NAME)),
|
||||
body: Center(child: currentView),
|
||||
drawer: Drawer(
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: APP_GRADIENT,
|
||||
),
|
||||
height: 150,
|
||||
child: Center(
|
||||
child: Text(
|
||||
APP_NAME,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
ListTile(
|
||||
title: const Text('Your Trips'),
|
||||
leading: const Icon(Icons.map),
|
||||
selected: widget.mainScreen == "map",
|
||||
onTap: () {},
|
||||
trailing: ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const NewTripPage()
|
||||
)
|
||||
);
|
||||
},
|
||||
child: const Text('New'),
|
||||
),
|
||||
),
|
||||
|
||||
// Adds a ListView to the drawer. This ensures the user can scroll
|
||||
// through the options in the drawer if there isn't enough vertical
|
||||
// space to fit everything.
|
||||
Expanded(
|
||||
child: TripsOverview(trips: trips),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
removeAllTripsFromPrefs();
|
||||
},
|
||||
child: const Text('Clear trips'),
|
||||
),
|
||||
const Divider(indent: 10, endIndent: 10),
|
||||
ListTile(
|
||||
title: const Text('How to use'),
|
||||
leading: Icon(Icons.help),
|
||||
selected: widget.mainScreen == "tutorial",
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => BasePage(mainScreen: "tutorial")
|
||||
)
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// settings in the bottom of the drawer
|
||||
ListTile(
|
||||
title: const Text('Settings'),
|
||||
leading: const Icon(Icons.settings),
|
||||
selected: widget.mainScreen == "settings",
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => BasePage(mainScreen: "settings")
|
||||
)
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
121
frontend/lib/layouts/scaffold.dart
Normal file
@@ -0,0 +1,121 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:anyway/constants.dart';
|
||||
|
||||
import 'package:anyway/main.dart';
|
||||
import 'package:anyway/modules/help_dialog.dart';
|
||||
import 'package:anyway/modules/trips_saved_list.dart';
|
||||
|
||||
import 'package:anyway/pages/onboarding.dart';
|
||||
import 'package:anyway/pages/current_trip.dart';
|
||||
import 'package:anyway/pages/settings.dart';
|
||||
import 'package:anyway/pages/new_trip_location.dart';
|
||||
|
||||
|
||||
mixin ScaffoldLayout<T extends StatefulWidget> on State<T> {
|
||||
Widget mainScaffold(
|
||||
BuildContext context,
|
||||
{
|
||||
Widget child = const Text("emptiness"),
|
||||
Widget title = const Text(APP_NAME),
|
||||
List<String> helpTexts = const []
|
||||
}
|
||||
) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: title,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.help),
|
||||
tooltip: 'Help',
|
||||
onPressed: () {
|
||||
if (helpTexts.isNotEmpty) {
|
||||
helpDialog(context, helpTexts[0], helpTexts[1]);
|
||||
}
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Center(child: child),
|
||||
drawer: Drawer(
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: APP_GRADIENT,
|
||||
),
|
||||
height: 150,
|
||||
child: const Center(
|
||||
child: Text(
|
||||
APP_NAME,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
ListTile(
|
||||
title: const Text('Your Trips'),
|
||||
leading: const Icon(Icons.map),
|
||||
selected: widget is TripPage,
|
||||
onTap: () {},
|
||||
trailing: ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const NewTripPage()
|
||||
)
|
||||
);
|
||||
},
|
||||
child: const Text('New'),
|
||||
),
|
||||
),
|
||||
|
||||
// Adds a ListView to the drawer. This ensures the user can scroll
|
||||
// through the options in the drawer if there isn't enough vertical
|
||||
// space to fit everything.
|
||||
Expanded(
|
||||
child: TripsOverview(trips: savedTrips),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
savedTrips.clearTrips();
|
||||
},
|
||||
child: const Text('Clear trips'),
|
||||
),
|
||||
const Divider(indent: 10, endIndent: 10),
|
||||
ListTile(
|
||||
title: const Text('How to use'),
|
||||
leading: const Icon(Icons.help),
|
||||
selected: widget is OnboardingPage,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const OnboardingPage()
|
||||
)
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// settings in the bottom of the drawer
|
||||
ListTile(
|
||||
title: const Text('Settings'),
|
||||
leading: const Icon(Icons.settings),
|
||||
selected: widget is SettingsPage,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => SettingsPage()
|
||||
)
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,22 +1,26 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:anyway/constants.dart';
|
||||
import 'package:anyway/layout.dart';
|
||||
import 'package:anyway/utils/get_first_page.dart';
|
||||
import 'package:anyway/utils/load_trips.dart';
|
||||
|
||||
|
||||
void main() => runApp(const App());
|
||||
|
||||
// Some global variables
|
||||
final GlobalKey<ScaffoldMessengerState> rootScaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
|
||||
final SavedTrips savedTrips = SavedTrips();
|
||||
// the list of saved trips is then populated implicitly by getFirstPage()
|
||||
|
||||
|
||||
class App extends StatelessWidget {
|
||||
const App({super.key});
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: APP_NAME,
|
||||
home: BasePage(mainScreen: "map"),
|
||||
theme: APP_THEME,
|
||||
scaffoldMessengerKey: rootScaffoldMessengerKey
|
||||
);
|
||||
}
|
||||
Widget build(BuildContext context) => MaterialApp(
|
||||
title: APP_NAME,
|
||||
home: getFirstPage(),
|
||||
theme: APP_THEME,
|
||||
scaffoldMessengerKey: rootScaffoldMessengerKey
|
||||
);
|
||||
}
|
||||
|
@@ -1,21 +1,18 @@
|
||||
import 'dart:developer';
|
||||
import 'package:anyway/modules/step_between_landmarks.dart';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:anyway/modules/landmark_card.dart';
|
||||
import 'package:anyway/structs/landmark.dart';
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
import 'package:anyway/main.dart';
|
||||
import 'package:anyway/modules/step_between_landmarks.dart';
|
||||
import 'package:anyway/modules/landmark_card.dart';
|
||||
|
||||
|
||||
|
||||
List<Widget> landmarksList(Trip trip) {
|
||||
log("Trip ${trip.uuid} ${trip.landmarks.length} landmarks");
|
||||
// Returns a list of widgets that represent the landmarks matching the given selector
|
||||
List<Widget> landmarksList(Trip trip, {required bool Function(Landmark) selector}) {
|
||||
|
||||
List<Widget> children = [];
|
||||
|
||||
log("Trip ${trip.uuid} ${trip.landmarks.length} landmarks");
|
||||
|
||||
if (trip.landmarks.isEmpty || trip.landmarks.length <= 1 && trip.landmarks.first.type == typeStart ) {
|
||||
children.add(
|
||||
const Text("No landmarks in this trip"),
|
||||
@@ -24,40 +21,24 @@ List<Widget> landmarksList(Trip trip) {
|
||||
}
|
||||
|
||||
for (Landmark landmark in trip.landmarks) {
|
||||
children.add(
|
||||
Dismissible(
|
||||
key: ValueKey<int>(landmark.hashCode),
|
||||
child: LandmarkCard(landmark),
|
||||
dismissThresholds: {DismissDirection.endToStart: 0.95, DismissDirection.startToEnd: 0.95},
|
||||
onDismissed: (direction) {
|
||||
log('Removing ${landmark.name}');
|
||||
trip.removeLandmark(landmark);
|
||||
|
||||
rootScaffoldMessengerKey.currentState!.showSnackBar(
|
||||
SnackBar(content: Text("We won't show ${landmark.name} again"))
|
||||
);
|
||||
},
|
||||
|
||||
background: Container(color: Colors.red),
|
||||
secondaryBackground: Container(
|
||||
color: Colors.red,
|
||||
child: Icon(
|
||||
Icons.delete,
|
||||
color: Colors.white,
|
||||
),
|
||||
padding: EdgeInsets.all(15),
|
||||
alignment: Alignment.centerRight,
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
if (landmark.next != null) {
|
||||
if (selector(landmark)) {
|
||||
children.add(
|
||||
StepBetweenLandmarks(current: landmark, next: landmark.next!)
|
||||
LandmarkCard(landmark, trip),
|
||||
);
|
||||
|
||||
if (!landmark.visited) {
|
||||
Landmark? nextLandmark = landmark.next;
|
||||
while (nextLandmark != null && nextLandmark.visited) {
|
||||
nextLandmark = nextLandmark.next;
|
||||
}
|
||||
if (nextLandmark != null) {
|
||||
children.add(
|
||||
StepBetweenLandmarks(current: landmark, next: nextLandmark!)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
|
@@ -1,9 +1,21 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
import 'package:anyway/pages/current_trip.dart';
|
||||
|
||||
|
||||
final List<String> statusTexts = [
|
||||
'Parsing your preferences...',
|
||||
'Finding the best places...',
|
||||
'Crunching the numbers...',
|
||||
'Calculating the best route...',
|
||||
'Making sure you have a great time...',
|
||||
];
|
||||
|
||||
|
||||
class CurrentTripLoadingIndicator extends StatefulWidget {
|
||||
final Trip trip;
|
||||
const CurrentTripLoadingIndicator({
|
||||
@@ -15,46 +27,123 @@ class CurrentTripLoadingIndicator extends StatefulWidget {
|
||||
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,
|
||||
],
|
||||
);
|
||||
}
|
||||
)
|
||||
Widget build(BuildContext context) => Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
// In the very center of the panel, show the greeter which tells the user that the trip is being generated
|
||||
Center(child: loadingText(widget.trip)),
|
||||
// As a gimmick, and a way to show that the app is still working, show a few loading dots
|
||||
const Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: 12),
|
||||
child: StatusText(),
|
||||
)
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// automatically cycle through the greeter texts
|
||||
class StatusText extends StatefulWidget {
|
||||
const StatusText({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_StatusTextState createState() => _StatusTextState();
|
||||
}
|
||||
|
||||
class _StatusTextState extends State<StatusText> {
|
||||
int statusIndex = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Future.delayed(const Duration(seconds: 5), () {
|
||||
setState(() {
|
||||
statusIndex = (statusIndex + 1) % statusTexts.length;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AutoSizeText(
|
||||
statusTexts[statusIndex],
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Widget loadingText(Trip trip) => FutureBuilder(
|
||||
future: trip.cityName,
|
||||
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
|
||||
Widget greeter;
|
||||
|
||||
if (snapshot.hasData) {
|
||||
greeter = AnimatedDotsText(
|
||||
baseText: 'Creating your trip to ${snapshot.data}',
|
||||
style: greeterStyle,
|
||||
);
|
||||
} else if (snapshot.hasError) {
|
||||
// the exact error is shown in the central part of the trip overview. No need to show it here
|
||||
greeter = Text(
|
||||
'Error while loading trip.',
|
||||
style: greeterStyle,
|
||||
);
|
||||
} else {
|
||||
greeter = AnimatedDotsText(
|
||||
baseText: 'Creating your trip',
|
||||
style: greeterStyle,
|
||||
);
|
||||
}
|
||||
return greeter;
|
||||
}
|
||||
);
|
||||
|
||||
class AnimatedDotsText extends StatefulWidget {
|
||||
final String baseText;
|
||||
final TextStyle style;
|
||||
|
||||
const AnimatedDotsText({
|
||||
Key? key,
|
||||
required this.baseText,
|
||||
required this.style,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_AnimatedDotsTextState createState() => _AnimatedDotsTextState();
|
||||
}
|
||||
|
||||
class _AnimatedDotsTextState extends State<AnimatedDotsText> {
|
||||
int dotCount = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
dotCount = (dotCount + 1) % 4;
|
||||
// show up to 3 dots
|
||||
});
|
||||
} else {
|
||||
timer.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String dots = '.' * dotCount;
|
||||
return AutoSizeText(
|
||||
'${widget.baseText}$dots',
|
||||
style: widget.style,
|
||||
maxLines: 2,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -1,22 +1,19 @@
|
||||
import 'dart:collection';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:widget_to_marker/widget_to_marker.dart';
|
||||
|
||||
import 'package:anyway/constants.dart';
|
||||
import 'package:anyway/modules/landmark_map_marker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:anyway/structs/landmark.dart';
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:widget_to_marker/widget_to_marker.dart';
|
||||
import 'package:anyway/modules/landmark_map_marker.dart';
|
||||
|
||||
|
||||
class CurrentTripMap extends StatefulWidget {
|
||||
|
||||
final Trip? trip;
|
||||
|
||||
CurrentTripMap({
|
||||
this.trip
|
||||
});
|
||||
CurrentTripMap({this.trip});
|
||||
|
||||
@override
|
||||
State<CurrentTripMap> createState() => _CurrentTripMapState();
|
||||
@@ -30,7 +27,23 @@ class _CurrentTripMapState extends State<CurrentTripMap> {
|
||||
zoom: 11.0,
|
||||
);
|
||||
Set<Marker> mapMarkers = <Marker>{};
|
||||
|
||||
Set<Polyline> mapPolylines = <Polyline>{};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.trip?.addListener(setMapMarkers);
|
||||
widget.trip?.addListener(setMapRoute);
|
||||
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.trip?.removeListener(setMapMarkers);
|
||||
widget.trip?.removeListener(setMapRoute);
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onMapCreated(GoogleMapController controller) async {
|
||||
mapController = controller;
|
||||
@@ -40,38 +53,70 @@ class _CurrentTripMapState extends State<CurrentTripMap> {
|
||||
controller.moveCamera(update);
|
||||
}
|
||||
setMapMarkers();
|
||||
setMapRoute();
|
||||
}
|
||||
|
||||
void _onCameraIdle() {
|
||||
// print(mapController.getLatLng(ScreenCoordinate(x: 0, y: 0)));
|
||||
}
|
||||
|
||||
|
||||
void setMapMarkers() async {
|
||||
List<Landmark> landmarks = widget.trip?.landmarks.toList() ?? [];
|
||||
Set<Marker> newMarkers = <Marker>{};
|
||||
for (int i = 0; i < landmarks.length; i++) {
|
||||
Landmark landmark = landmarks[i];
|
||||
Iterator<(int, Landmark)> it = (widget.trip?.landmarks.toList() ?? []).indexed.iterator;
|
||||
|
||||
while (it.moveNext()) {
|
||||
int i = it.current.$1;
|
||||
Landmark landmark = it.current.$2;
|
||||
|
||||
MarkerId markerId = MarkerId("${landmark.uuid} - ${landmark.visited}");
|
||||
List<double> location = landmark.location;
|
||||
Marker marker = Marker(
|
||||
markerId: MarkerId(landmark.uuid),
|
||||
position: LatLng(location[0], location[1]),
|
||||
icon: await ThemedMarker(landmark: landmark, position: i).toBitmapDescriptor(
|
||||
logicalSize: const Size(150, 150),
|
||||
imageSize: const Size(150, 150)
|
||||
),
|
||||
);
|
||||
newMarkers.add(marker);
|
||||
// only create a new marker, if there is no marker for this landmark
|
||||
if (!mapMarkers.any((Marker marker) => marker.markerId == markerId)) {
|
||||
Marker marker = Marker(
|
||||
markerId: markerId,
|
||||
position: LatLng(location[0], location[1]),
|
||||
icon: await ThemedMarker(landmark: landmark, position: i).toBitmapDescriptor(
|
||||
logicalSize: const Size(150, 150),
|
||||
imageSize: const Size(150, 150),
|
||||
)
|
||||
);
|
||||
setState(() {
|
||||
mapMarkers.add(marker);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void setMapRoute() async {
|
||||
List<Landmark> landmarks = widget.trip?.landmarks.toList() ?? [];
|
||||
Set<Polyline> polyLines = <Polyline>{};
|
||||
|
||||
if (landmarks.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Landmark landmark in landmarks) {
|
||||
if (landmark.next != null) {
|
||||
List<LatLng> step = [
|
||||
LatLng(landmark.location[0], landmark.location[1]),
|
||||
LatLng(landmark.next!.location[0], landmark.next!.location[1])
|
||||
];
|
||||
Polyline stepLine = Polyline(
|
||||
polylineId: PolylineId('step-${landmark.uuid}'),
|
||||
points: step,
|
||||
color: landmark.visited || (landmark.next?.visited ?? false) ? Colors.grey : PRIMARY_COLOR,
|
||||
width: 5
|
||||
);
|
||||
polyLines.add(stepLine);
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
mapMarkers = newMarkers;
|
||||
mapPolylines = polyLines;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
widget.trip?.addListener(setMapMarkers);
|
||||
Future<SharedPreferences> preferences = SharedPreferences.getInstance();
|
||||
|
||||
return FutureBuilder(
|
||||
@@ -84,7 +129,7 @@ class _CurrentTripMapState extends State<CurrentTripMap> {
|
||||
} else {
|
||||
return const CircularProgressIndicator();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -93,8 +138,8 @@ class _CurrentTripMapState extends State<CurrentTripMap> {
|
||||
onMapCreated: _onMapCreated,
|
||||
initialCameraPosition: _cameraPosition,
|
||||
onCameraIdle: _onCameraIdle,
|
||||
// onLongPress: ,
|
||||
markers: mapMarkers,
|
||||
polylines: mapPolylines,
|
||||
cloudMapId: MAP_ID,
|
||||
mapToolbarEnabled: false,
|
||||
zoomControlsEnabled: false,
|
||||
@@ -102,5 +147,4 @@ class _CurrentTripMapState extends State<CurrentTripMap> {
|
||||
myLocationButtonEnabled: false,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,9 +1,12 @@
|
||||
import 'package:anyway/constants.dart';
|
||||
import 'package:anyway/modules/current_trip_error_message.dart';
|
||||
import 'package:anyway/modules/current_trip_loading_indicator.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:anyway/constants.dart';
|
||||
|
||||
import 'package:anyway/structs/landmark.dart';
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
|
||||
import 'package:anyway/modules/current_trip_error_message.dart';
|
||||
import 'package:anyway/modules/current_trip_loading_indicator.dart';
|
||||
import 'package:anyway/modules/current_trip_summary.dart';
|
||||
import 'package:anyway/modules/current_trip_save_button.dart';
|
||||
import 'package:anyway/modules/current_trip_landmarks_list.dart';
|
||||
@@ -36,7 +39,7 @@ class _CurrentTripPanelState extends State<CurrentTripPanel> {
|
||||
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,
|
||||
height: MediaQuery.of(context).size.height * TRIP_PANEL_MIN_HEIGHT,
|
||||
child: CurrentTripErrorMessage(trip: widget.trip)
|
||||
),
|
||||
);
|
||||
@@ -46,37 +49,66 @@ class _CurrentTripPanelState extends State<CurrentTripPanel> {
|
||||
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,
|
||||
height: MediaQuery.of(context).size.height * TRIP_PANEL_MIN_HEIGHT,
|
||||
child: CurrentTripLoadingIndicator(trip: widget.trip),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return ListView(
|
||||
controller: widget.controller,
|
||||
padding: const EdgeInsets.only(bottom: 30),
|
||||
padding: const EdgeInsets.only(top: 10, left: 10, right: 10, bottom: 30),
|
||||
children: [
|
||||
SizedBox(
|
||||
// reuse the exact same height as the panel has when collapsed
|
||||
// this way the greeter will be centered when the panel is collapsed
|
||||
height: MediaQuery.of(context).size.height * TRIP_PANEL_MIN_HEIGHT - 20,
|
||||
// note that we need to account for the padding above
|
||||
height: MediaQuery.of(context).size.height * TRIP_PANEL_MIN_HEIGHT - 10,
|
||||
child: CurrentTripGreeter(trip: widget.trip),
|
||||
),
|
||||
|
||||
const Padding(padding: EdgeInsets.only(top: 10)),
|
||||
|
||||
// CurrentTripSummary(trip: widget.trip),
|
||||
|
||||
// const Divider(),
|
||||
|
||||
...landmarksList(widget.trip),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
CurrentTripSummary(trip: widget.trip),
|
||||
if (widget.trip.landmarks.where((Landmark landmark) => landmark.visited).isNotEmpty)
|
||||
ExpansionTile(
|
||||
leading: const Icon(Icons.location_on),
|
||||
title: const Text('Visited Landmarks (tap to expand)'),
|
||||
children: [
|
||||
...landmarksList(widget.trip, selector: (Landmark landmark) => landmark.visited),
|
||||
],
|
||||
visualDensity: VisualDensity.compact,
|
||||
collapsedShape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
const Padding(padding: EdgeInsets.only(top: 10)),
|
||||
|
||||
Center(child: saveButton(widget.trip)),
|
||||
// upcoming landmarks
|
||||
...landmarksList(widget.trip, selector: (Landmark landmark) => landmark.visited == false),
|
||||
|
||||
const Padding(padding: EdgeInsets.only(top: 10)),
|
||||
|
||||
Center(child: saveButton(trip: widget.trip)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,41 +1,52 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
|
||||
import 'package:anyway/main.dart';
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
Widget saveButton(Trip trip) => ElevatedButton(
|
||||
onPressed: () async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
trip.toPrefs(prefs);
|
||||
rootScaffoldMessengerKey.currentState!.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Trip saved'),
|
||||
duration: Duration(seconds: 2),
|
||||
dismissDirection: DismissDirection.horizontal
|
||||
|
||||
class saveButton extends StatefulWidget {
|
||||
Trip trip;
|
||||
saveButton({super.key, required this.trip});
|
||||
|
||||
@override
|
||||
State<saveButton> createState() => _saveButtonState();
|
||||
}
|
||||
|
||||
class _saveButtonState extends State<saveButton> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ElevatedButton(
|
||||
onPressed: () async {
|
||||
savedTrips.addTrip(widget.trip);
|
||||
rootScaffoldMessengerKey.currentState!.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Trip saved'),
|
||||
duration: Duration(seconds: 2),
|
||||
dismissDirection: DismissDirection.horizontal
|
||||
)
|
||||
);
|
||||
},
|
||||
child: SizedBox(
|
||||
width: 100,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.save,
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 10, top: 5, bottom: 5, right: 5),
|
||||
child: AutoSizeText(
|
||||
'Save trip',
|
||||
maxLines: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
);
|
||||
},
|
||||
child: SizedBox(
|
||||
width: 100,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.save,
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 10, top: 5, bottom: 5, right: 5),
|
||||
child: AutoSizeText(
|
||||
'Save trip',
|
||||
maxLines: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
class CurrentTripSummary extends StatefulWidget {
|
||||
final Trip trip;
|
||||
const CurrentTripSummary({
|
||||
@@ -14,18 +15,29 @@ class CurrentTripSummary extends StatefulWidget {
|
||||
|
||||
class _CurrentTripSummaryState extends State<CurrentTripSummary> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Text('Summary'),
|
||||
// Text('Start: ${widget.trip.start}'),
|
||||
// Text('End: ${widget.trip.end}'),
|
||||
Text('Total duration: ${widget.trip.totalTime}'),
|
||||
Text('Total distance: ${widget.trip.totalTime}'),
|
||||
// Text('Fuel: ${widget.trip.fuel}'),
|
||||
// Text('Cost: ${widget.trip.cost}'),
|
||||
],
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
Widget build(BuildContext context) => ListenableBuilder(
|
||||
listenable: widget.trip,
|
||||
builder: (context, child) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.flag, size: 20),
|
||||
const Padding(padding: EdgeInsets.only(right: 10)),
|
||||
Text('${widget.trip.landmarks.length} stops', style: Theme.of(context).textTheme.bodyLarge),
|
||||
]
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.hourglass_bottom_rounded, size: 20),
|
||||
const Padding(padding: EdgeInsets.only(right: 10)),
|
||||
Text('${widget.trip.totalTime.inHours}h ${widget.trip.totalTime.inMinutes.remainder(60)}min', style: Theme.of(context).textTheme.bodyLarge),
|
||||
]
|
||||
),
|
||||
],
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
25
frontend/lib/modules/help_dialog.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
Future<void> helpDialog(BuildContext context, String title, String content) {
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(title),
|
||||
content: Text(content),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
textStyle: Theme.of(context).textTheme.labelLarge,
|
||||
),
|
||||
child: const Text('Got it!'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
@@ -1,13 +1,23 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'package:anyway/constants.dart';
|
||||
|
||||
import 'package:anyway/main.dart';
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:anyway/structs/landmark.dart';
|
||||
|
||||
|
||||
|
||||
class LandmarkCard extends StatefulWidget {
|
||||
final Landmark landmark;
|
||||
final Trip parentTrip;
|
||||
|
||||
LandmarkCard(this.landmark);
|
||||
LandmarkCard(
|
||||
this.landmark,
|
||||
this.parentTrip,
|
||||
);
|
||||
|
||||
@override
|
||||
_LandmarkCardState createState() => _LandmarkCardState();
|
||||
@@ -16,112 +26,184 @@ class LandmarkCard extends StatefulWidget {
|
||||
|
||||
class _LandmarkCardState extends State<LandmarkCard> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
ThemeData theme = Theme.of(context);
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 160,
|
||||
constraints: BoxConstraints(
|
||||
// express the max height in terms text lines
|
||||
maxHeight: 7 * (Theme.of(context).textTheme.titleMedium!.fontSize! + 10),
|
||||
),
|
||||
child: Card(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
),
|
||||
elevation: 5,
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
|
||||
// if the image is available, display it on the left side of the card, otherwise only display the text
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container( // the image on the left
|
||||
// inherit the height of the parent container
|
||||
height: double.infinity,
|
||||
// force a fixed width
|
||||
width: 160,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: widget.landmark.imageURL ?? '',
|
||||
placeholder: (context, url) => Center(child: CircularProgressIndicator()),
|
||||
errorWidget: (context, error, stackTrace) => Icon(Icons.question_mark_outlined),
|
||||
// TODO: make this a switch statement to load a placeholder if null
|
||||
// cover the whole container meaning the image will be cropped
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
widget.landmark.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 2,
|
||||
),
|
||||
// Image and landmark "type" on the left
|
||||
AspectRatio(
|
||||
aspectRatio: 3 / 4,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (widget.landmark.imageURL != null && widget.landmark.imageURL!.isNotEmpty)
|
||||
Expanded(
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: widget.landmark.imageURL!,
|
||||
placeholder: (context, url) => const Center(child: CircularProgressIndicator()),
|
||||
errorWidget: (context, url, error) => imagePlaceholder(widget.landmark),
|
||||
fit: BoxFit.cover
|
||||
)
|
||||
)
|
||||
else
|
||||
imagePlaceholder(widget.landmark),
|
||||
|
||||
if (widget.landmark.type != typeStart && widget.landmark.type != typeFinish)
|
||||
Container(
|
||||
color: PRIMARY_COLOR,
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(5),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.timer_outlined, size: 16),
|
||||
Text("${widget.landmark.duration?.inMinutes} minutes"),
|
||||
],
|
||||
)
|
||||
)
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
),
|
||||
|
||||
// Main information, useful buttons on the right
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.landmark.name,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 2,
|
||||
),
|
||||
|
||||
if (widget.landmark.nameEN != null)
|
||||
Text(
|
||||
widget.landmark.nameEN!,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
|
||||
// fill the vspace
|
||||
const Spacer(),
|
||||
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: EdgeInsets.only(left: 5, right: 5, bottom: 10),
|
||||
// the scroll view should be flush once the buttons are scrolled to the left
|
||||
// but initially there should be some padding
|
||||
child: Wrap(
|
||||
spacing: 10,
|
||||
// show the type, the website, and the wikipedia link as buttons/labels in a row
|
||||
children: [
|
||||
doneToggleButton(),
|
||||
if (widget.landmark.websiteURL != null)
|
||||
websiteButton(),
|
||||
|
||||
optionsButton()
|
||||
],
|
||||
),
|
||||
if (widget.landmark.nameEN != null)
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
widget.landmark.nameEN!,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
),
|
||||
maxLines: 1,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
SingleChildScrollView(
|
||||
// allows the buttons to be scrolled
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Wrap(
|
||||
spacing: 10,
|
||||
// show the type, the website, and the wikipedia link as buttons/labels in a row
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: () {},
|
||||
icon: widget.landmark.type.icon,
|
||||
label: Text(widget.landmark.type.name),
|
||||
),
|
||||
if (widget.landmark.duration != null && widget.landmark.duration!.inMinutes > 0)
|
||||
TextButton.icon(
|
||||
onPressed: () {},
|
||||
icon: Icon(Icons.hourglass_bottom),
|
||||
label: Text('${widget.landmark.duration!.inMinutes} minutes'),
|
||||
),
|
||||
if (widget.landmark.websiteURL != null)
|
||||
TextButton.icon(
|
||||
onPressed: () async {
|
||||
// open a browser with the website link
|
||||
await launchUrl(Uri.parse(widget.landmark.websiteURL!));
|
||||
},
|
||||
icon: Icon(Icons.link),
|
||||
label: Text('Website'),
|
||||
),
|
||||
if (widget.landmark.wikipediaURL != null)
|
||||
TextButton.icon(
|
||||
onPressed: () async {
|
||||
// open a browser with the wikipedia link
|
||||
await launchUrl(Uri.parse(widget.landmark.wikipediaURL!));
|
||||
},
|
||||
icon: Icon(Icons.book),
|
||||
label: Text('Wikipedia'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget doneToggleButton() {
|
||||
return TextButton.icon(
|
||||
onPressed: () async {
|
||||
widget.landmark.visited = !widget.landmark.visited;
|
||||
widget.parentTrip.notifyUpdate();
|
||||
},
|
||||
icon: Icon(widget.landmark.visited ? Icons.undo_sharp : Icons.check),
|
||||
label: Text(widget.landmark.visited ? "Add to overview" : "Done!"),
|
||||
);
|
||||
}
|
||||
|
||||
Widget websiteButton () => TextButton.icon(
|
||||
onPressed: () async {
|
||||
// open a browser with the website link
|
||||
await launchUrl(Uri.parse(widget.landmark.websiteURL!));
|
||||
},
|
||||
icon: const Icon(Icons.link),
|
||||
label: const Text('Website'),
|
||||
);
|
||||
|
||||
|
||||
Widget optionsButton () => PopupMenuButton(
|
||||
icon: const Icon(Icons.settings),
|
||||
style: TextButtonTheme.of(context).style,
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.delete),
|
||||
title: const Text('Delete'),
|
||||
onTap: () async {
|
||||
widget.parentTrip.removeLandmark(widget.landmark);
|
||||
rootScaffoldMessengerKey.currentState!.showSnackBar(
|
||||
SnackBar(content: Text("${widget.landmark.name} won't be shown again"))
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.star),
|
||||
title: const Text('Favorite'),
|
||||
onTap: () async {
|
||||
rootScaffoldMessengerKey.currentState!.showSnackBar(
|
||||
SnackBar(content: Text("Not implemented yet"))
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
Widget imagePlaceholder (Landmark landmark) => Expanded(
|
||||
child:
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [GRADIENT_START, GRADIENT_END],
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(landmark.type.icon.icon, size: 50),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@@ -40,7 +40,7 @@ class ThemedMarker extends StatelessWidget {
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: APP_GRADIENT,
|
||||
gradient: landmark.visited ? LinearGradient(colors: [Colors.grey, Colors.white]) : APP_GRADIENT,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.black, width: 5),
|
||||
),
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import 'package:anyway/layout.dart';
|
||||
import 'package:anyway/main.dart';
|
||||
import 'package:anyway/pages/current_trip.dart';
|
||||
import 'package:anyway/structs/preferences.dart';
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
import 'package:anyway/utils/fetch_trip.dart';
|
||||
@@ -46,21 +46,20 @@ class _NewTripButtonState extends State<NewTripButton> {
|
||||
UserPreferences preferences = widget.preferences;
|
||||
if (preferences.nature.value == 0 && preferences.shopping.value == 0 && preferences.sightseeing.value == 0){
|
||||
rootScaffoldMessengerKey.currentState!.showSnackBar(
|
||||
SnackBar(content: Text("Please specify at least one preference"))
|
||||
const SnackBar(content: Text("Please specify at least one preference"))
|
||||
);
|
||||
} else if (preferences.maxTime.value == 0){
|
||||
rootScaffoldMessengerKey.currentState!.showSnackBar(
|
||||
SnackBar(content: Text("Please choose a longer duration"))
|
||||
const SnackBar(content: Text("Please choose a longer duration"))
|
||||
);
|
||||
} else {
|
||||
Trip trip = widget.trip;
|
||||
fetchTrip(trip, widget.preferences);
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => BasePage(mainScreen: "map", trip: trip)
|
||||
builder: (context) => TripPage(trip: trip)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -9,6 +9,15 @@ import 'package:flutter/material.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
const Map<String, List> debugLocations = {
|
||||
'paris': [48.8575, 2.3514],
|
||||
'london': [51.5074, -0.1278],
|
||||
'new york': [40.7128, -74.0060],
|
||||
'tokyo': [35.6895, 139.6917],
|
||||
};
|
||||
|
||||
|
||||
|
||||
class NewTripLocationSearch extends StatefulWidget {
|
||||
Future<SharedPreferences> prefs = SharedPreferences.getInstance();
|
||||
Trip trip;
|
||||
@@ -27,26 +36,35 @@ class _NewTripLocationSearchState extends State<NewTripLocationSearch> {
|
||||
|
||||
setTripLocation (String query) async {
|
||||
List<Location> locations = [];
|
||||
Location startLocation;
|
||||
log('Searching for: $query');
|
||||
|
||||
try{
|
||||
locations = await locationFromAddress(query);
|
||||
} catch (e) {
|
||||
log('No results found for: $query : $e');
|
||||
if (GeocodingPlatform.instance != null) {
|
||||
locations.addAll(await locationFromAddress(query));
|
||||
}
|
||||
|
||||
if (locations.isNotEmpty) {
|
||||
Location location = locations.first;
|
||||
widget.trip.landmarks.clear();
|
||||
widget.trip.addLandmark(
|
||||
Landmark(
|
||||
uuid: 'pending',
|
||||
name: query,
|
||||
location: [location.latitude, location.longitude],
|
||||
type: typeStart
|
||||
)
|
||||
startLocation = locations.first;
|
||||
} else {
|
||||
log('No results found for: $query. Is geocoding available?');
|
||||
log('Setting Fallback location');
|
||||
List coordinates = debugLocations[query.toLowerCase()] ?? [48.8575, 2.3514];
|
||||
startLocation = Location(
|
||||
latitude: coordinates[0],
|
||||
longitude: coordinates[1],
|
||||
timestamp: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
widget.trip.landmarks.clear();
|
||||
widget.trip.addLandmark(
|
||||
Landmark(
|
||||
uuid: 'pending',
|
||||
name: query,
|
||||
location: [startLocation.latitude, startLocation.longitude],
|
||||
type: typeStart
|
||||
)
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
late Widget locationSearchBar = SearchBar(
|
||||
|
@@ -1,14 +1,14 @@
|
||||
// A map that allows the user to select a location for a new trip.
|
||||
import 'dart:developer';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:widget_to_marker/widget_to_marker.dart';
|
||||
|
||||
import 'package:anyway/constants.dart';
|
||||
import 'package:anyway/modules/landmark_map_marker.dart';
|
||||
import 'package:anyway/structs/landmark.dart';
|
||||
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:widget_to_marker/widget_to_marker.dart';
|
||||
import 'package:anyway/structs/landmark.dart';
|
||||
import 'package:anyway/modules/landmark_map_marker.dart';
|
||||
|
||||
|
||||
class NewTripMap extends StatefulWidget {
|
||||
@@ -26,11 +26,10 @@ class _NewTripMapState extends State<NewTripMap> {
|
||||
target: LatLng(48.8566, 2.3522),
|
||||
zoom: 11.0,
|
||||
);
|
||||
late GoogleMapController _mapController;
|
||||
GoogleMapController? _mapController;
|
||||
final Set<Marker> _markers = <Marker>{};
|
||||
|
||||
_onLongPress(LatLng location) {
|
||||
log('Long press: $location');
|
||||
widget.trip.landmarks.clear();
|
||||
widget.trip.addLandmark(
|
||||
Landmark(
|
||||
@@ -56,11 +55,15 @@ class _NewTripMapState extends State<NewTripMap> {
|
||||
),
|
||||
)
|
||||
);
|
||||
_mapController.moveCamera(
|
||||
CameraUpdate.newLatLng(
|
||||
LatLng(landmark.location[0], landmark.location[1])
|
||||
)
|
||||
);
|
||||
// check if the controller is ready
|
||||
|
||||
if (_mapController != null) {
|
||||
_mapController!.animateCamera(
|
||||
CameraUpdate.newLatLng(
|
||||
LatLng(landmark.location[0], landmark.location[1])
|
||||
)
|
||||
);
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
@@ -75,6 +78,7 @@ class _NewTripMapState extends State<NewTripMap> {
|
||||
widget.trip.addListener(updateTripDetails);
|
||||
Future<SharedPreferences> preferences = SharedPreferences.getInstance();
|
||||
|
||||
|
||||
return FutureBuilder(
|
||||
future: preferences,
|
||||
builder: (context, snapshot) {
|
||||
|
118
frontend/lib/modules/onbarding_agreement_card.dart
Normal file
@@ -0,0 +1,118 @@
|
||||
import 'package:anyway/structs/agreement.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
|
||||
import 'package:anyway/modules/onboarding_card.dart';
|
||||
|
||||
|
||||
class OnboardingAgreementCard extends StatefulWidget {
|
||||
final String title;
|
||||
final String description;
|
||||
final String imagePath;
|
||||
final String agreementTextPath;
|
||||
final ValueChanged<bool> onAgreementChanged;
|
||||
|
||||
|
||||
OnboardingAgreementCard({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.imagePath,
|
||||
required this.agreementTextPath,
|
||||
required this.onAgreementChanged
|
||||
});
|
||||
|
||||
@override
|
||||
State<OnboardingAgreementCard> createState() => _OnboardingAgreementCardState();
|
||||
}
|
||||
|
||||
class _OnboardingAgreementCardState extends State<OnboardingAgreementCard> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
OnboardingCard(title: widget.title, description: widget.description, imagePath: widget.imagePath),
|
||||
Padding(padding: EdgeInsets.only(top: 20)),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// The checkbox of the agreement
|
||||
FutureBuilder(
|
||||
future: getAgreement(),
|
||||
builder: (context, snapshot) {
|
||||
bool agreed = false;
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
if (snapshot.hasData) {
|
||||
Agreement agreement = snapshot.data!;
|
||||
agreed = agreement.agreed;
|
||||
} else {
|
||||
agreed = false;
|
||||
}
|
||||
} else {
|
||||
agreed = false;
|
||||
}
|
||||
return Checkbox(
|
||||
value: agreed,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
widget.onAgreementChanged(value!);
|
||||
});
|
||||
saveAgreement(value!);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// The text of the agreement
|
||||
Text(
|
||||
"I agree to the ",
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
|
||||
// The clickable text of the agreement that shows the agreement text
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
// show a dialog with the agreement text
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
scrollable: true,
|
||||
content: FutureBuilder(
|
||||
future: DefaultAssetBundle.of(context).loadString(widget.agreementTextPath),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return MarkdownBody(
|
||||
data: snapshot.data.toString(),
|
||||
);
|
||||
} else {
|
||||
return CircularProgressIndicator();
|
||||
}
|
||||
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
},
|
||||
child: Text(
|
||||
"Terms of Service (click to view)",
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@@ -2,13 +2,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
class OnboardingCard extends StatelessWidget {
|
||||
int index;
|
||||
String title;
|
||||
String description;
|
||||
String imagePath;
|
||||
final String title;
|
||||
final String description;
|
||||
final String imagePath;
|
||||
|
||||
OnboardingCard({
|
||||
required this.index,
|
||||
const OnboardingCard({
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.imagePath,
|
||||
@@ -16,41 +14,32 @@ class OnboardingCard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Color baseColor = Theme.of(context).colorScheme.secondary;
|
||||
// have a different color for each card, incrementing the hue
|
||||
Color currentColor = baseColor.withAlpha(baseColor.alpha - index * 30);
|
||||
return Container(
|
||||
color: currentColor,
|
||||
alignment: Alignment.center,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.headlineLarge!.copyWith(
|
||||
color: Colors.white,
|
||||
),
|
||||
Padding(padding: EdgeInsets.only(top: 20)),
|
||||
SvgPicture.asset(
|
||||
imagePath,
|
||||
height: 200,
|
||||
),
|
||||
Padding(padding: EdgeInsets.only(top: 20)),
|
||||
SvgPicture.asset(
|
||||
imagePath,
|
||||
height: 200,
|
||||
),
|
||||
Padding(padding: EdgeInsets.only(top: 20)),
|
||||
Text(
|
||||
description,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Colors.white,
|
||||
),
|
||||
Padding(padding: EdgeInsets.only(top: 20)),
|
||||
Text(
|
||||
description,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
|
||||
]
|
||||
),
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,9 @@
|
||||
import 'package:anyway/structs/landmark.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:anyway/structs/landmark.dart';
|
||||
import 'package:anyway/modules/map_chooser.dart';
|
||||
|
||||
|
||||
class StepBetweenLandmarks extends StatefulWidget {
|
||||
final Landmark current;
|
||||
final Landmark next;
|
||||
@@ -19,12 +21,23 @@ class StepBetweenLandmarks extends StatefulWidget {
|
||||
class _StepBetweenLandmarksState extends State<StepBetweenLandmarks> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
int timeRounded = 5 * ((widget.current.tripTime?.inMinutes ?? 0) ~/ 5);
|
||||
// ~/ is integer division (rounding)
|
||||
int? time = widget.current.tripTime?.inMinutes;
|
||||
|
||||
// since landmarks might have been marked as visited, the next landmark might not be the immediate next in the list
|
||||
// => the precomputed trip time is not valid anymore
|
||||
if (widget.current.next != widget.next) {
|
||||
time = null;
|
||||
}
|
||||
|
||||
// round 0 travel time to 1 minute
|
||||
if (time != null && time < 1) {
|
||||
time = 1;
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: EdgeInsets.all(10),
|
||||
padding: EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
margin: const EdgeInsets.all(10),
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
left: BorderSide(width: 3.0, color: Colors.black),
|
||||
),
|
||||
@@ -33,21 +46,22 @@ class _StepBetweenLandmarksState extends State<StepBetweenLandmarks> {
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Icon(Icons.directions_walk),
|
||||
Text("~$timeRounded min", style: TextStyle(fontSize: 10)),
|
||||
const Icon(Icons.directions_walk),
|
||||
Text(
|
||||
time == null ? "" : "$time min",
|
||||
style: const TextStyle(fontSize: 10)
|
||||
),
|
||||
],
|
||||
),
|
||||
Spacer(),
|
||||
ElevatedButton(
|
||||
|
||||
const Spacer(),
|
||||
|
||||
ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
showMapChooser(context, widget.current, widget.next);
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.directions),
|
||||
Text("Directions"),
|
||||
],
|
||||
),
|
||||
icon: const Icon(Icons.directions),
|
||||
label: const Text("Directions"),
|
||||
)
|
||||
],
|
||||
),
|
||||
|
@@ -1,11 +1,12 @@
|
||||
import 'package:anyway/pages/current_trip.dart';
|
||||
import 'package:anyway/utils/load_trips.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:anyway/layout.dart';
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
|
||||
|
||||
class TripsOverview extends StatefulWidget {
|
||||
final Future<List<Trip>> trips;
|
||||
final SavedTrips trips;
|
||||
const TripsOverview({
|
||||
super.key,
|
||||
required this.trips,
|
||||
@@ -16,50 +17,34 @@ class TripsOverview extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _TripsOverviewState extends State<TripsOverview> {
|
||||
|
||||
Widget listBuild (BuildContext context, AsyncSnapshot<List<Trip>> snapshot) {
|
||||
Widget listBuild (BuildContext context, SavedTrips trips) {
|
||||
List<Widget> children;
|
||||
if (snapshot.hasData) {
|
||||
children = List<Widget>.generate(snapshot.data!.length, (index) {
|
||||
Trip trip = snapshot.data![index];
|
||||
return ListTile(
|
||||
title: FutureBuilder(
|
||||
future: trip.cityName,
|
||||
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return Text("Trip to ${snapshot.data}");
|
||||
} else if (snapshot.hasError) {
|
||||
return Text("Error: ${snapshot.error}");
|
||||
} else {
|
||||
return const Text("Trip to ...");
|
||||
}
|
||||
},
|
||||
),
|
||||
leading: Icon(Icons.pin_drop),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => BasePage(mainScreen: "map", trip: trip)
|
||||
)
|
||||
);
|
||||
List<Trip> items = trips.trips;
|
||||
children = List<Widget>.generate(items.length, (index) {
|
||||
Trip trip = items[index];
|
||||
return ListTile(
|
||||
title: FutureBuilder(
|
||||
future: trip.cityName,
|
||||
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return Text("Trip to ${snapshot.data}");
|
||||
} else if (snapshot.hasError) {
|
||||
return Text("Error: ${snapshot.error}");
|
||||
} else {
|
||||
return const Text("Trip to ...");
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
} else if (snapshot.hasError) {
|
||||
children = [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
color: Colors.red,
|
||||
size: 60,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
child: Text('Error: ${snapshot.error}'),
|
||||
),
|
||||
];
|
||||
} else {
|
||||
children = [Center(child: CircularProgressIndicator())];
|
||||
}
|
||||
leading: Icon(Icons.pin_drop),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => TripPage(trip: trip)
|
||||
)
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
return ListView(
|
||||
children: children,
|
||||
@@ -69,9 +54,11 @@ class _TripsOverviewState extends State<TripsOverview> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder(
|
||||
future: widget.trips,
|
||||
builder: listBuild,
|
||||
return ListenableBuilder(
|
||||
listenable: widget.trips,
|
||||
builder: (BuildContext context, Widget? child) {
|
||||
return listBuild(context, widget.trips);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import 'package:anyway/constants.dart';
|
||||
import 'package:anyway/layouts/scaffold.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:sliding_up_panel/sliding_up_panel.dart';
|
||||
|
||||
@@ -10,7 +11,7 @@ final Shader textGradient = APP_GRADIENT.createShader(Rect.fromLTWH(0.0, 0.0, 20
|
||||
TextStyle greeterStyle = TextStyle(
|
||||
foreground: Paint()..shader = textGradient,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 26
|
||||
fontSize: 25
|
||||
);
|
||||
|
||||
|
||||
@@ -27,11 +28,13 @@ class TripPage extends StatefulWidget {
|
||||
|
||||
|
||||
|
||||
class _TripPageState extends State<TripPage> {
|
||||
class _TripPageState extends State<TripPage> with ScaffoldLayout{
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SlidingUpPanel(
|
||||
return mainScaffold(
|
||||
context,
|
||||
child: SlidingUpPanel(
|
||||
// use panelBuilder instead of panel so that we can reuse the scrollcontroller for the listview
|
||||
panelBuilder: (scrollcontroller) => CurrentTripPanel(controller: scrollcontroller, trip: widget.trip),
|
||||
// using collapsed and panelBuilder seems to show both at the same time, so we include the greeter in the panelBuilder
|
||||
@@ -41,7 +44,7 @@ class _TripPageState extends State<TripPage> {
|
||||
maxHeight: MediaQuery.of(context).size.height * TRIP_PANEL_MAX_HEIGHT,
|
||||
// padding in this context is annoying: it offsets the notion of vertical alignment.
|
||||
// children that want to be centered vertically need to have their size adjusted by 2x the padding
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
// padding: const EdgeInsets.all(10.0),
|
||||
// Panel snapping should not be disabled because it significantly improves the user experience
|
||||
// panelSnapping: false
|
||||
borderRadius: const BorderRadius.only(topLeft: Radius.circular(25), topRight: Radius.circular(25)),
|
||||
@@ -52,6 +55,17 @@ class _TripPageState extends State<TripPage> {
|
||||
color: Colors.black,
|
||||
)
|
||||
],
|
||||
),
|
||||
title: FutureBuilder(
|
||||
future: widget.trip.cityName,
|
||||
builder: (context, snapshot) => Text(
|
||||
'Trip to ${snapshot.hasData ? snapshot.data! : "..."}',
|
||||
)
|
||||
),
|
||||
helpTexts: [
|
||||
'Current trip',
|
||||
'You can see and edit your current trip here. Swipe up from the bottom to see a detailed view of the recommendations.'
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import 'package:anyway/modules/new_trip_button.dart';
|
||||
import 'package:anyway/layouts/scaffold.dart';
|
||||
import 'package:anyway/modules/new_trip_options_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -14,28 +14,34 @@ class NewTripPage extends StatefulWidget {
|
||||
_NewTripPageState createState() => _NewTripPageState();
|
||||
}
|
||||
|
||||
class _NewTripPageState extends State<NewTripPage> {
|
||||
class _NewTripPageState extends State<NewTripPage> with ScaffoldLayout {
|
||||
final TextEditingController latController = TextEditingController();
|
||||
final TextEditingController lonController = TextEditingController();
|
||||
Trip trip = Trip();
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// floating search bar and map as a background
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('New Trip'),
|
||||
return mainScaffold(
|
||||
context,
|
||||
child: Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
NewTripMap(trip),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(15),
|
||||
child: NewTripLocationSearch(trip),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: NewTripOptionsButton(trip: trip),
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
NewTripMap(trip),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(15),
|
||||
child: NewTripLocationSearch(trip),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: NewTripOptionsButton(trip: trip),
|
||||
title: Text("New Trip"),
|
||||
helpTexts: [
|
||||
"Setting the start location",
|
||||
"To set the starting point, type a city name in the search bar. You can also navigate the map like you're used to and long press anywhere to set a starting point."
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,6 @@
|
||||
import 'package:anyway/layouts/scaffold.dart';
|
||||
import 'package:anyway/modules/new_trip_button.dart';
|
||||
import 'package:anyway/structs/landmark.dart';
|
||||
import 'package:anyway/structs/preferences.dart';
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
@@ -14,30 +16,40 @@ class NewTripPreferencesPage extends StatefulWidget {
|
||||
_NewTripPreferencesPageState createState() => _NewTripPreferencesPageState();
|
||||
}
|
||||
|
||||
class _NewTripPreferencesPageState extends State<NewTripPreferencesPage> {
|
||||
class _NewTripPreferencesPageState extends State<NewTripPreferencesPage> with ScaffoldLayout {
|
||||
UserPreferences preferences = UserPreferences();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: ListView(
|
||||
children: [
|
||||
// Center(
|
||||
// child: CircleAvatar(
|
||||
// radius: 100,
|
||||
// child: Icon(Icons.person, size: 100),
|
||||
// )
|
||||
// ),
|
||||
Padding(padding: EdgeInsets.only(top: 30)),
|
||||
Center(
|
||||
child: FutureBuilder(
|
||||
future: widget.trip.cityName,
|
||||
builder: (context, snapshot) => Text(
|
||||
'Your trip to ${snapshot.hasData ? snapshot.data! : "..."}',
|
||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)
|
||||
)
|
||||
)
|
||||
),
|
||||
// Ensure that the trip is "empty" save for the start landmark
|
||||
// This is necessary because users can swipe back to this page even after the trip has been created
|
||||
if (widget.trip.landmarks.length > 1) {
|
||||
Landmark start = widget.trip.landmarks.first;
|
||||
widget.trip.landmarks.clear();
|
||||
widget.trip.addLandmark(start);
|
||||
}
|
||||
|
||||
return mainScaffold(
|
||||
context,
|
||||
child: Scaffold(
|
||||
body: ListView(
|
||||
children: [
|
||||
// Center(
|
||||
// child: CircleAvatar(
|
||||
// radius: 100,
|
||||
// child: Icon(Icons.person, size: 100),
|
||||
// )
|
||||
// ),
|
||||
// Padding(padding: EdgeInsets.only(top: 30)),
|
||||
// Center(
|
||||
// child: FutureBuilder(
|
||||
// future: widget.trip.cityName,
|
||||
// builder: (context, snapshot) => Text(
|
||||
// 'Your trip to ${snapshot.hasData ? snapshot.data! : "..."}',
|
||||
// style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)
|
||||
// )
|
||||
// )
|
||||
// ),
|
||||
|
||||
Center(
|
||||
child: Padding(
|
||||
@@ -54,6 +66,17 @@ class _NewTripPreferencesPageState extends State<NewTripPreferencesPage> {
|
||||
]
|
||||
),
|
||||
floatingActionButton: NewTripButton(trip: widget.trip, preferences: preferences),
|
||||
),
|
||||
title: FutureBuilder(
|
||||
future: widget.trip.cityName,
|
||||
builder: (context, snapshot) => Text(
|
||||
'Your trip to ${snapshot.hasData ? snapshot.data! : "..."}',
|
||||
)
|
||||
),
|
||||
helpTexts: [
|
||||
'Trip preferences',
|
||||
'Set your preferences for this trip. These will be used to generate a custom itinerary.'
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
50
frontend/lib/pages/no_trips_page.dart
Normal file
@@ -0,0 +1,50 @@
|
||||
import 'package:anyway/pages/new_trip_location.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:anyway/layouts/scaffold.dart';
|
||||
class NoTripsPage extends StatefulWidget {
|
||||
const NoTripsPage({super.key});
|
||||
|
||||
@override
|
||||
State<NoTripsPage> createState() => _NoTripsPageState();
|
||||
}
|
||||
|
||||
class _NoTripsPageState extends State<NoTripsPage> with ScaffoldLayout {
|
||||
@override
|
||||
Widget build(BuildContext context) => mainScaffold(
|
||||
context,
|
||||
child: Scaffold(
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"No trips yet",
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
Text(
|
||||
"You can start a new trip by clicking the button below",
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const NewTripPage()
|
||||
)
|
||||
);
|
||||
},
|
||||
label: const Row(
|
||||
children: [
|
||||
Text("Start planning!"),
|
||||
Padding(padding: EdgeInsets.only(right: 8.0)),
|
||||
Icon(Icons.map_outlined)
|
||||
],
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
@@ -1,6 +1,37 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:anyway/constants.dart';
|
||||
import 'package:anyway/modules/onbarding_agreement_card.dart';
|
||||
import 'package:anyway/modules/onboarding_card.dart';
|
||||
import 'package:anyway/pages/new_trip_location.dart';
|
||||
import 'package:anyway/structs/agreement.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
|
||||
List<Widget> onboardingCards = [
|
||||
const OnboardingCard(
|
||||
title: "Welcome to anyway!",
|
||||
description: "Anyway helps you plan a city trip that suits your wishes.",
|
||||
imagePath: "assets/cld-server.svg"
|
||||
),
|
||||
const OnboardingCard(
|
||||
title: "Do your thing",
|
||||
description: "Bored by churches? No problem! Hate shopping? No worries! Instead of suggesting the generic trips that bore you, anyway will try to give you recommendations that really suit you.",
|
||||
imagePath: "assets/con-drill.svg"
|
||||
),
|
||||
const OnboardingCard(
|
||||
title: "Change your mind",
|
||||
description: "Feet get sore, the weather changes. Anyway understands that! Move or remove destinations, visit hidden gems along your journey, do your own thing. Anyway adapts to your spontaneous decisions.",
|
||||
imagePath: "assets/cel-snow-globe.svg"
|
||||
),
|
||||
const OnboardingCard(
|
||||
title: "Feeling lost?",
|
||||
description: "Whenever you are confused or need help with the app, look out for the question mark in the top right corner. Help is just a tap away!",
|
||||
imagePath: "assets/gen-lifebelt.svg"
|
||||
),
|
||||
];
|
||||
|
||||
|
||||
class OnboardingPage extends StatefulWidget {
|
||||
const OnboardingPage({super.key});
|
||||
@@ -10,38 +41,121 @@ class OnboardingPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _OnboardingPageState extends State<OnboardingPage> {
|
||||
final PageController _controller = PageController();
|
||||
late List<Widget> fullCards;
|
||||
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final PageController _controller = PageController();
|
||||
|
||||
Widget agreementCard = OnboardingAgreementCard(
|
||||
title: "The annoying stuff",
|
||||
description: "By using anyway, you agree to our terms and conditions and privacy policy. We don't use cookies or tracking, we don't store the data you submit. We are not responsible for any damage or loss caused by using anyway.",
|
||||
imagePath: "assets/con-warning.svg",
|
||||
agreementTextPath: "assets/terms_and_conditions.md",
|
||||
onAgreementChanged: onAgreementChanged,
|
||||
);
|
||||
// need to add the agreement from within the function because it needs to be able to call setState
|
||||
fullCards = onboardingCards + [agreementCard];
|
||||
|
||||
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
return Stack(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: APP_GRADIENT.colors,
|
||||
stops: [
|
||||
(_controller.hasClients ? (_controller.page ?? _controller.initialPage) : _controller.initialPage) / onboardingCards.length,
|
||||
(_controller.hasClients ? (_controller.page ?? _controller.initialPage) + 1 : _controller.initialPage + 1) / onboardingCards.length,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 100, sigmaY: 100),
|
||||
child: Container(
|
||||
color: Colors.black.withValues(alpha: 0.2)
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
PageView(
|
||||
// horizontally scrollable list of pages
|
||||
controller: _controller,
|
||||
|
||||
children: [
|
||||
OnboardingCard(index: 1, title: "Welcome to anyway!", description: "Anyway helps you plan a city trip that suits your wishes.", imagePath: "assets/city.svg"),
|
||||
OnboardingCard(index: 2, title: "Find your way", description: "Bored by churches? No problem! Hate shopping? No worries! More than showing you the typical 'must-sees' of a city, anyway will try to give you recommendations that really suit you.", imagePath: "assets/plan.svg"),
|
||||
OnboardingCard(index: 3, title: "Change your mind", description: "Life happens when you're busy making plans. Anyway understands that! Move or remove destinations, visit hidden gems along your journey, do your own thing. Anyway adapts to your spontaneous decisions.", imagePath: "assets/cat.svg"),
|
||||
],
|
||||
children: List.generate(
|
||||
fullCards.length,
|
||||
(index) {
|
||||
return Container(
|
||||
alignment: Alignment.center,
|
||||
child: fullCards[index],
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {
|
||||
if (_controller.page == 2) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const NewTripPage()
|
||||
|
||||
floatingActionButton: nextButton(_controller)
|
||||
);
|
||||
}
|
||||
|
||||
Widget nextButton(PageController controller) => AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
if ((_controller.hasClients ? (_controller.page ?? _controller.initialPage) : 0) != fullCards.length - 1) {
|
||||
return FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
controller.nextPage(duration: Duration(milliseconds: 500), curve: Curves.ease);
|
||||
},
|
||||
label: Icon(Icons.arrow_forward)
|
||||
);
|
||||
} else {
|
||||
// only allow the user to proceed if they have agreed to the terms and conditions
|
||||
Future<bool> hasAgreed = getAgreement().then((agreement) => agreement.agreed);
|
||||
|
||||
return FutureBuilder(
|
||||
future: hasAgreed,
|
||||
builder: (context, snapshot){
|
||||
if (snapshot.hasData && snapshot.data!) {
|
||||
return FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const NewTripPage()
|
||||
)
|
||||
);
|
||||
},
|
||||
label: const Row(
|
||||
children: [
|
||||
Text("Start planning!"),
|
||||
Padding(padding: EdgeInsets.only(right: 8.0)),
|
||||
Icon(Icons.map_outlined)
|
||||
],
|
||||
)
|
||||
);
|
||||
} else {
|
||||
_controller.nextPage(duration: Duration(milliseconds: 500), curve: Curves.ease);
|
||||
return Container();
|
||||
}
|
||||
},
|
||||
child: Icon(Icons.arrow_forward),
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
void onAgreementChanged(bool value) async {
|
||||
saveAgreement(value);
|
||||
// Update the state of the OnboardingPage
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,10 +1,12 @@
|
||||
import 'package:anyway/constants.dart';
|
||||
import 'package:anyway/main.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'package:anyway/main.dart';
|
||||
import 'package:anyway/constants.dart';
|
||||
import 'package:anyway/layouts/scaffold.dart';
|
||||
|
||||
|
||||
bool debugMode = false;
|
||||
|
||||
@@ -13,10 +15,11 @@ class SettingsPage extends StatefulWidget {
|
||||
_SettingsPageState createState() => _SettingsPageState();
|
||||
}
|
||||
|
||||
class _SettingsPageState extends State<SettingsPage> {
|
||||
class _SettingsPageState extends State<SettingsPage> with ScaffoldLayout {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
Widget build (BuildContext context) => mainScaffold(
|
||||
context,
|
||||
child: ListView(
|
||||
padding: EdgeInsets.all(15),
|
||||
children: [
|
||||
// First a round, centered image
|
||||
@@ -40,8 +43,13 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
|
||||
privacyInfo(),
|
||||
]
|
||||
);
|
||||
}
|
||||
),
|
||||
title: Text('Settings'),
|
||||
helpTexts: [
|
||||
'Settings',
|
||||
'Preferences set in this page are global and will affect the entire application.'
|
||||
],
|
||||
);
|
||||
|
||||
Widget setDebugMode() {
|
||||
return Row(
|
||||
@@ -169,7 +177,9 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
return Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Text('Our privacy policy is available under:'),
|
||||
Text('AnyWay does not collect or store any of the data that is submitted via the app. The location of your trip is not stored. The location feature is only used to show your current location on the map, it is not transmitted to our servers.', textAlign: TextAlign.center),
|
||||
Padding(padding: EdgeInsets.only(top: 3)),
|
||||
Text('Our full privacy policy is available under:', textAlign: TextAlign.center),
|
||||
|
||||
TextButton.icon(
|
||||
icon: Icon(Icons.info),
|
||||
|
17
frontend/lib/structs/agreement.dart
Normal file
@@ -0,0 +1,17 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
final class Agreement{
|
||||
bool agreed;
|
||||
|
||||
Agreement({required this.agreed});
|
||||
}
|
||||
|
||||
void saveAgreement(bool agreed) async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
prefs.setBool('agreed_to_terms_and_conditions', agreed);
|
||||
}
|
||||
|
||||
Future<Agreement> getAgreement() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
return Agreement(agreed: prefs.getBool('agreed_to_terms_and_conditions') ?? false);
|
||||
}
|
@@ -24,15 +24,15 @@ final class Landmark extends LinkedListEntry<Landmark>{
|
||||
// description to be shown in the overview
|
||||
final String? nameEN;
|
||||
final String? websiteURL;
|
||||
final String? wikipediaURL;
|
||||
final String? imageURL;
|
||||
String? imageURL; // not final because it can be patched
|
||||
final String? description;
|
||||
final Duration? duration;
|
||||
final bool? visited;
|
||||
bool visited;
|
||||
|
||||
// Next node
|
||||
// Next node is implicitly available through the LinkedListEntry mixin
|
||||
// final Landmark? next;
|
||||
final Duration? tripTime;
|
||||
Duration? tripTime;
|
||||
// the trip time depends on the next landmark, so it is not final
|
||||
|
||||
|
||||
Landmark({
|
||||
@@ -44,11 +44,10 @@ final class Landmark extends LinkedListEntry<Landmark>{
|
||||
|
||||
this.nameEN,
|
||||
this.websiteURL,
|
||||
this.wikipediaURL,
|
||||
this.imageURL,
|
||||
this.description,
|
||||
this.duration,
|
||||
this.visited,
|
||||
this.visited = false,
|
||||
|
||||
// this.next,
|
||||
this.tripTime,
|
||||
@@ -70,13 +69,12 @@ final class Landmark extends LinkedListEntry<Landmark>{
|
||||
final isSecondary = json['is_secondary'] as bool?;
|
||||
final nameEN = json['name_en'] as String?;
|
||||
final websiteURL = json['website_url'] as String?;
|
||||
final wikipediaURL = json['wikipedia_url'] as String?;
|
||||
final imageURL = json['image_url'] as String?;
|
||||
final description = json['description'] as String?;
|
||||
var duration = Duration(minutes: json['duration'] ?? 0) as Duration?;
|
||||
final visited = json['visited'] as bool?;
|
||||
var duration = Duration(minutes: json['duration']);
|
||||
final visited = json['visited'] ?? false;
|
||||
var tripTime = Duration(minutes: json['time_to_reach_next'] ?? 0) as Duration?;
|
||||
|
||||
|
||||
return Landmark(
|
||||
uuid: uuid,
|
||||
name: name,
|
||||
@@ -85,7 +83,6 @@ final class Landmark extends LinkedListEntry<Landmark>{
|
||||
isSecondary: isSecondary,
|
||||
nameEN: nameEN,
|
||||
websiteURL: websiteURL,
|
||||
wikipediaURL: wikipediaURL,
|
||||
imageURL: imageURL,
|
||||
description: description,
|
||||
duration: duration,
|
||||
@@ -112,7 +109,6 @@ final class Landmark extends LinkedListEntry<Landmark>{
|
||||
'is_secondary': isSecondary,
|
||||
'name_en': nameEN,
|
||||
'website_url': websiteURL,
|
||||
'wikipedia_url': wikipediaURL,
|
||||
'image_url': imageURL,
|
||||
'description': description,
|
||||
'duration': duration?.inMinutes,
|
||||
@@ -130,7 +126,7 @@ class LandmarkType {
|
||||
LandmarkType({required this.name, this.icon = const Icon(Icons.location_on)}) {
|
||||
switch (name) {
|
||||
case 'sightseeing':
|
||||
icon = const Icon(Icons.church);
|
||||
icon = const Icon(Icons.castle);
|
||||
break;
|
||||
case 'nature':
|
||||
icon = const Icon(Icons.eco);
|
||||
|
@@ -12,7 +12,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class Trip with ChangeNotifier {
|
||||
String uuid;
|
||||
int totalTime;
|
||||
Duration totalTime;
|
||||
LinkedList<Landmark> landmarks;
|
||||
// could be empty as well
|
||||
String? errorDescription;
|
||||
@@ -29,10 +29,22 @@ class Trip with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> landmarkPosition (Landmark landmark) async {
|
||||
int i = 0;
|
||||
for (Landmark l in landmarks) {
|
||||
if (l.uuid == landmark.uuid) {
|
||||
return i;
|
||||
} else if (l.type != typeStart && l.type != typeFinish) {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
Trip({
|
||||
this.uuid = 'pending',
|
||||
this.totalTime = 0,
|
||||
this.totalTime = Duration.zero,
|
||||
LinkedList<Landmark>? landmarks
|
||||
// a trip can be created with no landmarks, but the list should be initialized anyway
|
||||
}) : landmarks = landmarks ?? LinkedList<Landmark>();
|
||||
@@ -41,7 +53,7 @@ class Trip with ChangeNotifier {
|
||||
factory Trip.fromJson(Map<String, dynamic> json) {
|
||||
Trip trip = Trip(
|
||||
uuid: json['uuid'],
|
||||
totalTime: json['total_time'],
|
||||
totalTime: Duration(minutes: json['total_time']),
|
||||
);
|
||||
|
||||
return trip;
|
||||
@@ -49,9 +61,10 @@ class Trip with ChangeNotifier {
|
||||
|
||||
void loadFromJson(Map<String, dynamic> json) {
|
||||
uuid = json['uuid'];
|
||||
totalTime = json['total_time'];
|
||||
totalTime = Duration(minutes: json['total_time']);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void addLandmark(Landmark landmark) {
|
||||
landmarks.add(landmark);
|
||||
notifyListeners();
|
||||
@@ -62,16 +75,31 @@ class Trip with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void removeLandmark(Landmark landmark) {
|
||||
void removeLandmark (Landmark landmark) async {
|
||||
Landmark? previous = landmark.previous;
|
||||
Landmark? next = landmark.next;
|
||||
landmarks.remove(landmark);
|
||||
// removing the landmark means we need to recompute the time between the two adjoined landmarks
|
||||
if (previous != null && next != null) {
|
||||
// previous.next = next happens automatically since we are using a LinkedList
|
||||
this.totalTime -= previous.tripTime ?? Duration.zero;
|
||||
previous.tripTime = null;
|
||||
// TODO
|
||||
}
|
||||
this.totalTime -= landmark.tripTime ?? Duration.zero;
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
|
||||
void updateError(String error) {
|
||||
errorDescription = error;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void notifyUpdate(){
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
factory Trip.fromPrefs(SharedPreferences prefs, String uuid) {
|
||||
String? content = prefs.getString('trip_$uuid');
|
||||
Map<String, dynamic> json = jsonDecode(content!);
|
||||
@@ -80,14 +108,13 @@ class Trip with ChangeNotifier {
|
||||
log('Loading trip $uuid with first landmark $firstUUID');
|
||||
LinkedList<Landmark> landmarks = readLandmarks(prefs, firstUUID);
|
||||
trip.landmarks = landmarks;
|
||||
// notifyListeners();
|
||||
return trip;
|
||||
}
|
||||
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'uuid': uuid,
|
||||
'total_time': totalTime,
|
||||
'total_time': totalTime.inMinutes,
|
||||
'first_landmark_uuid': landmarks.first.uuid
|
||||
};
|
||||
|
||||
@@ -113,10 +140,3 @@ LinkedList<Landmark> readLandmarks(SharedPreferences prefs, String? firstUUID) {
|
||||
}
|
||||
return landmarks;
|
||||
}
|
||||
|
||||
|
||||
|
||||
void removeAllTripsFromPrefs () async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
prefs.clear();
|
||||
}
|
||||
|
@@ -1,32 +1,34 @@
|
||||
import "dart:convert";
|
||||
import "dart:developer";
|
||||
import "package:anyway/main.dart";
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import 'package:anyway/constants.dart';
|
||||
import "package:anyway/utils/load_landmark_image.dart";
|
||||
import "package:anyway/structs/landmark.dart";
|
||||
import "package:anyway/structs/trip.dart";
|
||||
import "package:anyway/structs/preferences.dart";
|
||||
|
||||
|
||||
Dio dio = Dio(
|
||||
BaseOptions(
|
||||
baseUrl: API_URL_BASE,
|
||||
connectTimeout: const Duration(seconds: 5),
|
||||
receiveTimeout: const Duration(seconds: 120),
|
||||
// also accept 500 errors, since we cannot rule out that the server is at fault. We still want to gracefully handle these errors
|
||||
validateStatus: (status) => status! <= 500,
|
||||
receiveDataWhenStatusError: true,
|
||||
// api is notoriously slow
|
||||
// headers: {
|
||||
// HttpHeaders.userAgentHeader: 'dio',
|
||||
// 'api': '1.0.0',
|
||||
// },
|
||||
contentType: Headers.jsonContentType,
|
||||
responseType: ResponseType.json,
|
||||
|
||||
BaseOptions(
|
||||
baseUrl: API_URL_BASE,
|
||||
connectTimeout: const Duration(seconds: 5),
|
||||
receiveTimeout: const Duration(seconds: 120),
|
||||
// also accept 500 errors, since we cannot rule out that the server is at fault. We still want to gracefully handle these errors
|
||||
validateStatus: (status) => status! <= 500,
|
||||
receiveDataWhenStatusError: true,
|
||||
// api is notoriously slow
|
||||
// headers: {
|
||||
// HttpHeaders.userAgentHeader: 'dio',
|
||||
// 'api': '1.0.0',
|
||||
// },
|
||||
contentType: Headers.jsonContentType,
|
||||
responseType: ResponseType.json,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
fetchTrip(
|
||||
Trip trip,
|
||||
UserPreferences preferences,
|
||||
@@ -81,10 +83,26 @@ fetchTrip(
|
||||
}
|
||||
|
||||
log(response.data.toString());
|
||||
// Also save the trip for the user's convenience
|
||||
savedTrips.addTrip(trip);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
patchLandmarkImage(Landmark landmark) async {
|
||||
// patch the landmark to include an image from an external source
|
||||
if (landmark.imageURL == null) {
|
||||
String? newUrl = await getImageUrlFromName(landmark.name);
|
||||
if (newUrl != null) {
|
||||
landmark.imageURL = newUrl;
|
||||
}
|
||||
} else if (landmark.imageURL!.contains("photos.app.goo.gl")) {
|
||||
// the image is a google photos link, we should get the image behind the link
|
||||
String? newUrl = await getImageUrlFromGooglePhotos(landmark.imageURL!);
|
||||
// also set the new url if it is null
|
||||
landmark.imageURL = newUrl;
|
||||
}
|
||||
}
|
||||
|
||||
Future<(Landmark, String?)> fetchLandmark(String uuid) async {
|
||||
final response = await dio.get(
|
||||
@@ -98,8 +116,10 @@ Future<(Landmark, String?)> fetchLandmark(String uuid) async {
|
||||
if (response.data["detail"] != null) {
|
||||
throw Exception(response.data["detail"]);
|
||||
}
|
||||
log(response.data.toString());
|
||||
// log(response.data.toString());
|
||||
Map<String, dynamic> json = response.data;
|
||||
String? nextUUID = json["next_uuid"];
|
||||
return (Landmark.fromJson(json), nextUUID);
|
||||
Landmark landmark = Landmark.fromJson(json);
|
||||
patchLandmarkImage(landmark);
|
||||
return (landmark, nextUUID);
|
||||
}
|
||||
|
50
frontend/lib/utils/get_first_page.dart
Normal file
@@ -0,0 +1,50 @@
|
||||
import 'package:anyway/main.dart';
|
||||
import 'package:anyway/pages/no_trips_page.dart';
|
||||
import 'package:anyway/structs/agreement.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
import 'package:anyway/pages/current_trip.dart';
|
||||
import 'package:anyway/pages/onboarding.dart';
|
||||
|
||||
|
||||
Widget getFirstPage() {
|
||||
// check if the user has already seen the onboarding and agreed to the terms of service
|
||||
return FutureBuilder(
|
||||
future: getAgreement(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
if (snapshot.hasData) {
|
||||
Agreement agrement = snapshot.data!;
|
||||
if (agrement.agreed) {
|
||||
return FutureBuilder(
|
||||
future: savedTrips.loadTrips(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
if (snapshot.hasData) {
|
||||
List<Trip> trips = snapshot.data!;
|
||||
if (trips.length > 0) {
|
||||
return TripPage(trip: trips[0]);
|
||||
} else {
|
||||
return NoTripsPage();
|
||||
}
|
||||
} else {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}
|
||||
} else {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return OnboardingPage();
|
||||
}
|
||||
} else {
|
||||
return OnboardingPage();
|
||||
}
|
||||
} else {
|
||||
return OnboardingPage();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
71
frontend/lib/utils/load_landmark_image.dart
Normal file
@@ -0,0 +1,71 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:fuzzywuzzy/model/extracted_result.dart';
|
||||
|
||||
const String baseUrl = "https://en.wikipedia.org/w/api.php";
|
||||
final Dio dio = Dio();
|
||||
|
||||
Future<int?> bestPageMatch(String title) async {
|
||||
final response = await dio.get(baseUrl, queryParameters: {
|
||||
"action": "query",
|
||||
"format": "json",
|
||||
"list": "prefixsearch",
|
||||
"pssearch": title,
|
||||
});
|
||||
|
||||
final data = jsonDecode(response.toString());
|
||||
log(data.toString());
|
||||
final List<dynamic> results = data["query"]["prefixsearch"] ?? {};
|
||||
final Map<String, int> titlesAndIds = {
|
||||
for (var d in results) d["title"]: d["pageid"]
|
||||
};
|
||||
if (titlesAndIds.isEmpty) {
|
||||
log("No pages found for $title");
|
||||
return null;
|
||||
}
|
||||
|
||||
// after the empty check, we can safely assume that there is a best match
|
||||
final ExtractedResult<String> bestMatch = extractOne(
|
||||
query: title,
|
||||
choices: titlesAndIds.keys.toList(),
|
||||
cutoff: 70,
|
||||
);
|
||||
return titlesAndIds[bestMatch.choice];
|
||||
}
|
||||
|
||||
Future<String?> getImageUrl(int pageId) async {
|
||||
final response = await dio.get(baseUrl, queryParameters: {
|
||||
"action": "query",
|
||||
"format": "json",
|
||||
"prop": "pageimages",
|
||||
"pageids": pageId,
|
||||
"pithumbsize": 500,
|
||||
});
|
||||
|
||||
final data = jsonDecode(response.toString());
|
||||
final pageData = data["query"]["pages"][pageId.toString()];
|
||||
return pageData["thumbnail"]?["source"];
|
||||
}
|
||||
|
||||
Future<String?> getImageUrlFromName(String title) async {
|
||||
int? pageId = await bestPageMatch(title);
|
||||
if (pageId == null) {
|
||||
return null;
|
||||
}
|
||||
return await getImageUrl(pageId);
|
||||
}
|
||||
|
||||
|
||||
Future<String?> getImageUrlFromGooglePhotos(String url) async {
|
||||
// this is a very simple implementation that just gets the image behind the link
|
||||
// it is not guaranteed to work for all google photos links
|
||||
final response = await dio.get(url);
|
||||
final data = response.toString();
|
||||
final int start = data.indexOf("https://lh3.googleusercontent.com");
|
||||
final int end = data.indexOf('"', start);
|
||||
return data.substring(start, end);
|
||||
}
|
@@ -1,19 +1,40 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
import 'package:anyway/structs/landmark.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
Future<List<Trip>> loadTrips() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
List<Trip> trips = [];
|
||||
Set<String> keys = prefs.getKeys();
|
||||
for (String key in keys) {
|
||||
if (key.startsWith('trip_')) {
|
||||
String uuid = key.replaceFirst('trip_', '');
|
||||
trips.add(Trip.fromPrefs(prefs, uuid));
|
||||
class SavedTrips extends ChangeNotifier {
|
||||
List<Trip> _trips = [];
|
||||
|
||||
List<Trip> get trips => _trips;
|
||||
|
||||
Future<List<Trip>> loadTrips() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
|
||||
List<Trip> trips = [];
|
||||
Set<String> keys = prefs.getKeys();
|
||||
for (String key in keys) {
|
||||
if (key.startsWith('trip_')) {
|
||||
String uuid = key.replaceFirst('trip_', '');
|
||||
trips.add(Trip.fromPrefs(prefs, uuid));
|
||||
}
|
||||
}
|
||||
_trips = trips;
|
||||
notifyListeners();
|
||||
return trips;
|
||||
}
|
||||
|
||||
void addTrip(Trip trip) async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
trip.toPrefs(prefs);
|
||||
_trips.add(trip);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void clearTrips () async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
prefs.clear();
|
||||
_trips = [];
|
||||
notifyListeners();
|
||||
}
|
||||
return trips;
|
||||
}
|
||||
|
@@ -21,10 +21,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
|
||||
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.11.0"
|
||||
version: "2.12.0"
|
||||
auto_size_text:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -37,10 +37,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
|
||||
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.1.2"
|
||||
cached_network_image:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -69,10 +69,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
|
||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
version: "1.4.0"
|
||||
checked_yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -93,18 +93,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: clock
|
||||
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
|
||||
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
version: "1.1.2"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.18.0"
|
||||
version: "1.19.1"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -149,10 +149,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
|
||||
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
version: "1.3.2"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -206,6 +206,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
flutter_markdown:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_markdown
|
||||
sha256: e7bbc718adc9476aa14cfddc1ef048d2e21e4e8f18311aaac723266db9f9e7b5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.6+2"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -232,6 +240,14 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
fuzzywuzzy:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: fuzzywuzzy
|
||||
sha256: "3004379ffd6e7f476a0c2091f38f16588dc45f67de7adf7c41aa85dec06b432c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
geocoding:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -404,18 +420,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
|
||||
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.5"
|
||||
version: "10.0.8"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
|
||||
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.5"
|
||||
version: "3.0.9"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -440,14 +456,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.5.0"
|
||||
markdown:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: markdown
|
||||
sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.3.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
|
||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.16+1"
|
||||
version: "0.12.17"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -460,10 +484,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
|
||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.15.0"
|
||||
version: "1.16.0"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -484,10 +508,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path
|
||||
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
|
||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.0"
|
||||
version: "1.9.1"
|
||||
path_parsing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -700,7 +724,7 @@ packages:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.99"
|
||||
version: "0.0.0"
|
||||
sliding_up_panel:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -713,10 +737,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
|
||||
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
version: "1.10.1"
|
||||
sprintf:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -745,18 +769,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
||||
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.11.1"
|
||||
version: "1.12.1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
|
||||
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
version: "2.1.4"
|
||||
stream_transform:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -769,10 +793,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
|
||||
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.4.1"
|
||||
synchronized:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -785,18 +809,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
|
||||
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
version: "1.2.2"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
|
||||
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.2"
|
||||
version: "0.7.4"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -913,10 +937,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
|
||||
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.2.5"
|
||||
version: "14.3.1"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -958,5 +982,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
sdks:
|
||||
dart: ">=3.5.0 <4.0.0"
|
||||
dart: ">=3.7.0-0 <4.0.0"
|
||||
flutter: ">=3.24.0"
|
||||
|
@@ -51,6 +51,8 @@ dependencies:
|
||||
flutter_launcher_icons: ^0.13.1
|
||||
permission_handler: ^11.3.1
|
||||
geolocator: ^13.0.1
|
||||
fuzzywuzzy: ^1.2.0
|
||||
flutter_markdown: ^0.7.6+2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
@@ -1,30 +0,0 @@
|
||||
// This is a basic Flutter widget test.
|
||||
//
|
||||
// To perform an interaction with a widget in your test, use the WidgetTester
|
||||
// utility in the flutter_test package. For example, you can send tap and scroll
|
||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
||||
// tree, read text, and verify that the values of widget properties are correct.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
// import 'package:anyway/main.dart';
|
||||
import 'package:anyway/layout.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||
// Build our app and trigger a frame.
|
||||
await tester.pumpWidget(BasePage(mainScreen: "map",));
|
||||
|
||||
// Verfiy that the title is displayed
|
||||
expect(find.text('City Nav'), findsOneWidget);
|
||||
|
||||
// Tap the '+' icon and trigger a frame.
|
||||
await tester.tap(find.byIcon(Icons.add));
|
||||
await tester.pump();
|
||||
|
||||
// Verify that our counter has incremented.
|
||||
expect(find.text('0'), findsNothing);
|
||||
expect(find.text('1'), findsOneWidget);
|
||||
});
|
||||
}
|
48
status
Normal file
@@ -0,0 +1,48 @@
|
||||
error: wrong number of arguments, should be from 1 to 2
|
||||
usage: git config [<options>]
|
||||
|
||||
Config file location
|
||||
--[no-]global use global config file
|
||||
--[no-]system use system config file
|
||||
--[no-]local use repository config file
|
||||
--[no-]worktree use per-worktree config file
|
||||
-f, --[no-]file <file>
|
||||
use given config file
|
||||
--[no-]blob <blob-id> read config from given blob object
|
||||
|
||||
Action
|
||||
--[no-]get get value: name [value-pattern]
|
||||
--[no-]get-all get all values: key [value-pattern]
|
||||
--[no-]get-regexp get values for regexp: name-regex [value-pattern]
|
||||
--[no-]get-urlmatch get value specific for the URL: section[.var] URL
|
||||
--[no-]replace-all replace all matching variables: name value [value-pattern]
|
||||
--[no-]add add a new variable: name value
|
||||
--[no-]unset remove a variable: name [value-pattern]
|
||||
--[no-]unset-all remove all matches: name [value-pattern]
|
||||
--[no-]rename-section rename section: old-name new-name
|
||||
--[no-]remove-section remove a section: name
|
||||
-l, --[no-]list list all
|
||||
--[no-]fixed-value use string equality when comparing values to 'value-pattern'
|
||||
-e, --[no-]edit open an editor
|
||||
--[no-]get-color find the color configured: slot [default]
|
||||
--[no-]get-colorbool find the color setting: slot [stdout-is-tty]
|
||||
|
||||
Type
|
||||
-t, --[no-]type <type>
|
||||
value is given this type
|
||||
--bool value is "true" or "false"
|
||||
--int value is decimal number
|
||||
--bool-or-int value is --bool or --int
|
||||
--bool-or-str value is --bool or string
|
||||
--path value is a path (file or directory name)
|
||||
--expiry-date value is an expiry date
|
||||
|
||||
Other
|
||||
-z, --[no-]null terminate values with NUL byte
|
||||
--[no-]name-only show variable names only
|
||||
--[no-]includes respect include directives on lookup
|
||||
--[no-]show-origin show origin of config (file, standard input, blob, command line)
|
||||
--[no-]show-scope show scope of config (worktree, local, global, system, command)
|
||||
--[no-]default <value>
|
||||
with --get, use default value when missing entry
|
||||
|