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)