diff --git a/.eslintrc.yml b/.eslintrc.yml
new file mode 100644
index 0000000..97c1864
--- /dev/null
+++ b/.eslintrc.yml
@@ -0,0 +1,11 @@
+env:
+ browser: true
+ es2021: true
+extends:
+ - standard
+parserOptions:
+ ecmaFeatures:
+ jsx: true
+ ecmaVersion: 12
+ sourceType: module
+rules: {}
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 2ae7776..411f528 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -5,4 +5,4 @@ repos:
rev: 1.3.1 # The version of Prospector to use, at least 1.1.7
hooks:
- id: prospector
- language: system
+ language: system
\ No newline at end of file
diff --git a/.prospector.yaml b/.prospector.yaml
index 5e0b84a..59e3ee8 100644
--- a/.prospector.yaml
+++ b/.prospector.yaml
@@ -3,6 +3,7 @@ use: flask
max-line-length: 120
ignore-paths:
- psa_connectedcar
+ - test
pep8:
disable:
- E722
diff --git a/docs/Install.md b/docs/Install.md
index 7b9cb40..6467e27 100644
--- a/docs/Install.md
+++ b/docs/Install.md
@@ -13,7 +13,7 @@ We will retrieve this information:
- On debian based distribution you can install some requirement from repos, it's faster than installtion with pip:
```
- sudo apt-get install python3-typing-extensions python3-pandas python3-plotly python3-paho-mqtt python3-six python3-dateutil python3-brotli libblas-dev liblapack-dev gfortran python3-pycryptodome python3-numpy libatlas3-base python3-cryptography
+ sudo apt-get install python3-typing-extensions python3-plotly python3-paho-mqtt python3-six python3-dateutil python3-brotli libblas-dev liblapack-dev gfortran python3-pycryptodome python3-cryptography
```
- For everyone :
diff --git a/libs/car.py b/libs/car.py
index 27f1d0b..d3fc416 100644
--- a/libs/car.py
+++ b/libs/car.py
@@ -38,6 +38,12 @@ class Car:
def is_hybrid(self) -> bool:
return self.fuel_capacity > 0 and self.battery_power > 0
+ def has_battery(self):
+ return self.battery_power > 0
+
+ def has_fuel(self):
+ return self.fuel_capacity > 0
+
def get_status(self):
if self.status is not None:
return self.status
diff --git a/libs/charging.py b/libs/charging.py
index 50400f0..6b4190b 100644
--- a/libs/charging.py
+++ b/libs/charging.py
@@ -13,17 +13,9 @@ class Charging:
elec_price: ElecPrice = ElecPrice(None)
@staticmethod
- def get_chargings(mini=None, maxi=None) -> List[dict]:
+ def get_chargings() -> List[dict]:
conn = Database.get_db()
- if mini is not None:
- if maxi is not None:
- res = conn.execute("select * from battery WHERE start_at>=? and start_at<=?", (mini, maxi)).fetchall()
- else:
- res = conn.execute("select * from battery WHERE start_at>=?", (mini,)).fetchall()
- elif maxi is not None:
- res = conn.execute("select * from battery WHERE start_at<=?", (maxi,)).fetchall()
- else:
- res = conn.execute("select * from battery").fetchall()
+ res = conn.execute("select * from battery ORDER BY start_at").fetchall()
conn.close()
return list(map(dict, res))
diff --git a/utils.py b/libs/utils.py
similarity index 100%
rename from utils.py
rename to libs/utils.py
diff --git a/my_psacc.py b/my_psacc.py
index 692cd8e..30e05e8 100644
--- a/my_psacc.py
+++ b/my_psacc.py
@@ -22,7 +22,7 @@ from otp.otp import load_otp, new_otp_session, save_otp, ConfigException, Otp
from psa_connectedcar.rest import ApiException
from mylogger import logger
-from utils import rate_limit
+from libs.utils import rate_limit
from web.abrp import Abrp
from web.db import Database
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 2af2088..b3f631e 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,3 +1,2 @@
prospector>=1.3.0
pre-commit
-deepdiff
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index dc113c9..bb3e91c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,10 +4,8 @@ dash_daq
plotly>=4
cryptography>=2.6
Werkzeug>=1.0.0
-pandas
oauth2_client
requests
-numpy
pytz
typing
argparse
@@ -17,6 +15,7 @@ geojson
reverse_geocode
androguard
pycryptodomex
+deepdiff
#swagger req
certifi >= 14.05.14
diff --git a/server.py b/server.py
index d878e7c..b775663 100755
--- a/server.py
+++ b/server.py
@@ -16,7 +16,7 @@ from libs.elec_price import ElecPrice
from mylogger import my_logger
from mylogger import logger
from my_psacc import MyPSACC
-from utils import is_port_in_use
+from libs.utils import is_port_in_use
from web.app import start_app, save_config
CONFIG_NAME = "config.json"
diff --git a/test/test_unit.py b/test/test_unit.py
new file mode 100644
index 0000000..86ca49c
--- /dev/null
+++ b/test/test_unit.py
@@ -0,0 +1,255 @@
+# flake8: noqa
+import json
+import os
+import unittest
+from datetime import datetime, timedelta
+from psa_connectedcar import ApiClient
+import psa_connectedcar as psacc
+import reverse_geocode
+from libs.car import Car, Cars
+from libs.charging import Charging
+from libs.elec_price import ElecPrice
+from my_psacc import MyPSACC
+from ecomix import Ecomix
+from libs.car_model import CarModel
+from mylogger import my_logger
+from otp.otp import load_otp, save_otp
+from charge_control import ChargeControls
+from trip import Trips
+from libs.utils import get_temp
+from web.db import Database
+from web.figures import get_figures, get_battery_curve_fig, get_altitude_fig
+import pytz
+from deepdiff import DeepDiff
+
+latitude = 47.2183
+longitude = -1.55362
+date3 = datetime.utcnow().replace(2021, 3, 1, 12, 00, 00, 00, tzinfo=pytz.UTC)
+date2 = date3 - timedelta(minutes=20)
+date1 = date3 - timedelta(minutes=40)
+date0 = date3 - timedelta(minutes=60)
+DATA_DIR = os.path.dirname(os.path.realpath(__file__)) + "/data/"
+
+
+def compare_dict(result, expected):
+ diff = DeepDiff(expected, result)
+ if diff != {}:
+ raise AssertionError(str(diff))
+ return True
+
+
+dummy_value = 0
+
+
+def callback_test():
+ global dummy_value
+ dummy_value += 1
+
+
+class TestUnit(unittest.TestCase):
+ def __init__(self, methodName='runTest'):
+ super().__init__(methodName)
+ self.test_online = os.environ.get("TEST_ONLINE", "0") == "1"
+ self.vehicule_list = Cars()
+ self.vehicule_list.extend(
+ [Car("VR3UHZKX", "vid", "Peugeot"), Car("VXXXXX", "XXXX", "Peugeot", label="SUV 3008")])
+
+ @staticmethod
+ def get_new_test_db():
+ try:
+ os.remove(DATA_DIR + "tmp.db")
+ except:
+ pass
+ Database.DEFAULT_DB_FILE = DATA_DIR + "tmp.db"
+ Database.db_initialized = False
+ conn = Database.get_db()
+ return conn
+
+ def test_car(self):
+ car1 = Car("VRAAAAAAA", "1sdfdksnfk222", "Peugeot", "208", 46, 0)
+ car2 = Car("VR3UHZKX", "1sdfdksnfk222", "Peugeot")
+ cars = Cars([car1, car2])
+ cars.save_cars(name=DATA_DIR + "test_car.json")
+ Cars.load_cars(name=DATA_DIR + "test_car.json")
+
+ def test_otp_config(self):
+ otp_config = load_otp(filename=DATA_DIR + "otp_test.bin")
+ assert otp_config is not None
+ save_otp(otp_config, filename=DATA_DIR + "otp_test2.bin")
+
+ def test_mypsacc(self):
+ if self.test_online:
+ myp = MyPSACC.load_config("config.json")
+ myp.refresh_token()
+ myp.get_vehicles()
+ car = myp.vehicles_list[0]
+ myp.abrp.abrp_enable_vin.add(car.vin)
+ res = myp.get_vehicle_info(myp.vehicles_list[0].vin)
+ myp.abrp.call(car, 22.1)
+ myp.save_config()
+ assert isinstance(get_temp(str(latitude), str(longitude), myp.weather_api), float)
+
+ def test_car_model(self):
+ assert CarModel.find_model_by_vin("VR3UHZKXZL").name == "e-208"
+ assert CarModel.find_model_by_vin("VR3UKZKXZM").name == "e-2008"
+ assert CarModel.find_model_by_vin("VXKUHZKXZL").name == "corsa-e"
+
+ def test_c02_signal_cache(self):
+ start = datetime.now() - timedelta(minutes=30)
+ end = datetime.now()
+ Ecomix._cache = {'FR': [[start - timedelta(days=1), 100],
+ [start + timedelta(minutes=1), 10],
+ [start + timedelta(minutes=2), 20],
+ [start + timedelta(minutes=3), 30]]}
+ assert Ecomix.get_co2_from_signal_cache(start, end, "FR") == 20
+
+ def test_c02_signal(self):
+ if self.test_online:
+ key = "d186c74bfbcd1da8"
+ Ecomix.co2_signal_key = key
+ def_country = "FR"
+ Ecomix.get_data_from_co2_signal(latitude, longitude, def_country)
+ res = Ecomix.get_co2_from_signal_cache(datetime.now() - timedelta(minutes=5), datetime.now(), def_country)
+ assert isinstance(res, float)
+
+ def test_charge_control(self):
+ charge_control = ChargeControls()
+ charge_control.file_name = "test_charge_control.json"
+ charge_control.save_config(force=True)
+
+ def test_battery_curve(self):
+ from libs.car import Car
+ from libs.charging import Charging
+ try:
+ os.remove("tmp.db")
+ except:
+ pass
+ Database.DEFAULT_DB_FILE = "tmp.db"
+ conn = Database.get_db()
+ list(map(dict, conn.execute('PRAGMA database_list').fetchall()))
+ vin = "VR3UHZKXZL"
+ car = Car(vin, "id", "Peugeot")
+ Charging.record_charging(car, "InProgress", date0, 50, latitude, longitude, "FR", "slow")
+ Charging.record_charging(car, "InProgress", date1, 75, latitude, longitude, "FR", "slow")
+ Charging.record_charging(car, "InProgress", date2, 85, latitude, longitude, "FR", "slow")
+ Charging.record_charging(car, "InProgress", date3, 90, latitude, longitude, "FR", "slow")
+
+ res = Database.get_battery_curve(Database.get_db(), date0, vin)
+ assert len(res) == 3
+
+ def test_sdk(self):
+
+ res = {
+ 'lastPosition': {'type': 'Feature', 'geometry': {'type': 'Point', 'coordinates': [9.65457, 49.96119, 21]},
+ 'properties': {'updatedAt': '2021-03-29T05:16:10Z', 'heading': 126,
+ 'type': 'Estimated'}}, 'preconditionning': {
+ 'airConditioning': {'updatedAt': '2021-04-01T16:17:01Z', 'status': 'Disabled', 'programs': [
+ {'enabled': False, 'slot': 1, 'recurrence': 'Daily', 'start': 'PT21H40M',
+ 'occurence': {'day': ['Sat']}}]}},
+ 'energy': [{'updatedAt': '2021-02-23T22:29:03Z', 'type': 'Fuel', 'level': 0},
+ {'updatedAt': '2021-04-01T16:17:01Z', 'type': 'Electric', 'level': 70, 'autonomy': 192,
+ 'charging': {'plugged': False, 'status': 'Disconnected', 'remainingTime': 'PT0S',
+ 'chargingRate': 0, 'chargingMode': 'No', 'nextDelayedTime': 'PT21H30M'}}],
+ 'createdAt': '2021-04-01T16:17:01Z',
+ 'battery': {'voltage': 99, 'current': 0, 'createdAt': '2021-04-01T16:17:01Z'},
+ 'kinetic': {'createdAt': '2021-03-29T05:16:10Z', 'moving': False},
+ 'privacy': {'createdAt': '2021-04-01T16:17:01Z', 'state': 'None'},
+ 'service': {'type': 'Electric', 'updatedAt': '2021-02-23T21:10:29Z'}, '_links': {'self': {
+ 'href': 'https://api.groupe-psa.com/connectedcar/v4/user/vehicles/myid/status'},
+ 'vehicles': {
+ 'href': 'https://api.groupe-psa.com/connectedcar/v4/user/vehicles/myid'}},
+ 'timed.odometer': {'createdAt': None, 'mileage': 1107.1}, 'updatedAt': '2021-04-01T16:17:01Z'}
+ api = ApiClient()
+ status: psacc.models.status.Status = api._ApiClient__deserialize(res, "Status")
+ geocode_res = reverse_geocode.search([(status.last_position.geometry.coordinates[:2])[::-1]])[0]
+ assert geocode_res["country_code"] == "DE"
+ TestUnit.get_new_test_db()
+ car = Car("XX", "vid", "Peugeot")
+ car.status = status
+ myp = MyPSACC.load_config(DATA_DIR + "config.json")
+ myp.record_info(car)
+ assert "features" in json.loads(Database.get_recorded_position())
+ # electric should be first
+ assert car.status.energy[0].type == 'Electric'
+
+ def test_record_position_charging(self):
+ TestUnit.get_new_test_db()
+ ElecPrice.CONFIG_FILENAME = DATA_DIR + "config.ini"
+ car = self.vehicule_list[0]
+ Database.record_position(None, car.vin, 11, latitude, longitude - 0.05, None, date0, 40, None, False)
+ Database.record_position(None, car.vin, 20, latitude, longitude, 32, date1, 35, None, False)
+ Database.record_position(None, car.vin, 30, latitude, longitude, 42, date2, 30, None, False)
+ Database.add_altitude_to_db(Database.get_db())
+ data = json.loads(Database.get_recorded_position())
+ assert data["features"][1]["geometry"]["coordinates"] == [float(longitude), float(latitude)]
+ trips = Trips.get_trips(self.vehicule_list)[car.vin]
+ trip = trips[0]
+ map(trip.add_temperature, [10, 13, 15])
+ res = trip.get_info()
+ assert compare_dict(res, {'consumption_km': 24.21052631578947,
+ 'start_at': date0,
+ 'consumption_by_temp': None,
+ 'positions': {'lat': [latitude], 'long': [longitude]},
+ 'duration': 40.0, 'speed_average': 28.5, 'distance': 19.0, 'mileage': 30.0,
+ 'altitude_diff': 2, 'id': 1, 'consumption': 4.6})
+
+ Charging.elec_price = ElecPrice.read_config()
+ start_level = 40
+ end_level = 85
+ Charging.record_charging(car, "InProgress", date0, start_level, latitude, longitude, None, "slow")
+ Charging.record_charging(car, "InProgress", date1, 70, latitude, longitude, "FR", "slow")
+ Charging.record_charging(car, "InProgress", date1, 70, latitude, longitude, "FR", "slow")
+ Charging.record_charging(car, "InProgress", date2, 80, latitude, longitude, "FR", "slow")
+ Charging.record_charging(car, "Stopped", date3, end_level, latitude, longitude, "FR", "slow")
+ chargings = Charging.get_chargings()
+ co2 = chargings[0]["co2"]
+ assert isinstance(co2, float)
+ assert compare_dict(chargings, [{'start_at': date0,
+ 'stop_at': date3,
+ 'VIN': 'VR3UHZKX',
+ 'start_level': 40,
+ 'end_level': 85,
+ 'co2': co2,
+ 'kw': 20.7,
+ 'price': 3.84,
+ 'charging_mode': 'slow'}])
+ assert get_figures(car)
+ row = {"start_at": date0.strftime("%Y-%m-%dT%H:%M:%S+00:00"),
+ "stop_at": date3.strftime("%Y-%m-%dT%H:%M:%S+00:00"), "start_level": start_level, "end_level": end_level}
+ assert get_battery_curve_fig(row, car) is not None
+ assert get_altitude_fig(trip) is not None
+
+ def test_fuel_car(self):
+ TestUnit.get_new_test_db()
+ ElecPrice.CONFIG_FILENAME = DATA_DIR + "config.ini"
+ car = self.vehicule_list[1]
+ Database.record_position(None, car.vin, 11, latitude, longitude, 22, date0, 40, 30, False)
+ Database.record_position(None, car.vin, 20, latitude, longitude, 22, date1, 35, 29, False)
+ Database.record_position(None, car.vin, 30, latitude, longitude, 22, date2, 30, 28, False)
+ trips = Trips.get_trips(self.vehicule_list)
+ res = trips[car.vin].get_trips_as_dict()
+ assert compare_dict(res, [{'consumption_km': 5.684210526315789,
+ 'start_at': date0,
+ 'consumption_by_temp': None,
+ 'positions': {'lat': [latitude],
+ 'long': [longitude]},
+ 'duration': 40.0,
+ 'speed_average': 28.5,
+ 'distance': 19.0,
+ 'mileage': 30.0,
+ 'altitude_diff': 0,
+ 'id': 1,
+ 'consumption': 1.08,
+ 'consumption_fuel_km': 10.53}])
+
+ def test_db_callback(self):
+ old_dummy_value = dummy_value
+ TestUnit.get_new_test_db()
+ Database.set_db_callback(callback_test)
+ assert old_dummy_value == dummy_value
+ Database.record_position(None, "xx", 11, latitude, longitude - 0.05, None, date0, 40, None, False)
+ assert old_dummy_value != dummy_value
+
+if __name__ == '__main__':
+ my_logger(handler_level=os.environ.get("DEBUG_LEVEL", 20))
+ unittest.main()
diff --git a/trip.py b/trip.py
index 207e1a9..a3bfe75 100644
--- a/trip.py
+++ b/trip.py
@@ -2,7 +2,6 @@ import logging
from statistics import mean
from typing import List, Dict
-from dateutil import tz
from geojson import Feature, FeatureCollection, MultiLineString
from libs.car import Cars, Car
@@ -38,6 +37,7 @@ class Trip:
self.car: Car = None
self.altitude_diff = None
self.temperatures = []
+ self.id = None
def add_points(self, latitude, longitude):
self.positions.append(Points(latitude, longitude))
@@ -78,19 +78,22 @@ class Trip:
"average consumption": self.consumption_km,
"average consumption fuel": self.consumption_fuel_km})
- def get_info(self, row_id=None):
- res = {"start_at": self.start_at.astimezone(tz.tzlocal()).replace(tzinfo=None).strftime("%x %X"),
- # convert to naive tz,
- "duration": self.duration * 60, "speed_average": self.speed_average,
- "consumption_km": self.consumption_km, "consumption_fuel_km": self.consumption_fuel_km,
- "distance": self.distance, "mileage": self.mileage, "altitude_diff": self.altitude_diff}
- if row_id is not None:
- res["id"] = row_id
- return res
- def get_consumption(self):
- return {"speed": self.speed_average, "consumption_km": self.consumption_km, "date": self.start_at,
- "consumption_by_temp": self.get_temperature()}
+ def get_info(self):
+
+ res = {"consumption_km": self.consumption_km, "start_at": self.start_at,
+ "consumption_by_temp": self.get_temperature(), "positions": self.get_positions(),
+ "duration": self.duration * 60, "speed_average": self.speed_average, "distance": self.distance,
+ "mileage": self.mileage, "altitude_diff": self.altitude_diff, "id": self.id,
+ "consumption": self.consumption
+ }
+ if self.car.has_battery():
+ res["consumption_km"] = self.consumption_km
+
+ if self.car.has_fuel():
+ res["consumption_fuel_km"] = self.consumption_fuel_km
+
+ return res
def set_altitude_diff(self, start, end):
try:
@@ -98,18 +101,26 @@ class Trip:
except (NameError, TypeError):
pass
+ def get_positions(self):
+ lat = []
+ long = []
+ for position in self.positions:
+ lat.append(position.latitude)
+ long.append(position.longitude)
+ return {"lat": lat, "long": long}
+
class Trips(list):
def __init__(self, *args):
list.__init__(self, *args)
+ self.trip_num = 1
def to_geo_json(self):
feature_collection = FeatureCollection(self)
return feature_collection
- def get_long_trips(self):
- res = [trip.get_consumption() for trip in self if trip.consumption > 1.8]
- return res
+ def get_trips_as_dict(self):
+ return [trip.get_info() for trip in self]
def get_distance(self):
return self[-1].mileage - self[0].mileage
@@ -117,6 +128,8 @@ class Trips(list):
def check_and_append(self, trip: Trip):
if trip.consumption_km <= trip.car.max_elec_consumption and \
trip.consumption_fuel_km <= trip.car.max_fuel_consumption:
+ trip.id = self.trip_num
+ self.trip_num += 1
self.append(trip)
return True
logger.debugv("trip discarded")
@@ -210,11 +223,3 @@ class Trips(list):
trips_by_vin[vin] = trips
conn.close()
return trips_by_vin
-
- def get_info(self):
- res = []
- row_id = 1
- for trip in self:
- res.append(trip.get_info(row_id))
- row_id += 1
- return res
diff --git a/web/app.py b/web/app.py
index 9c7aebf..4b3bb2f 100644
--- a/web/app.py
+++ b/web/app.py
@@ -27,7 +27,8 @@ myp: MyPSACC = None
chc: ChargeControls = None
-def start_app(title, base_path, debug: bool, host, port, reloader=False): # pylint: disable=too-many-arguments
+def start_app(title, base_path, debug: bool, host, port, reloader=False, # pylint: disable=too-many-arguments
+ unminified=False):
global app, dash_app, dispatcher
try:
lang = locale.getlocale()[0].split("_")[0]
@@ -36,6 +37,8 @@ def start_app(title, base_path, debug: bool, host, port, reloader=False): # pyl
except (IndexError, locale.Error):
locale_url = None
logger.warning("Can't get language")
+ if unminified:
+ locale_url = ["assets/plotly-with-meta.js"]
app = Flask(__name__)
app.config["DEBUG"] = debug
if base_path == "/":
@@ -46,6 +49,7 @@ def start_app(title, base_path, debug: bool, host, port, reloader=False): # pyl
requests_pathname_prefix = base_path + "/"
dash_app = dash.Dash(external_stylesheets=[dbc.themes.BOOTSTRAP], external_scripts=locale_url, title=title,
server=app, requests_pathname_prefix=requests_pathname_prefix)
+ dash_app.enable_dev_tools(reloader)
# keep this line
import web.views # pylint: disable=unused-import,import-outside-toplevel
return run_simple(host, port, application, use_reloader=reloader, use_debugger=debug)
diff --git a/web/assets/clientside.js b/web/assets/clientside.js
new file mode 100644
index 0000000..cb88d46
--- /dev/null
+++ b/web/assets/clientside.js
@@ -0,0 +1,227 @@
+class Avg {
+ constructor () {
+ this.total = 0
+ this.count = 0
+ }
+
+ addValue (value) {
+ if (typeof value === 'number') {
+ this.count++
+ this.total = ((this.total * (this.count - 1)) / this.count) + (value / this.count)
+ }
+ }
+
+ average () {
+ return this.total
+ }
+
+ static getAverageFromKey (array, key) {
+ const avg = new Avg()
+ array.forEach(function (obj) { avg.addValue(obj[key]) })
+ return avg.average()
+ }
+}
+const logger = (function () {
+ let oldConsoleLog = null
+ const pub = {}
+
+ pub.enableLogger = function enableLogger () {
+ if (oldConsoleLog == null) {
+ return
+ }
+
+ window.console.log = oldConsoleLog
+ }
+
+ pub.disableLogger = function disableLogger () {
+ oldConsoleLog = console.log
+ window.console.log = function () {}
+ }
+
+ return pub
+}())
+function addLocaleDate (data, dateKey) {
+ const dateOption = [undefined, { hour: 'numeric', minute: 'numeric' }]
+ function dateToLocale (row, key) {
+ const date = new Date(row[key])
+ row[key] = date
+ row[key + '_str'] = date.toLocaleDateString(...dateOption)
+ }
+ let datasetName, dataset
+ for ([datasetName, dataset] of Object.entries(data)) {
+ dataset.forEach(function (row) {
+ dateKey[datasetName].forEach(key => dateToLocale(row, key))
+ })
+ }
+}
+
+function filterDataset (data, range) {
+ function dateFromISO (st) {
+ return new Date(st).getTime() / 1000
+ }
+ function isInRange (st) {
+ const tsDate = dateFromISO(st)
+ return tsDate >= range[0] && tsDate <= range[1]
+ }
+ const res = {}
+ res.trips = data.trips.filter(line => isInRange(line.start_at))
+ res.chargings = data.chargings.filter(line => isInRange(line.start_at))
+ console.log('filtered_dataset', res)
+ return res
+}
+
+function filterShortTrip (data) {
+ const longTrips = {
+ trips: data.trips.filter(line => line.distance > 10),
+ chargings: data.chargings
+ }
+ console.log('long trips:', longTrips)
+ return longTrips
+}
+
+function updateFigures (data, oldFigure, x, y) {
+ const trips = data.trips
+ const figures = []
+ let i = 0
+ y.forEach(function (yLabel) {
+ const xLabel = x[i]
+ const figure = Object.assign({}, oldFigure[i])
+ i++
+ // console.log(oldFigure[i]);
+ // var unique_y_label = y[i].filter((v, i, a) => a.indexOf(v) === i);
+ // var data_nonnull = trips
+ // unique_y_label.forEach(function(label) {
+ // data_nonnull = data_nonnull.filter(line => line[label]);
+ // });
+ if ('mapbox' in figure.layout) {
+ figure.data[0].lat = []
+ figure.data[0].lon = []
+ figure.data[0].hovertext = []
+ let trip = null
+ for (trip of trips) {
+ const xPos = trip.positions[xLabel]
+ figure.data[0].lat.push(...xPos, null)
+ figure.data[0].lon.push(...trip.positions[yLabel[0]])
+ figure.data[0].hovertext.push(...Array(xPos.length).fill(trip[yLabel[1]]), null)
+ }
+ if (trip) {
+ const lastPos = trip.positions[yLabel[0]].length - 1
+ figure.layout.mapbox.center.lat = trip.positions[xLabel][lastPos]
+ figure.layout.mapbox.center.lon = trip.positions[yLabel[0]][lastPos]
+ figure.data[1].lat = [figure.layout.mapbox.center.lat]
+ figure.data[1].lon = [figure.layout.mapbox.center.lon]
+ }
+ } else {
+ const xValues = trips.map(a => a[xLabel])
+ // for each y label
+ for (let j = 0; j < yLabel.length; j++) {
+ figure.data[j].y = trips.map(a => a[yLabel[j]])
+ figure.data[j].x = xValues
+ }
+ }
+ console.log(xLabel, figure)
+ figures.push(figure)
+ })
+ return figures
+}
+
+function updateTables (data, tables) {
+ console.log('tables', tables)
+ const figures = []
+ tables.forEach(function (table) {
+ figures.push(data[table.src])
+ })
+ return figures
+}
+
+function updateCardsValue (data) {
+ const res = {}
+ let avgPriceKw
+ let avgC02 = new Avg(); let avgKw = new Avg(); const avgTime = new Avg()
+ const avgPrice = new Avg()
+ data.chargings.forEach(function (charge) {
+ const diff = ((new Date(charge.stop_at)) - (new Date(charge.start_at))) / 3600000
+ avgKw.addValue(charge.kw)
+ avgC02.addValue(charge.co2)
+ avgPrice.addValue(charge.price)
+ if (diff > 0) {
+ avgTime.addValue(diff)
+ }
+ })
+ if (data.chargings.length > 0) {
+ avgKw = avgKw.average()
+ avgC02 = avgC02.average()
+ avgPriceKw = avgPrice.average() / avgKw
+ res.avg_emission_kw = avgC02
+ res.avg_chg_speed = avgKw / avgTime.average()
+ }
+ if (data.trips.length > 0) {
+ const totalDistance = data.trips[data.trips.length - 1].mileage - data.trips[0].mileage
+ res.avg_consum_kw = Avg.getAverageFromKey(data.trips, 'consumption_km')
+ res.elec_consum_kw = totalDistance * res.avg_consum_kw / 100
+ }
+ if (data.trips.length > 0 && data.chargings.length > 0) {
+ res.avg_emission_km = res.avg_emission_kw * res.avg_consum_kw / 100
+ res.elec_consum_price = avgPriceKw * res.elec_consum_kw
+ res.avg_consum_price = avgPriceKw * res.avg_consum_kw
+ }
+ for (const [key, value] of Object.entries(res)) {
+ document.getElementById(key).innerHTML = value.toPrecision(3)
+ }
+}
+
+function sortDataset (ctx, data, tables) {
+ const tableId = ctx.prop_id.split('.')[0]
+ if (ctx.value.length > 0) {
+ const asc = ctx.value[0].direction === 'asc'
+ let columnId = ctx.value[0].column_id
+ const table = tables.filter(table => table.table_id === tableId)[0]
+ let sorted
+ if (columnId.endsWith('_str')) {
+ columnId = columnId.slice(0, -4)
+ sorted = data[table.src].sort(function (a, b) {
+ return a[columnId] - b[columnId]
+ })
+ } else if (typeof data[table.src][0][columnId] === 'number') {
+ sorted = data[table.src].sort(function (a, b) {
+ return a[columnId] - b[columnId]
+ })
+ } else {
+ sorted = data[table.src].sort((a, b) => a[columnId].localeCompare(b[columnId]))
+ }
+ if (asc === false) {
+ sorted = sorted.reverse()
+ }
+ data[table.src] = sorted
+ }
+}
+
+function filterAndSort (data, range, figures, p, log) { // eslint-disable-line no-unused-vars
+ if (log > 10) {
+ logger.disableLogger()
+ }
+ const ctx = dash_clientside.callback_context.triggered // eslint-disable-line no-undef
+ const outFigures = []; let dataFiltered
+ console.log('figures:', figures)
+ console.log('data:', data)
+ console.log('ctx', ctx)
+ if (ctx.length > 0 && ctx[0].prop_id.endsWith('sort_by')) {
+ dataFiltered = filterDataset(data, range)
+ sortDataset(ctx[0], dataFiltered, p.table_src)
+ outFigures.push(...updateTables(dataFiltered, p.table_src))
+ outFigures.push(...figures.graph)
+ outFigures.push(...figures.maps)
+ } else {
+ addLocaleDate(data, p.date_columns)
+ dataFiltered = filterDataset(data, range)
+ outFigures.push(...updateTables(dataFiltered, p.table_src))
+ console.log(dataFiltered.trips.length)
+ const longTrips = filterShortTrip(dataFiltered)
+ console.log('trips', dataFiltered.trips.length)
+ console.log('longTrips', longTrips.trips.length)
+ outFigures.push(...updateFigures(longTrips, figures.graph, p.graph_x_label, p.graph_y_label))
+ outFigures.push(...updateFigures(dataFiltered, figures.maps, p.map_x_label, p.map_y_label))
+ updateCardsValue(longTrips)
+ }
+ return outFigures
+}
diff --git a/web/static/images/battery-charge-line.svg b/web/assets/images/battery-charge-line.svg
similarity index 100%
rename from web/static/images/battery-charge-line.svg
rename to web/assets/images/battery-charge-line.svg
diff --git a/web/static/images/consumption.svg b/web/assets/images/consumption.svg
similarity index 100%
rename from web/static/images/consumption.svg
rename to web/assets/images/consumption.svg
diff --git a/web/static/images/electricity bill.svg b/web/assets/images/electricity bill.svg
similarity index 100%
rename from web/static/images/electricity bill.svg
rename to web/assets/images/electricity bill.svg
diff --git a/web/static/images/pollution.svg b/web/assets/images/pollution.svg
similarity index 100%
rename from web/static/images/pollution.svg
rename to web/assets/images/pollution.svg
diff --git a/web/assets/sprites/osm-liberty.json b/web/assets/sprites/osm-liberty.json
new file mode 100644
index 0000000..4aae1dd
--- /dev/null
+++ b/web/assets/sprites/osm-liberty.json
@@ -0,0 +1,1745 @@
+{
+ "aerialway-11": {
+ "height": 15,
+ "pixelRatio": 1,
+ "width": 15,
+ "x": 314,
+ "y": 0
+ },
+ "aerialway-15": {
+ "height": 19,
+ "pixelRatio": 1,
+ "width": 19,
+ "x": 122,
+ "y": 229
+ },
+ "airfield-11": {
+ "height": 15,
+ "pixelRatio": 1,
+ "width": 15,
+ "x": 329,
+ "y": 0
+ },
+ "airfield-15": {
+ "height": 19,
+ "pixelRatio": 1,
+ "width": 19,
+ "x": 141,
+ "y": 229
+ },
+ "airport-11": {
+ "height": 15,
+ "pixelRatio": 1,
+ "width": 15,
+ "x": 344,
+ "y": 0
+ },
+ "airport-15": {
+ "height": 19,
+ "pixelRatio": 1,
+ "width": 19,
+ "x": 160,
+ "y": 229
+ },
+ "alcohol_shop-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 485,
+ "y": 145
+ },
+ "alcohol_shop-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 104,
+ "y": 94
+ },
+ "america_football-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 418,
+ "y": 166
+ },
+ "america_football-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 117,
+ "y": 64
+ },
+ "amusement_park-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 435,
+ "y": 166
+ },
+ "amusement_park-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 138,
+ "y": 64
+ },
+ "aquarium-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 452,
+ "y": 166
+ },
+ "aquarium-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 159,
+ "y": 64
+ },
+ "arrow": {
+ "height": 7,
+ "pixelRatio": 1,
+ "width": 20,
+ "x": 285,
+ "y": 250
+ },
+ "art_gallery-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 469,
+ "y": 166
+ },
+ "art_gallery-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 180,
+ "y": 64
+ },
+ "attraction-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 486,
+ "y": 166
+ },
+ "attraction-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 201,
+ "y": 64
+ },
+ "bakery-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 252,
+ "y": 187
+ },
+ "bakery-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 222,
+ "y": 64
+ },
+ "bank-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 269,
+ "y": 187
+ },
+ "bank-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 125,
+ "y": 94
+ },
+ "bar-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 286,
+ "y": 187
+ },
+ "bar-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 146,
+ "y": 94
+ },
+ "baseball-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 303,
+ "y": 187
+ },
+ "baseball-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 167,
+ "y": 94
+ },
+ "basketball-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 320,
+ "y": 187
+ },
+ "basketball-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 188,
+ "y": 94
+ },
+ "beer-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 337,
+ "y": 187
+ },
+ "beer-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 209,
+ "y": 94
+ },
+ "bicycle-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 354,
+ "y": 187
+ },
+ "bicycle-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 230,
+ "y": 94
+ },
+ "bicycle_rental-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 371,
+ "y": 187
+ },
+ "bicycle_rental-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 124,
+ "y": 0
+ },
+ "building-11": {
+ "height": 15,
+ "pixelRatio": 1,
+ "width": 15,
+ "x": 359,
+ "y": 0
+ },
+ "building-15": {
+ "height": 19,
+ "pixelRatio": 1,
+ "width": 19,
+ "x": 179,
+ "y": 229
+ },
+ "bus-11": {
+ "height": 15,
+ "pixelRatio": 1,
+ "width": 15,
+ "x": 374,
+ "y": 0
+ },
+ "bus-15": {
+ "height": 19,
+ "pixelRatio": 1,
+ "width": 19,
+ "x": 198,
+ "y": 229
+ },
+ "butcher-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 388,
+ "y": 187
+ },
+ "butcher-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 145,
+ "y": 0
+ },
+ "ca-transcanada_2": {
+ "height": 30,
+ "pixelRatio": 1,
+ "width": 30,
+ "x": 64,
+ "y": 0
+ },
+ "cafe-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 405,
+ "y": 187
+ },
+ "cafe-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 166,
+ "y": 0
+ },
+ "campsite-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 422,
+ "y": 187
+ },
+ "campsite-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 187,
+ "y": 0
+ },
+ "car-11": {
+ "height": 15,
+ "pixelRatio": 1,
+ "width": 15,
+ "x": 389,
+ "y": 0
+ },
+ "car-15": {
+ "height": 19,
+ "pixelRatio": 1,
+ "width": 19,
+ "x": 217,
+ "y": 229
+ },
+ "castle-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 439,
+ "y": 187
+ },
+ "castle-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 208,
+ "y": 0
+ },
+ "cemetery-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 456,
+ "y": 187
+ },
+ "cemetery-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 229,
+ "y": 0
+ },
+ "cinema-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 473,
+ "y": 187
+ },
+ "cinema-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 0,
+ "y": 124
+ },
+ "circle-stroked-11": {
+ "height": 15,
+ "pixelRatio": 1,
+ "width": 15,
+ "x": 404,
+ "y": 0
+ },
+ "circle-stroked-15": {
+ "height": 19,
+ "pixelRatio": 1,
+ "width": 19,
+ "x": 236,
+ "y": 229
+ },
+ "circle-11": {
+ "height": 15,
+ "pixelRatio": 1,
+ "width": 15,
+ "x": 419,
+ "y": 0
+ },
+ "circle-15": {
+ "height": 19,
+ "pixelRatio": 1,
+ "width": 19,
+ "x": 252,
+ "y": 124
+ },
+ "clothing_store-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 490,
+ "y": 187
+ },
+ "clothing_store-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 21,
+ "y": 124
+ },
+ "college-11": {
+ "height": 16,
+ "pixelRatio": 1,
+ "width": 16,
+ "x": 250,
+ "y": 0
+ },
+ "college-15": {
+ "height": 20,
+ "pixelRatio": 1,
+ "width": 20,
+ "x": 42,
+ "y": 229
+ },
+ "commercial-11": {
+ "height": 15,
+ "pixelRatio": 1,
+ "width": 15,
+ "x": 434,
+ "y": 0
+ },
+ "commercial-15": {
+ "height": 19,
+ "pixelRatio": 1,
+ "width": 19,
+ "x": 271,
+ "y": 124
+ },
+ "cricket-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 252,
+ "y": 208
+ },
+ "cricket-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 42,
+ "y": 124
+ },
+ "cross-11": {
+ "height": 15,
+ "pixelRatio": 1,
+ "width": 15,
+ "x": 449,
+ "y": 0
+ },
+ "cross-15": {
+ "height": 19,
+ "pixelRatio": 1,
+ "width": 19,
+ "x": 290,
+ "y": 124
+ },
+ "dam-11": {
+ "height": 15,
+ "pixelRatio": 1,
+ "width": 15,
+ "x": 464,
+ "y": 0
+ },
+ "dam-15": {
+ "height": 19,
+ "pixelRatio": 1,
+ "width": 19,
+ "x": 309,
+ "y": 124
+ },
+ "danger-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 269,
+ "y": 208
+ },
+ "danger-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 63,
+ "y": 124
+ },
+ "default_1": {
+ "height": 18,
+ "pixelRatio": 1,
+ "width": 18,
+ "x": 442,
+ "y": 145
+ },
+ "default_2": {
+ "height": 18,
+ "pixelRatio": 1,
+ "width": 25,
+ "x": 460,
+ "y": 145
+ },
+ "default_3": {
+ "height": 18,
+ "pixelRatio": 1,
+ "width": 32,
+ "x": 252,
+ "y": 166
+ },
+ "default_4": {
+ "height": 18,
+ "pixelRatio": 1,
+ "width": 39,
+ "x": 284,
+ "y": 166
+ },
+ "default_5": {
+ "height": 18,
+ "pixelRatio": 1,
+ "width": 45,
+ "x": 323,
+ "y": 166
+ },
+ "default_6": {
+ "height": 18,
+ "pixelRatio": 1,
+ "width": 50,
+ "x": 368,
+ "y": 166
+ },
+ "dentist-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 286,
+ "y": 208
+ },
+ "dentist-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 84,
+ "y": 124
+ },
+ "doctor-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 303,
+ "y": 208
+ },
+ "doctor-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 105,
+ "y": 124
+ },
+ "dog_park-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 320,
+ "y": 208
+ },
+ "dog_park-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 126,
+ "y": 124
+ },
+ "dot-10": {
+ "height": 10,
+ "pixelRatio": 1,
+ "width": 10,
+ "x": 266,
+ "y": 250
+ },
+ "dot-11": {
+ "height": 11,
+ "pixelRatio": 1,
+ "width": 11,
+ "x": 255,
+ "y": 250
+ },
+ "dot_9": {
+ "height": 9,
+ "pixelRatio": 1,
+ "width": 9,
+ "x": 276,
+ "y": 250
+ },
+ "drinking-water-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 337,
+ "y": 208
+ },
+ "drinking_water-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 147,
+ "y": 124
+ },
+ "embassy-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 354,
+ "y": 208
+ },
+ "embassy-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 168,
+ "y": 124
+ },
+ "entrance-11": {
+ "height": 15,
+ "pixelRatio": 1,
+ "width": 15,
+ "x": 479,
+ "y": 0
+ },
+ "entrance-15": {
+ "height": 19,
+ "pixelRatio": 1,
+ "width": 19,
+ "x": 328,
+ "y": 124
+ },
+ "fast_food-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 371,
+ "y": 208
+ },
+ "fast_food-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 189,
+ "y": 124
+ },
+ "ferry-11": {
+ "height": 15,
+ "pixelRatio": 1,
+ "width": 15,
+ "x": 494,
+ "y": 0
+ },
+ "ferry-15": {
+ "height": 19,
+ "pixelRatio": 1,
+ "width": 19,
+ "x": 347,
+ "y": 124
+ },
+ "fire-station-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 388,
+ "y": 208
+ },
+ "fire-station-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 210,
+ "y": 124
+ },
+ "fuel-11": {
+ "height": 15,
+ "pixelRatio": 1,
+ "width": 15,
+ "x": 0,
+ "y": 250
+ },
+ "fuel-15": {
+ "height": 19,
+ "pixelRatio": 1,
+ "width": 19,
+ "x": 366,
+ "y": 124
+ },
+ "garden-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 405,
+ "y": 208
+ },
+ "garden-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 231,
+ "y": 124
+ },
+ "gb-motorway_3": {
+ "height": 30,
+ "pixelRatio": 1,
+ "width": 50,
+ "x": 0,
+ "y": 64
+ },
+ "gift-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 422,
+ "y": 208
+ },
+ "gift-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 0,
+ "y": 145
+ },
+ "golf-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 439,
+ "y": 208
+ },
+ "golf-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 21,
+ "y": 145
+ },
+ "grocery-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 456,
+ "y": 208
+ },
+ "grocery-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 42,
+ "y": 145
+ },
+ "hairdresser-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 473,
+ "y": 208
+ },
+ "hairdresser-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 63,
+ "y": 145
+ },
+ "harbor-11": {
+ "height": 15,
+ "pixelRatio": 1,
+ "width": 15,
+ "x": 15,
+ "y": 250
+ },
+ "harbor-15": {
+ "height": 19,
+ "pixelRatio": 1,
+ "width": 19,
+ "x": 385,
+ "y": 124
+ },
+ "heart-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 490,
+ "y": 208
+ },
+ "heart-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 84,
+ "y": 145
+ },
+ "heliport-11": {
+ "height": 15,
+ "pixelRatio": 1,
+ "width": 15,
+ "x": 30,
+ "y": 250
+ },
+ "heliport-15": {
+ "height": 19,
+ "pixelRatio": 1,
+ "width": 19,
+ "x": 404,
+ "y": 124
+ },
+ "hospital-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 255,
+ "y": 229
+ },
+ "hospital-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 105,
+ "y": 145
+ },
+ "ice_cream-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 272,
+ "y": 229
+ },
+ "ice_cream-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 126,
+ "y": 145
+ },
+ "industry-11": {
+ "height": 15,
+ "pixelRatio": 1,
+ "width": 15,
+ "x": 45,
+ "y": 250
+ },
+ "industry-15": {
+ "height": 19,
+ "pixelRatio": 1,
+ "width": 19,
+ "x": 423,
+ "y": 124
+ },
+ "information-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 289,
+ "y": 229
+ },
+ "information-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 147,
+ "y": 145
+ },
+ "laundry-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 306,
+ "y": 229
+ },
+ "laundry-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 168,
+ "y": 145
+ },
+ "library-11": {
+ "height": 16,
+ "pixelRatio": 1,
+ "width": 16,
+ "x": 266,
+ "y": 0
+ },
+ "library-15": {
+ "height": 20,
+ "pixelRatio": 1,
+ "width": 20,
+ "x": 62,
+ "y": 229
+ },
+ "lighthouse-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 323,
+ "y": 229
+ },
+ "lighthouse-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 189,
+ "y": 145
+ },
+ "lodging-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 340,
+ "y": 229
+ },
+ "lodging-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 210,
+ "y": 145
+ },
+ "marker-11": {
+ "height": 15,
+ "pixelRatio": 1,
+ "width": 15,
+ "x": 60,
+ "y": 250
+ },
+ "marker-15": {
+ "height": 19,
+ "pixelRatio": 1,
+ "width": 19,
+ "x": 442,
+ "y": 124
+ },
+ "monument-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 357,
+ "y": 229
+ },
+ "monument-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 231,
+ "y": 145
+ },
+ "mountain-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 374,
+ "y": 229
+ },
+ "mountain-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 0,
+ "y": 166
+ },
+ "museum-11": {
+ "height": 16,
+ "pixelRatio": 1,
+ "width": 16,
+ "x": 282,
+ "y": 0
+ },
+ "museum-15": {
+ "height": 20,
+ "pixelRatio": 1,
+ "width": 20,
+ "x": 82,
+ "y": 229
+ },
+ "music-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 391,
+ "y": 229
+ },
+ "music-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 21,
+ "y": 166
+ },
+ "park-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 408,
+ "y": 229
+ },
+ "park-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 42,
+ "y": 166
+ },
+ "parking-11": {
+ "height": 15,
+ "pixelRatio": 1,
+ "width": 15,
+ "x": 75,
+ "y": 250
+ },
+ "parking-15": {
+ "height": 19,
+ "pixelRatio": 1,
+ "width": 19,
+ "x": 461,
+ "y": 124
+ },
+ "parking_garage-11": {
+ "height": 15,
+ "pixelRatio": 1,
+ "width": 15,
+ "x": 90,
+ "y": 250
+ },
+ "parking_garage-15": {
+ "height": 19,
+ "pixelRatio": 1,
+ "width": 19,
+ "x": 480,
+ "y": 124
+ },
+ "pedestrian_polygon": {
+ "height": 64,
+ "pixelRatio": 1,
+ "width": 64,
+ "x": 0,
+ "y": 0
+ },
+ "pharmacy-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 425,
+ "y": 229
+ },
+ "pharmacy-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 63,
+ "y": 166
+ },
+ "picnic_site-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 442,
+ "y": 229
+ },
+ "picnic_site-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 84,
+ "y": 166
+ },
+ "pitch-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 459,
+ "y": 229
+ },
+ "pitch-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 105,
+ "y": 166
+ },
+ "place_of_worship-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 476,
+ "y": 229
+ },
+ "place_of_worship-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 126,
+ "y": 166
+ },
+ "playground-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 493,
+ "y": 229
+ },
+ "playground-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 147,
+ "y": 166
+ },
+ "police-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 243,
+ "y": 64
+ },
+ "police-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 168,
+ "y": 166
+ },
+ "post-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 260,
+ "y": 64
+ },
+ "post-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 189,
+ "y": 166
+ },
+ "prison-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 277,
+ "y": 64
+ },
+ "prison-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 210,
+ "y": 166
+ },
+ "railway-11": {
+ "height": 15,
+ "pixelRatio": 1,
+ "width": 15,
+ "x": 105,
+ "y": 250
+ },
+ "railway-15": {
+ "height": 19,
+ "pixelRatio": 1,
+ "width": 19,
+ "x": 252,
+ "y": 145
+ },
+ "railway_light-11": {
+ "height": 15,
+ "pixelRatio": 1,
+ "width": 15,
+ "x": 120,
+ "y": 250
+ },
+ "railway_light-15": {
+ "height": 19,
+ "pixelRatio": 1,
+ "width": 19,
+ "x": 271,
+ "y": 145
+ },
+ "railway_metro-11": {
+ "height": 15,
+ "pixelRatio": 1,
+ "width": 15,
+ "x": 135,
+ "y": 250
+ },
+ "railway_metro-15": {
+ "height": 19,
+ "pixelRatio": 1,
+ "width": 19,
+ "x": 290,
+ "y": 145
+ },
+ "ranger_station-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 294,
+ "y": 64
+ },
+ "ranger_station-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 231,
+ "y": 166
+ },
+ "religious_christian-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 311,
+ "y": 64
+ },
+ "religious_christian-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 0,
+ "y": 187
+ },
+ "religious_jewish-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 328,
+ "y": 64
+ },
+ "religious_jewish-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 21,
+ "y": 187
+ },
+ "religious_muslim-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 345,
+ "y": 64
+ },
+ "religious_muslim-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 42,
+ "y": 187
+ },
+ "restaurant-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 362,
+ "y": 64
+ },
+ "restaurant-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 63,
+ "y": 187
+ },
+ "roadblock-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 379,
+ "y": 64
+ },
+ "roadblock-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 84,
+ "y": 187
+ },
+ "rocket-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 396,
+ "y": 64
+ },
+ "rocket-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 105,
+ "y": 187
+ },
+ "school-11": {
+ "height": 16,
+ "pixelRatio": 1,
+ "width": 16,
+ "x": 298,
+ "y": 0
+ },
+ "school-15": {
+ "height": 20,
+ "pixelRatio": 1,
+ "width": 20,
+ "x": 102,
+ "y": 229
+ },
+ "shelter-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 413,
+ "y": 64
+ },
+ "shelter-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 126,
+ "y": 187
+ },
+ "shop-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 430,
+ "y": 64
+ },
+ "shop-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 147,
+ "y": 187
+ },
+ "skiing-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 447,
+ "y": 64
+ },
+ "skiing-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 168,
+ "y": 187
+ },
+ "soccer-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 464,
+ "y": 64
+ },
+ "soccer-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 189,
+ "y": 187
+ },
+ "square-stroke-11": {
+ "height": 15,
+ "pixelRatio": 1,
+ "width": 15,
+ "x": 150,
+ "y": 250
+ },
+ "square-stroke-15": {
+ "height": 19,
+ "pixelRatio": 1,
+ "width": 19,
+ "x": 309,
+ "y": 145
+ },
+ "square-11": {
+ "height": 15,
+ "pixelRatio": 1,
+ "width": 15,
+ "x": 165,
+ "y": 250
+ },
+ "square-15": {
+ "height": 19,
+ "pixelRatio": 1,
+ "width": 19,
+ "x": 328,
+ "y": 145
+ },
+ "stadium-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 481,
+ "y": 64
+ },
+ "stadium-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 210,
+ "y": 187
+ },
+ "star-stroke-11": {
+ "height": 15,
+ "pixelRatio": 1,
+ "width": 15,
+ "x": 180,
+ "y": 250
+ },
+ "star-stroke-15": {
+ "height": 19,
+ "pixelRatio": 1,
+ "width": 19,
+ "x": 347,
+ "y": 145
+ },
+ "star-11": {
+ "height": 15,
+ "pixelRatio": 1,
+ "width": 15,
+ "x": 195,
+ "y": 250
+ },
+ "star-15": {
+ "height": 19,
+ "pixelRatio": 1,
+ "width": 19,
+ "x": 366,
+ "y": 145
+ },
+ "suitcase-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 251,
+ "y": 94
+ },
+ "suitcase-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 231,
+ "y": 187
+ },
+ "sushi-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 268,
+ "y": 94
+ },
+ "sushi-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 0,
+ "y": 208
+ },
+ "swimming-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 285,
+ "y": 94
+ },
+ "swimming-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 21,
+ "y": 208
+ },
+ "telephone-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 302,
+ "y": 94
+ },
+ "telephone-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 42,
+ "y": 208
+ },
+ "tennis-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 319,
+ "y": 94
+ },
+ "tennis-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 63,
+ "y": 208
+ },
+ "theatre-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 336,
+ "y": 94
+ },
+ "theatre-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 84,
+ "y": 208
+ },
+ "toilet-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 353,
+ "y": 94
+ },
+ "toilet-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 105,
+ "y": 208
+ },
+ "town_hall-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 370,
+ "y": 94
+ },
+ "town_hall-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 126,
+ "y": 208
+ },
+ "triangle-11": {
+ "height": 15,
+ "pixelRatio": 1,
+ "width": 15,
+ "x": 210,
+ "y": 250
+ },
+ "triangle-15": {
+ "height": 19,
+ "pixelRatio": 1,
+ "width": 19,
+ "x": 385,
+ "y": 145
+ },
+ "triangle_stroked-11": {
+ "height": 15,
+ "pixelRatio": 1,
+ "width": 15,
+ "x": 225,
+ "y": 250
+ },
+ "triangle_stroked-15": {
+ "height": 19,
+ "pixelRatio": 1,
+ "width": 19,
+ "x": 404,
+ "y": 145
+ },
+ "us-highway_2": {
+ "height": 30,
+ "pixelRatio": 1,
+ "width": 30,
+ "x": 50,
+ "y": 64
+ },
+ "us-highway_3": {
+ "height": 30,
+ "pixelRatio": 1,
+ "width": 37,
+ "x": 80,
+ "y": 64
+ },
+ "us-interstate_2": {
+ "height": 30,
+ "pixelRatio": 1,
+ "width": 30,
+ "x": 94,
+ "y": 0
+ },
+ "us-interstate_3": {
+ "height": 30,
+ "pixelRatio": 1,
+ "width": 37,
+ "x": 0,
+ "y": 94
+ },
+ "us-state_2": {
+ "height": 30,
+ "pixelRatio": 1,
+ "width": 30,
+ "x": 37,
+ "y": 94
+ },
+ "us-state_3": {
+ "height": 30,
+ "pixelRatio": 1,
+ "width": 37,
+ "x": 67,
+ "y": 94
+ },
+ "veterinary-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 387,
+ "y": 94
+ },
+ "veterinary-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 147,
+ "y": 208
+ },
+ "volcano-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 404,
+ "y": 94
+ },
+ "volcano-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 168,
+ "y": 208
+ },
+ "warehouse-11": {
+ "height": 15,
+ "pixelRatio": 1,
+ "width": 15,
+ "x": 240,
+ "y": 250
+ },
+ "warehouse-15": {
+ "height": 19,
+ "pixelRatio": 1,
+ "width": 19,
+ "x": 423,
+ "y": 145
+ },
+ "waste_basket-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 421,
+ "y": 94
+ },
+ "waste_basket-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 189,
+ "y": 208
+ },
+ "water-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 438,
+ "y": 94
+ },
+ "water-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 210,
+ "y": 208
+ },
+ "wetland-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 455,
+ "y": 94
+ },
+ "wetland-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 231,
+ "y": 208
+ },
+ "wheelchair-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 472,
+ "y": 94
+ },
+ "wheelchair-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 0,
+ "y": 229
+ },
+ "zoo-11": {
+ "height": 17,
+ "pixelRatio": 1,
+ "width": 17,
+ "x": 489,
+ "y": 94
+ },
+ "zoo-15": {
+ "height": 21,
+ "pixelRatio": 1,
+ "width": 21,
+ "x": 21,
+ "y": 229
+ }
+}
\ No newline at end of file
diff --git a/web/assets/sprites/osm-liberty.png b/web/assets/sprites/osm-liberty.png
new file mode 100644
index 0000000..61f15ac
Binary files /dev/null and b/web/assets/sprites/osm-liberty.png differ
diff --git a/web/assets/sprites/osm-liberty@2x.json b/web/assets/sprites/osm-liberty@2x.json
new file mode 100644
index 0000000..649cea8
--- /dev/null
+++ b/web/assets/sprites/osm-liberty@2x.json
@@ -0,0 +1,1745 @@
+{
+ "aerialway-11": {
+ "height": 30,
+ "pixelRatio": 2,
+ "width": 30,
+ "x": 628,
+ "y": 0
+ },
+ "aerialway-15": {
+ "height": 38,
+ "pixelRatio": 2,
+ "width": 38,
+ "x": 244,
+ "y": 458
+ },
+ "airfield-11": {
+ "height": 30,
+ "pixelRatio": 2,
+ "width": 30,
+ "x": 658,
+ "y": 0
+ },
+ "airfield-15": {
+ "height": 38,
+ "pixelRatio": 2,
+ "width": 38,
+ "x": 282,
+ "y": 458
+ },
+ "airport-11": {
+ "height": 30,
+ "pixelRatio": 2,
+ "width": 30,
+ "x": 688,
+ "y": 0
+ },
+ "airport-15": {
+ "height": 38,
+ "pixelRatio": 2,
+ "width": 38,
+ "x": 320,
+ "y": 458
+ },
+ "alcohol_shop-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 970,
+ "y": 290
+ },
+ "alcohol_shop-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 210,
+ "y": 188
+ },
+ "america_football-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 836,
+ "y": 332
+ },
+ "america_football-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 235,
+ "y": 128
+ },
+ "amusement_park-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 870,
+ "y": 332
+ },
+ "amusement_park-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 277,
+ "y": 128
+ },
+ "aquarium-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 904,
+ "y": 332
+ },
+ "aquarium-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 319,
+ "y": 128
+ },
+ "arrow": {
+ "height": 14,
+ "pixelRatio": 2,
+ "width": 40,
+ "x": 570,
+ "y": 500
+ },
+ "art_gallery-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 938,
+ "y": 332
+ },
+ "art_gallery-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 361,
+ "y": 128
+ },
+ "attraction-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 972,
+ "y": 332
+ },
+ "attraction-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 403,
+ "y": 128
+ },
+ "bakery-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 504,
+ "y": 374
+ },
+ "bakery-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 445,
+ "y": 128
+ },
+ "bank-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 538,
+ "y": 374
+ },
+ "bank-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 252,
+ "y": 188
+ },
+ "bar-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 572,
+ "y": 374
+ },
+ "bar-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 294,
+ "y": 188
+ },
+ "baseball-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 606,
+ "y": 374
+ },
+ "baseball-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 336,
+ "y": 188
+ },
+ "basketball-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 640,
+ "y": 374
+ },
+ "basketball-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 378,
+ "y": 188
+ },
+ "beer-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 674,
+ "y": 374
+ },
+ "beer-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 420,
+ "y": 188
+ },
+ "bicycle-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 708,
+ "y": 374
+ },
+ "bicycle-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 462,
+ "y": 188
+ },
+ "bicycle_rental-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 742,
+ "y": 374
+ },
+ "bicycle_rental-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 248,
+ "y": 0
+ },
+ "building-11": {
+ "height": 30,
+ "pixelRatio": 2,
+ "width": 30,
+ "x": 718,
+ "y": 0
+ },
+ "building-15": {
+ "height": 38,
+ "pixelRatio": 2,
+ "width": 38,
+ "x": 358,
+ "y": 458
+ },
+ "bus-11": {
+ "height": 30,
+ "pixelRatio": 2,
+ "width": 30,
+ "x": 748,
+ "y": 0
+ },
+ "bus-15": {
+ "height": 38,
+ "pixelRatio": 2,
+ "width": 38,
+ "x": 396,
+ "y": 458
+ },
+ "butcher-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 776,
+ "y": 374
+ },
+ "butcher-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 290,
+ "y": 0
+ },
+ "ca-transcanada_2": {
+ "height": 60,
+ "pixelRatio": 2,
+ "width": 60,
+ "x": 128,
+ "y": 0
+ },
+ "cafe-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 810,
+ "y": 374
+ },
+ "cafe-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 332,
+ "y": 0
+ },
+ "campsite-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 844,
+ "y": 374
+ },
+ "campsite-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 374,
+ "y": 0
+ },
+ "car-11": {
+ "height": 30,
+ "pixelRatio": 2,
+ "width": 30,
+ "x": 778,
+ "y": 0
+ },
+ "car-15": {
+ "height": 38,
+ "pixelRatio": 2,
+ "width": 38,
+ "x": 434,
+ "y": 458
+ },
+ "castle-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 878,
+ "y": 374
+ },
+ "castle-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 416,
+ "y": 0
+ },
+ "cemetery-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 912,
+ "y": 374
+ },
+ "cemetery-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 458,
+ "y": 0
+ },
+ "cinema-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 946,
+ "y": 374
+ },
+ "cinema-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 0,
+ "y": 248
+ },
+ "circle-stroked-11": {
+ "height": 30,
+ "pixelRatio": 2,
+ "width": 30,
+ "x": 808,
+ "y": 0
+ },
+ "circle-stroked-15": {
+ "height": 38,
+ "pixelRatio": 2,
+ "width": 38,
+ "x": 472,
+ "y": 458
+ },
+ "circle-11": {
+ "height": 30,
+ "pixelRatio": 2,
+ "width": 30,
+ "x": 838,
+ "y": 0
+ },
+ "circle-15": {
+ "height": 38,
+ "pixelRatio": 2,
+ "width": 38,
+ "x": 504,
+ "y": 248
+ },
+ "clothing_store-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 980,
+ "y": 374
+ },
+ "clothing_store-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 42,
+ "y": 248
+ },
+ "college-11": {
+ "height": 32,
+ "pixelRatio": 2,
+ "width": 32,
+ "x": 500,
+ "y": 0
+ },
+ "college-15": {
+ "height": 40,
+ "pixelRatio": 2,
+ "width": 40,
+ "x": 84,
+ "y": 458
+ },
+ "commercial-11": {
+ "height": 30,
+ "pixelRatio": 2,
+ "width": 30,
+ "x": 868,
+ "y": 0
+ },
+ "commercial-15": {
+ "height": 38,
+ "pixelRatio": 2,
+ "width": 38,
+ "x": 542,
+ "y": 248
+ },
+ "cricket-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 504,
+ "y": 416
+ },
+ "cricket-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 84,
+ "y": 248
+ },
+ "cross-11": {
+ "height": 30,
+ "pixelRatio": 2,
+ "width": 30,
+ "x": 898,
+ "y": 0
+ },
+ "cross-15": {
+ "height": 38,
+ "pixelRatio": 2,
+ "width": 38,
+ "x": 580,
+ "y": 248
+ },
+ "dam-11": {
+ "height": 30,
+ "pixelRatio": 2,
+ "width": 30,
+ "x": 928,
+ "y": 0
+ },
+ "dam-15": {
+ "height": 38,
+ "pixelRatio": 2,
+ "width": 38,
+ "x": 618,
+ "y": 248
+ },
+ "danger-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 538,
+ "y": 416
+ },
+ "danger-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 126,
+ "y": 248
+ },
+ "default_1": {
+ "height": 36,
+ "pixelRatio": 2,
+ "width": 36,
+ "x": 884,
+ "y": 290
+ },
+ "default_2": {
+ "height": 36,
+ "pixelRatio": 2,
+ "width": 50,
+ "x": 920,
+ "y": 290
+ },
+ "default_3": {
+ "height": 36,
+ "pixelRatio": 2,
+ "width": 64,
+ "x": 504,
+ "y": 332
+ },
+ "default_4": {
+ "height": 36,
+ "pixelRatio": 2,
+ "width": 78,
+ "x": 568,
+ "y": 332
+ },
+ "default_5": {
+ "height": 36,
+ "pixelRatio": 2,
+ "width": 90,
+ "x": 646,
+ "y": 332
+ },
+ "default_6": {
+ "height": 36,
+ "pixelRatio": 2,
+ "width": 100,
+ "x": 736,
+ "y": 332
+ },
+ "dentist-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 572,
+ "y": 416
+ },
+ "dentist-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 168,
+ "y": 248
+ },
+ "doctor-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 606,
+ "y": 416
+ },
+ "doctor-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 210,
+ "y": 248
+ },
+ "dog_park-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 640,
+ "y": 416
+ },
+ "dog_park-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 252,
+ "y": 248
+ },
+ "dot-10": {
+ "height": 20,
+ "pixelRatio": 2,
+ "width": 20,
+ "x": 532,
+ "y": 500
+ },
+ "dot-11": {
+ "height": 22,
+ "pixelRatio": 2,
+ "width": 22,
+ "x": 510,
+ "y": 500
+ },
+ "dot_9": {
+ "height": 18,
+ "pixelRatio": 2,
+ "width": 18,
+ "x": 552,
+ "y": 500
+ },
+ "drinking-water-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 674,
+ "y": 416
+ },
+ "drinking_water-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 294,
+ "y": 248
+ },
+ "embassy-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 708,
+ "y": 416
+ },
+ "embassy-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 336,
+ "y": 248
+ },
+ "entrance-11": {
+ "height": 30,
+ "pixelRatio": 2,
+ "width": 30,
+ "x": 958,
+ "y": 0
+ },
+ "entrance-15": {
+ "height": 38,
+ "pixelRatio": 2,
+ "width": 38,
+ "x": 656,
+ "y": 248
+ },
+ "fast_food-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 742,
+ "y": 416
+ },
+ "fast_food-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 378,
+ "y": 248
+ },
+ "ferry-11": {
+ "height": 30,
+ "pixelRatio": 2,
+ "width": 30,
+ "x": 988,
+ "y": 0
+ },
+ "ferry-15": {
+ "height": 38,
+ "pixelRatio": 2,
+ "width": 38,
+ "x": 694,
+ "y": 248
+ },
+ "fire-station-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 776,
+ "y": 416
+ },
+ "fire-station-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 420,
+ "y": 248
+ },
+ "fuel-11": {
+ "height": 30,
+ "pixelRatio": 2,
+ "width": 30,
+ "x": 0,
+ "y": 500
+ },
+ "fuel-15": {
+ "height": 38,
+ "pixelRatio": 2,
+ "width": 38,
+ "x": 732,
+ "y": 248
+ },
+ "garden-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 810,
+ "y": 416
+ },
+ "garden-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 462,
+ "y": 248
+ },
+ "gb-motorway_3": {
+ "height": 60,
+ "pixelRatio": 2,
+ "width": 100,
+ "x": 0,
+ "y": 128
+ },
+ "gift-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 844,
+ "y": 416
+ },
+ "gift-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 0,
+ "y": 290
+ },
+ "golf-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 878,
+ "y": 416
+ },
+ "golf-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 42,
+ "y": 290
+ },
+ "grocery-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 912,
+ "y": 416
+ },
+ "grocery-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 84,
+ "y": 290
+ },
+ "hairdresser-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 946,
+ "y": 416
+ },
+ "hairdresser-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 126,
+ "y": 290
+ },
+ "harbor-11": {
+ "height": 30,
+ "pixelRatio": 2,
+ "width": 30,
+ "x": 30,
+ "y": 500
+ },
+ "harbor-15": {
+ "height": 38,
+ "pixelRatio": 2,
+ "width": 38,
+ "x": 770,
+ "y": 248
+ },
+ "heart-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 980,
+ "y": 416
+ },
+ "heart-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 168,
+ "y": 290
+ },
+ "heliport-11": {
+ "height": 30,
+ "pixelRatio": 2,
+ "width": 30,
+ "x": 60,
+ "y": 500
+ },
+ "heliport-15": {
+ "height": 38,
+ "pixelRatio": 2,
+ "width": 38,
+ "x": 808,
+ "y": 248
+ },
+ "hospital-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 510,
+ "y": 458
+ },
+ "hospital-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 210,
+ "y": 290
+ },
+ "ice_cream-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 544,
+ "y": 458
+ },
+ "ice_cream-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 252,
+ "y": 290
+ },
+ "industry-11": {
+ "height": 30,
+ "pixelRatio": 2,
+ "width": 30,
+ "x": 90,
+ "y": 500
+ },
+ "industry-15": {
+ "height": 38,
+ "pixelRatio": 2,
+ "width": 38,
+ "x": 846,
+ "y": 248
+ },
+ "information-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 578,
+ "y": 458
+ },
+ "information-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 294,
+ "y": 290
+ },
+ "laundry-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 612,
+ "y": 458
+ },
+ "laundry-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 336,
+ "y": 290
+ },
+ "library-11": {
+ "height": 32,
+ "pixelRatio": 2,
+ "width": 32,
+ "x": 532,
+ "y": 0
+ },
+ "library-15": {
+ "height": 40,
+ "pixelRatio": 2,
+ "width": 40,
+ "x": 124,
+ "y": 458
+ },
+ "lighthouse-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 646,
+ "y": 458
+ },
+ "lighthouse-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 378,
+ "y": 290
+ },
+ "lodging-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 680,
+ "y": 458
+ },
+ "lodging-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 420,
+ "y": 290
+ },
+ "marker-11": {
+ "height": 30,
+ "pixelRatio": 2,
+ "width": 30,
+ "x": 120,
+ "y": 500
+ },
+ "marker-15": {
+ "height": 38,
+ "pixelRatio": 2,
+ "width": 38,
+ "x": 884,
+ "y": 248
+ },
+ "monument-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 714,
+ "y": 458
+ },
+ "monument-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 462,
+ "y": 290
+ },
+ "mountain-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 748,
+ "y": 458
+ },
+ "mountain-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 0,
+ "y": 332
+ },
+ "museum-11": {
+ "height": 32,
+ "pixelRatio": 2,
+ "width": 32,
+ "x": 564,
+ "y": 0
+ },
+ "museum-15": {
+ "height": 40,
+ "pixelRatio": 2,
+ "width": 40,
+ "x": 164,
+ "y": 458
+ },
+ "music-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 782,
+ "y": 458
+ },
+ "music-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 42,
+ "y": 332
+ },
+ "park-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 816,
+ "y": 458
+ },
+ "park-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 84,
+ "y": 332
+ },
+ "parking-11": {
+ "height": 30,
+ "pixelRatio": 2,
+ "width": 30,
+ "x": 150,
+ "y": 500
+ },
+ "parking-15": {
+ "height": 38,
+ "pixelRatio": 2,
+ "width": 38,
+ "x": 922,
+ "y": 248
+ },
+ "parking_garage-11": {
+ "height": 30,
+ "pixelRatio": 2,
+ "width": 30,
+ "x": 180,
+ "y": 500
+ },
+ "parking_garage-15": {
+ "height": 38,
+ "pixelRatio": 2,
+ "width": 38,
+ "x": 960,
+ "y": 248
+ },
+ "pedestrian_polygon": {
+ "height": 128,
+ "pixelRatio": 2,
+ "width": 128,
+ "x": 0,
+ "y": 0
+ },
+ "pharmacy-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 850,
+ "y": 458
+ },
+ "pharmacy-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 126,
+ "y": 332
+ },
+ "picnic_site-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 884,
+ "y": 458
+ },
+ "picnic_site-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 168,
+ "y": 332
+ },
+ "pitch-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 918,
+ "y": 458
+ },
+ "pitch-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 210,
+ "y": 332
+ },
+ "place_of_worship-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 952,
+ "y": 458
+ },
+ "place_of_worship-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 252,
+ "y": 332
+ },
+ "playground-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 986,
+ "y": 458
+ },
+ "playground-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 294,
+ "y": 332
+ },
+ "police-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 487,
+ "y": 128
+ },
+ "police-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 336,
+ "y": 332
+ },
+ "post-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 521,
+ "y": 128
+ },
+ "post-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 378,
+ "y": 332
+ },
+ "prison-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 555,
+ "y": 128
+ },
+ "prison-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 420,
+ "y": 332
+ },
+ "railway-11": {
+ "height": 30,
+ "pixelRatio": 2,
+ "width": 30,
+ "x": 210,
+ "y": 500
+ },
+ "railway-15": {
+ "height": 38,
+ "pixelRatio": 2,
+ "width": 38,
+ "x": 504,
+ "y": 290
+ },
+ "railway_light-11": {
+ "height": 30,
+ "pixelRatio": 2,
+ "width": 30,
+ "x": 240,
+ "y": 500
+ },
+ "railway_light-15": {
+ "height": 38,
+ "pixelRatio": 2,
+ "width": 38,
+ "x": 542,
+ "y": 290
+ },
+ "railway_metro-11": {
+ "height": 30,
+ "pixelRatio": 2,
+ "width": 30,
+ "x": 270,
+ "y": 500
+ },
+ "railway_metro-15": {
+ "height": 38,
+ "pixelRatio": 2,
+ "width": 38,
+ "x": 580,
+ "y": 290
+ },
+ "ranger_station-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 589,
+ "y": 128
+ },
+ "ranger_station-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 462,
+ "y": 332
+ },
+ "religious_christian-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 623,
+ "y": 128
+ },
+ "religious_christian-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 0,
+ "y": 374
+ },
+ "religious_jewish-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 657,
+ "y": 128
+ },
+ "religious_jewish-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 42,
+ "y": 374
+ },
+ "religious_muslim-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 691,
+ "y": 128
+ },
+ "religious_muslim-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 84,
+ "y": 374
+ },
+ "restaurant-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 725,
+ "y": 128
+ },
+ "restaurant-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 126,
+ "y": 374
+ },
+ "roadblock-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 759,
+ "y": 128
+ },
+ "roadblock-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 168,
+ "y": 374
+ },
+ "rocket-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 793,
+ "y": 128
+ },
+ "rocket-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 210,
+ "y": 374
+ },
+ "school-11": {
+ "height": 32,
+ "pixelRatio": 2,
+ "width": 32,
+ "x": 596,
+ "y": 0
+ },
+ "school-15": {
+ "height": 40,
+ "pixelRatio": 2,
+ "width": 40,
+ "x": 204,
+ "y": 458
+ },
+ "shelter-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 827,
+ "y": 128
+ },
+ "shelter-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 252,
+ "y": 374
+ },
+ "shop-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 861,
+ "y": 128
+ },
+ "shop-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 294,
+ "y": 374
+ },
+ "skiing-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 895,
+ "y": 128
+ },
+ "skiing-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 336,
+ "y": 374
+ },
+ "soccer-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 929,
+ "y": 128
+ },
+ "soccer-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 378,
+ "y": 374
+ },
+ "square-stroke-11": {
+ "height": 30,
+ "pixelRatio": 2,
+ "width": 30,
+ "x": 300,
+ "y": 500
+ },
+ "square-stroke-15": {
+ "height": 38,
+ "pixelRatio": 2,
+ "width": 38,
+ "x": 618,
+ "y": 290
+ },
+ "square-11": {
+ "height": 30,
+ "pixelRatio": 2,
+ "width": 30,
+ "x": 330,
+ "y": 500
+ },
+ "square-15": {
+ "height": 38,
+ "pixelRatio": 2,
+ "width": 38,
+ "x": 656,
+ "y": 290
+ },
+ "stadium-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 963,
+ "y": 128
+ },
+ "stadium-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 420,
+ "y": 374
+ },
+ "star-stroke-11": {
+ "height": 30,
+ "pixelRatio": 2,
+ "width": 30,
+ "x": 360,
+ "y": 500
+ },
+ "star-stroke-15": {
+ "height": 38,
+ "pixelRatio": 2,
+ "width": 38,
+ "x": 694,
+ "y": 290
+ },
+ "star-11": {
+ "height": 30,
+ "pixelRatio": 2,
+ "width": 30,
+ "x": 390,
+ "y": 500
+ },
+ "star-15": {
+ "height": 38,
+ "pixelRatio": 2,
+ "width": 38,
+ "x": 732,
+ "y": 290
+ },
+ "suitcase-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 504,
+ "y": 188
+ },
+ "suitcase-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 462,
+ "y": 374
+ },
+ "sushi-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 538,
+ "y": 188
+ },
+ "sushi-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 0,
+ "y": 416
+ },
+ "swimming-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 572,
+ "y": 188
+ },
+ "swimming-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 42,
+ "y": 416
+ },
+ "telephone-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 606,
+ "y": 188
+ },
+ "telephone-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 84,
+ "y": 416
+ },
+ "tennis-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 640,
+ "y": 188
+ },
+ "tennis-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 126,
+ "y": 416
+ },
+ "theatre-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 674,
+ "y": 188
+ },
+ "theatre-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 168,
+ "y": 416
+ },
+ "toilet-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 708,
+ "y": 188
+ },
+ "toilet-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 210,
+ "y": 416
+ },
+ "town_hall-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 742,
+ "y": 188
+ },
+ "town_hall-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 252,
+ "y": 416
+ },
+ "triangle-11": {
+ "height": 30,
+ "pixelRatio": 2,
+ "width": 30,
+ "x": 420,
+ "y": 500
+ },
+ "triangle-15": {
+ "height": 38,
+ "pixelRatio": 2,
+ "width": 38,
+ "x": 770,
+ "y": 290
+ },
+ "triangle_stroked-11": {
+ "height": 30,
+ "pixelRatio": 2,
+ "width": 30,
+ "x": 450,
+ "y": 500
+ },
+ "triangle_stroked-15": {
+ "height": 38,
+ "pixelRatio": 2,
+ "width": 38,
+ "x": 808,
+ "y": 290
+ },
+ "us-highway_2": {
+ "height": 60,
+ "pixelRatio": 2,
+ "width": 60,
+ "x": 100,
+ "y": 128
+ },
+ "us-highway_3": {
+ "height": 60,
+ "pixelRatio": 2,
+ "width": 75,
+ "x": 160,
+ "y": 128
+ },
+ "us-interstate_2": {
+ "height": 60,
+ "pixelRatio": 2,
+ "width": 60,
+ "x": 188,
+ "y": 0
+ },
+ "us-interstate_3": {
+ "height": 60,
+ "pixelRatio": 2,
+ "width": 75,
+ "x": 0,
+ "y": 188
+ },
+ "us-state_2": {
+ "height": 60,
+ "pixelRatio": 2,
+ "width": 60,
+ "x": 75,
+ "y": 188
+ },
+ "us-state_3": {
+ "height": 60,
+ "pixelRatio": 2,
+ "width": 75,
+ "x": 135,
+ "y": 188
+ },
+ "veterinary-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 776,
+ "y": 188
+ },
+ "veterinary-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 294,
+ "y": 416
+ },
+ "volcano-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 810,
+ "y": 188
+ },
+ "volcano-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 336,
+ "y": 416
+ },
+ "warehouse-11": {
+ "height": 30,
+ "pixelRatio": 2,
+ "width": 30,
+ "x": 480,
+ "y": 500
+ },
+ "warehouse-15": {
+ "height": 38,
+ "pixelRatio": 2,
+ "width": 38,
+ "x": 846,
+ "y": 290
+ },
+ "waste_basket-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 844,
+ "y": 188
+ },
+ "waste_basket-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 378,
+ "y": 416
+ },
+ "water-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 878,
+ "y": 188
+ },
+ "water-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 420,
+ "y": 416
+ },
+ "wetland-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 912,
+ "y": 188
+ },
+ "wetland-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 462,
+ "y": 416
+ },
+ "wheelchair-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 946,
+ "y": 188
+ },
+ "wheelchair-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 0,
+ "y": 458
+ },
+ "zoo-11": {
+ "height": 34,
+ "pixelRatio": 2,
+ "width": 34,
+ "x": 980,
+ "y": 188
+ },
+ "zoo-15": {
+ "height": 42,
+ "pixelRatio": 2,
+ "width": 42,
+ "x": 42,
+ "y": 458
+ }
+}
\ No newline at end of file
diff --git a/web/assets/sprites/osm-liberty@2x.png b/web/assets/sprites/osm-liberty@2x.png
new file mode 100644
index 0000000..1b54e3e
Binary files /dev/null and b/web/assets/sprites/osm-liberty@2x.png differ
diff --git a/web/assets/style.json b/web/assets/style.json
new file mode 100644
index 0000000..1e9b3ed
--- /dev/null
+++ b/web/assets/style.json
@@ -0,0 +1,22 @@
+{
+ "version": 8,
+ "sources": {
+ "osm": {
+ "type": "raster",
+ "tiles": [
+ "https://tile.openstreetmap.org/{z}/{x}/{y}.png"
+ ],
+ "tileSize": 256,
+ "attribution": "Map tiles by OpenStreetMap tile servers, under the tile usage policy. Data by OpenStreetMap"
+ }
+ },
+ "sprite": "",
+ "glyphs": "https://api.maptiler.com/fonts/{fontstack}/{range}.pbf",
+ "layers": [
+ {
+ "id": "osm",
+ "type": "raster",
+ "source": "osm"
+ }
+ ]
+}
diff --git a/web/db.py b/web/db.py
index b65eabe..2a2e526 100644
--- a/web/db.py
+++ b/web/db.py
@@ -11,7 +11,7 @@ from geojson import Feature, Point, FeatureCollection
from geojson import dumps as geo_dumps
from mylogger import logger
-from utils import get_temp
+from libs.utils import get_temp
NEW_BATTERY_COLUMNS = [["price", "INTEGER"], ["charging_mode", "TEXT"]]
NEW_POSITION_COLUMNS = [["level_fuel", "INTEGER"], ["altitude", "INTEGER"]]
@@ -126,7 +126,10 @@ class Database:
def clean_battery(conn):
# delete charging longer than 17h
conn.execute("DElETE FROM battery WHERE JULIANDAY(stop_at)-JULIANDAY(start_at)>0.7;")
- conn.execute("DELETE FROM battery WHERE start_level==end_level;")
+ # delete charging not finished longer than 17h
+ conn.execute("DELETE from battery where stop_at is NULL and JULIANDAY()-JULIANDAY(start_at)>0.7;")
+ #delete little charge
+ conn.execute("DELETE FROM battery WHERE start_level >= end_level-1;")
@staticmethod
def clean_position(conn):
diff --git a/web/figure_filter.py b/web/figure_filter.py
new file mode 100644
index 0000000..83f1d8e
--- /dev/null
+++ b/web/figure_filter.py
@@ -0,0 +1,121 @@
+import json
+from logging import DEBUG
+
+from dash.dependencies import Output, Input
+from dash_core_components import Store
+from mylogger import logger
+
+
+class Graph:
+ def __init__(self, graph_id, x, y: [], figure):
+ self.graph_id = graph_id
+ self.x = x
+ self.y = y
+ self.figure = figure
+
+
+class Table:
+ def __init__(self, table_id, src, figure):
+ self.table_id = table_id
+ self.src = src
+ self.figure = figure
+ self.date_columns = []
+
+
+def figures_to_dict(figures):
+ el_list = []
+ for figure in figures:
+ res = {}
+ for key, value in figure.__dict__.items():
+ if key != "figure":
+ res[key] = value
+ el_list.append(res)
+ return el_list
+
+
+class Figure_Filter:
+
+ def __init__(self):
+ self.graphs = []
+ self.tables = []
+ self.maps = []
+ self.src = {}
+
+ def add_map(self, dash_Graph, latitude, longitude, figure):
+ self.maps.append(Graph(dash_Graph.id, latitude, longitude, figure))
+ return dash_Graph
+
+ def add_graph(self, dash_Graph, x, y, figure):
+ self.graphs.append(Graph(dash_Graph.id, x, y, figure))
+ return dash_Graph
+
+ def add_table(self, src, figure):
+ table = Table(figure.id, src, figure)
+ table.date_columns = [col["id"][:-4] for col in figure.columns if col["type"] == "datetime" and
+ col["id"].endswith("_str")]
+ self.tables.append(table)
+
+ def __get_table_date_column_id(self):
+ res = {table.src: table.date_columns for table in self.tables}
+ return res
+
+ def __get_table_src(self):
+ return [table.src for table in self.tables]
+
+ def __get_figures(self):
+ return {"graph": [graph.figure for graph in self.graphs],
+ "tables": [table.figure for table in self.tables],
+ "maps": [map.figure for map in self.maps]}
+
+ def __get_output(self) -> list:
+ outputs = [Output(table.table_id, "data") for table in self.tables]
+ outputs.extend([Output(graph.graph_id, "figure") for graph in self.graphs])
+ outputs.extend([Output(graph.graph_id, "figure") for graph in self.maps])
+ return outputs
+
+ def __get_graph_x_label(self, graphs):
+ return [graph.x for graph in graphs]
+
+ def __get_graph_y_label(self, graphs):
+ return [graph.y for graph in graphs]
+
+ def __get_table_input_sort_by(self):
+ inputs = [Input(table.table_id, 'sort_by') for table in self.tables]
+ return inputs
+
+ def gen_unused_variable(self):
+ res = ", ".join([chr(i) for i in range(ord('a'), ord('a') + len(self.tables))])
+ return res
+
+ def get_params(self):
+ params = json.dumps({
+ "date_columns": self.__get_table_date_column_id(),
+ "table_src": figures_to_dict(self.tables),
+ "graph_x_label": self.__get_graph_x_label(self.graphs),
+ "graph_y_label": self.__get_graph_y_label(self.graphs),
+ "map_x_label": self.__get_graph_x_label(self.maps),
+ "map_y_label": self.__get_graph_y_label(self.maps)
+ }, indent=4)
+ return params
+
+ def get_clientside_callback(self):
+ if logger.isEnabledFor(DEBUG):
+ log_level = 10
+ else:
+ log_level = 20
+ fct_def = f"""function(data,range, figures, {self.gen_unused_variable()}) {{
+ const params={self.get_params()};
+ const logLevel={log_level};
+ return filterAndSort(data, range, figures, params, logLevel);
+ }}"""
+ res = [fct_def,
+ *self.__get_output(),
+ Input('clientside-data-store', 'data'),
+ Input('date-slider', 'value'),
+ Input('clientside-figure-store', 'data'),
+ *self.__get_table_input_sort_by()]
+ return res
+
+ def get_store(self):
+ return [Store(id='clientside-figure-store', data=self.__get_figures()),
+ Store(id='clientside-data-store', data=self.src)]
diff --git a/web/figures.py b/web/figures.py
index f23110a..eb85b0c 100644
--- a/web/figures.py
+++ b/web/figures.py
@@ -1,52 +1,18 @@
from copy import deepcopy
-from typing import List
-
import dash_bootstrap_components as dbc
import dash_table
-import numpy as np
from dash_core_components import Graph
from dash_table.Format import Format, Scheme, Symbol
-from dateutil.relativedelta import relativedelta
import plotly.express as px
import plotly.graph_objects as go
-from pandas import DataFrame
-from pandas import options as pandas_options
import dash_html_components as html
from libs.car import Car
from libs.elec_price import ElecPrice
-from trip import Trips, Trip
+from trip import Trip
from web.db import Database
-
-
-def unix_time_millis(date):
- return int(date.timestamp())
-
-
-def get_marks_from_start_end(start, end):
- nb_marks = 10
- result = []
- time_delta = int((end - start).total_seconds() / nb_marks)
- current = start
- if time_delta > 0:
- while current <= end:
- result.append(current)
- current += relativedelta(seconds=time_delta)
- result[-1] = end
- if time_delta < 3600 * 24:
- if time_delta > 3600:
- date_f = '%x %Hh'
- else:
- date_f = '%x %Hh%M'
- else:
- date_f = '%x'
- marks = {}
- for date in result:
- marks[unix_time_millis(date)] = str(date.strftime(date_f))
- return marks
- return None
-
+from web.utils import card_value_div, dash_date_to_datetime
# pylint: disable=invalid-name
ERROR_DIV = dbc.Alert("No data to show, there is probably no trips recorded yet", color="danger")
@@ -55,45 +21,59 @@ consumption_fig = ERROR_DIV
consumption_df = ERROR_DIV
trips_map = ERROR_DIV
consumption_fig_by_speed = ERROR_DIV
-consumption_graph_by_temp = ERROR_DIV
+consumption_fig_by_temp = ERROR_DIV
table_fig = ERROR_DIV
-pandas_options.display.float_format = '${:.2f}'.format
info = ""
-battery_info = ERROR_DIV
-battery_table = None
+battery_table = ERROR_DIV
-SUMMARY_CARDS = {"Average consumption": {"text": None, "src": "static/images/consumption.svg"},
- "Average emission": {"text": None, "src": "static/images/pollution.svg"},
- "Average charge speed": {"text": None, "src": "static/images/battery-charge-line.svg"},
- "Electricity consumption": {"text": None, "src": "static/images/electricity bill.svg"}
+AVG_CHARGE_SPEED = "avg_chg_speed"
+AVG_EMISSION_KM = "avg_emission_km"
+AVG_EMISSION_KW = "avg_emission_kw"
+ELEC_CONSUM_KW = "elec_consum_kw"
+ELEC_CONSUM_PRICE = "elec_consum_price"
+AVG_CONSUM_KW = "avg_consum_kw"
+AVG_CONSUM_PRICE = "avg_consum_price"
+
+SUMMARY_CARDS = {"Average consumption": {"text": [card_value_div(AVG_CONSUM_KW, "kWh/100km"),
+ card_value_div(AVG_CONSUM_PRICE, f"{ElecPrice.currency}/100km")],
+ "src": "assets/images/consumption.svg"},
+ "Average emission": {"text": [card_value_div(AVG_EMISSION_KM, " g/km"),
+ card_value_div(AVG_EMISSION_KW, "g/kWh")],
+ "src": "assets/images/pollution.svg"},
+ "Average charge speed": {"text": [card_value_div(AVG_CHARGE_SPEED, " kW")],
+ "src": "assets/images/battery-charge-line.svg"},
+ "Electricity consumption": {"text": [card_value_div(ELEC_CONSUM_KW, "kWh"),
+ card_value_div(ELEC_CONSUM_PRICE, ElecPrice.currency)],
+ "src": "assets/images/electricity bill.svg"}
}
# pylint: disable=too-many-locals
-def get_figures(trips: Trips, charging: List[dict]):
- global consumption_fig, consumption_df, trips_map, consumption_fig_by_speed, table_fig, info, battery_info, \
- battery_table, consumption_graph_by_temp
- lats = []
- lons = []
- names = []
- for trip in trips:
- for points in trip.positions:
- lats = np.append(lats, points.latitude)
- lons = np.append(lons, points.longitude)
- names = np.append(names, [str(trip.start_at)])
- lats = np.append(lats, None)
- lons = np.append(lons, None)
- names = np.append(names, None)
- trips_map = px.line_mapbox(lat=lats, lon=lons, hover_name=names,
- mapbox_style="stamen-terrain", zoom=12)
+def get_figures(car: Car):
+ global consumption_fig, consumption_df, trips_map, consumption_fig_by_speed, table_fig, info, \
+ battery_table, consumption_fig_by_temp
+ lats = [42, 41]
+ lons = [1, 2]
+ names = ["undefined", "undefined"]
+ trips_map = px.line_mapbox(lat=lats, lon=lons, hover_name=names, zoom=12, mapbox_style="assets/style2.json")
+ trips_map.add_trace(go.Scattermapbox(
+ mode="markers",
+ marker={"symbol": "marker", "size": 20},
+ lon=[lons[0]], lat=[lats[0]],
+ showlegend=False, name="Last Position"))
# table
nb_format = Format(precision=2, scheme=Scheme.fixed, symbol=Symbol.yes) # pylint: disable=no-member
+ style_cell_conditional = []
+ if car.is_electric():
+ style_cell_conditional.append({'if': {'column_id': 'consumption_fuel_km', }, 'display': 'None', })
+ if car.is_thermal():
+ style_cell_conditional.append({'if': {'column_id': 'consumption_km', }, 'display': 'None', })
table_fig = dash_table.DataTable(
id='trips-table',
- sort_action='native',
+ sort_action='custom',
sort_by=[{'column_id': 'id', 'direction': 'desc'}],
columns=[{'id': 'id', 'name': '#', 'type': 'numeric'},
- {'id': 'start_at', 'name': 'start at', 'type': 'datetime'},
+ {'id': 'start_at_str', 'name': 'start at', 'type': 'datetime'},
{'id': 'duration', 'name': 'duration', 'type': 'numeric',
'format': deepcopy(nb_format).symbol_suffix(" min").precision(0)},
{'id': 'speed_average', 'name': 'average speed', 'type': 'numeric',
@@ -116,51 +96,31 @@ def get_figures(trips: Trips, charging: List[dict]):
"text-decoration": "underline"
}
],
- data=trips.get_info(),
+ style_cell_conditional=style_cell_conditional,
+ data=[],
page_size=50
)
# consumption_fig
- consumption_df = DataFrame.from_records(trips.get_long_trips())
- consumption_fig = px.histogram(consumption_df, x="date", y="consumption_km", title='Consumption of the car',
+ consumption_fig = px.histogram(x=[0], y=[1], title='Consumption of the car',
histfunc="avg")
- consumption_fig.update_layout(yaxis_title="Consumption kWh/100Km")
+ consumption_fig.update_layout(yaxis_title="Consumption kWh/100Km", xaxis_title="date")
- consumption_fig_by_speed = px.histogram(consumption_df, x="speed", y="consumption_km", histfunc="avg",
+ consumption_fig_by_speed = px.histogram(data_frame=[{"start_at": 1, "speed_average": 2}], x="start_at",
+ y="speed_average", histfunc="avg",
title="Consumption by speed")
consumption_fig_by_speed.update_traces(xbins_size=15)
consumption_fig_by_speed.update_layout(bargap=0.05)
- consumption_fig_by_speed.add_trace(
- go.Scatter(mode="markers", x=consumption_df["speed"], y=consumption_df["consumption_km"],
- name="Trips"))
+ consumption_fig_by_speed.add_trace(go.Scatter(mode="markers", x=[0],
+ y=[0], name="Trips"))
consumption_fig_by_speed.update_layout(xaxis_title="average Speed km/h", yaxis_title="Consumption kWh/100Km")
- kw_per_km = float(consumption_df["consumption_km"].mean())
- info = "Average consumption: {:.1f} kWh/100km".format(kw_per_km)
- # charging
- charging_data = DataFrame.from_records(charging)
- co2_per_kw = __calculate_co2_per_kw(charging_data)
- co2_per_km = co2_per_kw * kw_per_km / 100
- try:
- charge_speed = 3600 * charging_data["kw"].mean() / \
- (charging_data["stop_at"] - charging_data["start_at"]).mean().total_seconds()
- price_kw = (charging_data["price"] / charging_data["kw"]).mean()
- total_elec = kw_per_km * trips.get_distance() / 100
- except (TypeError, KeyError, ZeroDivisionError): # when there is no data yet:
- charge_speed = 0
- price_kw = 0
- total_elec = 0
-
- SUMMARY_CARDS["Average charge speed"]["text"] = f"{charge_speed:.2f} kW"
- SUMMARY_CARDS["Average emission"]["text"] = [html.P(f"{co2_per_km:.1f} g/km"), html.P(f"{co2_per_kw:.1f} g/kWh")]
- SUMMARY_CARDS["Electricity consumption"]["text"] = [f"{total_elec:.0f} kWh", html.Br(), \
- f"{total_elec * price_kw:.0f} {ElecPrice.currency}"]
- SUMMARY_CARDS["Average consumption"]["text"] = f"{consumption_df['consumption_km'].mean():.1f} kWh/100km"
+ # battery_table
battery_table = dash_table.DataTable(
id='battery-table',
- sort_action='native',
- sort_by=[{'column_id': 'start_at', 'direction': 'desc'}],
- columns=[{'id': 'start_at', 'name': 'start at', 'type': 'datetime'},
- {'id': 'stop_at', 'name': 'stop at', 'type': 'datetime'},
+ sort_action='custom',
+ sort_by=[{'column_id': 'start_at_str', 'direction': 'desc'}],
+ columns=[{'id': 'start_at_str', 'name': 'start at', 'type': 'datetime'},
+ {'id': 'stop_at_str', 'name': 'stop at', 'type': 'datetime'},
{'id': 'start_level', 'name': 'start level', 'type': 'numeric'},
{'id': 'end_level', 'name': 'end level', 'type': 'numeric'},
{'id': 'co2', 'name': 'CO2', 'type': 'numeric',
@@ -170,7 +130,7 @@ def get_figures(trips: Trips, charging: List[dict]):
{'id': 'price', 'name': 'price', 'type': 'numeric',
'format': deepcopy(nb_format).symbol_suffix(" " + ElecPrice.currency).precision(2), 'editable': True}
],
- data=charging,
+ data=[],
style_data_conditional=[
{
'if': {'column_id': ['start_level', "end_level"]},
@@ -179,42 +139,25 @@ def get_figures(trips: Trips, charging: List[dict]):
},
{
'if': {'column_id': 'price'},
- 'backgroundColor': 'rgb(230, 246, 254)'
+ 'backgroundColor': '#ABE2FB'
}
],
)
- consumption_by_temp_df = consumption_df[consumption_df["consumption_by_temp"].notnull()]
- if len(consumption_by_temp_df) > 0:
- consumption_fig_by_temp = px.histogram(consumption_by_temp_df, x="consumption_by_temp", y="consumption_km",
- histfunc="avg", title="Consumption by temperature")
- consumption_fig_by_temp.update_traces(xbins_size=2)
- consumption_fig_by_temp.update_layout(bargap=0.05)
- consumption_fig_by_temp.add_trace(
- go.Scatter(mode="markers", x=consumption_by_temp_df["consumption_by_temp"],
- y=consumption_by_temp_df["consumption_km"], name="Trips"))
- consumption_fig_by_temp.update_layout(xaxis_title="average temperature in °C",
- yaxis_title="Consumption kWh/100Km")
- consumption_graph_by_temp = html.Div(Graph(figure=consumption_fig_by_temp), id="consumption_graph_by_temp")
-
- else:
- consumption_graph_by_temp = html.Div(Graph(style={'display': 'none'}), id="consumption_graph_by_temp")
+ consumption_fig_by_temp = px.histogram(x=[0], y=[0],
+ histfunc="avg", title="Consumption by temperature")
+ consumption_fig_by_temp.update_traces(xbins_size=2)
+ consumption_fig_by_temp.update_layout(bargap=0.05)
+ consumption_fig_by_temp.add_trace(
+ go.Scatter(mode="markers", x=[0],
+ y=[0], name="Trips"))
+ consumption_fig_by_temp.update_layout(xaxis_title="average temperature in °C",
+ yaxis_title="Consumption kWh/100Km")
return True
-def __calculate_co2_per_kw(charging_data):
- try:
- co2_data = charging_data[charging_data["co2"] > 0]
- co2_kw_sum = co2_data["kw"].sum()
- if co2_kw_sum > 0:
- return co2_data["co2"].sum() / co2_kw_sum
- except KeyError:
- return 0
- return 0
-
-
def get_battery_curve_fig(row: dict, car: Car):
- start_date = Database.convert_datetime_from_string(row["start_at"])
- stop_at = Database.convert_datetime_from_string(row["stop_at"])
+ start_date = dash_date_to_datetime(row["start_at"])
+ stop_at = dash_date_to_datetime(row["stop_at"])
conn = Database.get_db()
res = Database.get_battery_curve(conn, start_date, car.vin)
conn.close()
diff --git a/web/utils.py b/web/utils.py
new file mode 100644
index 0000000..aba5fc6
--- /dev/null
+++ b/web/utils.py
@@ -0,0 +1,66 @@
+from datetime import datetime, timedelta
+
+import dash_bootstrap_components as dbc
+import dash_html_components as html
+from dash.development.base_component import Component
+
+
+def unix_time_millis(date):
+ return int(date.timestamp())
+
+
+def get_marks_from_start_end(start, end):
+ nb_marks = 10
+ result = []
+ time_delta = int((end - start).total_seconds() / nb_marks)
+ current = start
+ if time_delta > 0:
+ while current <= end:
+ result.append(current)
+ current += timedelta(seconds=time_delta)
+ result[-1] = end
+ if time_delta < 3600 * 24:
+ if time_delta > 3600:
+ date_f = '%x %Hh'
+ else:
+ date_f = '%x %Hh%M'
+ else:
+ date_f = '%x'
+ marks = {}
+ for date in result:
+ marks[unix_time_millis(date)] = str(date.strftime(date_f))
+ return marks
+ return None
+
+
+def card_value_div(card_id, unit, value="-"):
+ return html.Div([html.Div(value, id=card_id, className="mr-2"), html.Div(unit)],
+ className="d-flex flex-row justify-content-center")
+
+
+def dash_date_to_datetime(st):
+ return datetime.strptime(st, "%Y-%m-%dT%H:%M:%S.000Z")
+
+
+def create_card(card: dict):
+ res = []
+ for tile, value in card.items():
+ rows = value["text"]
+ # if isinstance(text, str):
+ # text = html.H3(text)
+ html_text = []
+ for row in rows:
+ html_text.append(html.Div(row, className="d-flex flex-row justify-content-center"))
+ res.append(html.Div(
+ dbc.Card([
+ html.H4(tile, className="card-title text-center"),
+ dbc.Row([
+ dbc.Col(dbc.CardBody(html_text, style={"whiteSpace": "nowrap", "fontSize": "160%"}),
+ className="text-center"),
+ dbc.Col(dbc.CardImg(src=value.get("src", Component.UNDEFINED), style={"maxHeight": "7rem"}))
+ ],
+ className="align-items-center flex-nowrap")
+ ], className="h-100 p-2"),
+ className="col-sm-12 col-md-6 col-lg-3 py-2"
+ ))
+ return res
diff --git a/web/views.py b/web/views.py
index e5daa0d..a11579a 100644
--- a/web/views.py
+++ b/web/views.py
@@ -1,17 +1,17 @@
import json
-from datetime import datetime, timezone
from typing import List
import dash_bootstrap_components as dbc
from dash.dependencies import Output, Input, MATCH, State
-from dash.development.base_component import Component
from dash.exceptions import PreventUpdate
import dash_core_components as dcc
import dash_html_components as html
import dash_daq as daq
-import pandas as pd
+from deepdiff import DeepDiff
from flask import jsonify, request, Response as FlaskResponse
+import web.utils
+from libs.car import Cars, Car
from mylogger import logger
from trip import Trips
@@ -23,65 +23,22 @@ from web.app import app, dash_app, myp, chc
from web.db import Database
# pylint: disable=invalid-name
+from web.figure_filter import Figure_Filter
+from web.utils import dash_date_to_datetime, create_card
+
RESPONSE = "-response"
EMPTY_DIV = "empty-div"
ABRP_SWITCH = 'abrp-switch'
CALLBACK_CREATED = False
-trips: Trips
+trips: Trips = Trips()
chargings: List[dict]
min_date = max_date = min_millis = max_millis = step = marks = cached_layout = None
-def diff_dashtable(data, data_previous, row_id_name="row_id"):
- df, df_previous = pd.DataFrame(data=data), pd.DataFrame(data_previous)
- for _df in [df, df_previous]:
- assert row_id_name in _df.columns
- _df = _df.set_index(row_id_name)
- mask = df.ne(df_previous)
- df_diff = df[mask].dropna(how="all", axis="columns").dropna(how="all", axis="rows")
- changes = []
- for idx, row in df_diff.iterrows():
- row.dropna(inplace=True)
- for change in row.iteritems():
- changes.append(
- {
- row_id_name: data[idx][row_id_name],
- "column_name": change[0],
- "current_value": change[1],
- "previous_value": df_previous.at[idx, change[0]],
- }
- )
- return changes
-
-
def create_callback(): # noqa: MC0001
global CALLBACK_CREATED
if not CALLBACK_CREATED:
- @dash_app.callback(Output('trips_map', 'figure'),
- Output('consumption_fig', 'figure'),
- Output('consumption_fig_by_speed', 'figure'),
- Output('consumption_graph_by_temp', 'children'),
- Output('summary-cards', 'children'),
- Output('tab_trips_fig', 'children'),
- Output('tab_charge', 'children'),
- Output('date-slider', 'max'),
- Output('date-slider', 'step'),
- Output('date-slider', 'marks'),
- Input('date-slider', 'value'))
- def display_value(value): # pylint: disable=unused-variable
- mini = datetime.fromtimestamp(value[0], tz=timezone.utc)
- maxi = datetime.fromtimestamp(value[1], tz=timezone.utc)
- filtered_trips = Trips()
- for trip in trips:
- if mini <= trip.start_at <= maxi:
- filtered_trips.append(trip)
- filtered_chargings = Charging.get_chargings(mini, maxi)
- figures.get_figures(filtered_trips, filtered_chargings)
- return figures.trips_map, figures.consumption_fig, figures.consumption_fig_by_speed, \
- figures.consumption_graph_by_temp, create_card(figures.SUMMARY_CARDS), \
- figures.table_fig, figures.battery_table, max_millis, step, marks
-
@dash_app.callback(Output(EMPTY_DIV, "children"),
[Input("battery-table", "data_timestamp")],
[State("battery-table", "data"),
@@ -89,17 +46,22 @@ def create_callback(): # noqa: MC0001
def capture_diffs_in_battery_table(timestamp, data, data_previous): # pylint: disable=unused-variable
if timestamp is None:
raise PreventUpdate
- diff_data = diff_dashtable(data, data_previous, "start_at")
- for changed_line in diff_data:
- if changed_line['column_name'] == 'price':
+ diff_data = DeepDiff(data_previous, data, ignore_numeric_type_changes=True, ignore_order=True, view="tree",
+ verbose_level=1)
+ for value_changed in diff_data["values_changed"]:
+ index, column_name = value_changed.path(output_format='list')
+ new_value = value_changed.t2
+ if column_name == 'price':
conn = Database.get_db()
- if not Database.set_chargings_price( conn, changed_line['start_at'],
- changed_line['current_value']):
+ date = dash_date_to_datetime(data[index]['start_at'])
+ if not Database.set_chargings_price(conn, date, new_value):
logger.error("Can't find line to update in the database")
+ else:
+ logger.debug("update price %s of %s", value_changed, date)
conn.close()
- return ""
+ return "" # don't need to update dashboard
- @dash_app.callback([Output("tab_battery_popup_graph", "children"), Output("tab_battery_popup", "is_open"), ],
+ @dash_app.callback([Output("tab_battery_popup_graph", "children"), Output("tab_battery_popup", "is_open")],
[Input("battery-table", "active_cell"),
Input("tab_battery_popup-close", "n_clicks")],
[State('battery-table', 'data'),
@@ -116,7 +78,7 @@ def create_callback(): # noqa: MC0001
[Input("trips-table", "active_cell"),
Input("tab_trips_popup-close", "n_clicks")],
State("tab_trips_popup", "is_open"))
- def get_altitude(active_cell, close, is_open): # pylint: disable=unused-argument, unused-variable
+ def get_altitude_graph(active_cell, close, is_open): # pylint: disable=unused-argument, unused-variable
if is_open is None:
is_open = False
if active_cell is not None and active_cell["column_id"] in ["altitude_diff"] and not is_open:
@@ -160,6 +122,21 @@ def get_vehicle_info(vin):
return response
+STYLE_CACHE = None
+
+
+@app.route("/assets/style2.json")
+def get_style():
+ global STYLE_CACHE
+ if not STYLE_CACHE:
+ with open(app.root_path + "/assets/style.json", "r") as f:
+ res = json.loads(f.read())
+ STYLE_CACHE = res
+ url_root = request.url_root
+ STYLE_CACHE["sprite"] = url_root + "assets/sprites/osm-liberty@2x"
+ return jsonify(STYLE_CACHE)
+
+
@app.route('/charge_now//')
def charge_now(vin, charge):
return jsonify(myp.charge_now(vin, charge != 0))
@@ -248,14 +225,17 @@ def update_trips():
conn.close()
min_date = None
max_date = None
+ car = myp.vehicles_list[0] # todo handle multiple car
try:
- trips_by_vin = Trips.get_trips(myp.vehicles_list)
- trips = next(iter(trips_by_vin.values())) # todo handle multiple car
+ trips_by_vin = Trips.get_trips(Cars([car]))
+ trips = trips_by_vin[car.vin]
assert len(trips) > 0
min_date = trips[0].start_at
max_date = trips[-1].start_at
- except (StopIteration, AssertionError):
+ figures.get_figures(trips[0].car)
+ except (AssertionError, KeyError):
logger.debug("No trips yet")
+ figures.get_figures(Car("vin","vid","brand"))
try:
chargings = Charging.get_chargings()
assert len(chargings) > 0
@@ -272,11 +252,12 @@ def update_trips():
# update for slider
try:
logger.debug("min_date:%s - max_date:%s", min_date, max_date)
- min_millis = figures.unix_time_millis(min_date)
- max_millis = figures.unix_time_millis(max_date)
+ min_millis = web.utils.unix_time_millis(min_date)
+ max_millis = web.utils.unix_time_millis(max_date)
step = (max_millis - min_millis) / 100
- marks = figures.get_marks_from_start_end(min_date, max_date)
+ marks = web.utils.get_marks_from_start_end(min_date, max_date)
cached_layout = None # force regenerate layout
+ figures.get_figures(car)
except (ValueError, IndexError):
logger.error("update_trips (slider): %s", exc_info=True)
except AttributeError:
@@ -303,40 +284,12 @@ def __get_control_tabs():
return tabs
-def create_card(card: dict):
- res = []
- for tile, value in card.items():
- text = value["text"]
- # if isinstance(text, str):
- # text = html.H3(text)
- res.append(html.Div(
- dbc.Card([
- html.H4(tile, className="card-title text-center"),
- dbc.Row([
- dbc.Col(dbc.CardBody(text, style={"white-space": "nowrap", "font-size": "160%"}),
- className="text-center"),
- dbc.Col(dbc.CardImg(src=value.get("src", Component.UNDEFINED), style={"max-height": "7rem"}))
- ],
- className="align-items-center flex-nowrap")
- ], className="h-100 p-2"),
- className="col-sm-12 col-md-6 col-lg-3 py-2"
- ))
- return res
-
-
def serve_layout():
global cached_layout
if cached_layout is None:
logger.debug("Create new layout")
+ fig_filter = Figure_Filter()
try:
- figures.get_figures(trips, chargings)
- summary_tab = [dbc.Container(dbc.Row(id="summary-cards",
- children=create_card(figures.SUMMARY_CARDS)), fluid=True),
- dcc.Graph(figure=figures.consumption_fig, id="consumption_fig"),
- dcc.Graph(figure=figures.consumption_fig_by_speed, id="consumption_fig_by_speed"),
- figures.consumption_graph_by_temp]
- maps = dcc.Graph(figure=figures.trips_map, id="trips_map", style={"height": '90vh'})
- create_callback()
range_slider = dcc.RangeSlider(
id='date-slider',
min=min_millis,
@@ -345,12 +298,31 @@ def serve_layout():
marks=marks,
value=[min_millis, max_millis],
)
- except (IndexError, TypeError, NameError):
+ summary_tab = [
+ dbc.Container(dbc.Row(id="summary-cards",
+ children=create_card(figures.SUMMARY_CARDS)), fluid=True),
+ fig_filter.add_graph(dcc.Graph(id="consumption_fig"), "start_at", ["consumption_km"],
+ figures.consumption_fig),
+ fig_filter.add_graph(dcc.Graph(id="consumption_fig_by_speed"), "speed_average",
+ ["consumption_km"] * 2, figures.consumption_fig_by_speed),
+ fig_filter.add_graph(dcc.Graph(id="consumption_graph_by_temp"), "consumption_by_temp",
+ ["consumption_km"] * 2, figures.consumption_fig_by_temp)]
+ maps = fig_filter.add_map(dcc.Graph(id="trips_map", style={"height": '90vh'}), "lat",
+ ["long", "start_at"], figures.trips_map)
+ fig_filter.add_table("trips", figures.table_fig)
+ fig_filter.add_table("chargings", figures.battery_table)
+ fig_filter.src = {"trips": trips.get_trips_as_dict(), "chargings": chargings}
+ dash_app.clientside_callback(*fig_filter.get_clientside_callback())
+ create_callback()
+ except (IndexError, TypeError, NameError, AssertionError, NameError):
summary_tab = figures.ERROR_DIV
maps = figures.ERROR_DIV
logger.warning("Failed to generate figure, there is probably not enough data yet", exc_info_debug=True)
range_slider = html.Div()
+ figures.battery_table = figures.ERROR_DIV
+
data_div = html.Div([
+ *fig_filter.get_store(),
range_slider,
html.Div([
dbc.Tabs([
@@ -404,8 +376,8 @@ def serve_layout():
try:
- Database.set_db_callback(update_trips)
Charging.set_default_price()
+ Database.set_db_callback(update_trips)
update_trips()
except (IndexError, TypeError):
logger.debug("Failed to get trips, there is probably not enough data yet:", exc_info=True)