Compare commits
1 Commits
v0.0.31
...
9a04cac86c
Author | SHA1 | Date | |
---|---|---|---|
9a04cac86c |
@@ -1,17 +1,10 @@
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
tag:
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
PACKAGE_REGISTRY_ACCESS:
|
||||
required: true
|
||||
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
name: Build and push docker image
|
||||
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
@@ -19,13 +12,13 @@ jobs:
|
||||
steps:
|
||||
|
||||
- uses: https://gitea.com/actions/checkout@v4
|
||||
|
||||
|
||||
- name: Login to Docker Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: git.kluster.moll.re
|
||||
username: ${{ gitea.repository_owner }}
|
||||
password: ${{ secrets.PACKAGE_REGISTRY_ACCESS }}
|
||||
password: ${{ secrets.GITEA_TOKEN}}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
@@ -34,5 +27,4 @@ jobs:
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: backend
|
||||
tags: git.kluster.moll.re/anydev/anyway-backend:${{ inputs.tag }}
|
||||
push: true
|
||||
tags: git.kluster.moll.re/renoll/fast_network_navigation/backend:latest
|
@@ -1,24 +0,0 @@
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
|
||||
name: Build and deploy the backend to production
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
name: Build and push image
|
||||
uses: ./.gitea/workflows/workflow_build-image.yaml
|
||||
with:
|
||||
tag: stable
|
||||
secrets:
|
||||
PACKAGE_REGISTRY_ACCESS: ${{ secrets.PACKAGE_REGISTRY_ACCESS }}
|
||||
|
||||
deploy-prod:
|
||||
name: Deploy to production
|
||||
uses: ./.gitea/workflows/workflow_deploy-container.yaml
|
||||
with:
|
||||
overlay: prod
|
||||
secrets:
|
||||
KUBE_CONFIG: ${{ secrets.KUBE_CONFIG }}
|
||||
needs: build-and-push
|
@@ -1,26 +0,0 @@
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- backend/**
|
||||
|
||||
name: Build and deploy the backend to staging
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
name: Build and push image
|
||||
uses: ./.gitea/workflows/workflow_build-image.yaml
|
||||
with:
|
||||
tag: unstable
|
||||
secrets:
|
||||
PACKAGE_REGISTRY_ACCESS: ${{ secrets.PACKAGE_REGISTRY_ACCESS }}
|
||||
|
||||
deploy-prod:
|
||||
name: Deploy to staging
|
||||
uses: ./.gitea/workflows/workflow_deploy-container.yaml
|
||||
with:
|
||||
overlay: stg
|
||||
secrets:
|
||||
KUBE_CONFIG: ${{ secrets.KUBE_CONFIG }}
|
||||
needs: build-and-push
|
@@ -2,38 +2,31 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- frontend/**
|
||||
|
||||
|
||||
name: Build and release debug APK
|
||||
name: Build and release APK
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build APK
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: k8s
|
||||
steps:
|
||||
|
||||
- name: Install prerequisites
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y jq
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y xz-utils unzip
|
||||
|
||||
- uses: https://gitea.com/actions/checkout@v4
|
||||
|
||||
|
||||
- uses: https://github.com/actions/setup-java@v4
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'zulu'
|
||||
|
||||
- name: Fix flutter SDK folder permission
|
||||
run: git config --global --add safe.directory "*"
|
||||
|
||||
- uses: https://github.com/subosito/flutter-action@v2
|
||||
with:
|
||||
channel: stable
|
||||
flutter-version: 3.22.0
|
||||
flutter-version: 3.19.6
|
||||
cache: true
|
||||
|
||||
- name: Setup Android SDK
|
||||
@@ -42,26 +35,21 @@ jobs:
|
||||
- run: flutter pub get
|
||||
working-directory: ./frontend
|
||||
|
||||
- name: Add required secrets
|
||||
env:
|
||||
ANDROID_SECRETS_PROPERTIES: ${{ secrets.ANDROID_SECRETS_PROPERTIES }}
|
||||
run: |
|
||||
echo "$ANDROID_SECRETS_PROPERTIES" >> ./android/secrets.properties
|
||||
- run: flutter build apk --debug --split-per-abi
|
||||
working-directory: ./frontend
|
||||
|
||||
- name: Sanity check
|
||||
run: |
|
||||
ls
|
||||
ls -lah android
|
||||
working-directory: ./frontend
|
||||
|
||||
- run: flutter build apk --debug --split-per-abi --build-number=${{ gitea.run_number }}
|
||||
working-directory: ./frontend
|
||||
|
||||
- name: Upload APKs to artifacts
|
||||
uses: https://gitea.com/actions/upload-artifact@v3
|
||||
- name: Release APK
|
||||
uses: https://gitea.com/akkuman/gitea-release-action@v1
|
||||
with:
|
||||
name: app-release
|
||||
path: frontend/build/app/outputs/flutter-apk/
|
||||
if-no-files-found: error
|
||||
retention-days: 15
|
||||
files: ./frontendbuild/app/outputs/flutter-apk/*.apk
|
||||
name: Testing release
|
||||
release_name: testing
|
||||
tag: testing
|
||||
tag_name: testing
|
||||
release_body: "This is a testing release."
|
||||
prerelease: true
|
||||
token: ${{ secrets.GITEA_TOKEN }}
|
||||
env:
|
||||
NODE_OPTIONS: '--experimental-fetch'
|
||||
|
||||
|
@@ -1,34 +1,32 @@
|
||||
# on:
|
||||
# pull_request:
|
||||
# branches:
|
||||
# - main
|
||||
# paths:
|
||||
# - frontend/**
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
name: Build web
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Web
|
||||
runs-on: k8s
|
||||
steps:
|
||||
|
||||
- name: Install prerequisites
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y xz-utils
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: https://github.com/subosito/flutter-action@v2
|
||||
with:
|
||||
channel: stable
|
||||
flutter-version: 3.19.6
|
||||
cache: true
|
||||
|
||||
- run: flutter pub get
|
||||
working-directory: ./frontend
|
||||
|
||||
|
||||
# name: Build web
|
||||
|
||||
# jobs:
|
||||
# build:
|
||||
# name: Build Web
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
|
||||
# - name: Install prerequisites
|
||||
# run: |
|
||||
# sudo apt-get update
|
||||
# sudo apt-get install -y xz-utils
|
||||
|
||||
# - uses: actions/checkout@v4
|
||||
|
||||
# - uses: https://github.com/subosito/flutter-action@v2
|
||||
# with:
|
||||
# channel: stable
|
||||
# flutter-version: 3.19.6
|
||||
# cache: true
|
||||
|
||||
# - run: flutter pub get
|
||||
# working-directory: ./frontend
|
||||
|
||||
# - run: flutter build web
|
||||
# working-directory: ./frontend
|
||||
- run: flutter build web
|
||||
working-directory: ./frontend
|
||||
|
33
.gitea/workflows/frontend_test.yaml
Normal file
@@ -0,0 +1,33 @@
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
|
||||
name: Test code
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test code
|
||||
runs-on: k8s
|
||||
steps:
|
||||
|
||||
- name: Install prerequisites
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y xz-utils
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: https://github.com/subosito/flutter-action@v2
|
||||
with:
|
||||
channel: stable
|
||||
flutter-version: 3.19.6
|
||||
cache: true
|
||||
|
||||
- run: flutter pub get
|
||||
working-directory: ./frontend
|
||||
|
||||
- run: flutter test
|
||||
working-directory: ./frontend
|
@@ -1,39 +0,0 @@
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
|
||||
jobs:
|
||||
push-to-remote:
|
||||
# We want to use the macos runner provided by github actions. This requires to push to a remote first.
|
||||
# After the push we can use the action under frontend/.github/actions/ to deploy properly using fastlane on macos.
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: 'src'
|
||||
|
||||
- name: Checkout remote repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: 'dest'
|
||||
ref: 'main'
|
||||
github-server-url: 'https://github.com'
|
||||
repository: 'moll-re/anyway-frontend-builder'
|
||||
token: ${{ secrets.PUSH_GITHUB_API_TOKEN }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: true
|
||||
|
||||
- name: Copy files to remote repository
|
||||
run: cp -r src/frontend/. dest/
|
||||
|
||||
- name: Commit and push changes
|
||||
run: |
|
||||
cd dest
|
||||
git config --global user.email "me@moll.re"
|
||||
git config --global user.name "[bot]"
|
||||
git add .
|
||||
git commit -m "Automatic code update for tag"
|
||||
git tag -a ${{ github.ref_name }} -m "mirrored tag"
|
||||
git push origin main --tags
|
@@ -1,35 +0,0 @@
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
overlay:
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
KUBE_CONFIG:
|
||||
required: true
|
||||
|
||||
|
||||
name: Deploy the newly built container
|
||||
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- uses: https://gitea.com/actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: setup kubectl
|
||||
uses: https://github.com/azure/setup-kubectl@v4
|
||||
|
||||
- name: Set kubeconfig
|
||||
run: |
|
||||
echo "${{ secrets.KUBE_CONFIG }}" > kubeconfig
|
||||
|
||||
- name: Deploy to k8s
|
||||
run: |
|
||||
kubectl apply -k backend/deployment/overlays/${{ inputs.overlay }} --kubeconfig=kubeconfig
|
||||
kubectl -n anyway-backend rollout restart deployment/anyway-backend-${{ inputs.overlay }} --kubeconfig=kubeconfig
|
1
.gitignore
vendored
@@ -1 +0,0 @@
|
||||
cache/
|
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
||||
[submodule "backend/deployment"]
|
||||
path = backend/deployment
|
||||
url = https://git.kluster.moll.re/anydev/anyway-backend-deployment
|
52
.vscode/launch.json
vendored
@@ -1,52 +0,0 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
// backend - python using fastapi
|
||||
{
|
||||
"name": "Backend - debug",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"module": "uvicorn",
|
||||
"env": {
|
||||
"DEBUG": "true"
|
||||
},
|
||||
"args": [
|
||||
"--app-dir",
|
||||
"src",
|
||||
"main:app",
|
||||
"--reload",
|
||||
],
|
||||
"jinja": true,
|
||||
"cwd": "${workspaceFolder}/backend"
|
||||
},
|
||||
{
|
||||
"name": "Backend - tester",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"program": "src/tester.py",
|
||||
"env": {
|
||||
"DEBUG": "true"
|
||||
},
|
||||
"cwd": "${workspaceFolder}/backend"
|
||||
},
|
||||
// frontend - flutter app
|
||||
{
|
||||
"name": "Frontend - debug",
|
||||
"type": "dart",
|
||||
"request": "launch",
|
||||
"program": "lib/main.dart",
|
||||
"cwd": "${workspaceFolder}/frontend"
|
||||
},
|
||||
{
|
||||
"name": "Frontend - profile",
|
||||
"type": "dart",
|
||||
"request": "launch",
|
||||
"program": "lib/main.dart",
|
||||
"flutterMode": "profile",
|
||||
"cwd": "${workspaceFolder}/frontend"
|
||||
}
|
||||
]
|
||||
}
|
42
README.md
@@ -1,40 +1,16 @@
|
||||
# AnyWay - plan city trips your way
|
||||
|
||||
AnyWay is a mobile application that helps users plan city trips. The app allows users to specify their preferences and constraints, and then generates a personalized itinerary for them. The planning follows some guiding principles:
|
||||
- **Personalization**:The user's preferences should be reflected in the choice of destinations.
|
||||
- **Efficiency**:The itinerary should be optimized for the user's constraints.
|
||||
- **Flexibility**: We aknowledge that tourism is a dynamic activity, and that users may want to change their plans on the go.
|
||||
- **Discoverability**: Tourism is an inherently exploratory activity. Once a rough itinerary is generated, detours and spontaneous decisions should be encouraged.
|
||||
|
||||
|
||||
## Architecture
|
||||
This project is divided into two main components: a frontend and a backend. The frontend is a Flutter application that runs on Android and iOS. The backend is a python server that runs on a cloud provider. The two components communicate via a REST API. Since both components are very interdependent and share many data structures, we opted for a monorepo approach.
|
||||
|
||||
|
||||
### Frontend
|
||||
See the [frontend README](frontend/README.md) for more information. The application is centered around its map view, which displays the user's itinerary. This is based on the Google Maps API.
|
||||
|
||||
### Backend
|
||||
See the [backend README](backend/README.md) for more information. The backend is responsible for generating the itinerary based on the user's preferences and constraints. Rather than using google maps, we use the OpenStreetMap API, which is much more flexible.
|
||||
# fast_network_navigation
|
||||
|
||||
A new Flutter project.
|
||||
|
||||
## Getting Started
|
||||
Refer to the READMEs in the `frontend` and `backend` directories for instructions on how to run the application locally. Notable prerequisites include:
|
||||
- Flutter SDK
|
||||
- `google_maps_flutter` plugin
|
||||
- Python 3
|
||||
- `fastapi`
|
||||
- `OSMPythonTools`
|
||||
- `numpy, scipy`
|
||||
- Docker
|
||||
|
||||
This project is a starting point for a Flutter application.
|
||||
|
||||
## Releases
|
||||
The project is still in its early stages. We are currently working on a prototype that demonstrates the core functionality of the application.
|
||||
A few resources to get you started if this is your first Flutter project:
|
||||
|
||||
Official releases will be made available on the Google Play Store and the Apple App Store.
|
||||
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
||||
|
||||
|
||||
## Roadmap
|
||||
See the [project board](https://todos.kluster.moll.re/share/L8vZJGU45Vx9RzzVqTzyCPrpRybXm1OAqaW7YkWb/auth?view=list):
|
||||
<iframe src="https://todos.kluster.moll.re/share/L8vZJGU45Vx9RzzVqTzyCPrpRybXm1OAqaW7YkWb/auth?view=list" title="Todos" width="100%">
|
||||
For help getting started with Flutter development, view the
|
||||
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||
samples, guidance on mobile development, and a full API reference.
|
||||
|
169
backend/.gitignore
vendored
@@ -1,169 +0,0 @@
|
||||
# osm-cache and wikidata cache
|
||||
cache/
|
||||
apicache/
|
||||
|
||||
# wikidata throttle
|
||||
*.ctrl
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||
.pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
@@ -1,4 +1,4 @@
|
||||
FROM python:3.11-slim
|
||||
FROM python:3
|
||||
|
||||
WORKDIR /app
|
||||
COPY Pipfile Pipfile.lock .
|
||||
@@ -6,13 +6,6 @@ COPY Pipfile Pipfile.lock .
|
||||
RUN pip install pipenv
|
||||
RUN pipenv install --deploy --system
|
||||
|
||||
COPY src src
|
||||
COPY . /src
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
# Set environment variables used by the deployment. These can be overridden by the user using this image.
|
||||
ENV NUM_WORKERS=1
|
||||
ENV OSM_CACHE_DIR=/cache
|
||||
ENV MEMCACHED_HOST_PATH=none
|
||||
|
||||
CMD fastapi run src/main.py --port 8000 --workers $NUM_WORKERS
|
||||
CMD ["pipenv", "run", "python", "/app/src/main.py"]
|
||||
|
@@ -1,17 +1,14 @@
|
||||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[dev-packages]
|
||||
|
||||
[packages]
|
||||
numpy = "*"
|
||||
fastapi = "*"
|
||||
pydantic = "*"
|
||||
shapely = "*"
|
||||
scipy = "*"
|
||||
osmpythontools = "*"
|
||||
pywikibot = "*"
|
||||
pymemcache = "*"
|
||||
fastapi-cli = "*"
|
||||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
numpy = "*"
|
||||
scipy = "*"
|
||||
fastapi = "*"
|
||||
|
||||
[dev-packages]
|
||||
|
||||
[requires]
|
||||
python_version = "3.12"
|
||||
|
2550
backend/Pipfile.lock
generated
@@ -1,19 +0,0 @@
|
||||
# Backend
|
||||
|
||||
This repository contains the backend code for the application. It utilizes FastAPI that allows to quickly create a RESTful API that exposes the endpoints of the route optimizer.
|
||||
|
||||
|
||||
## Getting Started
|
||||
- The code of the python application is located in the `src` directory.
|
||||
- Package management is handled with `pipenv` and the dependencies are listed in the `Pipfile`.
|
||||
- Since the application is aimed to be deployed in a container, the `Dockerfile` is provided to build the image.
|
||||
|
||||
### Deployment
|
||||
To deploy the backend docker container, we use kubernetes. Modifications to the backend are automatically pushed to a two-stage environment through the CI pipeline. See [deployment/README](deployment/README.md] for further information.
|
||||
|
||||
The deployment configuration is included as a submodule in the `deployment` directory. The standalone repository is under [https://git.kluster.moll.re/anydev/anyway-backend-deployment/](https://git.kluster.moll.re/anydev/anyway-backend-deployment/).
|
||||
|
||||
|
||||
## Development
|
||||
TBD
|
||||
|
@@ -1,35 +0,0 @@
|
||||
import logging.config
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
LOCATION_PREFIX = Path('src')
|
||||
PARAMETERS_DIR = LOCATION_PREFIX / 'parameters'
|
||||
AMENITY_SELECTORS_PATH = PARAMETERS_DIR / 'amenity_selectors.yaml'
|
||||
LANDMARK_PARAMETERS_PATH = PARAMETERS_DIR / 'landmark_parameters.yaml'
|
||||
OPTIMIZER_PARAMETERS_PATH = PARAMETERS_DIR / 'optimizer_parameters.yaml'
|
||||
|
||||
|
||||
|
||||
cache_dir_string = os.getenv('OSM_CACHE_DIR', './cache')
|
||||
OSM_CACHE_DIR = Path(cache_dir_string)
|
||||
|
||||
|
||||
import logging
|
||||
# if we are in a debug session, set verbose and rich logging
|
||||
if os.getenv('DEBUG', "false") == "true":
|
||||
from rich.logging import RichHandler
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=[RichHandler()]
|
||||
)
|
||||
else:
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
)
|
||||
|
||||
|
||||
MEMCACHED_HOST_PATH = os.getenv('MEMCACHED_HOST_PATH', None)
|
||||
if MEMCACHED_HOST_PATH == "none":
|
||||
MEMCACHED_HOST_PATH = None
|
@@ -1,87 +1,23 @@
|
||||
import logging
|
||||
from fastapi import FastAPI, Query, Body, HTTPException
|
||||
|
||||
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.optimizer import Optimizer
|
||||
from utils.refiner import Refiner
|
||||
from persistence import client as cache_client
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = FastAPI()
|
||||
manager = LandmarkManager()
|
||||
optimizer = Optimizer()
|
||||
refiner = Refiner(optimizer=optimizer)
|
||||
|
||||
|
||||
@app.post("/trip/new")
|
||||
def new_trip(preferences: Preferences, start: tuple[float, float], end: tuple[float, float] | None = None) -> Trip:
|
||||
'''
|
||||
Main function to call the optimizer.
|
||||
:param preferences: the preferences specified by the user as the post body
|
||||
:param start: the coordinates of the starting point as a tuple of floats (as url query parameters)
|
||||
:param end: the coordinates of the finishing point as a tuple of floats (as url query parameters)
|
||||
:return: the uuid of the first landmark in the optimized route
|
||||
'''
|
||||
if preferences is None:
|
||||
raise HTTPException(status_code=406, detail="Preferences not provided")
|
||||
if preferences.shopping.score == 0 and preferences.sightseeing.score == 0 and preferences.nature.score == 0:
|
||||
raise HTTPException(status_code=406, detail="All preferences are 0.")
|
||||
if start is None:
|
||||
raise HTTPException(status_code=406, detail="Start coordinates not provided")
|
||||
if end is None:
|
||||
end = start
|
||||
logger.info("No end coordinates provided. Using start=end.")
|
||||
|
||||
start_landmark = Landmark(name='start', type='start', location=(start[0], start[1]), osm_type='start', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
|
||||
end_landmark = Landmark(name='finish', type='finish', location=(end[0], end[1]), osm_type='end', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
|
||||
|
||||
# Generate the landmarks from the start location
|
||||
landmarks, landmarks_short = manager.generate_landmarks_list(
|
||||
center_coordinates = start,
|
||||
preferences = preferences
|
||||
)
|
||||
|
||||
# insert start and finish to the landmarks list
|
||||
landmarks_short.insert(0, start_landmark)
|
||||
landmarks_short.append(end_landmark)
|
||||
|
||||
# First stage optimization
|
||||
try:
|
||||
base_tour = optimizer.solve_optimization(preferences.max_time_minute, landmarks_short)
|
||||
except ArithmeticError:
|
||||
raise HTTPException(status_code=500, detail="No solution found")
|
||||
except TimeoutError:
|
||||
raise HTTPException(status_code=500, detail="Optimzation took too long")
|
||||
|
||||
# Second stage optimization
|
||||
refined_tour = refiner.refine_optimization(landmarks, base_tour, preferences.max_time_minute, preferences.detour_tolerance_minute)
|
||||
|
||||
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)
|
||||
return trip
|
||||
|
||||
|
||||
#### For already existing trips/landmarks
|
||||
@app.get("/trip/{trip_uuid}")
|
||||
def get_trip(trip_uuid: str) -> Trip:
|
||||
try:
|
||||
trip = cache_client.get(f"trip_{trip_uuid}")
|
||||
return trip
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="Trip not found")
|
||||
|
||||
|
||||
@app.get("/landmark/{landmark_uuid}")
|
||||
def get_landmark(landmark_uuid: str) -> Landmark:
|
||||
try:
|
||||
landmark = cache_client.get(f"landmark_{landmark_uuid}")
|
||||
return landmark
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="Landmark not found")
|
||||
import fastapi
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Destination:
|
||||
name: str
|
||||
location: tuple
|
||||
attractiveness: int
|
||||
|
||||
|
||||
|
||||
d = Destination()
|
||||
|
||||
|
||||
|
||||
def get_route() -> list[Destination]:
|
||||
return {"route": "Hello World"}
|
||||
|
||||
endpoint = ("/get_route", get_route)
|
||||
end
|
||||
if __name__ == "__main__":
|
||||
fastapi.run()
|
||||
|
206
backend/src/optimizer.py
Normal file
@@ -0,0 +1,206 @@
|
||||
from scipy.optimize import linprog
|
||||
import numpy as np
|
||||
from scipy.linalg import block_diag
|
||||
|
||||
|
||||
# Defines the landmark class (aka some place there is to visit)
|
||||
class landmark :
|
||||
def __init__(self, name: str, attractiveness: int, loc: tuple):
|
||||
self.name = name
|
||||
self.attractiveness = attractiveness
|
||||
self.loc = loc
|
||||
|
||||
# Convert the result (edges from j to k like d_25 = edge between vertex 2 and vertex 5) into the list of indices corresponding to the landmarks
|
||||
def untangle(res: list) :
|
||||
N = len(res) # length of res
|
||||
L = int(np.sqrt(N)) # number of landmarks. CAST INTO INT but should not be a problem because N = L**2 by def.
|
||||
n_landmarks = res.sum() # number of visited landmarks
|
||||
visit_order = []
|
||||
cnt = 0
|
||||
|
||||
if n_landmarks % 2 == 1 : # if odd number of visited checkpoints
|
||||
for i in range(L) :
|
||||
for j in range(L) :
|
||||
if res[i*L + j] == 1 : # if index is 1
|
||||
cnt += 1 # increment counter
|
||||
if cnt % 2 == 1 : # if counter odd
|
||||
visit_order.append(i)
|
||||
visit_order.append(j)
|
||||
else : # if even number of ones
|
||||
for i in range(L) :
|
||||
for j in range(L) :
|
||||
if res[i*L + j] == 1 : # if index is one
|
||||
cnt += 1 # increment counter
|
||||
if j % (L-1) == 0 : # if last node
|
||||
visit_order.append(j) # append only the last index
|
||||
return visit_order # return
|
||||
if cnt % 2 == 1 :
|
||||
visit_order.append(i)
|
||||
visit_order.append(j)
|
||||
return visit_order
|
||||
|
||||
# Just to print the result
|
||||
def print_res(res: list, P) :
|
||||
X = abs(res.x)
|
||||
order = untangle(X)
|
||||
|
||||
# print("Optimal value:", -res.fun) # Minimization, so we negate to get the maximum
|
||||
# print("Optimal point:", res.x)
|
||||
# N = int(np.sqrt(len(X)))
|
||||
# for i in range(N):
|
||||
# print(X[i*N:i*N+N])
|
||||
# print(order)
|
||||
|
||||
if (X.sum()+1)**2 == len(X) :
|
||||
print('\nAll landmarks can be visited within max_steps, the following order is most likely not the fastest')
|
||||
else :
|
||||
print('Could not visit all the landmarks, the following order could be the fastest but not sure')
|
||||
print("Order of visit :")
|
||||
for i, elem in enumerate(landmarks) :
|
||||
if i in order : print('- ' + elem.name)
|
||||
|
||||
steps = path_length(P, abs(res.x))
|
||||
print("\nSteps walked : " + str(steps))
|
||||
|
||||
# Constraint to use only the upper triangular indices for travel
|
||||
def break_sym(landmarks, A_eq, b_eq):
|
||||
L = len(landmarks)
|
||||
l = [0]*L*L
|
||||
for i in range(L) :
|
||||
for j in range(L) :
|
||||
if i >= j :
|
||||
l[j+i*L] = 1
|
||||
|
||||
A_eq = np.vstack((A_eq,l))
|
||||
b_eq.append(0)
|
||||
|
||||
return A_eq, b_eq
|
||||
|
||||
# Constraint to respect max number of travels
|
||||
def respect_number(landmarks, A_ub, b_ub):
|
||||
h = []
|
||||
for i in range(len(landmarks)) : h.append([1]*len(landmarks))
|
||||
T = block_diag(*h)
|
||||
return np.vstack((A_ub, T)), b_ub + [1]*len(landmarks)
|
||||
|
||||
# Constraint to tie the problem together and have a connected path
|
||||
def respect_order(landmarks: list, A_eq, b_eq):
|
||||
N = len(landmarks)
|
||||
for i in range(N-1) : # Prevent stacked ones
|
||||
if i == 0 :
|
||||
continue
|
||||
else :
|
||||
l = [0]*N
|
||||
l[i] = -1
|
||||
l = l*N
|
||||
for j in range(N) :
|
||||
l[i*N + j] = 1
|
||||
|
||||
A_eq = np.vstack((A_eq,l))
|
||||
b_eq.append(0)
|
||||
|
||||
return A_eq, b_eq
|
||||
|
||||
# Compute manhattan distance between 2 locations
|
||||
def manhattan_distance(loc1: tuple, loc2: tuple):
|
||||
x1, y1 = loc1
|
||||
x2, y2 = loc2
|
||||
return abs(x1 - x2) + abs(y1 - y2)
|
||||
|
||||
# Constraint to not stay in position
|
||||
def init_eq_not_stay(landmarks):
|
||||
L = len(landmarks)
|
||||
l = [0]*L*L
|
||||
for i in range(L) :
|
||||
for j in range(L) :
|
||||
if j == i :
|
||||
l[j + i*L] = 1
|
||||
#A_eq = np.array([np.array(xi) for xi in A_eq]) # Must convert A_eq into an np array
|
||||
l = np.array(np.array(l))
|
||||
return [l], [0]
|
||||
|
||||
# Initialize A and c. Compute the distances from all landmarks to each other and store attractiveness
|
||||
# We want to maximize the sightseeing : max(c) st. A*x < b and A_eq*x = b_eq
|
||||
def init_ub_dist(landmarks: list, max_steps: int):
|
||||
# Objective function coefficients. a*x1 + b*x2 + c*x3 + ...
|
||||
c = []
|
||||
# Coefficients of inequality constraints (left-hand side)
|
||||
A = []
|
||||
for i, spot1 in enumerate(landmarks) :
|
||||
dist_table = [0]*len(landmarks)
|
||||
c.append(-spot1.attractiveness)
|
||||
for j, spot2 in enumerate(landmarks) :
|
||||
dist_table[j] = manhattan_distance(spot1.loc, spot2.loc)
|
||||
A.append(dist_table)
|
||||
c = c*len(landmarks)
|
||||
A_ub = []
|
||||
for line in A :
|
||||
A_ub += line
|
||||
return c, A_ub, [max_steps]
|
||||
|
||||
# Go through the landmarks and force the optimizer to use landmarks where attractiveness is set to -1
|
||||
def respect_user_mustsee(landmarks: list, A_eq: list, b_eq: list) :
|
||||
L = len(landmarks)
|
||||
for i, elem in enumerate(landmarks) :
|
||||
if elem.attractiveness == -1 :
|
||||
l = [0]*L*L
|
||||
if elem.name != "arrivée" :
|
||||
for j in range(L) :
|
||||
l[j +i*L] = 1
|
||||
else : # This ensures we go to goal
|
||||
for k in range(L-1) :
|
||||
l[k*L+L-1] = 1
|
||||
A_eq = np.vstack((A_eq,l))
|
||||
b_eq.append(1)
|
||||
return A_eq, b_eq
|
||||
|
||||
# Computes the path length given path matrix (dist_table) and a result
|
||||
def path_length(P: list, resx: list) :
|
||||
return np.dot(P, resx)
|
||||
|
||||
# Initialize all landmarks (+ start and goal). Order matters here
|
||||
landmarks = []
|
||||
landmarks.append(landmark("départ", -1, (0, 0)))
|
||||
landmarks.append(landmark("concorde", -1, (5,5)))
|
||||
landmarks.append(landmark("tour eiffel", 99, (1,1))) # PUT IN JSON
|
||||
landmarks.append(landmark("arc de triomphe", 99, (2,3)))
|
||||
landmarks.append(landmark("louvre", 70, (4,2)))
|
||||
landmarks.append(landmark("montmartre", 20, (0,2)))
|
||||
landmarks.append(landmark("arrivée", -1, (0, 0)))
|
||||
|
||||
|
||||
|
||||
# CONSTRAINT TO RESPECT MAX NUMBER OF STEPS
|
||||
max_steps = 25
|
||||
|
||||
# SET CONSTRAINTS FOR INEQUALITY
|
||||
c, A_ub, b_ub = init_ub_dist(landmarks, max_steps) # Add the distances from each landmark to the other
|
||||
P = A_ub # store the paths for later. Needed to compute path length
|
||||
A_ub, b_ub = respect_number(landmarks, A_ub, b_ub) # Respect max number of visits.
|
||||
|
||||
# SET CONSTRAINTS FOR EQUALITY
|
||||
A_eq, b_eq = init_eq_not_stay(landmarks) # Force solution not to stay in same place
|
||||
A_eq, b_eq = respect_user_mustsee(landmarks, A_eq, b_eq) # Check if there are user_defined must_see. Also takes care of start/goal
|
||||
A_eq, b_eq = break_sym(landmarks, A_eq, b_eq) # break the symmetry. Only use the upper diagonal values
|
||||
A_eq, b_eq = respect_order(landmarks, A_eq, b_eq) # Respect order of visit (only works when max_steps is limiting factor)
|
||||
|
||||
# Bounds for variables (x can only be 0 or 1)
|
||||
x_bounds = [(0, 1)] * len(c)
|
||||
|
||||
# Solve linear programming problem
|
||||
res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq = b_eq, bounds=x_bounds, method='highs', integrality=3)
|
||||
|
||||
# Raise error if no solution is found
|
||||
if not res.success :
|
||||
raise ValueError("No solution has been found, please adapt your max steps")
|
||||
|
||||
# Print result
|
||||
print_res(res, P)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@@ -1,90 +0,0 @@
|
||||
# Tags were picked mostly arbitrarily, based on the OSM wiki and the OSM tags page.
|
||||
# See https://taginfo.openstreetmap.org for more inspiration.
|
||||
|
||||
nature:
|
||||
leisure: park
|
||||
geological: ''
|
||||
natural:
|
||||
- geyser
|
||||
- hot_spring
|
||||
- arch
|
||||
- volcano
|
||||
- stone
|
||||
tourism:
|
||||
- alpine_hut
|
||||
- viewpoint
|
||||
- zoo
|
||||
- resort
|
||||
- picnic_site
|
||||
water:
|
||||
- pond
|
||||
- lake
|
||||
- river
|
||||
- basin
|
||||
- stream
|
||||
- lagoon
|
||||
- rapids
|
||||
waterway:
|
||||
- waterfall
|
||||
- river
|
||||
- canal
|
||||
- dam
|
||||
- dock
|
||||
- boatyard
|
||||
|
||||
|
||||
shopping:
|
||||
shop:
|
||||
- department_store
|
||||
- mall
|
||||
|
||||
sightseeing:
|
||||
tourism:
|
||||
- museum
|
||||
- attraction
|
||||
- gallery
|
||||
- artwork
|
||||
- aquarium
|
||||
|
||||
historic: ''
|
||||
amenity:
|
||||
- planetarium
|
||||
- place_of_worship
|
||||
- fountain
|
||||
- townhall
|
||||
water:
|
||||
- reflecting_pool
|
||||
bridge:
|
||||
- aqueduct
|
||||
- viaduct
|
||||
- boardwalk
|
||||
- cantilever
|
||||
- abandoned
|
||||
building:
|
||||
- church
|
||||
- chapel
|
||||
- mosque
|
||||
- synagogue
|
||||
- ruins
|
||||
- temple
|
||||
- government
|
||||
- cathedral
|
||||
- castle
|
||||
- museum
|
||||
|
||||
|
||||
|
||||
|
||||
# to be used later on
|
||||
restauration:
|
||||
shop:
|
||||
- coffee
|
||||
- bakery
|
||||
- restaurant
|
||||
- pastry
|
||||
amenity:
|
||||
- restaurant
|
||||
- cafe
|
||||
- ice_cream
|
||||
- food_court
|
||||
- biergarten
|
@@ -1,11 +0,0 @@
|
||||
city_bbox_side: 7500 #m
|
||||
radius_close_to: 50
|
||||
church_coeff: 0.5
|
||||
nature_coeff: 1.25
|
||||
overall_coeff: 10
|
||||
tag_exponent: 1.15
|
||||
image_bonus: 10
|
||||
viewpoint_bonus: 15
|
||||
wikipedia_bonus: 6
|
||||
N_important: 40
|
||||
pay_bonus: -1
|
@@ -1,6 +0,0 @@
|
||||
detour_factor: 1.4
|
||||
detour_corridor_width: 300
|
||||
average_walking_speed: 4.8
|
||||
max_landmarks: 10
|
||||
max_landmarks_refiner: 30
|
||||
overshoot: 1.8
|
@@ -1,28 +0,0 @@
|
||||
from pymemcache.client.base import Client
|
||||
from pymemcache import serde
|
||||
|
||||
import constants
|
||||
|
||||
|
||||
class DummyClient:
|
||||
_data = {}
|
||||
def set(self, key, value, **kwargs):
|
||||
self._data[key] = value
|
||||
|
||||
def set_many(self, data, **kwargs):
|
||||
self._data.update(data)
|
||||
|
||||
def get(self, key, **kwargs):
|
||||
return self._data[key]
|
||||
|
||||
|
||||
if constants.MEMCACHED_HOST_PATH is None:
|
||||
client = DummyClient()
|
||||
else:
|
||||
client = Client(
|
||||
constants.MEMCACHED_HOST_PATH,
|
||||
timeout = 1,
|
||||
allow_unicode_keys = True,
|
||||
encoding = 'utf-8',
|
||||
serde = serde.pickle_serde
|
||||
)
|
@@ -1,52 +0,0 @@
|
||||
from typing import Optional, Literal
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
# Output to frontend
|
||||
class Landmark(BaseModel) :
|
||||
|
||||
# Properties of the landmark
|
||||
name : str
|
||||
type: Literal['sightseeing', 'nature', 'shopping', 'start', 'finish']
|
||||
location : tuple
|
||||
osm_type : str
|
||||
osm_id : int
|
||||
attractiveness : int
|
||||
n_tags : int
|
||||
image_url : Optional[str] = None
|
||||
website_url : Optional[str] = None
|
||||
description : Optional[str] = None # TODO future
|
||||
duration : Optional[int] = 0
|
||||
name_en : Optional[str] = None
|
||||
|
||||
# Unique ID of a given landmark
|
||||
uuid: str = Field(default_factory=uuid4)
|
||||
|
||||
# Additional properties depending on specific tour
|
||||
must_do : Optional[bool] = False
|
||||
must_avoid : Optional[bool] = False
|
||||
is_secondary : Optional[bool] = False # TODO future
|
||||
|
||||
time_to_reach_next : Optional[int] = 0
|
||||
next_uuid : Optional[str] = None
|
||||
|
||||
def __str__(self) -> str:
|
||||
time_to_next_str = f", time_to_next={self.time_to_reach_next}" if self.time_to_reach_next else ""
|
||||
is_secondary_str = f", secondary" if self.is_secondary else ""
|
||||
type_str = '(' + self.type + ')'
|
||||
if self.type in ["start", "finish", "nature", "shopping"] : type_str += '\t '
|
||||
return f'Landmark{type_str}: [{self.name} @{self.location}, score={self.attractiveness}{time_to_next_str}{is_secondary_str}]'
|
||||
|
||||
def distance(self, value: 'Landmark') -> float:
|
||||
return (self.location[0] - value.location[0])**2 + (self.location[1] - value.location[1])**2
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.name)
|
||||
|
||||
def __eq__(self, value: 'Landmark') -> bool:
|
||||
# eq and hash must be consistent
|
||||
# in particular, if two objects are equal, their hash must be equal
|
||||
# uuid and osm_id are just shortcuts to avoid comparing all the properties
|
||||
# if they are equal, we know that the name is also equal and in turn the hash is equal
|
||||
return self.uuid == value.uuid or self.osm_id == value.osm_id or (self.name == value.name and self.distance(value) < 0.001)
|
@@ -1,64 +0,0 @@
|
||||
from .landmark import Landmark
|
||||
from utils.get_time_separation import get_time
|
||||
|
||||
class LinkedLandmarks:
|
||||
"""
|
||||
A list of landmarks that are linked together, e.g. in a route.
|
||||
Each landmark serves as a node in the linked list, but since we expect these to be consumed through the rest API, a pythonic reference to the next landmark is not well suited. Instead we use the uuid of the next landmark to reference the next landmark in the list. This is not very efficient, but appropriate for the expected use case ("short" trips with onyl few landmarks).
|
||||
"""
|
||||
|
||||
_landmarks = list[Landmark]
|
||||
total_time: int = 0
|
||||
|
||||
def __init__(self, data: list[Landmark] = None) -> None:
|
||||
"""
|
||||
Initialize a new LinkedLandmarks object. This expects an ORDERED list of landmarks, where the first landmark is the starting point and the last landmark is the end point.
|
||||
|
||||
Args:
|
||||
data (list[Landmark], optional): The list of landmarks that are linked together. Defaults to None.
|
||||
"""
|
||||
self._landmarks = data if data else []
|
||||
self._link_landmarks()
|
||||
|
||||
|
||||
def _link_landmarks(self) -> None:
|
||||
"""
|
||||
Create the links between the landmarks in the list by setting their .next_uuid and the .time_to_next attributes.
|
||||
"""
|
||||
|
||||
# Mark secondary landmarks as such
|
||||
self.update_secondary_landmarks()
|
||||
|
||||
|
||||
for i, landmark in enumerate(self._landmarks[:-1]):
|
||||
landmark.next_uuid = self._landmarks[i + 1].uuid
|
||||
time_to_next = get_time(landmark.location, self._landmarks[i + 1].location)
|
||||
landmark.time_to_reach_next = time_to_next
|
||||
self.total_time += time_to_next
|
||||
|
||||
self._landmarks[-1].next_uuid = None
|
||||
self._landmarks[-1].time_to_reach_next = 0
|
||||
|
||||
def update_secondary_landmarks(self) -> None:
|
||||
# Extract the attractiveness scores and sort them in descending order
|
||||
scores = sorted([landmark.attractiveness for landmark in self._landmarks], reverse=True)
|
||||
|
||||
# Determine the 10th highest score
|
||||
if len(scores) >= 10:
|
||||
threshold_score = scores[9]
|
||||
else:
|
||||
# If there are fewer than 10 landmarks, use the lowest score in the list as the threshold
|
||||
threshold_score = min(scores) if scores else 0
|
||||
|
||||
# Update 'is_secondary' for landmarks with attractiveness below the threshold score
|
||||
for landmark in self._landmarks:
|
||||
if landmark.attractiveness < threshold_score and landmark.type not in ["start", "finish"]:
|
||||
landmark.is_secondary = True
|
||||
|
||||
|
||||
def __getitem__(self, index: int) -> Landmark:
|
||||
return self._landmarks[index]
|
||||
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"LinkedLandmarks [{' ->'.join([str(landmark) for landmark in self._landmarks])}]"
|
@@ -1,20 +0,0 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, Literal
|
||||
|
||||
class Preference(BaseModel) :
|
||||
type: Literal['sightseeing', 'nature', 'shopping', 'start', 'finish']
|
||||
score: int # score could be from 1 to 5
|
||||
|
||||
# Input for optimization
|
||||
class Preferences(BaseModel) :
|
||||
# Sightseeing / History & Culture (Musées, bâtiments historiques, opéras, églises)
|
||||
sightseeing : Preference
|
||||
|
||||
# Nature (parcs, jardins, rivières, plages)
|
||||
nature: Preference
|
||||
|
||||
# Shopping (diriger plutôt vers des zones / rues commerçantes)
|
||||
shopping : Preference
|
||||
|
||||
max_time_minute: Optional[int] = 6*60
|
||||
detour_tolerance_minute: Optional[int] = 0
|
@@ -1,31 +0,0 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from pymemcache.client.base import Client
|
||||
|
||||
from .linked_landmarks import LinkedLandmarks
|
||||
import uuid
|
||||
|
||||
class Trip(BaseModel):
|
||||
uuid: str = Field(default_factory=uuid.uuid4)
|
||||
total_time: int
|
||||
first_landmark_uuid: str
|
||||
|
||||
|
||||
@classmethod
|
||||
def from_linked_landmarks(self, landmarks: LinkedLandmarks, cache_client: Client) -> "Trip":
|
||||
"""
|
||||
Initialize a new Trip object and ensure it is stored in the cache.
|
||||
"""
|
||||
trip = Trip(
|
||||
total_time = landmarks.total_time,
|
||||
first_landmark_uuid = str(landmarks[0].uuid)
|
||||
)
|
||||
|
||||
# Store the trip in the cache
|
||||
cache_client.set(f"trip_{trip.uuid}", trip)
|
||||
# make sure to await the result (noreply=False). Otherwise the cache might not be inplace when the trip is actually requested
|
||||
cache_client.set_many({f"landmark_{landmark.uuid}": landmark for landmark in landmarks}, expire=3600, noreply=False)
|
||||
# is equivalent to:
|
||||
# for landmark in landmarks:
|
||||
# cache_client.set(f"landmark_{landmark.uuid}", landmark, expire=3600)
|
||||
|
||||
return trip
|
@@ -1,79 +0,0 @@
|
||||
import logging
|
||||
import yaml
|
||||
|
||||
from utils.landmarks_manager import LandmarkManager
|
||||
from utils.optimizer import Optimizer
|
||||
from utils.refiner import Refiner
|
||||
from structs.landmark import Landmark
|
||||
from structs.linked_landmarks import LinkedLandmarks
|
||||
from structs.preferences import Preferences, Preference
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
def test(start_coords: tuple[float, float], finish_coords: tuple[float, float] = None) -> list[Landmark]:
|
||||
manager = LandmarkManager()
|
||||
optimizer = Optimizer()
|
||||
refiner = Refiner(optimizer=optimizer)
|
||||
|
||||
|
||||
preferences = Preferences(
|
||||
sightseeing=Preference(type='sightseeing', score = 5),
|
||||
nature=Preference(type='nature', score = 5),
|
||||
shopping=Preference(type='shopping', score = 5),
|
||||
max_time_minute=100,
|
||||
detour_tolerance_minute=0
|
||||
)
|
||||
|
||||
# Create start and finish
|
||||
if finish_coords is None :
|
||||
finish_coords = start_coords
|
||||
start = Landmark(name='start', type='start', location=start_coords, osm_type='', osm_id=0, attractiveness=0, n_tags = 0)
|
||||
finish = Landmark(name='finish', type='finish', location=finish_coords, osm_type='', osm_id=0, attractiveness=0, n_tags = 0)
|
||||
#finish = Landmark(name='finish', type=LandmarkType(landmark_type='finish'), location=(48.8777055, 2.3640967), osm_type='finish', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
|
||||
#start = Landmark(name='start', type=LandmarkType(landmark_type='start'), location=(48.847132, 2.312359), osm_type='start', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
|
||||
#finish = Landmark(name='finish', type=LandmarkType(landmark_type='finish'), location=(48.843185, 2.344533), osm_type='finish', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
|
||||
#finish = Landmark(name='finish', type=LandmarkType(landmark_type='finish'), location=(48.847132, 2.312359), osm_type='finish', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
|
||||
|
||||
|
||||
|
||||
# Generate the landmarks from the start location
|
||||
landmarks, landmarks_short = manager.generate_landmarks_list(
|
||||
center_coordinates = start_coords,
|
||||
preferences = preferences
|
||||
)
|
||||
|
||||
# Store data to file for debug purposes
|
||||
# write_data(landmarks, "landmarks_Strasbourg.txt")
|
||||
|
||||
# Insert start and finish to the landmarks list
|
||||
landmarks_short.insert(0, start)
|
||||
landmarks_short.append(finish)
|
||||
|
||||
# First stage optimization
|
||||
base_tour = optimizer.solve_optimization(max_time=preferences.max_time_minute, landmarks=landmarks_short)
|
||||
|
||||
# Second stage using linear optimization
|
||||
refined_tour = refiner.refine_optimization(all_landmarks=landmarks, base_tour=base_tour, max_time = preferences.max_time_minute, detour = preferences.detour_tolerance_minute)
|
||||
|
||||
linked_tour = LinkedLandmarks(refined_tour)
|
||||
total_time = 0
|
||||
logger.info("Optimized route : ")
|
||||
for l in linked_tour :
|
||||
logger.info(f"{l}")
|
||||
logger.info(f"Estimated length of tour : {linked_tour.total_time} mintutes and visiting {len(linked_tour._landmarks)} landmarks.")
|
||||
|
||||
# with open('linked_tour.yaml', 'w') as f:
|
||||
# yaml.dump(linked_tour.asdict(), f)
|
||||
|
||||
return linked_tour
|
||||
|
||||
|
||||
# test(tuple((48.8344400, 2.3220540))) # Café Chez César
|
||||
# test(tuple((48.8375946, 2.2949904))) # Point random
|
||||
# test(tuple((47.377859, 8.540585))) # Zurich HB
|
||||
# test(tuple((45.758217, 4.831814))) # Lyon Bellecour
|
||||
test(tuple((48.5848435, 7.7332974))) # Strasbourg Gare
|
||||
# test(tuple((48.2067858, 16.3692340))) # Vienne
|
@@ -1,50 +0,0 @@
|
||||
import yaml
|
||||
from math import sin, cos, sqrt, atan2, radians
|
||||
|
||||
import constants
|
||||
|
||||
with constants.OPTIMIZER_PARAMETERS_PATH.open('r') as f:
|
||||
parameters = yaml.safe_load(f)
|
||||
DETOUR_FACTOR = parameters['detour_factor']
|
||||
AVERAGE_WALKING_SPEED = parameters['average_walking_speed']
|
||||
|
||||
EARTH_RADIUS_KM = 6373
|
||||
|
||||
def get_time(p1: tuple[float, float], p2: tuple[float, float]) -> int:
|
||||
"""
|
||||
Calculate the time in minutes to travel from one location to another.
|
||||
|
||||
Args:
|
||||
p1 (Tuple[float, float]): Coordinates of the starting location.
|
||||
p2 (Tuple[float, float]): Coordinates of the destination.
|
||||
|
||||
Returns:
|
||||
int: Time to travel from p1 to p2 in minutes.
|
||||
"""
|
||||
|
||||
|
||||
if p1 == p2:
|
||||
return 0
|
||||
else:
|
||||
# Compute the distance in km along the surface of the Earth
|
||||
# (assume spherical Earth)
|
||||
# this is the haversine formula, stolen from stackoverflow
|
||||
# in order to not use any external libraries
|
||||
lat1, lon1 = radians(p1[0]), radians(p1[1])
|
||||
lat2, lon2 = radians(p2[0]), radians(p2[1])
|
||||
|
||||
dlon = lon2 - lon1
|
||||
dlat = lat2 - lat1
|
||||
|
||||
a = sin(dlat / 2)**2 + cos(lat1) * cos(lat2) * sin(dlon / 2)**2
|
||||
c = 2 * atan2(sqrt(a), sqrt(1 - a))
|
||||
|
||||
distance = EARTH_RADIUS_KM * c
|
||||
|
||||
# Consider the detour factor for average an average city
|
||||
walk_distance = distance * DETOUR_FACTOR
|
||||
|
||||
# Time to walk this distance (in minutes)
|
||||
walk_time = walk_distance / AVERAGE_WALKING_SPEED * 60
|
||||
|
||||
return round(walk_time)
|
@@ -1,349 +0,0 @@
|
||||
import math
|
||||
import yaml
|
||||
import logging
|
||||
|
||||
from OSMPythonTools.overpass import Overpass, overpassQueryBuilder
|
||||
from OSMPythonTools.cachingStrategy import CachingStrategy, JSON
|
||||
|
||||
from structs.preferences import Preferences
|
||||
from structs.landmark import Landmark
|
||||
from .take_most_important import take_most_important
|
||||
import constants
|
||||
|
||||
# silence the overpass logger
|
||||
logging.getLogger('OSMPythonTools').setLevel(level=logging.CRITICAL)
|
||||
|
||||
|
||||
class LandmarkManager:
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
radius_close_to: int # radius in meters
|
||||
church_coeff: float # coeff to adjsut score of churches
|
||||
nature_coeff: float # coeff to adjust score of parks
|
||||
overall_coeff: float # coeff to adjust weight of tags
|
||||
N_important: int # number of important landmarks to consider
|
||||
|
||||
|
||||
def __init__(self) -> None:
|
||||
|
||||
with constants.AMENITY_SELECTORS_PATH.open('r') as f:
|
||||
self.amenity_selectors = yaml.safe_load(f)
|
||||
|
||||
with constants.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.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.wikipedia_bonus = parameters['wikipedia_bonus']
|
||||
self.viewpoint_bonus = parameters['viewpoint_bonus']
|
||||
self.pay_bonus = parameters['pay_bonus']
|
||||
self.N_important = parameters['N_important']
|
||||
|
||||
with constants.OPTIMIZER_PARAMETERS_PATH.open('r') as f:
|
||||
parameters = yaml.safe_load(f)
|
||||
self.walking_speed = parameters['average_walking_speed']
|
||||
self.detour_factor = parameters['detour_factor']
|
||||
|
||||
self.overpass = Overpass()
|
||||
CachingStrategy.use(JSON, cacheDir=constants.OSM_CACHE_DIR)
|
||||
|
||||
|
||||
def generate_landmarks_list(self, center_coordinates: tuple[float, float], preferences: Preferences) -> tuple[list[Landmark], list[Landmark]]:
|
||||
"""
|
||||
Generate and prioritize a list of landmarks based on user preferences.
|
||||
|
||||
This method fetches landmarks from various categories (sightseeing, nature, shopping) based on the user's preferences
|
||||
and current location. It scores and corrects these landmarks, removes duplicates, and then selects the most important
|
||||
landmarks based on a predefined criterion.
|
||||
|
||||
Parameters:
|
||||
center_coordinates (tuple[float, float]): The latitude and longitude of the center location around which to search.
|
||||
preferences (Preferences): The user's preference settings that influence the landmark selection.
|
||||
|
||||
Returns:
|
||||
tuple[list[Landmark], list[Landmark]]:
|
||||
- A list of all existing landmarks.
|
||||
- A list of the most important landmarks based on the user's preferences.
|
||||
"""
|
||||
|
||||
max_walk_dist = (preferences.max_time_minute/2)/60*self.walking_speed*1000/self.detour_factor
|
||||
reachable_bbox_side = min(max_walk_dist, self.max_bbox_side)
|
||||
|
||||
# use set to avoid duplicates, this requires some __methods__ to be set in Landmark
|
||||
all_landmarks = set()
|
||||
|
||||
bbox = self.create_bbox(center_coordinates, reachable_bbox_side)
|
||||
# list for sightseeing
|
||||
if preferences.sightseeing.score != 0:
|
||||
score_function = lambda score: score * 10 * preferences.sightseeing.score / 5
|
||||
current_landmarks = self.fetch_landmarks(bbox, self.amenity_selectors['sightseeing'], preferences.sightseeing.type, score_function)
|
||||
all_landmarks.update(current_landmarks)
|
||||
|
||||
# list for nature
|
||||
if preferences.nature.score != 0:
|
||||
score_function = lambda score: score * 10 * self.nature_coeff * preferences.nature.score / 5
|
||||
current_landmarks = self.fetch_landmarks(bbox, self.amenity_selectors['nature'], preferences.nature.type, score_function)
|
||||
all_landmarks.update(current_landmarks)
|
||||
|
||||
# list for shopping
|
||||
if preferences.shopping.score != 0:
|
||||
score_function = lambda score: score * 10 * preferences.shopping.score / 5
|
||||
current_landmarks = self.fetch_landmarks(bbox, self.amenity_selectors['shopping'], preferences.shopping.type, score_function)
|
||||
all_landmarks.update(current_landmarks)
|
||||
|
||||
|
||||
landmarks_constrained = take_most_important(all_landmarks, self.N_important)
|
||||
self.logger.info(f'Generated {len(all_landmarks)} landmarks around {center_coordinates}, and constrained to {len(landmarks_constrained)} most important ones.')
|
||||
|
||||
return all_landmarks, landmarks_constrained
|
||||
|
||||
|
||||
|
||||
def count_elements_close_to(self, coordinates: tuple[float, float]) -> int:
|
||||
"""
|
||||
Count the number of OpenStreetMap elements (nodes, ways, relations) within a specified radius of the given location.
|
||||
|
||||
This function constructs a bounding box around the specified coordinates based on the radius. It then queries
|
||||
OpenStreetMap data to count the number of elements within that bounding box.
|
||||
|
||||
Args:
|
||||
coordinates (tuple[float, float]): The latitude and longitude of the location to search around.
|
||||
|
||||
Returns:
|
||||
int: The number of elements (nodes, ways, relations) within the specified radius. Returns 0 if no elements
|
||||
are found or if an error occurs during the query.
|
||||
"""
|
||||
|
||||
lat = coordinates[0]
|
||||
lon = coordinates[1]
|
||||
|
||||
radius = self.radius_close_to
|
||||
|
||||
alpha = (180 * radius) / (6371000 * math.pi)
|
||||
bbox = {'latLower':lat-alpha,'lonLower':lon-alpha,'latHigher':lat+alpha,'lonHigher': lon+alpha}
|
||||
|
||||
# Build the query to find elements within the radius
|
||||
radius_query = overpassQueryBuilder(
|
||||
bbox=[bbox['latLower'],
|
||||
bbox['lonLower'],
|
||||
bbox['latHigher'],
|
||||
bbox['lonHigher']],
|
||||
elementType=['node', 'way', 'relation']
|
||||
)
|
||||
|
||||
try:
|
||||
radius_result = self.overpass.query(radius_query)
|
||||
N_elem = radius_result.countWays() + radius_result.countRelations()
|
||||
self.logger.debug(f"There are {N_elem} ways/relations within 50m")
|
||||
if N_elem is None:
|
||||
return 0
|
||||
return N_elem
|
||||
except:
|
||||
return 0
|
||||
|
||||
|
||||
def create_bbox(self, coordinates: tuple[float, float], reachable_bbox_side: int) -> tuple[float, float, float, float]:
|
||||
"""
|
||||
Create a bounding box around the given coordinates.
|
||||
|
||||
Args:
|
||||
coordinates (tuple[float, float]): The latitude and longitude of the center of the bounding box.
|
||||
reachable_bbox_side (int): The 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.
|
||||
"""
|
||||
|
||||
lat = coordinates[0]
|
||||
lon = coordinates[1]
|
||||
|
||||
# Half the side length in km (since it's a square bbox)
|
||||
half_side_length_km = reachable_bbox_side / 2 / 1000
|
||||
|
||||
# Convert distance to degrees
|
||||
lat_diff = half_side_length_km / 111 # 1 degree latitude is approximately 111 km
|
||||
lon_diff = half_side_length_km / (111 * math.cos(math.radians(lat))) # Adjust for longitude based on latitude
|
||||
|
||||
# Calculate bbox
|
||||
min_lat = lat - lat_diff
|
||||
max_lat = lat + lat_diff
|
||||
min_lon = lon - lon_diff
|
||||
max_lon = lon + lon_diff
|
||||
|
||||
return min_lat, min_lon, max_lat, max_lon
|
||||
|
||||
|
||||
def fetch_landmarks(self, bbox: tuple, amenity_selector: dict, landmarktype: str, score_function: callable) -> list[Landmark]:
|
||||
"""
|
||||
Fetches landmarks of a specified type from OpenStreetMap (OSM) within a bounding box centered on given coordinates.
|
||||
|
||||
Args:
|
||||
bbox (tuple[float, float, float, float]): The bounding box coordinates (min_lat, min_lon, max_lat, max_lon).
|
||||
amenity_selector (dict): The Overpass API query selector for the desired landmark type.
|
||||
landmarktype (str): The type of the landmark (e.g., 'sightseeing', 'nature', 'shopping').
|
||||
score_function (callable): The function to compute the score of the landmark based on its attributes.
|
||||
|
||||
Returns:
|
||||
list[Landmark]: A list of Landmark objects that were fetched and filtered based on the provided criteria.
|
||||
|
||||
Notes:
|
||||
- Landmarks are fetched using Overpass API queries.
|
||||
- Selectors are translated from the dictionary to the Overpass query format. (e.g., 'amenity'='place_of_worship')
|
||||
- Landmarks are filtered based on various conditions including tags and type.
|
||||
- Scores are assigned to landmarks based on their attributes and surrounding elements.
|
||||
"""
|
||||
return_list = []
|
||||
|
||||
# caution, when applying a list of selectors, overpass will search for elements that match ALL selectors simultaneously
|
||||
# we need to split the selectors into separate queries and merge the results
|
||||
for sel in dict_to_selector_list(amenity_selector):
|
||||
self.logger.debug(f"Current selector: {sel}")
|
||||
query = overpassQueryBuilder(
|
||||
bbox = bbox,
|
||||
elementType = ['way', 'relation'],
|
||||
# selector can in principle be a list already,
|
||||
# but it generates the intersection of the queries
|
||||
# we want the union
|
||||
selector = sel,
|
||||
conditions = ['count_tags()>5'],
|
||||
includeCenter = True,
|
||||
out = 'body'
|
||||
)
|
||||
self.logger.debug(f"Query: {query}")
|
||||
|
||||
try:
|
||||
result = self.overpass.query(query)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error fetching landmarks: {e}")
|
||||
continue
|
||||
|
||||
for elem in result.elements():
|
||||
|
||||
name = elem.tag('name')
|
||||
location = (elem.centerLat(), elem.centerLon())
|
||||
|
||||
# TODO: exclude these from the get go
|
||||
# skip if unprecise location
|
||||
if name is None or location[0] is None:
|
||||
continue
|
||||
|
||||
# skip if part of another building
|
||||
if 'building:part' in elem.tags().keys() and elem.tag('building:part') == 'yes':
|
||||
continue
|
||||
|
||||
osm_type = elem.type() # Add type: 'way' or 'relation'
|
||||
osm_id = elem.id() # Add OSM id
|
||||
elem_type = landmarktype # Add the landmark type as 'sightseeing,
|
||||
n_tags = len(elem.tags().keys()) # Add number of tags
|
||||
score = n_tags**self.tag_exponent # Add score
|
||||
website_url = None
|
||||
image_url = None
|
||||
name_en = None
|
||||
|
||||
# remove specific tags
|
||||
skip = False
|
||||
for tag in elem.tags().keys():
|
||||
if "pay" in tag:
|
||||
# payment options are a good sign
|
||||
score += self.pay_bonus
|
||||
|
||||
if "disused" in tag:
|
||||
# skip disused amenities
|
||||
skip = True
|
||||
break
|
||||
|
||||
if "wiki" in tag:
|
||||
# wikipedia entries count more
|
||||
score += self.wikipedia_bonus
|
||||
|
||||
if "viewpoint" in tag:
|
||||
score += self.viewpoint_bonus
|
||||
duration = 10
|
||||
|
||||
if "image" in tag:
|
||||
score += self.image_bonus
|
||||
|
||||
if elem_type != "nature":
|
||||
if "leisure" in tag and elem.tag('leisure') == "park":
|
||||
elem_type = "nature"
|
||||
|
||||
if landmarktype != "shopping":
|
||||
if "shop" in tag:
|
||||
skip = True
|
||||
break
|
||||
|
||||
if tag == "building" and elem.tag('building') in ['retail', 'supermarket', 'parking']:
|
||||
skip = True
|
||||
break
|
||||
|
||||
if tag in ['website', 'contact:website']:
|
||||
website_url = elem.tag(tag)
|
||||
if tag == 'image':
|
||||
image_url = elem.tag('image')
|
||||
if tag =='name:en':
|
||||
name_en = elem.tag('name:en')
|
||||
|
||||
if skip:
|
||||
continue
|
||||
|
||||
score = score_function(score)
|
||||
if "place_of_worship" in elem.tags().values():
|
||||
score = score * self.church_coeff
|
||||
duration = 15
|
||||
|
||||
elif "museum" in elem.tags().values():
|
||||
score = score * self.church_coeff
|
||||
duration = 60
|
||||
|
||||
else:
|
||||
duration = 5
|
||||
|
||||
# finally create our own landmark object
|
||||
landmark = Landmark(
|
||||
name = name,
|
||||
type = elem_type,
|
||||
location = location,
|
||||
osm_type = osm_type,
|
||||
osm_id = osm_id,
|
||||
attractiveness = int(score),
|
||||
must_do = False,
|
||||
n_tags = int(n_tags),
|
||||
duration = int(duration),
|
||||
name_en = name_en,
|
||||
image_url = image_url,
|
||||
website_url = website_url
|
||||
)
|
||||
return_list.append(landmark)
|
||||
|
||||
self.logger.debug(f"Fetched {len(return_list)} landmarks of type {landmarktype} in {bbox}")
|
||||
|
||||
return return_list
|
||||
|
||||
|
||||
|
||||
def dict_to_selector_list(d: dict) -> list:
|
||||
"""
|
||||
Convert a dictionary of key-value pairs to a list of Overpass query strings.
|
||||
|
||||
Args:
|
||||
d (dict): A dictionary of key-value pairs representing the selector.
|
||||
|
||||
Returns:
|
||||
list: A list of strings representing the Overpass query selectors.
|
||||
"""
|
||||
return_list = []
|
||||
for key, value in d.items():
|
||||
if type(value) == list:
|
||||
val = '|'.join(value)
|
||||
return_list.append(f'{key}~"^({val})$"')
|
||||
elif type(value) == str and len(value) == 0:
|
||||
return_list.append(f'{key}')
|
||||
else:
|
||||
return_list.append(f'{key}={value}')
|
||||
return return_list
|
@@ -1,524 +0,0 @@
|
||||
import yaml, logging
|
||||
import numpy as np
|
||||
|
||||
from scipy.optimize import linprog
|
||||
from collections import defaultdict, deque
|
||||
|
||||
from structs.landmark import Landmark
|
||||
from .get_time_separation import get_time
|
||||
import constants
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class Optimizer:
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
detour: int = None # accepted max detour time (in minutes)
|
||||
detour_factor: float # detour factor of straight line vs real distance in cities
|
||||
average_walking_speed: float # average walking speed of adult
|
||||
max_landmarks: int # max number of landmarks to visit
|
||||
overshoot: float # overshoot to allow maxtime to overflow. Optimizer is a bit restrictive
|
||||
|
||||
|
||||
def __init__(self) :
|
||||
|
||||
# load parameters from file
|
||||
with constants.OPTIMIZER_PARAMETERS_PATH.open('r') as f:
|
||||
parameters = yaml.safe_load(f)
|
||||
self.detour_factor = parameters['detour_factor']
|
||||
self.average_walking_speed = parameters['average_walking_speed']
|
||||
self.max_landmarks = parameters['max_landmarks']
|
||||
self.overshoot = parameters['overshoot']
|
||||
|
||||
|
||||
|
||||
# Prevent the use of a particular solution
|
||||
def prevent_config(self, resx):
|
||||
"""
|
||||
Prevent the use of a particular solution by adding constraints to the optimization.
|
||||
|
||||
Args:
|
||||
resx (list[float]): List of edge weights.
|
||||
|
||||
Returns:
|
||||
Tuple[list[int], list[int]]: A tuple containing a new row for constraint matrix and new value for upper bound vector.
|
||||
"""
|
||||
|
||||
for i, elem in enumerate(resx):
|
||||
resx[i] = round(elem)
|
||||
|
||||
N = len(resx) # Number of edges
|
||||
L = int(np.sqrt(N)) # Number of landmarks
|
||||
|
||||
nonzeroind = np.nonzero(resx)[0] # the return is a little funky so I use the [0]
|
||||
nonzero_tup = np.unravel_index(nonzeroind, (L,L))
|
||||
|
||||
ind_a = nonzero_tup[0].tolist()
|
||||
vertices_visited = ind_a
|
||||
vertices_visited.remove(0)
|
||||
|
||||
ones = [1]*L
|
||||
h = [0]*N
|
||||
for i in range(L) :
|
||||
if i in vertices_visited :
|
||||
h[i*L:i*L+L] = ones
|
||||
|
||||
return h, [len(vertices_visited)-1]
|
||||
|
||||
|
||||
# Prevents the creation of the same circle (both directions)
|
||||
def prevent_circle(self, circle_vertices: list, L: int) :
|
||||
"""
|
||||
Prevent circular paths by by adding constraints to the optimization.
|
||||
|
||||
Args:
|
||||
circle_vertices (list): List of vertices forming a circle.
|
||||
L (int): Number of landmarks.
|
||||
|
||||
Returns:
|
||||
Tuple[np.ndarray, list[int]]: A tuple containing a new row for constraint matrix and new value for upper bound vector.
|
||||
"""
|
||||
|
||||
l1 = [0]*L*L
|
||||
l2 = [0]*L*L
|
||||
for i, node in enumerate(circle_vertices[:-1]) :
|
||||
next = circle_vertices[i+1]
|
||||
|
||||
l1[node*L + next] = 1
|
||||
l2[next*L + node] = 1
|
||||
|
||||
s = circle_vertices[0]
|
||||
g = circle_vertices[-1]
|
||||
|
||||
l1[g*L + s] = 1
|
||||
l2[s*L + g] = 1
|
||||
|
||||
return np.vstack((l1, l2)), [0, 0]
|
||||
|
||||
|
||||
def is_connected(self, resx) :
|
||||
"""
|
||||
Determine the order of visits and detect any circular paths in the given configuration.
|
||||
|
||||
Args:
|
||||
resx (list): List of edge weights.
|
||||
|
||||
Returns:
|
||||
Tuple[list[int], Optional[list[list[int]]]]: A tuple containing the visit order and a list of any detected circles.
|
||||
"""
|
||||
|
||||
# first round the results to have only 0-1 values
|
||||
for i, elem in enumerate(resx):
|
||||
resx[i] = round(elem)
|
||||
|
||||
N = len(resx) # length of res
|
||||
L = int(np.sqrt(N)) # number of landmarks. CAST INTO INT but should not be a problem because N = L**2 by def.
|
||||
|
||||
nonzeroind = np.nonzero(resx)[0] # the return is a little funny so I use the [0]
|
||||
nonzero_tup = np.unravel_index(nonzeroind, (L,L))
|
||||
|
||||
ind_a = nonzero_tup[0].tolist()
|
||||
ind_b = nonzero_tup[1].tolist()
|
||||
|
||||
# Step 1: Create a graph representation
|
||||
graph = defaultdict(list)
|
||||
for a, b in zip(ind_a, ind_b):
|
||||
graph[a].append(b)
|
||||
|
||||
# Step 2: Function to perform BFS/DFS to extract journeys
|
||||
def get_journey(start):
|
||||
journey_nodes = []
|
||||
visited = set()
|
||||
stack = deque([start])
|
||||
|
||||
while stack:
|
||||
node = stack.pop()
|
||||
if node not in visited:
|
||||
visited.add(node)
|
||||
journey_nodes.append(node)
|
||||
for neighbor in graph[node]:
|
||||
if neighbor not in visited:
|
||||
stack.append(neighbor)
|
||||
|
||||
return journey_nodes
|
||||
|
||||
# Step 3: Extract all journeys
|
||||
all_journeys_nodes = []
|
||||
visited_nodes = set()
|
||||
|
||||
for node in ind_a:
|
||||
if node not in visited_nodes:
|
||||
journey_nodes = get_journey(node)
|
||||
all_journeys_nodes.append(journey_nodes)
|
||||
visited_nodes.update(journey_nodes)
|
||||
|
||||
for l in all_journeys_nodes :
|
||||
if 0 in l :
|
||||
order = l
|
||||
all_journeys_nodes.remove(l)
|
||||
break
|
||||
|
||||
if len(all_journeys_nodes) == 0 :
|
||||
return order, None
|
||||
|
||||
return order, all_journeys_nodes
|
||||
|
||||
|
||||
|
||||
def init_ub_dist(self, landmarks: list[Landmark], max_time: int):
|
||||
"""
|
||||
Initialize the objective function coefficients and inequality constraints for the optimization problem.
|
||||
|
||||
This function computes the distances between all landmarks and stores their attractiveness to maximize sightseeing.
|
||||
The goal is to maximize the objective function subject to the constraints A*x < b and A_eq*x = b_eq.
|
||||
|
||||
Args:
|
||||
landmarks (list[Landmark]): List of landmarks.
|
||||
max_time (int): Maximum time of visit allowed.
|
||||
|
||||
Returns:
|
||||
Tuple[list[float], list[float], list[int]]: Objective function coefficients, inequality constraint coefficients, and the right-hand side of the inequality constraint.
|
||||
"""
|
||||
|
||||
# Objective function coefficients. a*x1 + b*x2 + c*x3 + ...
|
||||
c = []
|
||||
# Coefficients of inequality constraints (left-hand side)
|
||||
A_ub = []
|
||||
|
||||
for spot1 in landmarks :
|
||||
dist_table = [0]*len(landmarks)
|
||||
c.append(-spot1.attractiveness)
|
||||
for j, spot2 in enumerate(landmarks) :
|
||||
t = get_time(spot1.location, spot2.location) + spot1.duration
|
||||
dist_table[j] = t
|
||||
closest = sorted(dist_table)[:25]
|
||||
for i, dist in enumerate(dist_table) :
|
||||
if dist not in closest :
|
||||
dist_table[i] = 32700
|
||||
A_ub += dist_table
|
||||
c = c*len(landmarks)
|
||||
|
||||
return c, A_ub, [max_time*self.overshoot]
|
||||
|
||||
|
||||
def respect_number(self, L, max_landmarks: int):
|
||||
"""
|
||||
Generate constraints to ensure each landmark is visited only once and cap the total number of visited landmarks.
|
||||
|
||||
Args:
|
||||
L (int): Number of landmarks.
|
||||
|
||||
Returns:
|
||||
Tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
|
||||
"""
|
||||
|
||||
ones = [1]*L
|
||||
zeros = [0]*L
|
||||
A = ones + zeros*(L-1)
|
||||
b = [1]
|
||||
for i in range(L-1) :
|
||||
h_new = zeros*i + ones + zeros*(L-1-i)
|
||||
A = np.vstack((A, h_new))
|
||||
b.append(1)
|
||||
|
||||
A = np.vstack((A, ones*L))
|
||||
b.append(max_landmarks+1)
|
||||
|
||||
return A, b
|
||||
|
||||
|
||||
# Constraint to not have d14 and d41 simultaneously. Does not prevent cyclic paths with more elements
|
||||
def break_sym(self, L):
|
||||
"""
|
||||
Generate constraints to prevent simultaneous travel between two landmarks in both directions.
|
||||
|
||||
Args:
|
||||
L (int): Number of landmarks.
|
||||
|
||||
Returns:
|
||||
Tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
|
||||
"""
|
||||
|
||||
upper_ind = np.triu_indices(L,0,L)
|
||||
|
||||
up_ind_x = upper_ind[0]
|
||||
up_ind_y = upper_ind[1]
|
||||
|
||||
A = [0]*L*L
|
||||
b = [1]
|
||||
|
||||
for i, _ in enumerate(up_ind_x[1:]) :
|
||||
l = [0]*L*L
|
||||
if up_ind_x[i] != up_ind_y[i] :
|
||||
l[up_ind_x[i]*L + up_ind_y[i]] = 1
|
||||
l[up_ind_y[i]*L + up_ind_x[i]] = 1
|
||||
|
||||
A = np.vstack((A,l))
|
||||
b.append(1)
|
||||
|
||||
return A, b
|
||||
|
||||
|
||||
def init_eq_not_stay(self, L: int):
|
||||
"""
|
||||
Generate constraints to prevent staying in the same position (e.g., removing d11, d22, d33, etc.).
|
||||
|
||||
Args:
|
||||
L (int): Number of landmarks.
|
||||
|
||||
Returns:
|
||||
Tuple[list[np.ndarray], list[int]]: Equality constraint coefficients and the right-hand side of the equality constraints.
|
||||
"""
|
||||
|
||||
l = [0]*L*L
|
||||
|
||||
for i in range(L) :
|
||||
for j in range(L) :
|
||||
if j == i :
|
||||
l[j + i*L] = 1
|
||||
|
||||
l = np.array(np.array(l), dtype=np.int8)
|
||||
|
||||
return [l], [0]
|
||||
|
||||
|
||||
def respect_user_must_do(self, landmarks: list[Landmark]) :
|
||||
"""
|
||||
Generate constraints to ensure that landmarks marked as 'must_do' are included in the optimization.
|
||||
|
||||
Args:
|
||||
landmarks (list[Landmark]): List of landmarks, where some are marked as 'must_do'.
|
||||
|
||||
Returns:
|
||||
Tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
|
||||
"""
|
||||
|
||||
L = len(landmarks)
|
||||
A = [0]*L*L
|
||||
b = [0]
|
||||
|
||||
for i, elem in enumerate(landmarks[1:]) :
|
||||
if elem.must_do is True and elem.name not in ['finish', 'start']:
|
||||
l = [0]*L*L
|
||||
l[i*L:i*L+L] = [1]*L # set mandatory departures from landmarks tagged as 'must_do'
|
||||
|
||||
A = np.vstack((A,l))
|
||||
b.append(1)
|
||||
|
||||
return A, b
|
||||
|
||||
|
||||
def respect_user_must_avoid(self, landmarks: list[Landmark]) :
|
||||
"""
|
||||
Generate constraints to ensure that landmarks marked as 'must_avoid' are skipped in the optimization.
|
||||
|
||||
Args:
|
||||
landmarks (list[Landmark]): List of landmarks, where some are marked as 'must_avoid'.
|
||||
|
||||
Returns:
|
||||
Tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
|
||||
"""
|
||||
|
||||
L = len(landmarks)
|
||||
A = [0]*L*L
|
||||
b = [0]
|
||||
|
||||
for i, elem in enumerate(landmarks[1:]) :
|
||||
if elem.must_avoid is True and elem.name not in ['finish', 'start']:
|
||||
l = [0]*L*L
|
||||
l[i*L:i*L+L] = [1]*L
|
||||
|
||||
A = np.vstack((A,l))
|
||||
b.append(0) # prevent departures from landmarks tagged as 'must_do'
|
||||
|
||||
return A, b
|
||||
|
||||
|
||||
# Constraint to ensure start at start and finish at goal
|
||||
def respect_start_finish(self, L: int):
|
||||
"""
|
||||
Generate constraints to ensure that the optimization starts at the designated start landmark and finishes at the goal landmark.
|
||||
|
||||
Args:
|
||||
L (int): Number of landmarks.
|
||||
|
||||
Returns:
|
||||
Tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
|
||||
"""
|
||||
|
||||
l_start = [1]*L + [0]*L*(L-1) # sets departures only for start (horizontal ones)
|
||||
l_start[L-1] = 0 # prevents the jump from start to finish
|
||||
l_goal = [0]*L*L # sets arrivals only for finish (vertical ones)
|
||||
l_L = [0]*L*(L-1) + [1]*L # prevents arrivals at start and departures from goal
|
||||
for k in range(L-1) : # sets only vertical ones for goal (go to)
|
||||
l_L[k*L] = 1
|
||||
if k != 0 :
|
||||
l_goal[k*L+L-1] = 1
|
||||
|
||||
A = np.vstack((l_start, l_goal))
|
||||
b = [1, 1]
|
||||
A = np.vstack((A,l_L))
|
||||
b.append(0)
|
||||
|
||||
return A, b
|
||||
|
||||
|
||||
def respect_order(self, L: int):
|
||||
"""
|
||||
Generate constraints to tie the optimization problem together and prevent stacked ones, although this does not fully prevent circles.
|
||||
|
||||
Args:
|
||||
L (int): Number of landmarks.
|
||||
|
||||
Returns:
|
||||
Tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
|
||||
"""
|
||||
|
||||
A = [0]*L*L
|
||||
b = [0]
|
||||
for i in range(L-1) : # Prevent stacked ones
|
||||
if i == 0 or i == L-1: # Don't touch start or finish
|
||||
continue
|
||||
else :
|
||||
l = [0]*L
|
||||
l[i] = -1
|
||||
l = l*L
|
||||
for j in range(L) :
|
||||
l[i*L + j] = 1
|
||||
|
||||
A = np.vstack((A,l))
|
||||
b.append(0)
|
||||
|
||||
return A, b
|
||||
|
||||
|
||||
def link_list(self, order: list[int], landmarks: list[Landmark])->list[Landmark] :
|
||||
"""
|
||||
Compute the time to reach from each landmark to the next and create a list of landmarks with updated travel times.
|
||||
|
||||
Args:
|
||||
order (list[int]): List of indices representing the order of landmarks to visit.
|
||||
landmarks (list[Landmark]): List of all landmarks.
|
||||
|
||||
Returns:
|
||||
list[Landmark]]: The updated linked list of landmarks with travel times
|
||||
"""
|
||||
|
||||
L = []
|
||||
j = 0
|
||||
while j < len(order)-1 :
|
||||
# get landmarks involved
|
||||
elem = landmarks[order[j]]
|
||||
next = landmarks[order[j+1]]
|
||||
|
||||
# get attributes
|
||||
elem.time_to_reach_next = get_time(elem.location, next.location)
|
||||
elem.must_do = True
|
||||
elem.location = (round(elem.location[0], 5), round(elem.location[1], 5))
|
||||
elem.next_uuid = next.uuid
|
||||
L.append(elem)
|
||||
j += 1
|
||||
|
||||
next.location = (round(next.location[0], 5), round(next.location[1], 5))
|
||||
next.must_do = True
|
||||
L.append(next)
|
||||
|
||||
return L
|
||||
|
||||
|
||||
# Main optimization pipeline
|
||||
def solve_optimization(
|
||||
self,
|
||||
max_time: int,
|
||||
landmarks: list[Landmark],
|
||||
max_landmarks: int = None
|
||||
) -> list[Landmark]:
|
||||
"""
|
||||
Main optimization pipeline to solve the landmark visiting problem.
|
||||
|
||||
This method sets up and solves a linear programming problem with constraints to find an optimal tour of landmarks,
|
||||
considering user-defined must-visit landmarks, start and finish points, and ensuring no cycles are present.
|
||||
|
||||
Args:
|
||||
max_time (int): Maximum time allowed for the tour in minutes.
|
||||
landmarks (list[Landmark]): List of landmarks to visit.
|
||||
max_landmarks (int): Maximum number of landmarks visited
|
||||
Returns:
|
||||
list[Landmark]: The optimized tour of landmarks with updated travel times, or None if no valid solution is found.
|
||||
"""
|
||||
if max_landmarks is None :
|
||||
max_landmarks = self.max_landmarks
|
||||
|
||||
L = len(landmarks)
|
||||
|
||||
# SET CONSTRAINTS FOR INEQUALITY
|
||||
c, A_ub, b_ub = self.init_ub_dist(landmarks, max_time) # Add the distances from each landmark to the other
|
||||
A, b = self.respect_number(L, max_landmarks) # Respect max number of visits (no more possible stops than landmarks).
|
||||
A_ub = np.vstack((A_ub, A), dtype=np.int16)
|
||||
b_ub += b
|
||||
A, b = self.break_sym(L) # break the 'zig-zag' symmetry
|
||||
A_ub = np.vstack((A_ub, A), dtype=np.int16)
|
||||
b_ub += b
|
||||
|
||||
|
||||
# SET CONSTRAINTS FOR EQUALITY
|
||||
A_eq, b_eq = self.init_eq_not_stay(L) # Force solution not to stay in same place
|
||||
A, b = self.respect_user_must_do(landmarks) # Check if there are user_defined must_see. Also takes care of start/goal
|
||||
A_eq = np.vstack((A_eq, A), dtype=np.int8)
|
||||
b_eq += b
|
||||
A, b = self.respect_user_must_avoid(landmarks) # Check if there are user_defined must_see. Also takes care of start/goal
|
||||
A_eq = np.vstack((A_eq, A), dtype=np.int8)
|
||||
b_eq += b
|
||||
A, b = self.respect_start_finish(L) # Force start and finish positions
|
||||
A_eq = np.vstack((A_eq, A), dtype=np.int8)
|
||||
b_eq += b
|
||||
A, b = self.respect_order(L) # Respect order of visit (only works when max_time is limiting factor)
|
||||
A_eq = np.vstack((A_eq, A), dtype=np.int8)
|
||||
b_eq += b
|
||||
|
||||
# SET BOUNDS FOR DECISION VARIABLE (x can only be 0 or 1)
|
||||
x_bounds = [(0, 1)]*L*L
|
||||
|
||||
# Solve linear programming problem
|
||||
res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq = b_eq, bounds=x_bounds, method='highs', integrality=3)
|
||||
|
||||
# Raise error if no solution is found
|
||||
if not res.success :
|
||||
raise ArithmeticError("No solution could be found, the problem is overconstrained. Please adapt your must_dos")
|
||||
|
||||
# If there is a solution, we're good to go, just check for connectiveness
|
||||
order, circles = self.is_connected(res.x)
|
||||
#nodes, edges = is_connected(res.x)
|
||||
i = 0
|
||||
timeout = 80
|
||||
while circles is not None and i < timeout:
|
||||
A, b = self.prevent_config(res.x)
|
||||
A_ub = np.vstack((A_ub, A))
|
||||
b_ub += b
|
||||
#A_ub, b_ub = prevent_circle(order, len(landmarks), A_ub, b_ub)
|
||||
for circle in circles :
|
||||
A, b = self.prevent_circle(circle, L)
|
||||
A_eq = np.vstack((A_eq, A))
|
||||
b_eq += b
|
||||
res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq = b_eq, bounds=x_bounds, method='highs', integrality=3)
|
||||
if not res.success :
|
||||
raise ArithmeticError("Solving failed because of overconstrained problem")
|
||||
return None
|
||||
order, circles = self.is_connected(res.x)
|
||||
#nodes, edges = is_connected(res.x)
|
||||
if circles is None :
|
||||
break
|
||||
# print(i)
|
||||
i += 1
|
||||
|
||||
if i == timeout :
|
||||
raise TimeoutError(f"Optimization took too long. No solution found after {timeout} iterations.")
|
||||
|
||||
#sort the landmarks in the order of the solution
|
||||
tour = [landmarks[i] for i in order]
|
||||
|
||||
self.logger.debug(f"Re-optimized {i} times, score: {int(-res.fun)}")
|
||||
return tour
|
@@ -1,341 +0,0 @@
|
||||
import yaml, logging
|
||||
|
||||
from shapely import buffer, LineString, Point, Polygon, MultiPoint, concave_hull
|
||||
from math import pi
|
||||
|
||||
from structs.landmark import Landmark
|
||||
from . import take_most_important, get_time_separation
|
||||
from .optimizer import Optimizer
|
||||
import constants
|
||||
|
||||
|
||||
|
||||
class Refiner :
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
detour_factor: float # detour factor of straight line vs real distance in cities
|
||||
detour_corridor_width: float # width of the corridor around the path
|
||||
average_walking_speed: float # average walking speed of adult
|
||||
max_landmarks_refiner: int # max number of landmarks to visit
|
||||
optimizer: Optimizer # optimizer object
|
||||
|
||||
def __init__(self, optimizer: Optimizer) :
|
||||
self.optimizer = optimizer
|
||||
|
||||
# load parameters from file
|
||||
with constants.OPTIMIZER_PARAMETERS_PATH.open('r') as f:
|
||||
parameters = yaml.safe_load(f)
|
||||
self.detour_factor = parameters['detour_factor']
|
||||
self.detour_corridor_width = parameters['detour_corridor_width']
|
||||
self.average_walking_speed = parameters['average_walking_speed']
|
||||
self.max_landmarks_refiner = parameters['max_landmarks_refiner']
|
||||
|
||||
|
||||
def create_corridor(self, landmarks: list[Landmark], width: float) :
|
||||
"""
|
||||
Create a corridor around the path connecting the landmarks.
|
||||
|
||||
Args:
|
||||
landmarks (list[Landmark]): the landmark path around which to create the corridor
|
||||
width (float): Width of the corridor in meters.
|
||||
|
||||
Returns:
|
||||
Geometry: A buffered geometry object representing the corridor around the path.
|
||||
"""
|
||||
|
||||
corrected_width = (180*width)/(6371000*pi)
|
||||
|
||||
path = self.create_linestring(landmarks)
|
||||
obj = buffer(path, corrected_width, join_style="mitre", cap_style="square", mitre_limit=2)
|
||||
|
||||
return obj
|
||||
|
||||
|
||||
def create_linestring(self, tour: list[Landmark]) -> LineString :
|
||||
"""
|
||||
Create a `LineString` object from a tour.
|
||||
|
||||
Args:
|
||||
tour (list[Landmark]): An ordered sequence of landmarks that represents the visiting order.
|
||||
|
||||
Returns:
|
||||
LineString: A `LineString` object representing the path through the landmarks.
|
||||
"""
|
||||
|
||||
points = []
|
||||
for landmark in tour :
|
||||
points.append(Point(landmark.location))
|
||||
|
||||
return LineString(points)
|
||||
|
||||
|
||||
# Check if some coordinates are in area. Used for the corridor
|
||||
def is_in_area(self, area: Polygon, coordinates) -> bool :
|
||||
"""
|
||||
Check if a given point is within a specified area.
|
||||
|
||||
Args:
|
||||
area (Polygon): The polygon defining the area.
|
||||
coordinates (tuple[float, float]): The coordinates of the point to check.
|
||||
|
||||
Returns:
|
||||
bool: True if the point is within the area, otherwise False.
|
||||
"""
|
||||
point = Point(coordinates)
|
||||
return point.within(area)
|
||||
|
||||
|
||||
# Function to determine if two landmarks are close to each other
|
||||
def is_close_to(self, location1: tuple[float], location2: tuple[float]):
|
||||
"""
|
||||
Determine if two locations are close to each other by rounding their coordinates to 3 decimal places.
|
||||
|
||||
Args:
|
||||
location1 (tuple[float, float]): The coordinates of the first location.
|
||||
location2 (tuple[float, float]): The coordinates of the second location.
|
||||
|
||||
Returns:
|
||||
bool: True if the locations are within 0.001 degrees of each other, otherwise False.
|
||||
"""
|
||||
|
||||
absx = abs(location1[0] - location2[0])
|
||||
absy = abs(location1[1] - location2[1])
|
||||
|
||||
return absx < 0.001 and absy < 0.001
|
||||
#return (round(location1[0], 3), round(location1[1], 3)) == (round(location2[0], 3), round(location2[1], 3))
|
||||
|
||||
|
||||
def rearrange(self, tour: list[Landmark]) -> list[Landmark]:
|
||||
"""
|
||||
Rearrange landmarks to group nearby visits together.
|
||||
|
||||
This function reorders landmarks so that nearby landmarks are adjacent to each other in the list,
|
||||
while keeping 'start' and 'finish' landmarks in their original positions.
|
||||
|
||||
Args:
|
||||
tour (list[Landmark]): Ordered list of landmarks to be rearranged.
|
||||
|
||||
Returns:
|
||||
list[Landmark]: The rearranged list of landmarks with grouped nearby visits.
|
||||
"""
|
||||
|
||||
i = 1
|
||||
while i < len(tour):
|
||||
j = i+1
|
||||
while j < len(tour):
|
||||
if self.is_close_to(tour[i].location, tour[j].location) and tour[i].name not in ['start', 'finish'] and tour[j].name not in ['start', 'finish']:
|
||||
# If they are not adjacent, move the j-th element to be adjacent to the i-th element
|
||||
if j != i + 1:
|
||||
tour.insert(i + 1, tour.pop(j))
|
||||
break # Move to the next i-th element after rearrangement
|
||||
j += 1
|
||||
i += 1
|
||||
|
||||
return tour
|
||||
|
||||
|
||||
def find_shortest_path_through_all_landmarks(self, landmarks: list[Landmark]) -> tuple[list[Landmark], Polygon]:
|
||||
"""
|
||||
Find the shortest path through all landmarks using a nearest neighbor heuristic.
|
||||
|
||||
This function constructs a path that starts from the 'start' landmark, visits all other landmarks in the order
|
||||
of their proximity, and ends at the 'finish' landmark. It returns both the ordered list of landmarks and a
|
||||
polygon representing the path.
|
||||
|
||||
Args:
|
||||
landmarks (list[Landmark]): list of all landmarks including 'start' and 'finish'.
|
||||
|
||||
Returns:
|
||||
tuple[list[Landmark], Polygon]: A tuple where the first element is the list of landmarks in the order they
|
||||
should be visited, and the second element is a `Polygon` representing
|
||||
the path connecting all landmarks.
|
||||
"""
|
||||
|
||||
# Step 1: Find 'start' and 'finish' landmarks
|
||||
start_idx = next(i for i, lm in enumerate(landmarks) if lm.type == 'start')
|
||||
finish_idx = next(i for i, lm in enumerate(landmarks) if lm.type == 'finish')
|
||||
|
||||
start_landmark = landmarks[start_idx]
|
||||
finish_landmark = landmarks[finish_idx]
|
||||
|
||||
|
||||
# Step 2: Create a list of unvisited landmarks excluding 'start' and 'finish'
|
||||
unvisited_landmarks = [lm for i, lm in enumerate(landmarks) if i not in [start_idx, finish_idx]]
|
||||
|
||||
# Step 3: Initialize the path with the 'start' landmark
|
||||
path = [start_landmark]
|
||||
coordinates = [landmarks[start_idx].location]
|
||||
|
||||
current_landmark = start_landmark
|
||||
|
||||
# Step 4: Use nearest neighbor heuristic to visit all landmarks
|
||||
while unvisited_landmarks:
|
||||
nearest_landmark = min(unvisited_landmarks, key=lambda lm: get_time_separation.get_time(current_landmark.location, lm.location))
|
||||
path.append(nearest_landmark)
|
||||
coordinates.append(nearest_landmark.location)
|
||||
current_landmark = nearest_landmark
|
||||
unvisited_landmarks.remove(nearest_landmark)
|
||||
|
||||
# Step 5: Finally add the 'finish' landmark to the path
|
||||
path.append(finish_landmark)
|
||||
coordinates.append(landmarks[finish_idx].location)
|
||||
|
||||
path_poly = Polygon(coordinates)
|
||||
|
||||
return path, path_poly
|
||||
|
||||
|
||||
# Returns a list of minor landmarks around the planned path to enhance experience
|
||||
def get_minor_landmarks(self, all_landmarks: list[Landmark], visited_landmarks: list[Landmark], width: float) -> list[Landmark] :
|
||||
"""
|
||||
Identify landmarks within a specified corridor that have not been visited yet.
|
||||
|
||||
This function creates a corridor around the path defined by visited landmarks and then finds landmarks that fall
|
||||
within this corridor. It returns a list of these landmarks, excluding those already visited, sorted by their importance.
|
||||
|
||||
Args:
|
||||
all_landmarks (list[Landmark]): list of all available landmarks.
|
||||
visited_landmarks (list[Landmark]): list of landmarks that have already been visited.
|
||||
width (float): Width of the corridor around the visited landmarks.
|
||||
|
||||
Returns:
|
||||
list[Landmark]: list of important landmarks within the corridor that have not been visited yet.
|
||||
"""
|
||||
|
||||
second_order_landmarks = []
|
||||
visited_names = []
|
||||
area = self.create_corridor(visited_landmarks, width)
|
||||
|
||||
for visited in visited_landmarks :
|
||||
visited_names.append(visited.name)
|
||||
|
||||
for landmark in all_landmarks :
|
||||
if self.is_in_area(area, landmark.location) and landmark.name not in visited_names:
|
||||
second_order_landmarks.append(landmark)
|
||||
|
||||
return take_most_important.take_most_important(second_order_landmarks, int(self.max_landmarks_refiner*0.75))
|
||||
|
||||
|
||||
# Try fix the shortest path using shapely
|
||||
def fix_using_polygon(self, tour: list[Landmark])-> list[Landmark] :
|
||||
"""
|
||||
Improve the tour path using geometric methods to ensure it follows a more optimal shape.
|
||||
|
||||
This function creates a polygon from the given tour and attempts to refine it using a concave hull. It reorders
|
||||
the landmarks to fit within this refined polygon and adjusts the tour to ensure the 'start' landmark is at the
|
||||
beginning. It also checks if the final polygon is simple and rearranges the tour if necessary.
|
||||
|
||||
Args:
|
||||
tour (list[Landmark]): list of landmarks representing the current tour path.
|
||||
|
||||
Returns:
|
||||
list[Landmark]: Refined list of landmarks in the order of visit to produce a better tour path.
|
||||
"""
|
||||
|
||||
coords = []
|
||||
coords_dict = {}
|
||||
for landmark in tour :
|
||||
coords.append(landmark.location)
|
||||
if landmark.name != 'finish' :
|
||||
coords_dict[landmark.location] = landmark
|
||||
|
||||
tour_poly = Polygon(coords)
|
||||
|
||||
better_tour_poly = tour_poly.buffer(0)
|
||||
try :
|
||||
xs, ys = better_tour_poly.exterior.xy
|
||||
|
||||
if len(xs) != len(tour) :
|
||||
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
|
||||
|
||||
except :
|
||||
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
|
||||
|
||||
|
||||
# reverse the xs and ys
|
||||
xs.reverse()
|
||||
ys.reverse()
|
||||
|
||||
better_tour = [] # list of ordered visit
|
||||
name_index = {} # Maps the name of a landmark to its index in the concave polygon
|
||||
|
||||
# Loop through the polygon and generate the better (ordered) tour
|
||||
for i,x in enumerate(xs[:-1]) :
|
||||
y = ys[i]
|
||||
better_tour.append(coords_dict[tuple((x,y))])
|
||||
name_index[coords_dict[tuple((x,y))].name] = i
|
||||
|
||||
|
||||
# Scroll the list to have start in front again
|
||||
start_index = name_index['start']
|
||||
better_tour = better_tour[start_index:] + better_tour[:start_index]
|
||||
|
||||
# Append the finish back and correct the time to reach
|
||||
better_tour.append(tour[-1])
|
||||
|
||||
# Rearrange only if polygon still not simple
|
||||
if not better_tour_poly.is_simple :
|
||||
better_tour = self.rearrange(better_tour)
|
||||
|
||||
return better_tour
|
||||
|
||||
|
||||
def refine_optimization(
|
||||
self,
|
||||
all_landmarks: list[Landmark],
|
||||
base_tour: list[Landmark],
|
||||
max_time: int,
|
||||
detour: int
|
||||
) -> list[Landmark]:
|
||||
"""
|
||||
This is the second stage of the optimization. It refines the initial tour path by considering additional minor landmarks and optimizes the path.
|
||||
|
||||
This method evaluates the need for further optimization based on the initial tour. If a detour is required
|
||||
it adds minor landmarks around the initial predicted path and solves a new optimization problem to find a potentially better
|
||||
tour. It then links the new tour and adjusts it using a nearest neighbor heuristic and polygon-based methods to
|
||||
ensure a valid path. The final tour is chosen based on the shortest distance.
|
||||
|
||||
Args:
|
||||
all_landmarks (list[Landmark]): The full list of landmarks available for the optimization.
|
||||
base_tour (list[Landmark]): The initial tour path to be refined.
|
||||
max_time (int): The maximum time available for the tour in minutes.
|
||||
detour (int): The maximum detour time allowed for the tour in minutes.
|
||||
Returns:
|
||||
list[Landmark]: The refined list of landmarks representing the optimized tour path.
|
||||
"""
|
||||
|
||||
# No need to refine if no detour is taken
|
||||
# if detour == 0:
|
||||
# return base_tour
|
||||
|
||||
minor_landmarks = self.get_minor_landmarks(all_landmarks, base_tour, self.detour_corridor_width)
|
||||
|
||||
self.logger.info(f"Using {len(minor_landmarks)} minor landmarks around the predicted path")
|
||||
|
||||
# full set of visitable landmarks
|
||||
full_set = base_tour[:-1] + minor_landmarks # create full set of possible landmarks (without finish)
|
||||
full_set.append(base_tour[-1]) # add finish back
|
||||
|
||||
# get a new tour
|
||||
new_tour = self.optimizer.solve_optimization(
|
||||
max_time = max_time + detour,
|
||||
landmarks = full_set,
|
||||
max_landmarks = self.max_landmarks_refiner
|
||||
)
|
||||
|
||||
if new_tour is None:
|
||||
self.logger.warning("No solution found for the refined tour. Returning the initial tour.")
|
||||
new_tour = base_tour
|
||||
|
||||
|
||||
# Find shortest path using the nearest neighbor heuristic
|
||||
better_tour, better_poly = self.find_shortest_path_through_all_landmarks(new_tour)
|
||||
|
||||
# Fix the tour using Polygons if the path looks weird
|
||||
if base_tour[0].location == base_tour[-1].location and not better_poly.is_valid :
|
||||
better_tour = self.fix_using_polygon(better_tour)
|
||||
|
||||
return better_tour
|
@@ -1,16 +0,0 @@
|
||||
from structs.landmark import Landmark
|
||||
|
||||
def take_most_important(landmarks: list[Landmark], n_important) -> list[Landmark]:
|
||||
"""
|
||||
Given a list of landmarks, return the n_important most important landmarks
|
||||
Parameters:
|
||||
landmarks: list[Landmark] - list of landmarks
|
||||
n_important: int - number of most important landmarks to return
|
||||
Returns:
|
||||
list[Landmark] - list of the n_important most important landmarks
|
||||
"""
|
||||
|
||||
# Sort landmarks by attractiveness (descending)
|
||||
sorted_landmarks = sorted(landmarks, key=lambda x: x.attractiveness, reverse=True)
|
||||
|
||||
return sorted_landmarks[:n_important]
|
@@ -1,56 +0,0 @@
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up ruby env
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.2.1
|
||||
bundler-cache: true
|
||||
|
||||
- name: Setup java for android build
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'zulu'
|
||||
|
||||
- name: Setup android SDK
|
||||
uses: android-actions/setup-android@v3
|
||||
|
||||
- name: Install Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: stable
|
||||
flutter-version: 3.22.0
|
||||
cache: true
|
||||
|
||||
- name: Infer version number from git tag
|
||||
id: version
|
||||
env:
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
run:
|
||||
# remove the 'v' prefix from the tag name
|
||||
echo "VERSION_NAME=${REF_NAME//v}" >> $GITHUB_ENV
|
||||
|
||||
- name: Load secrets from github
|
||||
run: |
|
||||
echo "${{ secrets.ANDROID_SECRET_PROPERTIES_BASE64 }}" | base64 -d > secrets.properties
|
||||
echo "${{ secrets.ANDROID_GOOGLE_PLAY_JSON_BASE64 }}" | base64 -d > google-key.json
|
||||
echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > release.keystore
|
||||
working-directory: android
|
||||
|
||||
- name: Install fastlane
|
||||
run: bundle install
|
||||
working-directory: android
|
||||
|
||||
- name: Run fastlane lane
|
||||
run: bundle exec fastlane deploy_testing
|
||||
working-directory: android
|
||||
# the environment variable VERSION_NAME is implicitly available
|
@@ -1,57 +0,0 @@
|
||||
# Frontend
|
||||
|
||||
The frontend of this project is a Flutter application designed to run on both Android and iOS devices (and possibly as a PWA). The frontend is responsible for displaying the user interface and handling user input. It communicates with the backend via a REST-api to retrieve and send data.
|
||||
|
||||
|
||||
## Getting Started
|
||||
The flutter application is divided into multiple chunks of code.
|
||||
- the `lib` directory contains the main code of the application.
|
||||
- the `android` and `ios` directories contain platform-specific code.
|
||||
- the root directory contains configuration files and metadata.
|
||||
|
||||
To run the application, you need to have the Flutter SDK installed. You can find instructions on how to do this [here](https://flutter.dev/docs/get-started/install).
|
||||
|
||||
Once you have the Flutter SDK installed, you can locally install the dependencies by running:
|
||||
```bash
|
||||
flutter pub get
|
||||
```
|
||||
|
||||
## Development
|
||||
### ...
|
||||
### Icons and logos
|
||||
The application uses a custom launcher icon and splash screen. These are managed platform-independently using the `flutter_launcher_icons` package.
|
||||
|
||||
To update the icons, change the `flutter_launcher_icons.yaml` configuration file. Especially the `image_path` is relevant. Then run
|
||||
```bash
|
||||
dart run flutter_launcher_icons
|
||||
```
|
||||
|
||||
### Deploying a new version
|
||||
To truly deploy a new version of the application, i.e. to the official app stores, a special CI step is required. This listens for new tags. To create a new tag position yourself on the main branch and run
|
||||
```bash
|
||||
git tag -a v<name> -m "Release <name>"
|
||||
git push origin v<name>
|
||||
```
|
||||
We adhere to the [Semantic Versioning](https://semver.org/) standard, so the tag should be of the form `v0.1.8` for example.
|
||||
|
||||
|
||||
## Fastlane - in depth
|
||||
The application is deployed to the Google Play Store and the Apple App Store using fastlane: [https://docs.fastlane.tools/](https://docs.fastlane.tools/)
|
||||
|
||||
Fastlane is installed as a Ruby gem. Since the bundler-gemfile is scoped to a single directory, a `Gemfile` is included in both the `android` and `ios` directories. Once installed, the usage is
|
||||
```bash
|
||||
cd frontend/android # or ios
|
||||
bundle install
|
||||
bundle exec fastlane <lane>
|
||||
```
|
||||
This is reused in the CI/CD pipeline to automate the deployment process.
|
||||
|
||||
Fastlane assumes mutliple secrets to be present as files in the platform directories. These are:
|
||||
- for android:
|
||||
- `secrets.properties` used by gradle to load secrets needed at execution time
|
||||
- `release.keystore` used by gradle to sign the apk
|
||||
- `google-key.json` used by fastlane to authenticate with the Google Play Store
|
||||
- for ios:
|
||||
- TODO
|
||||
|
||||
These files are stored as secrets in the GitHub repository so that the CI pipeline can access them.
|
10
frontend/android/.gitignore
vendored
@@ -1,10 +1,9 @@
|
||||
gradlew
|
||||
gradlew.bat
|
||||
gradle/
|
||||
gradle-wrapper.jar
|
||||
/.gradle
|
||||
/captures/
|
||||
/gradlew
|
||||
/gradlew.bat
|
||||
/local.properties
|
||||
/secrets.properties
|
||||
GeneratedPluginRegistrant.java
|
||||
|
||||
# Remember to never publicly share your keystore.
|
||||
@@ -12,6 +11,3 @@ GeneratedPluginRegistrant.java
|
||||
key.properties
|
||||
**/*.keystore
|
||||
**/*.jks
|
||||
|
||||
# Fastlane google cloud access
|
||||
google-key.json
|
||||
|
@@ -1,3 +0,0 @@
|
||||
source "https://rubygems.org"
|
||||
|
||||
gem "fastlane"
|
@@ -1,220 +0,0 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (3.0.7)
|
||||
base64
|
||||
nkf
|
||||
rexml
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.970.0)
|
||||
aws-sdk-core (3.202.2)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.88.0)
|
||||
aws-sdk-core (~> 3, >= 3.201.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.159.0)
|
||||
aws-sdk-core (~> 3, >= 3.201.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.9.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
base64 (0.2.0)
|
||||
claide (1.1.0)
|
||||
colored (1.2)
|
||||
colored2 (3.1.2)
|
||||
commander (4.6.0)
|
||||
highline (~> 2.0.0)
|
||||
declarative (0.0.20)
|
||||
digest-crc (0.6.5)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
domain_name (0.6.20240107)
|
||||
dotenv (2.8.1)
|
||||
emoji_regex (3.2.3)
|
||||
excon (0.111.0)
|
||||
faraday (1.10.3)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
faraday-excon (~> 1.1)
|
||||
faraday-httpclient (~> 1.0)
|
||||
faraday-multipart (~> 1.0)
|
||||
faraday-net_http (~> 1.0)
|
||||
faraday-net_http_persistent (~> 1.0)
|
||||
faraday-patron (~> 1.0)
|
||||
faraday-rack (~> 1.0)
|
||||
faraday-retry (~> 1.0)
|
||||
ruby2_keywords (>= 0.0.4)
|
||||
faraday-cookie_jar (0.0.7)
|
||||
faraday (>= 0.8.0)
|
||||
http-cookie (~> 1.0.0)
|
||||
faraday-em_http (1.0.0)
|
||||
faraday-em_synchrony (1.0.0)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.0.4)
|
||||
multipart-post (~> 2)
|
||||
faraday-net_http (1.0.2)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
faraday-patron (1.0.0)
|
||||
faraday-rack (1.0.0)
|
||||
faraday-retry (1.0.3)
|
||||
faraday_middleware (1.2.0)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.3.1)
|
||||
fastlane (2.222.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
aws-sdk-s3 (~> 1.0)
|
||||
babosa (>= 1.0.3, < 2.0.0)
|
||||
bundler (>= 1.12.0, < 3.0.0)
|
||||
colored (~> 1.2)
|
||||
commander (~> 4.6)
|
||||
dotenv (>= 2.1.1, < 3.0.0)
|
||||
emoji_regex (>= 0.1, < 4.0)
|
||||
excon (>= 0.71.0, < 1.0.0)
|
||||
faraday (~> 1.0)
|
||||
faraday-cookie_jar (~> 0.0.6)
|
||||
faraday_middleware (~> 1.0)
|
||||
fastimage (>= 2.1.0, < 3.0.0)
|
||||
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||
google-apis-androidpublisher_v3 (~> 0.3)
|
||||
google-apis-playcustomapp_v1 (~> 0.1)
|
||||
google-cloud-env (>= 1.6.0, < 2.0.0)
|
||||
google-cloud-storage (~> 1.31)
|
||||
highline (~> 2.0)
|
||||
http-cookie (~> 1.0.5)
|
||||
json (< 3.0.0)
|
||||
jwt (>= 2.1.0, < 3)
|
||||
mini_magick (>= 4.9.4, < 5.0.0)
|
||||
multipart-post (>= 2.0.0, < 3.0.0)
|
||||
naturally (~> 2.2)
|
||||
optparse (>= 0.1.1, < 1.0.0)
|
||||
plist (>= 3.1.0, < 4.0.0)
|
||||
rubyzip (>= 2.0.0, < 3.0.0)
|
||||
security (= 0.1.5)
|
||||
simctl (~> 1.6.3)
|
||||
terminal-notifier (>= 2.0.0, < 3.0.0)
|
||||
terminal-table (~> 3)
|
||||
tty-screen (>= 0.6.3, < 1.0.0)
|
||||
tty-spinner (>= 0.8.0, < 1.0.0)
|
||||
word_wrap (~> 1.0.0)
|
||||
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||
xcpretty (~> 0.3.0)
|
||||
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
|
||||
gh_inspector (1.1.3)
|
||||
google-apis-androidpublisher_v3 (0.54.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-core (0.11.3)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
httpclient (>= 2.8.1, < 3.a)
|
||||
mini_mime (~> 1.0)
|
||||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.a)
|
||||
rexml
|
||||
google-apis-iamcredentials_v1 (0.17.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-playcustomapp_v1 (0.13.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-storage_v1 (0.31.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-cloud-core (1.7.1)
|
||||
google-cloud-env (>= 1.0, < 3.a)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (1.6.0)
|
||||
faraday (>= 0.17.3, < 3.0)
|
||||
google-cloud-errors (1.4.0)
|
||||
google-cloud-storage (1.47.0)
|
||||
addressable (~> 2.8)
|
||||
digest-crc (~> 0.4)
|
||||
google-apis-iamcredentials_v1 (~> 0.1)
|
||||
google-apis-storage_v1 (~> 0.31.0)
|
||||
google-cloud-core (~> 1.6)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
mini_mime (~> 1.0)
|
||||
googleauth (1.8.1)
|
||||
faraday (>= 0.17.3, < 3.a)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (>= 0.16, < 2.a)
|
||||
highline (2.0.3)
|
||||
http-cookie (1.0.7)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.8.3)
|
||||
jmespath (1.6.2)
|
||||
json (2.7.2)
|
||||
jwt (2.8.2)
|
||||
base64
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.4.1)
|
||||
nanaimo (0.3.0)
|
||||
naturally (2.2.1)
|
||||
nkf (0.2.0)
|
||||
optparse (0.5.0)
|
||||
os (1.1.4)
|
||||
plist (3.7.1)
|
||||
public_suffix (6.0.1)
|
||||
rake (13.2.1)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
retriable (3.1.2)
|
||||
rexml (3.3.6)
|
||||
strscan
|
||||
rouge (2.0.7)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.3.2)
|
||||
security (0.1.5)
|
||||
signet (0.19.0)
|
||||
addressable (~> 2.8)
|
||||
faraday (>= 0.17.5, < 3.a)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
multi_json (~> 1.10)
|
||||
simctl (1.6.10)
|
||||
CFPropertyList
|
||||
naturally
|
||||
strscan (3.1.0)
|
||||
terminal-notifier (2.0.0)
|
||||
terminal-table (3.0.2)
|
||||
unicode-display_width (>= 1.1.1, < 3)
|
||||
trailblazer-option (0.1.2)
|
||||
tty-cursor (0.7.1)
|
||||
tty-screen (0.8.2)
|
||||
tty-spinner (0.9.3)
|
||||
tty-cursor (~> 0.7)
|
||||
uber (0.1.0)
|
||||
unicode-display_width (2.5.0)
|
||||
word_wrap (1.0.0)
|
||||
xcodeproj (1.25.0)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
atomos (~> 0.1.3)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
nanaimo (~> 0.3.0)
|
||||
rexml (>= 3.3.2, < 4.0)
|
||||
xcpretty (0.3.0)
|
||||
rouge (~> 2.0.7)
|
||||
xcpretty-travis-formatter (1.0.1)
|
||||
xcpretty (~> 0.2, >= 0.0.7)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
x86_64-linux
|
||||
|
||||
DEPENDENCIES
|
||||
fastlane
|
||||
|
||||
BUNDLED WITH
|
||||
2.5.18
|
@@ -1,73 +0,0 @@
|
||||
## Android Setup
|
||||
|
||||
### Keystore setup
|
||||
```bash
|
||||
keytool -genkey -v -keystore release.keystore -keyalg RSA -keysize 2048 -validity 10000 -alias upload
|
||||
```
|
||||
- This is required to store local credentials securely and more importantly to sign the app for google play store distribution.
|
||||
|
||||
|
||||
### Using secret credentials during build
|
||||
Following the guide under [https://developers.google.com/maps/flutter-package/config#android_1](https://developers.google.com/maps/flutter-package/config#android_1).
|
||||
- Add the following to `android/build.gradle`:
|
||||
```gradle
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1"
|
||||
}
|
||||
}
|
||||
```
|
||||
- Add the following to `android/app/build.gradle`:
|
||||
```gradle
|
||||
plugins {
|
||||
// ...
|
||||
id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'
|
||||
}
|
||||
```
|
||||
- Add the credentials to `android/secrets.properties`:
|
||||
```properties
|
||||
MAPS_API_KEY=YOUR_API_KEY
|
||||
```
|
||||
- Reference the credentials in `android/app/src/main/AndroidManifest.xml`:
|
||||
```xml
|
||||
<meta-data
|
||||
android:name="com.google.android.geo.API_KEY"
|
||||
android:value="${MAPS_API_KEY}" />
|
||||
```
|
||||
|
||||
### Signing the app
|
||||
Compared to the flutter template application, a few changes have to be made:
|
||||
- Added to `android/app/build.gradle`:
|
||||
```gradle
|
||||
signingConfigs {
|
||||
release {
|
||||
keyAlias = secretProperties['keyAlias']
|
||||
keyPassword = secretProperties['keyPassword']
|
||||
storeFile = secretProperties['storeFile'] ? file(secretProperties['storeFile']) : null
|
||||
storePassword = secretProperties['storePassword']
|
||||
}
|
||||
}
|
||||
```
|
||||
- Changed the `buildTypes` to use the `release` signing config:
|
||||
```gradle
|
||||
buildTypes {
|
||||
release {
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
}
|
||||
```
|
||||
This makes use of the `secretProperties` defined previously:
|
||||
```gradle
|
||||
secretPropertiesFile.withReader('UTF-8') { reader ->
|
||||
secretProperties.load(reader)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Using the credentials in CI
|
||||
- Add the secret files to the repository secrets (e.g. `ANDROID_SECRETS_PROPERTIES`).
|
||||
|
||||
- temporarily write them back to files during the CI execution:
|
||||
```bash
|
||||
echo {{ secrets.ANDROID_SECRETS }} >> android/secrets.properties
|
||||
```
|
@@ -2,19 +2,14 @@ plugins {
|
||||
id "com.android.application"
|
||||
id "kotlin-android"
|
||||
id "dev.flutter.flutter-gradle-plugin"
|
||||
id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'
|
||||
// last is probably not needed
|
||||
}
|
||||
|
||||
def localPropertiesFile = rootProject.file('local.properties')
|
||||
def localProperties = new Properties()
|
||||
|
||||
def localPropertiesFile = rootProject.file('local.properties')
|
||||
if (localPropertiesFile.exists()) {
|
||||
localPropertiesFile.withReader('UTF-8') { reader ->
|
||||
localProperties.load(reader)
|
||||
}
|
||||
} else {
|
||||
throw new GradleException("local.properties not found")
|
||||
}
|
||||
|
||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||
@@ -27,27 +22,8 @@ if (flutterVersionName == null) {
|
||||
flutterVersionName = '1.0'
|
||||
}
|
||||
|
||||
|
||||
|
||||
def secretPropertiesFile = rootProject.file('secrets.properties')
|
||||
def fallbackPropertiesFile = rootProject.file('fallback.properties')
|
||||
def secretProperties = new Properties()
|
||||
|
||||
if (secretPropertiesFile.exists()) {
|
||||
secretPropertiesFile.withReader('UTF-8') { reader ->
|
||||
secretProperties.load(reader)
|
||||
}
|
||||
} else if (fallbackPropertiesFile.exists()) {
|
||||
fallbackPropertiesFile.withReader('UTF-8') { reader ->
|
||||
secretProperties.load(reader)
|
||||
}
|
||||
} else {
|
||||
throw new GradleException("Secrets file (secrets.properties, fallback.properties) not found")
|
||||
}
|
||||
|
||||
|
||||
android {
|
||||
namespace "com.anydev.anyway"
|
||||
namespace "com.example.fast_network_navigation"
|
||||
compileSdk flutter.compileSdkVersion
|
||||
ndkVersion flutter.ndkVersion
|
||||
|
||||
@@ -66,33 +42,20 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "com.anydev.anyway"
|
||||
applicationId "com.example.fast_network_navigation"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
|
||||
// Minimum Android version for Google Maps SDK
|
||||
// https://developers.google.com/maps/flutter-package/config#android
|
||||
minSdk = 21
|
||||
minSdkVersion flutter.minSdkVersion
|
||||
targetSdkVersion flutter.targetSdkVersion
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
// // Placeholders of keys that are replaced by the build system.
|
||||
manifestPlaceholders += ['MAPS_API_KEY': secretProperties.getProperty('MAPS_API_KEY')]
|
||||
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
release {
|
||||
keyAlias = secretProperties['keyAlias']
|
||||
keyPassword = secretProperties['keyPassword']
|
||||
storeFile = secretProperties['storeFile'] ? file(secretProperties['storeFile']) : null
|
||||
storePassword = secretProperties['storePassword']
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
signingConfig = signingConfigs.release
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,12 +1,6 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- Required to fetch data from the internet. -->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<!-- Required to show user location -->
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
|
||||
<application
|
||||
android:label="anyway"
|
||||
android:label="fast_network_navigation"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
@@ -34,14 +28,8 @@
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2"
|
||||
/>
|
||||
<meta-data
|
||||
android:name="com.google.android.geo.API_KEY"
|
||||
android:value="${MAPS_API_KEY}"
|
||||
/>
|
||||
android:value="2" />
|
||||
</application>
|
||||
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility?hl=en and
|
||||
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||
|
@@ -1,4 +1,4 @@
|
||||
package com.anydev.anyway
|
||||
package com.example.fast_network_navigation
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 544 B |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 442 B |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 721 B |
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -16,14 +16,3 @@ subprojects {
|
||||
tasks.register("clean", Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1"
|
||||
}
|
||||
}
|
@@ -1,3 +0,0 @@
|
||||
# This file mirrors the state of secrets.properties as a reference for the developer.
|
||||
# And as a fallback for build.gradle
|
||||
MAPS_API_KEY=Key
|
@@ -1,2 +0,0 @@
|
||||
json_key_file("google-key.json") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one
|
||||
package_name("com.anydev.anyway") # e.g. com.krausefx.app
|
@@ -1,50 +0,0 @@
|
||||
# Uncomment the line if you want fastlane to automatically update itself
|
||||
# update_fastlane
|
||||
|
||||
default_platform(:android)
|
||||
|
||||
platform :android do
|
||||
|
||||
desc "Deploy a new version as a preview version"
|
||||
lane :deploy_testing do
|
||||
version_name = ENV["VERSION_NAME"]
|
||||
|
||||
sh(
|
||||
"flutter",
|
||||
"build",
|
||||
"appbundle",
|
||||
"--release",
|
||||
"--build-name=#{version_name}",
|
||||
)
|
||||
|
||||
upload_to_play_store(
|
||||
track: 'alpha',
|
||||
skip_upload_apk: true,
|
||||
skip_upload_changelogs: true,
|
||||
aab: "../build/app/outputs/bundle/release/app-release.aab",
|
||||
# this is the default output of flutter build ... --release
|
||||
# in particular this the build folder lies in the flutter root folder
|
||||
# this is the parent folder for the android folder
|
||||
)
|
||||
end
|
||||
|
||||
desc "Deploy a new version as a full release"
|
||||
lane :deploy_release do
|
||||
gradle(
|
||||
task: "clean assembleRelease",
|
||||
properties: {
|
||||
# loaded from environment
|
||||
"android.injected.version.name" => ENV["VERSION_NAME"],
|
||||
}
|
||||
)
|
||||
upload_to_play_store(
|
||||
track: "production",
|
||||
skip_upload_apk: true,
|
||||
skip_upload_changelogs: true,
|
||||
aab: "../build/app/outputs/bundle/release/app-release.aab",
|
||||
# this is the default output of flutter build ... --release
|
||||
# in particular this the build folder lies in the flutter root folder
|
||||
# this is the parent folder for the android folder
|
||||
)
|
||||
end
|
||||
end
|
@@ -1,40 +0,0 @@
|
||||
fastlane documentation
|
||||
----
|
||||
|
||||
# Installation
|
||||
|
||||
Make sure you have the latest version of the Xcode command line tools installed:
|
||||
|
||||
```sh
|
||||
xcode-select --install
|
||||
```
|
||||
|
||||
For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane)
|
||||
|
||||
# Available Actions
|
||||
|
||||
## Android
|
||||
|
||||
### android deploy_testing
|
||||
|
||||
```sh
|
||||
[bundle exec] fastlane android deploy_testing
|
||||
```
|
||||
|
||||
Deploy a new version as a preview version
|
||||
|
||||
### android deploy_release
|
||||
|
||||
```sh
|
||||
[bundle exec] fastlane android deploy_release
|
||||
```
|
||||
|
||||
Deploy a new version as a full release
|
||||
|
||||
----
|
||||
|
||||
This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
|
||||
|
||||
More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools).
|
||||
|
||||
The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).
|
@@ -1,7 +0,0 @@
|
||||
AnyWay - plan city trips your way
|
||||
|
||||
AnyWay is a mobile application that helps users plan city trips. The app allows users to specify their preferences and constraints, and then generates a personalized itinerary for them. The planning follows some guiding principles:
|
||||
- **Personalization**:The user's preferences should be reflected in the choice of destinations.
|
||||
- **Efficiency**:The itinerary should be optimized for the user's constraints.
|
||||
- **Flexibility**: We aknowledge that tourism is a dynamic activity, and that users may want to change their plans on the go.
|
||||
- **Discoverability**: Tourism is an inherently exploratory activity. Once a rough itinerary is generated, detours and spontaneous decisions should be encouraged.
|
Before Width: | Height: | Size: 106 KiB |
Before Width: | Height: | Size: 1.3 MiB |
Before Width: | Height: | Size: 637 KiB |
Before Width: | Height: | Size: 573 KiB |
Before Width: | Height: | Size: 175 KiB |
Before Width: | Height: | Size: 360 KiB |
@@ -1 +0,0 @@
|
||||
AnyWay - plan city trips your way!
|
@@ -1 +0,0 @@
|
||||
AnyWay
|
5
frontend/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip
|
@@ -20,7 +20,7 @@ pluginManagement {
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version "7.3.0" apply false
|
||||
id "org.jetbrains.kotlin.android" version "2.0.20" apply false
|
||||
id "org.jetbrains.kotlin.android" version "1.7.10" apply false
|
||||
}
|
||||
|
||||
include ":app"
|
||||
|
@@ -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 |
@@ -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 |
Before Width: | Height: | Size: 24 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 |
@@ -1,3 +0,0 @@
|
||||
description: This file stores settings for Dart & Flutter DevTools.
|
||||
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||
extensions:
|
@@ -1,17 +0,0 @@
|
||||
flutter_launcher_icons:
|
||||
image_path: "assets/launcher/icon.png"
|
||||
|
||||
# Android section
|
||||
android: true
|
||||
min_sdk_android: 21 # android min sdk min:16, default 21
|
||||
|
||||
# iOS section
|
||||
ios: true
|
||||
remove_alpha_ios: true # app store rejecs icons with alpha
|
||||
|
||||
# Web section
|
||||
web:
|
||||
generate: true
|
||||
image_path: "assets/launcher/icon.png"
|
||||
# background_color: "#hexcode"
|
||||
# theme_color: "#hexcode"
|
@@ -427,7 +427,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
@@ -484,7 +484,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 738 B After Width: | Height: | Size: 295 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 406 B |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 450 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 282 B |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 462 B |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 704 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 406 B |
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 586 B |
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 862 B |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 862 B |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 8.1 KiB |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 762 B |
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -13,7 +13,7 @@
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>anyway</string>
|
||||
<string>fast_network_navigation</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
|
@@ -1,86 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const String APP_NAME = 'AnyWay';
|
||||
|
||||
String API_URL_BASE = 'https://anyway.anydev.info';
|
||||
String API_URL_DEBUG = 'https://anyway.anydev.info';
|
||||
String PRIVACY_URL = 'https://anydev.info/privacy';
|
||||
|
||||
const String MAP_ID = '41c21ac9b81dbfd8';
|
||||
|
||||
|
||||
const Color GRADIENT_START = Color(0xFFF9B208);
|
||||
const Color GRADIENT_END = Color(0xFFE72E77);
|
||||
|
||||
const Color PRIMARY_COLOR = Color(0xFFF38F1A);
|
||||
|
||||
|
||||
|
||||
const double TRIP_PANEL_MAX_HEIGHT = 0.8;
|
||||
const double TRIP_PANEL_MIN_HEIGHT = 0.12;
|
||||
|
||||
ThemeData APP_THEME = ThemeData(
|
||||
primaryColor: PRIMARY_COLOR,
|
||||
|
||||
scaffoldBackgroundColor: Colors.white,
|
||||
cardColor: Colors.white,
|
||||
useMaterial3: true,
|
||||
|
||||
colorScheme: ColorScheme.light(
|
||||
primary: PRIMARY_COLOR,
|
||||
secondary: GRADIENT_END,
|
||||
surface: Colors.white,
|
||||
error: Colors.red,
|
||||
onPrimary: Colors.white,
|
||||
onSecondary: const Color.fromARGB(255, 30, 22, 22),
|
||||
onSurface: Colors.black,
|
||||
onError: Colors.white,
|
||||
brightness: Brightness.light,
|
||||
),
|
||||
|
||||
|
||||
textButtonTheme: const TextButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
foregroundColor: WidgetStatePropertyAll(PRIMARY_COLOR),
|
||||
side: WidgetStatePropertyAll(
|
||||
BorderSide(
|
||||
color: PRIMARY_COLOR,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
|
||||
elevatedButtonTheme: const ElevatedButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
foregroundColor: WidgetStatePropertyAll(PRIMARY_COLOR),
|
||||
)
|
||||
),
|
||||
|
||||
outlinedButtonTheme: const OutlinedButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
foregroundColor: WidgetStatePropertyAll(PRIMARY_COLOR),
|
||||
)
|
||||
),
|
||||
|
||||
|
||||
cardTheme: const CardTheme(
|
||||
shadowColor: Colors.grey,
|
||||
elevation: 2,
|
||||
margin: EdgeInsets.all(10),
|
||||
),
|
||||
|
||||
sliderTheme: const SliderThemeData(
|
||||
trackHeight: 15,
|
||||
inactiveTrackColor: Colors.grey,
|
||||
thumbColor: PRIMARY_COLOR,
|
||||
activeTrackColor: GRADIENT_END
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
const Gradient APP_GRADIENT = LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [GRADIENT_START, GRADIENT_END],
|
||||
);
|
@@ -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")
|
||||
)
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,22 +1,20 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:anyway/constants.dart';
|
||||
import 'package:anyway/layout.dart';
|
||||
import 'package:fast_network_navigation/modules/scaffold.dart';
|
||||
|
||||
void main() => runApp(const App());
|
||||
|
||||
final GlobalKey<ScaffoldMessengerState> rootScaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
|
||||
|
||||
class App extends StatelessWidget {
|
||||
const App({super.key});
|
||||
|
||||
static const appTitle = 'City Nav';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: APP_NAME,
|
||||
home: BasePage(mainScreen: "map"),
|
||||
theme: APP_THEME,
|
||||
scaffoldMessengerKey: rootScaffoldMessengerKey
|
||||
title: appTitle,
|
||||
home: BasePage(title: appTitle),
|
||||
theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.green),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,37 +0,0 @@
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CurrentTripErrorMessage extends StatefulWidget {
|
||||
final Trip trip;
|
||||
const CurrentTripErrorMessage({
|
||||
super.key,
|
||||
required this.trip,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CurrentTripErrorMessage> createState() => _CurrentTripErrorMessageState();
|
||||
}
|
||||
|
||||
class _CurrentTripErrorMessageState extends State<CurrentTripErrorMessage> {
|
||||
@override
|
||||
Widget build(BuildContext context) => Center(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
color: Colors.red,
|
||||
size: 50,
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(left: 10),
|
||||
),
|
||||
AutoSizeText(
|
||||
'Error: ${widget.trip.errorDescription}',
|
||||
maxLines: 3,
|
||||
),
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|