diff --git a/.jshintrc b/.jshintrc index 4247ffb3..2b885e31 100644 --- a/.jshintrc +++ b/.jshintrc @@ -31,7 +31,9 @@ "map_store": false, "deactivateChoropleth": false, "window": false, - "subscribeToEvents": false + "subscribeToEvents": false, + "createChart": false, + "hidePotentialLayers": false }, "strict": "implied" } diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 990ea49a..18e7de91 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ fail_fast: true repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v4.4.0 hooks: - id: check-json - id: end-of-file-fixer @@ -12,21 +12,21 @@ repos: - id: check-added-large-files - repo: https://github.com/pre-commit/mirrors-jshint - rev: v2.13.5 + rev: v2.13.6 hooks: - id: jshint + - repo: https://github.com/psf/black + rev: 23.3.0 + hooks: + - id: black + - repo: https://github.com/charliermarsh/ruff-pre-commit # Ruff version. - rev: 'v0.0.272' + rev: 'v0.0.277' hooks: - id: ruff - - repo: https://github.com/psf/black - rev: 23.1.0 - hooks: - - id: black - - repo: https://github.com/PyCQA/flake8 rev: 6.0.0 hooks: diff --git a/CHANGELOG.md b/CHANGELOG.md index 9054af14..5e4d581a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project tries to adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.5.0] - 2023-07-13 +### Added +- heat settings set for oemof simulation +- cluster popups +- url, view and calculations for result charts +- datapackage from digipipe +- models, layers and legend items from digipipe geodata + +### Changed +- legend layer colors and symbols +- static layer order +- paths for oemof hooks to digipipe scalars + +### Fixed +- units +- tour shows up after onboarding + ## [0.4.0] - 2023-06-20 ### Added - complete energy settings diff --git a/Makefile b/Makefile index 34169d28..02d8752d 100644 --- a/Makefile +++ b/Makefile @@ -28,6 +28,9 @@ empty_data: empty_raster: python manage.py shell --command="from digiplan.utils import data_processing; data_processing.empty_raster()" +empty_simulations: + python manage.py shell --command="from django_oemof.models import Simulation; Simulation.objects.all().delete()" + distill: python manage.py distill-local --force --exclude-staticfiles ./digiplan/static/mvts diff --git a/README.md b/README.md index cf7b6a22..270d34e4 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,8 @@ Obviously, you have to clone this repo first. ## Prepare Data -In both cases, geometry packages have to be placed into folder _digiplan/data/_, +In both cases, the datapackage from digipipe have to be placed into folder _digiplan/data/_ and renamed into `digipipe`, so that they can be found by the application and uploaded into database. -All packages are justed dropped into this folder (no hierarchy). ## Using standard python installation @@ -260,3 +259,11 @@ Example to only load specific data: ``` docker-compose -f production.yml run --rm django python -u manage.py shell --command="from djagora.utils import load_overlays; from djagora.utils.load_configs import DYNAMIC_OVERLAYS; overlays = [item for item in DYNAMIC_OVERLAYS if item['name'].startswith('settlement')]; load_overlays.run(overlays=overlays)" ``` + +If celery does not complete, but shows no errors you can check redis for errors: + +```bash +docker exec -it bash +redis-cli keys "*" +redis-cli get +``` diff --git a/config/__init__.py b/config/__init__.py index 5568b6d7..87b6ced1 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -1,3 +1,4 @@ +"""Initializes celery automatically.""" # This will make sure the app is always imported when # Django starts so that shared_task will use this app. from .celery import app as celery_app diff --git a/config/celery.py b/config/celery.py index a7d32e65..e4549332 100644 --- a/config/celery.py +++ b/config/celery.py @@ -1,3 +1,4 @@ +"""Module to set up celery.""" import os from celery import Celery diff --git a/config/settings/__init__.py b/config/settings/__init__.py index e69de29b..e9b8c688 100644 --- a/config/settings/__init__.py +++ b/config/settings/__init__.py @@ -0,0 +1 @@ +"""Settings init.""" diff --git a/config/settings/base.py b/config/settings/base.py index 69383535..110aa92f 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -1,5 +1,7 @@ """Base settings to build other settings files upon.""" +import logging import os +import sys import environ from django.core.exceptions import ValidationError @@ -9,6 +11,8 @@ ROOT_DIR = environ.Path(__file__) - 3 # (digiplan/config/settings/base.py - 3 = digiplan/) APPS_DIR = ROOT_DIR.path("digiplan") DATA_DIR = APPS_DIR.path("data") +DIGIPIPE_DIR = DATA_DIR.path("digipipe") +DIGIPIPE_GEODATA_DIR = DIGIPIPE_DIR.path("geodata") METADATA_DIR = APPS_DIR.path("metadata") env = environ.Env() @@ -235,6 +239,22 @@ # https://docs.celeryq.dev/en/stable/userguide/configuration.html#std:setting-result_backend CELERY_RESULT_BACKEND = CELERY_BROKER_URL +# test +TESTING = "test" in sys.argv[1:] +if TESTING: + logging.info("In TEST Mode - Disableling Migrations") + + class DisableMigrations: + """Disables migrations for test mode.""" + + def __contains__(self, item) -> bool: # noqa: D105, ANN001 + return True + + def __getitem__(self, item): # noqa: D105, ANN001, ANN204 + return None + + MIGRATION_MODULES = DisableMigrations() + # Your stuff... # ------------------------------------------------------------------------------ PASSWORD_PROTECTION = env.bool("PASSWORD_PROTECTION", False) @@ -260,6 +280,8 @@ setup.MapImage("hydro", "images/icons/map_hydro.png"), setup.MapImage("biomass", "images/icons/map_biomass.png"), setup.MapImage("combustion", "images/icons/map_combustion.png"), + setup.MapImage("gsgk", "images/icons/map_gsgk.png"), + setup.MapImage("storage", "images/icons/map_battery.png"), ] MAP_ENGINE_API_MVTS = { @@ -267,16 +289,58 @@ setup.MVTAPI("municipality", "map", "Municipality"), setup.MVTAPI("municipalitylabel", "map", "Municipality", "label_tiles"), ], + "static": [ + setup.MVTAPI("soil_quality_low", "map", "SoilQualityLow"), + setup.MVTAPI("soil_quality_high", "map", "SoilQualityHigh"), + setup.MVTAPI("landscape_protection_area", "map", "LandscapeProtectionArea"), + setup.MVTAPI("forest", "map", "Forest"), + setup.MVTAPI("special_protection_area", "map", "SpecialProtectionArea"), + setup.MVTAPI("air_traffic", "map", "AirTraffic"), + setup.MVTAPI("aviation", "map", "Aviation"), + setup.MVTAPI("biosphere_reserve", "map", "BiosphereReserve"), + setup.MVTAPI("drinking_water_protection_area", "map", "DrinkingWaterArea"), + setup.MVTAPI("fauna_flora_habitat", "map", "FaunaFloraHabitat"), + setup.MVTAPI("floodplain", "map", "Floodplain"), + setup.MVTAPI("grid", "map", "Grid"), + setup.MVTAPI("industry", "map", "Industry"), + setup.MVTAPI("less_favoured_areas_agricultural", "map", "LessFavouredAreasAgricultural"), + setup.MVTAPI("military", "map", "Military"), + setup.MVTAPI("nature_conservation_area", "map", "NatureConservationArea"), + setup.MVTAPI("railway", "map", "Railway"), + setup.MVTAPI("road_default", "map", "Road"), + setup.MVTAPI("road_railway-500m_region", "map", "RoadRailway500m"), + setup.MVTAPI("settlement-0m", "map", "Settlement0m"), + setup.MVTAPI("water", "map", "Water"), + ], + "potential": [ + setup.MVTAPI("potentialarea_pv_agriculture_lfa-off_region", "map", "PotentialareaPVAgricultureLFAOff"), + setup.MVTAPI("potentialarea_pv_road_railway_region", "map", "PotentialareaPVRoadRailway"), + setup.MVTAPI("potentialarea_wind_stp_2018_vreg", "map", "PotentialareaWindSTP2018Vreg"), + setup.MVTAPI("potentialarea_wind_stp_2027_repowering", "map", "PotentialareaWindSTP2027Repowering"), + setup.MVTAPI( + "potentialarea_wind_stp_2027_search_area_forest_area", + "map", + "PotentialareaWindSTP2027SearchAreaForestArea", + ), + setup.MVTAPI( + "potentialarea_wind_stp_2027_search_area_open_area", + "map", + "PotentialareaWindSTP2027SearchAreaOpenArea", + ), + setup.MVTAPI("potentialarea_wind_stp_2027_vr", "map", "PotentialareaWindSTP2027VR"), + ], "results": [setup.MVTAPI("results", "map", "Municipality")], } MAP_ENGINE_API_CLUSTERS = [ - setup.ClusterAPI("wind", "map", "WindTurbine"), - setup.ClusterAPI("pvroof", "map", "PVroof"), - setup.ClusterAPI("pvground", "map", "PVground"), - setup.ClusterAPI("hydro", "map", "Hydro"), - setup.ClusterAPI("biomass", "map", "Biomass"), - setup.ClusterAPI("combustion", "map", "Combustion"), + setup.ClusterAPI("wind", "map", "WindTurbine", properties=["id"]), + setup.ClusterAPI("pvroof", "map", "PVroof", properties=["id"]), + setup.ClusterAPI("pvground", "map", "PVground", properties=["id"]), + setup.ClusterAPI("hydro", "map", "Hydro", properties=["id"]), + setup.ClusterAPI("biomass", "map", "Biomass", properties=["id"]), + setup.ClusterAPI("combustion", "map", "Combustion", properties=["id"]), + setup.ClusterAPI("gsgk", "map", "GSGK", properties=["id"]), + setup.ClusterAPI("storage", "map", "Storage", properties=["id"]), ] MAP_ENGINE_STYLES_FOLDER = "digiplan/static/config/" @@ -285,28 +349,107 @@ } MAP_ENGINE_CHOROPLETHS = [ - setup.Choropleth("population", layers=["municipality"], title=_("Einwohner_innenzahl"), unit=_("EW")), - setup.Choropleth("population_density", layers=["municipality"], title=_("Einwohner_innenzahl"), unit=_("EW/qm")), - setup.Choropleth("capacity", layers=["municipality"], title=_("Installierte Leistung"), unit=_("MW")), + setup.Choropleth("population_statusquo", layers=["municipality"], title=_("Einwohner_innenzahl"), unit=_("")), setup.Choropleth( - "capacity_square", + "population_density_statusquo", + layers=["municipality"], + title=_("Einwohner_innenzahl pro km²"), + unit=_(""), + ), + setup.Choropleth("employees_statusquo", layers=["municipality"], title=_("Beschäftigte"), unit=_("")), + setup.Choropleth("companies_statusquo", layers=["municipality"], title=_("Betriebe"), unit=_("")), + setup.Choropleth("capacity_statusquo", layers=["municipality"], title=_("Installierte Leistung"), unit=_("MW")), + setup.Choropleth( + "capacity_square_statusquo", layers=["municipality"], title=_("Installierte Leistung pro qm"), - unit=_("MW/qm"), + unit=_("MW"), ), - setup.Choropleth("wind_turbines", layers=["municipality"], title=_("Anzahl Windturbinen"), unit=_("")), + setup.Choropleth("wind_turbines_statusquo", layers=["municipality"], title=_("Anzahl Windturbinen"), unit=_("")), setup.Choropleth( - "wind_turbines_square", + "wind_turbines_square_statusquo", layers=["municipality"], title=_("Anzahl Windturbinen pro qm"), unit=_(""), ), setup.Choropleth( - "renewable_electricity_production", + "energy_statusquo", + layers=["municipality"], + title=_("Energie Erneuerbare"), + unit=_("GWh"), + ), + setup.Choropleth( + "energy_2045", layers=["municipality"], title=_("Energie Erneuerbare"), unit=_("GWh"), ), + setup.Choropleth( + "energy_share_statusquo", + layers=["municipality"], + title=_("Anteil Erneuerbare Energien am Strombedarf"), + unit=_("%"), + ), + setup.Choropleth( + "energy_capita_statusquo", + layers=["municipality"], + title=_("Gewonnene Energie aus EE je EW"), + unit=_("MWh"), + ), + setup.Choropleth( + "energy_capita_2045", + layers=["municipality"], + title=_("Gewonnene Energie aus EE je EW"), + unit=_("MWh"), + ), + setup.Choropleth( + "energy_square_statusquo", + layers=["municipality"], + title=_("Gewonnene Energie aus EE je km²"), + unit=_("MWh"), + ), + setup.Choropleth( + "energy_square_2045", + layers=["municipality"], + title=_("Gewonnene Energie aus EE je km²"), + unit=_("MWh"), + ), + setup.Choropleth( + "electricity_demand_statusquo", + layers=["municipality"], + title=_("Strombedarf"), + unit=_("GWh"), + ), + setup.Choropleth( + "electricity_demand_capita_statusquo", + layers=["municipality"], + title=_("Strombedarf pro EinwohnerIn"), + unit=_("kWh"), + ), + setup.Choropleth( + "heat_demand_statusquo", + layers=["municipality"], + title=_("Wärmebedarf"), + unit=_("GWh"), + ), + setup.Choropleth( + "heat_demand_capita_statusquo", + layers=["municipality"], + title=_("Wärmebedarf pro EinwohnerIn"), + unit=_("kWh"), + ), + setup.Choropleth( + "batteries_statusquo", + layers=["municipality"], + title=_("Anzahl Batteriespeicher"), + unit=_("#"), + ), + setup.Choropleth( + "batteries_capacity_statusquo", + layers=["municipality"], + title=_("Kapazität Batteriespeicher"), + unit=_("MWh"), + ), ] MAP_ENGINE_POPUPS = [ @@ -314,13 +457,59 @@ "municipality", popup_at_default_layer=False, choropleths=[ - "population", - "population_density", - "capacity", - "capacity_square", - "wind_turbines", - "wind_turbines_square", - "renewable_electricity_production", + "population_statusquo", + "population_density_statusquo", + "employees_statusquo", + "companies_statusquo", + "capacity_statusquo", + "capacity_square_statusquo", + "wind_turbines_statusquo", + "wind_turbines_square_statusquo", + "energy_statusquo", + "energy_2045", + "energy_share_statusquo", + "energy_capita_statusquo", + "energy_capita_2045", + "energy_square_statusquo", + "energy_square_2045", + "electricity_demand_statusquo", + "electricity_demand_capita_statusquo", + "heat_demand_statusquo", + "heat_demand_capita_statusquo", + "batteries_statusquo", + "batteries_capacity_statusquo", ], ), + setup.Popup( + "wind", + popup_at_default_layer=True, + ), + setup.Popup( + "pvground", + popup_at_default_layer=True, + ), + setup.Popup( + "pvroof", + popup_at_default_layer=True, + ), + setup.Popup( + "hydro", + popup_at_default_layer=True, + ), + setup.Popup( + "biomass", + popup_at_default_layer=True, + ), + setup.Popup( + "combustion", + popup_at_default_layer=True, + ), + setup.Popup( + "gsgk", + popup_at_default_layer=True, + ), + setup.Popup( + "storage", + popup_at_default_layer=True, + ), ] diff --git a/digiplan/__init__.py b/digiplan/__init__.py index d9127a1c..94761f7d 100644 --- a/digiplan/__init__.py +++ b/digiplan/__init__.py @@ -1,4 +1,4 @@ """Digiplan init - holds current version.""" -__version__ = "0.4.0" +__version__ = "0.5.0" __version_info__ = tuple([int(num) if num.isdigit() else num for num in __version__.replace("-", ".", 1).split(".")]) diff --git a/digiplan/conftest.py b/digiplan/conftest.py index 0910280e..9a35154f 100644 --- a/digiplan/conftest.py +++ b/digiplan/conftest.py @@ -1,12 +1 @@ -import pytest -from django.test import RequestFactory - - -@pytest.fixture(autouse=True) -def media_storage(settings, tmpdir): - settings.MEDIA_ROOT = tmpdir.strpath - - -@pytest.fixture -def request_factory() -> RequestFactory: - return RequestFactory() +"""Module guarantees that pytest finds root directory.""" diff --git a/digiplan/data/README.md b/digiplan/data/README.md index 0e1c793a..d1e87e2b 100644 --- a/digiplan/data/README.md +++ b/digiplan/data/README.md @@ -1 +1,3 @@ - In this folder, all geopackages are stored. +Data folder must contain following folders: +- digipipe: datapackage from digipipe holding data and geodata +- oemof: holding ES datapackage from `oemof.tabular` diff --git a/digiplan/data/scenarios/.gitkeep b/digiplan/data/scenarios/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/digiplan/data/scenarios/README.md b/digiplan/data/scenarios/README.md deleted file mode 100644 index 744d980f..00000000 --- a/digiplan/data/scenarios/README.md +++ /dev/null @@ -1,11 +0,0 @@ -In this folder, scenario data for StatusQuo and Sceanrio2045 are stored. -Those values are read in while simulating energysystem with oemof -and for calculating results and plotting charts. - -## Electricity demand: - -https://github.com/rl-institut-private/digiplan/issues/231 - -For compatibility with oemof datapackage: -- rename _demand_cts_power_demand.csv_ into _demand_ghd_power_demand.csv_ -- rename _demand_ind_power_demand.csv_ into _demand_i_power_demand.csv_ diff --git a/digiplan/map/__init__.py b/digiplan/map/__init__.py index e69de29b..2d43f75a 100644 --- a/digiplan/map/__init__.py +++ b/digiplan/map/__init__.py @@ -0,0 +1 @@ +"""Map init.""" diff --git a/digiplan/map/apps.py b/digiplan/map/apps.py index e62ed130..0f447636 100644 --- a/digiplan/map/apps.py +++ b/digiplan/map/apps.py @@ -32,10 +32,10 @@ def ready(self) -> None: hooks.register_hook( hooks.HookType.PARAMETER, - hooks.Hook(scenario=hooks.ALL_SCENARIOS, function=digiplan_hooks.adapt_heat_demand), + hooks.Hook(scenario=hooks.ALL_SCENARIOS, function=digiplan_hooks.adapt_heat_settings), ) hooks.register_hook( hooks.HookType.PARAMETER, - hooks.Hook(scenario=hooks.ALL_SCENARIOS, function=digiplan_hooks.adapt_capacities), + hooks.Hook(scenario=hooks.ALL_SCENARIOS, function=digiplan_hooks.adapt_renewable_capacities), ) diff --git a/digiplan/map/calculations.py b/digiplan/map/calculations.py index 851bc2a2..32a09303 100644 --- a/digiplan/map/calculations.py +++ b/digiplan/map/calculations.py @@ -1,167 +1,247 @@ """Module for calculations used for choropleths or charts.""" -from typing import Optional - import pandas as pd +from django.conf import settings from django.db.models import Sum +from django.utils.translation import gettext_lazy as _ +from django_oemof.models import Simulation from django_oemof.results import get_results from oemof.tabular.postprocessing import calculations, core -from digiplan.map import config, models +from digiplan.map import config, datapackage, models -def calculate_square_for_value(value: int, municipality_id: Optional[int]) -> float: +def calculate_square_for_value(df: pd.DataFrame) -> pd.DataFrame: """ - Calculate value related to municipality area. + Calculate values related to municipality areas. Parameters ---------- - value: int - Value to calculate - municipality_id: Optional[int] - ID of municipality to get area from - If not given, value in relation to area of whole region is calculated. + df: pd.DataFrame + Index holds municipality IDs, columns hold random entries Returns ------- - float - Value per square meter + pd.DataFrame + Each value is multiplied by related municipality share """ - area = 0.0 - if municipality_id is not None: - area = models.Municipality.objects.get(pk=municipality_id).area - else: - for mun in models.Municipality.objects.all(): - area += models.Municipality.objects.get(pk=mun.id).area - if area != 0.0: # noqa: PLR2004 - return value / area - return value + is_series = False + if isinstance(df, pd.Series): + is_series = True + df = pd.DataFrame(df) # noqa: PD901 + areas = ( + pd.DataFrame.from_records(models.Municipality.objects.all().values("id", "area")).set_index("id").sort_index() + ) + result = df / areas.sum().sum() if len(df) == 1 else df.sort_index() / areas.to_numpy() + if is_series: + return result.iloc[:, 0] + return result + + +def value_per_municipality(series: pd.Series) -> pd.DataFrame: + """Shares values across areas (dummy function).""" + data = pd.concat([series] * 20, axis=1).transpose() + data.index = range(20) + areas = ( + pd.DataFrame.from_records(models.Municipality.objects.all().values("id", "area")).set_index("id").sort_index() + ) + result = data.sort_index() / areas.to_numpy() + return result / areas.sum().sum() -def capacity(mun_id: Optional[int] = None) -> float: +def calculate_capita_for_value(df: pd.DataFrame) -> pd.DataFrame: """ - Calculate capacity of renewables (either for municipality or for whole region). + Calculate values related to population. If only one region is given, whole region is assumed. Parameters ---------- - mun_id: Optional[int] - If given, capacity of renewables for given municipality are calculated. If not, for whole region. + df: pd.DataFrame + Index holds municipality IDs, columns hold random entries Returns ------- - float - Sum of installed renewables + pd.DataFrame + Each value is multiplied by related municipality population share """ - capacity = 0.0 - values = capacity_per_municipality() + is_series = False + if isinstance(df, pd.Series): + is_series = True + df = pd.DataFrame(df) # noqa: PD901 + + population = ( + pd.DataFrame.from_records(models.Population.objects.filter(year=2022).values("id", "value")) + .set_index("id") + .sort_index() + ) + result = df / population.sum().sum() if len(df) == 1 else df.sort_index() / population.to_numpy() + if is_series: + return result.iloc[:, 0] + return result + + +def employment_per_municipality() -> pd.DataFrame: + """Return employees per municipality.""" + return datapackage.get_employment()["employees_total"] + + +def companies_per_municipality() -> pd.DataFrame: + """Return companies per municipality.""" + return datapackage.get_employment()["companies_total"] - if mun_id is not None: - capacity = values[mun_id] - else: - for _key, value in values.items(): - capacity += value - return capacity +def batteries_per_municipality() -> pd.DataFrame: + """Return battery count per municipality.""" + return datapackage.get_batteries()["count"] -# pylint: disable=W0613 -def capacity_comparison(municipality_id: int) -> dict: # noqa: ARG001 + +def battery_capacities_per_municipality() -> pd.DataFrame: + """Return battery capacity per municipality.""" + return datapackage.get_batteries()["storage_capacity"] + + +def capacities_per_municipality() -> pd.DataFrame: """ - Get chart for capacity of renewables. + Calculate capacity of renewables per municipality in MW. - Parameters - ---------- - municipality_id: int - Related municipality + Returns + ------- + pd.DataFrame + Capacity per municipality (index) and technology (column) + """ + capacities = [] + for technology in ( + models.WindTurbine, + models.PVroof, + models.PVground, + models.Hydro, + models.Biomass, + models.Storage, + ): + res_capacity = pd.DataFrame.from_records( + technology.objects.values("mun_id").annotate(capacity=Sum("capacity_net")).values("mun_id", "capacity"), + ).set_index("mun_id") + res_capacity.columns = [technology._meta.verbose_name] # noqa: SLF001 + capacities.append(res_capacity) + return pd.concat(capacities, axis=1).fillna(0.0) * 1e-3 + + +def energies_per_municipality() -> pd.DataFrame: + """ + Calculate energy of renewables per municipality in GWh. Returns ------- - dict - Chart data to use in JS + pd.DataFrame + Energy per municipality (index) and technology (column) """ - return ([3600, 1000], [200, 100], [500, 1000], [300, 1000], [1700, 1000]) + capacities = capacities_per_municipality() + full_load_hours = pd.Series( + data=[technology_data["2022"] for technology_data in config.TECHNOLOGY_DATA["full_load_hours"].values()], + index=config.TECHNOLOGY_DATA["full_load_hours"].keys(), + ) + full_load_hours = full_load_hours.reindex(index=["wind", "pv_roof", "pv_ground", "ror", "bioenergy", "st"]) + return capacities * full_load_hours.values / 1e3 -def capacity_per_municipality() -> dict[int, int]: +def energies_per_municipality_2045(simulation_id: int) -> pd.DataFrame: + """Calculate energies from 2045 scenario per municipality.""" + results = get_results( + simulation_id, + { + "electricity_production": electricity_production, + }, + ) + renewables = results["electricity_production"][ + results["electricity_production"].index.get_level_values(0).isin(config.SIMULATION_RENEWABLES) + ] + renewables.index = ["hydro", "pv_ground", "pv_roof", "wind"] + renewables = renewables.reindex(["wind", "pv_roof", "pv_ground", "hydro"]) + + parameters = Simulation.objects.get(pk=simulation_id).parameters + renewables = renewables * calculate_potential_shares(parameters) + renewables["bioenergy"] = 0.0 + renewables["st"] = 0.0 + return renewables + + +def energy_shares_per_municipality() -> pd.DataFrame: """ - Calculate capacity of renewables per municipality. + Calculate energy shares of renewables from electric demand per municipality. Returns ------- - dict[int, int] - Capacity per municipality - """ - capacity = {} - municipalities = models.Municipality.objects.all() - - for mun in municipalities: - res_capacity = 0.0 - for renewable in models.RENEWABLES: - one_capacity = renewable.objects.filter(mun_id__exact=mun.id).aggregate(Sum("capacity_net"))[ - "capacity_net__sum" - ] - if one_capacity is None: - one_capacity = 0.0 - res_capacity += one_capacity - capacity[mun.id] = res_capacity - return capacity - - -def capacity_square(mun_id: Optional[int] = None) -> float: + pd.DataFrame + Energy share per municipality (index) and technology (column) """ - Calculate capacity of renewables per km² (either for municipality or for whole region). + energies = energies_per_municipality() + demands = datapackage.get_power_demand() + total_demand = pd.concat([d["2022"] for d in demands.values()], axis=1).sum(axis=1) + total_demand_share = total_demand / total_demand.sum() + energies = energies.reindex(range(20)) + return energies.mul(total_demand_share, axis=0) - Parameters - ---------- - mun_id: Optional[int] - If given, capacity of renewables per km² for given municipality are calculated. If not, for whole region. + +def electricity_demand_per_municipality() -> pd.DataFrame: + """ + Calculate electricity demand per sector per municipality in GWh. Returns ------- - float - Sum of installed renewables + pd.DataFrame + Electricity demand per municipality (index) and sector (column) """ - value = capacity(mun_id) - return calculate_square_for_value(value, mun_id) + demands_raw = datapackage.get_power_demand() + demands_per_sector = pd.concat([demand["2022"] for demand in demands_raw.values()], axis=1) + demands_per_sector.columns = [ + _("Electricity Household Demand"), + _("Electricity CTS Demand"), + _("Electricity Industry Demand"), + ] + return demands_per_sector * 1e-3 -# pylint: disable=W0613 -def capacity_square_comparison(municipality_id: int) -> dict: +def heat_demand_per_municipality() -> pd.DataFrame: """ - Get chart for capacity of renewables per km². - - Parameters - ---------- - municipality_id: int - Related municipality + Calculate heat demand per sector per municipality in GWh. Returns ------- - dict - Chart data to use in JS + pd.DataFrame + Heat demand per municipality (index) and sector (column) """ - capacity_square = [] - capacity = capacity_comparison(municipality_id) - for quo, future in capacity: - quo_new = calculate_square_for_value(quo, municipality_id) - future_new = calculate_square_for_value(future, municipality_id) - capacity_square.append([quo_new, future_new]) - - return capacity_square + demands_raw = datapackage.get_summed_heat_demand_per_municipality() + demands_per_sector = pd.concat( + [distributions["cen"]["2022"] + distributions["dec"]["2022"] for distributions in demands_raw.values()], + axis=1, + ) + demands_per_sector.columns = [ + _("Electricity Household Demand"), + _("Electricity CTS Demand"), + _("Electricity Industry Demand"), + ] + return demands_per_sector * 1e-3 -def capacity_square_per_municipality() -> dict[int, int]: +def detailed_overview(simulation_id: int) -> pd.DataFrame: # noqa: ARG001 """ - Calculate capacity of renewables per km² per municipality. + Calculate data for detailed overview chart from simulation ID. + + Parameters + ---------- + simulation_id: int + Simulation ID to calculate results from Returns ------- - dict[int, int] - Capacity per km² per municipality + pandas.DataFrame + holding data for detailed overview chart """ - capacity = capacity_per_municipality() - for key, value in capacity.items(): - capacity[key] = calculate_square_for_value(value, key) - return capacity + # TODO(Hendrik): Calculate real data + # https://github.com/rl-institut-private/digiplan/issues/164 + return pd.DataFrame( + data={"production": [300, 200, 200, 150, 520, 0], "consumption": [0, 0, 0, 0, 0, 1300]}, + index=["wind", "pv_roof", "pv_ground", "biomass", "fossil", "consumption"], + ) def electricity_from_from_biomass(simulation_id: int) -> pd.Series: @@ -278,6 +358,91 @@ def electricity_heat_demand(simulation_id: int) -> pd.Series: return electricity_for_heat_sum +def calculate_potential_shares(parameters: dict) -> pd.DataFrame: + """Calculate potential shares depending on user settings.""" + # DISAGGREGATION + # Wind + wind_areas = pd.read_csv( + settings.DIGIPIPE_DIR.path("scalars").path("potentialarea_wind_area_stats_muns.csv"), + index_col=0, + ) + if parameters["s_w_3"]: + wind_area_per_mun = wind_areas["stp_2018_vreg"] + elif parameters["s_w_4_1"]: + wind_area_per_mun = wind_areas["stp_2027_vr"] + elif parameters["s_w_4_2"]: + wind_area_per_mun = wind_areas["stp_2027_repowering"] + elif parameters["s_w_5"]: + wind_area_per_mun = ( + wind_areas["stp_2027_search_area_open_area"] * parameters["s_w_5_1"] / 100 + + wind_areas["stp_2027_search_area_forest_area"] * parameters["s_w_5_2"] / 100 + ) + else: + msg = "No wind switch set" + raise KeyError(msg) + wind_share_per_mun = wind_area_per_mun / wind_area_per_mun.sum() + + # PV ground + pv_ground_areas = pd.read_csv( + settings.DIGIPIPE_DIR.path("scalars").path("potentialarea_pv_ground_area_stats_muns.csv", index_col=0), + ) + pv_ground_area_per_mun = ( + pv_ground_areas["agriculture_lfa-off_region"] * parameters["s_pv_ff_3"] / 100 + + pv_ground_areas["road_railway_region"] * parameters["s_pv_ff_4"] / 100 + ) + pv_ground_share_per_mun = pv_ground_area_per_mun / pv_ground_area_per_mun.sum() + + # PV roof + pv_roof_areas = pd.read_csv( + settings.DIGIPIPE_DIR.path("scalars").path("potentialarea_pv_roof_area_stats_muns.csv"), + index_col=0, + ) + pv_roof_area_per_mun = pv_roof_areas["installable_power_total"] + pv_roof_share_per_mun = pv_roof_area_per_mun / pv_roof_area_per_mun.sum() + + # Hydro + hydro_areas = pd.read_csv( + settings.DIGIPIPE_DIR.path("scalars").path("bnetza_mastr_hydro_stats_muns.csv"), + index_col=0, + ) + hydro_area_per_mun = hydro_areas["capacity_net"] + hydro_share_per_mun = hydro_area_per_mun / hydro_area_per_mun.sum() + + shares = pd.concat( + [wind_share_per_mun, pv_roof_share_per_mun, pv_ground_share_per_mun, hydro_share_per_mun], + axis=1, + ) + shares.columns = ["wind", "pv_roof", "pv_ground", "hydro"] + return shares + + +def capacities_per_municipality_2045(simulation_id: int) -> pd.DataFrame: + """ + Return capacities per municipality. + + Parameters + ---------- + simulation_id: int + Simulation ID to get results from + + Returns + ------- + pd.DataFrame + containing renewable capacities disaggregated per municipality + """ + results = get_results(simulation_id, {"electricity_production": electricity_production}) + renewables = results["electricity_production"][ + results["electricity_production"].index.get_level_values(0).isin(config.SIMULATION_RENEWABLES) + ] + renewables.index = renewables.index.get_level_values(0) + + parameters = Simulation.objects.get(pk=simulation_id).parameters + potential_shares = calculate_potential_shares(parameters) + renewable_shares = potential_shares * renewables.values + renewable_shares.columns = renewables.index + return renewable_shares.fillna(0.0) + + def electricity_overview(simulation_id: int) -> pd.Series: """ Return data for electricity overview chart. diff --git a/digiplan/map/charts.py b/digiplan/map/charts.py index 434816c9..82198553 100644 --- a/digiplan/map/charts.py +++ b/digiplan/map/charts.py @@ -2,111 +2,856 @@ import json import pathlib -from collections.abc import Callable, Iterable -from typing import Optional +from typing import Any, Optional -from digiplan.map import config, models +import pandas as pd +from django.utils.translation import gettext_lazy as _ -CHARTS: dict[str, Callable] = { - "wind_turbines": models.WindTurbine.wind_turbines_history, - "wind_turbines_square": models.WindTurbine.wind_turbines_per_area_history, -} +from digiplan.map import calculations, config, models +from digiplan.map.utils import merge_dicts -def get_chart_options(lookup: str) -> dict: - """ - Get the options for a chart from the corresponding json file. - - Parameters - ---------- - lookup: str - Looks up related chart function in CHARTS - - Returns - ------- - dict - Containing the json that can be filled with data - - Raises - ------ - LookupError - if lookup can't be found in LOOKUPS - """ - lookup_path = pathlib.Path(config.CHARTS_DIR.path(f"{lookup}.json")) - if not lookup_path.exists(): - error_msg = f"Could not find {lookup=} in charts folder." - raise LookupError(error_msg) +class Chart: + """Base class for charts.""" - with lookup_path.open("r", encoding="utf-8") as lookup_json: - lookup_options = json.load(lookup_json) + lookup: str = None - with pathlib.Path(config.CHARTS_DIR.path("general_options.json")).open("r", encoding="utf-8") as general_chart_json: - general_chart_options = json.load(general_chart_json) + def __init__( + self, + lookup: Optional[str] = None, + chart_data: Optional[Any] = None, + **kwargs, # noqa: ARG002 + ) -> None: + """Initialize chart data and chart options.""" + if lookup: + self.lookup = lookup + self.chart_data = chart_data if chart_data is not None else self.get_chart_data() + self.chart_options = self.get_chart_options() - chart = merge_dicts(general_chart_options, lookup_options) + def render(self) -> dict: + """ + Create chart based on given lookup and municipality ID or result option. - return chart + Returns + ------- + dict + Containing chart filled with data + """ + if self.chart_data is not None: + series_type = self.chart_options["series"][0]["type"] + series_length = len(self.chart_options["series"]) + if series_type == "line": + data = [] + for key, value in self.chart_data.items(): + year_as_string = f"{key}" + data.append([year_as_string, value]) + self.chart_options["series"][0]["data"] = data + elif series_length > 1: + for i in range(0, series_length): + values = self.chart_data[i] + if not isinstance(values, (list, tuple)): + values = [values] + self.chart_options["series"][i]["data"] = values + else: + self.chart_options["series"][0]["data"] = self.chart_data -def create_chart(lookup: str, chart_data: Optional[Iterable[tuple[str, float]]] = None) -> dict: - """ - Create chart based on given lookup and municipality ID or result option. + return self.chart_options - Parameters - ---------- - lookup: str - Looks up related chart function in charts folder. - chart_data: list[tuple[str, float]] - Chart data separated into tuples holding key and value - If no data is given, data is expected to be set via lookup JSON + def get_chart_options(self) -> dict: + """ + Get the options for a chart from the corresponding json file. - Returns - ------- - dict - Containing chart filled with data + Returns + ------- + dict + Containing the json that can be filled with data + Raises + ------ + LookupError + if lookup can't be found in LOOKUPS + """ + lookup_path = pathlib.Path(config.CHARTS_DIR.path(f"{self.lookup}.json")) + if not lookup_path.exists(): + error_msg = f"Could not find lookup '{self.lookup}' in charts folder." + raise LookupError(error_msg) + + with lookup_path.open("r", encoding="utf-8") as lookup_json: + lookup_options = json.load(lookup_json) + + with pathlib.Path(config.CHARTS_DIR.path("general_options.json")).open( + "r", + encoding="utf-8", + ) as general_chart_json: + general_chart_options = json.load(general_chart_json) + + options = merge_dicts(general_chart_options, lookup_options) + return options + + def get_chart_data(self) -> None: + """ + Check if chart_data_function is valid. + + Returns + ------- + None + + """ + return + + +class SimulationChart(Chart): + """For charts based on simulations.""" + + def __init__(self, simulation_id: int) -> None: + """ + Init Detailed Overview Chart. + + Parameters + ---------- + simulation_id: any + id of used Simulation + """ + self.simulation_id = simulation_id + super().__init__() + + +class DetailedOverviewChart(Chart): + """Detailed Overview Chart.""" + + lookup = "detailed_overview" + + def __init__(self, simulation_id: int) -> None: + """ + Init Detailed Overview Chart. + + Parameters + ---------- + simulation_id: any + id of used Simulation + """ + self.simulation_id = simulation_id + super().__init__() + + def get_chart_data(self): # noqa: D102, ANN201 + return calculations.detailed_overview(simulation_id=self.simulation_id) + + def render(self) -> dict: # noqa: D102 + for item in self.chart_options["series"]: + profile = config.SIMULATION_NAME_MAPPING[item["name"]] + item["data"][1] = self.chart_data[profile] + + return self.chart_options + + +class CTSOverviewChart(Chart): + """CTS Overview Chart. Shows greenhouse gas emissions.""" + + lookup = "ghg_overview" + + def __init__(self, simulation_id: int) -> None: + """ + Init CTS Overview Chart. + + Parameters + ---------- + simulation_id: any + id of used Simulation + """ + self.simulation_id = simulation_id + super().__init__() + + def get_chart_data(self): # noqa: D102, ANN201 + return calculations.detailed_overview(simulation_id=self.simulation_id) + + def render(self) -> dict: # noqa: D102 + for item in self.chart_options["series"]: + profile = config.SIMULATION_NAME_MAPPING[item["name"]] + item["data"][2] = self.chart_data[profile] + + return self.chart_options + + +class ElectricityOverviewChart(Chart): + """Chart for electricity overview.""" + + lookup = "electricity_overview" + + def __init__(self, simulation_id: int) -> None: + """Store simulation ID.""" + self.simulation_id = simulation_id + super().__init__() + + def get_chart_data(self): # noqa: ANN201 + """Get chart data from electricity overview calculation.""" + return calculations.electricity_overview(simulation_id=self.simulation_id) + + def render(self) -> dict: + """Overwrite render function.""" + self.chart_options["series"][0]["data"][2] = self.chart_data["ABW-wind-onshore"] + self.chart_options["series"][1]["data"][2] = self.chart_data["ABW-solar-pv_ground"] + self.chart_options["series"][2]["data"][2] = self.chart_data["ABW-solar-pv_rooftop"] + self.chart_options["series"][3]["data"][2] = self.chart_data["ABW-biomass"] + self.chart_options["series"][4]["data"][2] = self.chart_data["ABW-hydro-ror"] + self.chart_options["series"][5]["data"][0] = self.chart_data["ABW-electricity-demand_cts"] + self.chart_options["series"][6]["data"][0] = self.chart_data["electricity_heat_demand_cts"] + self.chart_options["series"][7]["data"][0] = self.chart_data["ABW-electricity-demand_hh"] + self.chart_options["series"][8]["data"][0] = self.chart_data["electricity_heat_demand_hh"] + self.chart_options["series"][9]["data"][0] = self.chart_data["ABW-electricity-demand_ind"] + self.chart_options["series"][10]["data"][0] = self.chart_data["electricity_heat_demand_ind"] + self.chart_options["series"][11]["data"][0] = self.chart_data["ABW-electricity-bev_charging"] + return self.chart_options + + +class ElectricityCTSChart(Chart): + """Electricity CTS Chart. Shows greenhouse gas emissions.""" + + lookup = "electricity_ghg" + + def __init__(self, simulation_id: int) -> None: + """ + Init Electricity CTS Chart. + + Parameters + ---------- + simulation_id: any + id of used Simulation + """ + self.simulation_id = simulation_id + super().__init__() + + def get_chart_data(self): # noqa: D102, ANN201 + return calculations.detailed_overview(simulation_id=self.simulation_id) + + def render(self) -> dict: # noqa: D102 + for item in self.chart_options["series"]: + profile = config.SIMULATION_NAME_MAPPING[item["name"]] + item["data"][2] = self.chart_data[profile] + + return self.chart_options + + +class HeatOverviewChart(Chart): + """Heat Overview Chart.""" + + lookup = "overview_heat" + + def __init__(self, simulation_id: int) -> None: + """ + Init Heat Overview Chart. + + Parameters + ---------- + simulation_id: any + id of used Simulation + """ + self.simulation_id = simulation_id + super().__init__() + + def get_chart_data(self): # noqa: D102, ANN201 + return calculations.heat_overview(simulation_id=self.simulation_id) + + def render(self) -> dict: # noqa: D102 + for item in self.chart_options["series"]: + profile = config.SIMULATION_NAME_MAPPING[item["name"]] + item["data"][1] = self.chart_data[profile] + + return self.chart_options + + +class HeatProductionChart(Chart): + """Heat Production Chart. Shows decentralized and centralized heat.""" + + lookup = "decentralized_centralized_heat" + + def __init__(self, simulation_id: int) -> None: + """ + Init Heat Production Chart. + + Parameters + ---------- + simulation_id: any + id of used Simulation + """ + self.simulation_id = simulation_id + super().__init__() + + def get_chart_data(self): # noqa: D102, ANN201 + return calculations.heat_overview(simulation_id=self.simulation_id) + + def render(self) -> dict: # noqa: D102 + for item in self.chart_options["series"]: + profile = config.SIMULATION_NAME_MAPPING[item["name"]] + item["data"][1] = self.chart_data[profile] + + return self.chart_options + + +class MobilityOverviewChart(Chart): + """Mobility Overview Chart. Shows Number of Cars.""" + + lookup = "mobility_overview" + + def __init__(self, simulation_id: int) -> None: + """ + Init Mobility Overview Chart. + + Parameters + ---------- + simulation_id: any + id of used Simulation + """ + self.simulation_id = simulation_id + super().__init__() + + def get_chart_data(self): # noqa: D102, ANN201 + return calculations.heat_overview(simulation_id=self.simulation_id) + + def render(self) -> dict: # noqa: D102 + for item in self.chart_options["series"]: + profile = config.SIMULATION_NAME_MAPPING[item["name"]] + item["data"][1] = self.chart_data[profile] + + return self.chart_options + + +class MobilityCTSChart(Chart): + """Mobility CTS Chart. Shows greenhouse gas emissions.""" + + lookup = "mobility_ghg" + + def __init__(self, simulation_id: int) -> None: + """ + Init Mobility CTS Chart. + + Parameters + ---------- + simulation_id: any + id of used Simulation + """ + self.simulation_id = simulation_id + super().__init__() + + def get_chart_data(self): # noqa: D102, ANN201 + return calculations.detailed_overview(simulation_id=self.simulation_id) + + def render(self) -> dict: # noqa: D102 + for item in self.chart_options["series"]: + profile = config.SIMULATION_NAME_MAPPING[item["name"]] + item["data"][2] = self.chart_data[profile] + + return self.chart_options + + +class GhgHistoryChart(Chart): + """GHG history chart.""" + + lookup = "ghg_history" + + def __init__(self, simulation_id: int) -> None: + """ + Init GHG history chart. + + Parameters + ---------- + simulation_id: any + id of used Simulation + """ + self.simulation_id = simulation_id + super().__init__() + + def get_chart_data(self): # noqa: D102, ANN201 + # TODO(Hendrik): Get static data from digipipe datapackage # noqa: TD003 + return pd.DataFrame() + + def render(self) -> dict: # noqa: D102 + for item in self.chart_options["series"]: + profile = config.SIMULATION_NAME_MAPPING[item["name"]] + item["data"][1] = self.chart_data[profile] + + return self.chart_options + + +class GhgReductionChart(Chart): + """GHG reduction chart.""" + + lookup = "ghg_reduction" + + def __init__(self, simulation_id: int) -> None: + """ + Init GHG reduction chart. + + Parameters + ---------- + simulation_id: any + id of used Simulation + """ + self.simulation_id = simulation_id + super().__init__() + + def get_chart_data(self): # noqa: D102, ANN201 + # TODO(Hendrik): Get static data (1st column) from # noqa: TD003 + # digipipe datapackage + # and calc reductions for 2nd column. TD003 + return pd.DataFrame() + + def render(self) -> dict: # noqa: D102 + for item in self.chart_options["series"]: + profile = config.SIMULATION_NAME_MAPPING[item["name"]] + item["data"][1] = self.chart_data[profile] + + return self.chart_options + + +class GhgHistoryChart(Chart): + """GHG history chart.""" + + lookup = "ghg_history" + + def __init__(self, simulation_id: int) -> None: + """ + Init GHG history chart. + + Parameters + ---------- + simulation_id: any + id of used Simulation + """ + self.simulation_id = simulation_id + super().__init__() + + def get_chart_data(self): # noqa: D102, ANN201 + # TODO(Hendrik): Get static data from digipipe datapackage # noqa: TD003 + return pd.DataFrame() + + def render(self) -> dict: # noqa: D102 + for item in self.chart_options["series"]: + profile = config.SIMULATION_NAME_MAPPING[item["name"]] + item["data"][1] = self.chart_data[profile] + + return self.chart_options + + +class GhgReductionChart(Chart): + """GHG reduction chart.""" + + lookup = "ghg_reduction" + + def __init__(self, simulation_id: int) -> None: + """ + Init GHG reduction chart. + + Parameters + ---------- + simulation_id: any + id of used Simulation + """ + self.simulation_id = simulation_id + super().__init__() + + def get_chart_data(self): # noqa: D102, ANN201 + # TODO(Hendrik): Get static data (1st column) from # noqa: TD003 + # digipipe datapackage + # and calc reductions for 2nd column. TD003 + return pd.DataFrame() + + def render(self) -> dict: # noqa: D102 + for item in self.chart_options["series"]: + profile = config.SIMULATION_NAME_MAPPING[item["name"]] + item["data"][1] = self.chart_data[profile] + + return self.chart_options + + +class PopulationRegionChart(Chart): + """Chart for regional population.""" + + lookup = "population" + + def get_chart_data(self) -> None: + """Calculate population for whole region.""" + return models.Population.quantity_per_municipality_per_year().sum() + + def get_chart_options(self) -> dict: + """Overwrite title and unit.""" + chart_options = super().get_chart_options() + del chart_options["title"]["text"] + return chart_options + + +class PopulationDensityRegionChart(Chart): + """Chart for regional population density.""" + + lookup = "population" + + def get_chart_data(self) -> None: + """Calculate capacities for whole region.""" + return calculations.calculate_square_for_value( + pd.DataFrame(models.Population.quantity_per_municipality_per_year().sum()).transpose(), + ).sum() + + def get_chart_options(self) -> dict: + """Overwrite title and unit.""" + chart_options = super().get_chart_options() + del chart_options["title"]["text"] + chart_options["yAxis"]["name"] = _("EW/km²") + return chart_options + + +class EmployeesRegionChart(Chart): + """Chart for regional employees.""" + + lookup = "wind_turbines" + + def get_chart_data(self) -> list: + """Calculate population for whole region.""" + return [int(calculations.employment_per_municipality().sum())] + + def get_chart_options(self) -> dict: + """Overwrite title and unit.""" + chart_options = super().get_chart_options() + del chart_options["title"]["text"] + chart_options["yAxis"]["name"] = _("") + del chart_options["series"][0]["name"] + return chart_options + + +class CompaniesRegionChart(Chart): + """Chart for regional companies.""" + + lookup = "wind_turbines" + + def get_chart_data(self) -> list: + """Calculate population for whole region.""" + return [int(calculations.companies_per_municipality().sum())] + + def get_chart_options(self) -> dict: + """Overwrite title and unit.""" + chart_options = super().get_chart_options() + del chart_options["title"]["text"] + chart_options["yAxis"]["name"] = _("") + del chart_options["series"][0]["name"] + return chart_options + + +class CapacityRegionChart(Chart): + """Chart for regional capacities.""" + + lookup = "capacity" + + def get_chart_data(self) -> None: + """Calculate capacities for whole region.""" + return calculations.capacities_per_municipality().sum() + + def get_chart_options(self) -> dict: + """Overwrite title and unit.""" + chart_options = super().get_chart_options() + del chart_options["title"]["text"] + return chart_options + + +class CapacitySquareRegionChart(Chart): + """Chart for regional capacities per square meter.""" + + lookup = "capacity" + + def get_chart_data(self) -> None: + """Calculate capacities for whole region.""" + return calculations.calculate_square_for_value( + pd.DataFrame(calculations.capacities_per_municipality().sum()).transpose(), + ).sum() + + def get_chart_options(self) -> dict: + """Overwrite title and unit.""" + chart_options = super().get_chart_options() + del chart_options["title"]["text"] + chart_options["yAxis"]["name"] = _("MW") + return chart_options + + +class EnergyRegionChart(Chart): + """Chart for regional energy.""" + + lookup = "capacity" + + def get_chart_data(self) -> None: + """Calculate capacities for whole region.""" + return calculations.energies_per_municipality().sum() + + def get_chart_options(self) -> dict: + """Overwrite title and unit.""" + chart_options = super().get_chart_options() + del chart_options["title"]["text"] + chart_options["yAxis"]["name"] = _("GWh") + return chart_options + + +class Energy2045RegionChart(SimulationChart): + """Chart for regional energy.""" + + lookup = "capacity" + + def get_chart_data(self) -> None: + """Calculate capacities for whole region.""" + status_quo_data = calculations.energies_per_municipality().sum() + future_data = calculations.energies_per_municipality_2045(self.simulation_id).sum() + return list(zip(status_quo_data, future_data)) + + def get_chart_options(self) -> dict: + """Overwrite title and unit.""" + chart_options = super().get_chart_options() + del chart_options["title"]["text"] + chart_options["yAxis"]["name"] = _("MWh") + chart_options["xAxis"]["data"] = ["Status Quo", "Mein Szenario"] + return chart_options + + +class EnergyShareRegionChart(Chart): + """Chart for regional energy shares.""" + + lookup = "capacity" + + def get_chart_data(self) -> None: + """Calculate capacities for whole region.""" + return calculations.energy_shares_per_municipality().sum() + + def get_chart_options(self) -> dict: + """Overwrite title and unit.""" + chart_options = super().get_chart_options() + del chart_options["title"]["text"] + chart_options["yAxis"]["name"] = _("%") + return chart_options + + +class EnergyCapitaRegionChart(Chart): + """Chart for regional energy shares per capita.""" + + lookup = "capacity" + + def get_chart_data(self) -> None: + """Calculate capacities for whole region.""" + return ( + calculations.calculate_capita_for_value( + pd.DataFrame(calculations.energies_per_municipality().sum()).transpose(), + ).sum() + * 1e3 + ) + + def get_chart_options(self) -> dict: + """Overwrite title and unit.""" + chart_options = super().get_chart_options() + del chart_options["title"]["text"] + chart_options["yAxis"]["name"] = _("MWh") + return chart_options + + +class EnergySquareRegionChart(Chart): + """Chart for regional energy shares per square meter.""" + + lookup = "capacity" + + def get_chart_data(self) -> None: + """Calculate capacities for whole region.""" + return ( + calculations.calculate_square_for_value( + pd.DataFrame(calculations.energies_per_municipality().sum()).transpose(), + ).sum() + * 1e3 + ) + + def get_chart_options(self) -> dict: + """Overwrite title and unit.""" + chart_options = super().get_chart_options() + del chart_options["title"]["text"] + chart_options["yAxis"]["name"] = _("MWh") + return chart_options + + +class WindTurbinesRegionChart(Chart): + """Chart for regional wind turbines.""" + + lookup = "wind_turbines" + + def get_chart_data(self) -> list[int]: + """Calculate population for whole region.""" + return [int(models.WindTurbine.quantity_per_municipality().sum())] + + def get_chart_options(self) -> dict: + """Overwrite title and unit.""" + chart_options = super().get_chart_options() + del chart_options["title"]["text"] + return chart_options + + +class WindTurbinesSquareRegionChart(Chart): + """Chart for regional wind turbines per square meter.""" + + lookup = "wind_turbines" + + def get_chart_data(self) -> list[float]: + """Calculate population for whole region.""" + return [ + float( + calculations.calculate_square_for_value( + pd.DataFrame({"turbines": models.WindTurbine.quantity_per_municipality().sum()}, index=[1]), + ).sum(), + ), + ] + + def get_chart_options(self) -> dict: + """Overwrite title and unit.""" + chart_options = super().get_chart_options() + del chart_options["title"]["text"] + chart_options["yAxis"]["name"] = _("") + return chart_options + + +class ElectricityDemandRegionChart(Chart): + """Chart for regional electricity demand.""" + + lookup = "electricity_demand" + + def get_chart_data(self) -> None: + """Calculate capacities for whole region.""" + return calculations.electricity_demand_per_municipality().sum() + + def get_chart_options(self) -> dict: + """Overwrite title and unit.""" + chart_options = super().get_chart_options() + del chart_options["title"]["text"] + chart_options["yAxis"]["name"] = _("GWh") + return chart_options + + +class ElectricityDemandCapitaRegionChart(Chart): + """Chart for regional electricity demand per population.""" + + lookup = "electricity_demand" + + def get_chart_data(self) -> pd.DataFrame: + """Calculate capacities for whole region.""" + return ( + calculations.calculate_capita_for_value( + pd.DataFrame(calculations.electricity_demand_per_municipality().sum()).transpose(), + ).sum() + * 1e6 + ) + + def get_chart_options(self) -> dict: + """Overwrite title and unit.""" + chart_options = super().get_chart_options() + del chart_options["title"]["text"] + chart_options["yAxis"]["name"] = _("kWh") + return chart_options + + +class HeatDemandRegionChart(Chart): + """Chart for regional heat demand.""" + + lookup = "heat_demand" + + def get_chart_data(self) -> None: + """Calculate capacities for whole region.""" + return calculations.heat_demand_per_municipality().sum() + + def get_chart_options(self) -> dict: + """Overwrite title and unit.""" + chart_options = super().get_chart_options() + del chart_options["title"]["text"] + chart_options["yAxis"]["name"] = _("GWh") + return chart_options + + +class HeatDemandCapitaRegionChart(Chart): + """Chart for regional heat demand per population.""" + + lookup = "heat_demand" + + def get_chart_data(self) -> None: + """Calculate capacities for whole region.""" + return ( + calculations.calculate_capita_for_value( + pd.DataFrame(calculations.heat_demand_per_municipality().sum()).transpose(), + ).sum() + * 1e6 + ) + + def get_chart_options(self) -> dict: + """Overwrite title and unit.""" + chart_options = super().get_chart_options() + del chart_options["title"]["text"] + chart_options["yAxis"]["name"] = _("kWh") + return chart_options + + +class BatteriesRegionChart(Chart): + """Chart for regional battery count.""" + + lookup = "wind_turbines" + + def get_chart_data(self) -> list: + """Calculate population for whole region.""" + return [int(calculations.batteries_per_municipality().sum())] + + def get_chart_options(self) -> dict: + """Overwrite title and unit.""" + chart_options = super().get_chart_options() + del chart_options["title"]["text"] + chart_options["yAxis"]["name"] = _("#") + del chart_options["series"][0]["name"] + return chart_options + + +class BatteriesCapacityRegionChart(Chart): + """Chart for regional battery capacity.""" + + lookup = "wind_turbines" + + def get_chart_data(self) -> list: + """Calculate population for whole region.""" + return [int(calculations.battery_capacities_per_municipality().sum())] + + def get_chart_options(self) -> dict: + """Overwrite title and unit.""" + chart_options = super().get_chart_options() + del chart_options["title"]["text"] + chart_options["yAxis"]["name"] = _("#") + del chart_options["series"][0]["name"] + return chart_options + + +CHARTS: dict[str, type[Chart]] = { + "electricity_overview": ElectricityOverviewChart, + "heat_overview": HeatOverviewChart, + "population_statusquo_region": PopulationRegionChart, + "population_density_statusquo_region": PopulationDensityRegionChart, + "employees_statusquo_region": EmployeesRegionChart, + "companies_statusquo_region": CompaniesRegionChart, + "capacity_statusquo_region": CapacityRegionChart, + "capacity_square_statusquo_region": CapacitySquareRegionChart, + "energy_statusquo_region": EnergyRegionChart, + "energy_2045_region": Energy2045RegionChart, + "energy_share_statusquo_region": EnergyShareRegionChart, + "energy_capita_statusquo_region": EnergyCapitaRegionChart, + "energy_square_statusquo_region": EnergySquareRegionChart, + "wind_turbines_statusquo_region": WindTurbinesRegionChart, + "wind_turbines_square_statusquo_region": WindTurbinesSquareRegionChart, + "electricity_demand_statusquo_region": ElectricityDemandRegionChart, + "electricity_demand_capita_statusquo_region": ElectricityDemandCapitaRegionChart, + "heat_demand_statusquo_region": HeatDemandRegionChart, + "heat_demand_capita_statusquo_region": HeatDemandCapitaRegionChart, + "batteries_statusquo_region": BatteriesRegionChart, + "batteries_capacity_statusquo_region": BatteriesCapacityRegionChart, +} + + +def create_chart(lookup: str, chart_data: Optional[Any] = None) -> dict: """ - chart = get_chart_options(lookup) - if chart_data: - series_type = chart["series"][0]["type"] - series_length = len(chart["series"]) - if series_type == "line": - data = [] - for key, value in chart_data: - year_as_string = f"{key}" - data.append([year_as_string, value]) - chart["series"][0]["data"] = data - elif series_length > 1: - for i in range(0, series_length): - chart["series"][i]["data"] = chart_data[i] - else: - chart["series"][0]["data"] = chart_data - - return chart - - -def merge_dicts(dict1: dict, dict2: dict) -> dict: - """ - Recursively merge two dictionaries. - - Parameters - ---------- - dict1: dict - Containing the first chart structure. Objects will be first. - dict2: dict - Containing the second chart structure. Objects will be last and - if they have the same name as ones from dict1 they overwrite the ones in first. - - Returns - ------- - dict - First chart modified and appended by second chart. + Return chart for given lookup. + + If chart is listed in CHARTS, specific chart is returned. Otherwise, generic chart is returned. """ - for key in dict2: - if key in dict1 and isinstance(dict1[key], dict) and isinstance(dict2[key], dict): - merge_dicts(dict1[key], dict2[key]) - elif key in dict1 and isinstance(dict1[key], list) and isinstance(dict2[key], list): - dict1[key].extend(dict2[key]) - else: - dict1[key] = dict2[key] - return dict1 + if lookup in CHARTS: + return CHARTS[lookup](lookup, chart_data).render() + return Chart(lookup, chart_data).render() diff --git a/digiplan/map/charts/capacity.json b/digiplan/map/charts/capacity.json index 209e75ca..7f237570 100644 --- a/digiplan/map/charts/capacity.json +++ b/digiplan/map/charts/capacity.json @@ -7,20 +7,21 @@ "legend": { "data": [ "Wind", - "Freiflächen-PV", "Aufdach-PV", + "Freiflächen-PV", + "Wasserkraft", "Bioenergie", - "Konventionell" + "Speicher" ], "orient": "vertical", - "right": "1%", - "bottom": "5%" + "right": "-5%", + "bottom": "15%" }, - "color": ["#1F82C0","#F6B93B","#FFD660","#98D47E","#CFCFCF"], + "color": ["#6A89CC","#FFD660","#EFAD25","#A9BDE8","#52C41A","#8D2D5F"], "xAxis": { "type": "category", "boundaryGap": true, - "data": ["Status Quo", "2045"], + "data": ["Status Quo"], "axisTick": "alignWithLabel" }, "yAxis": { @@ -34,32 +35,38 @@ { "type": "bar", "stack": "five", - "data": [36, 10], + "data": [36], "name": "Wind" }, { "type": "bar", "stack": "five", - "data": [36, 10], + "data": [10], + "name": "Aufdach-PV" + }, + { + "type": "bar", + "stack": "five", + "data": [36], "name": "Freiflächen-PV" }, { "type": "bar", "stack": "five", - "data": [10, 10], - "name": "Aufdach-PV" + "data": [10], + "name": "Wasserkraft" }, { "type": "bar", "stack": "five", - "data": [10, 10], + "data": [10], "name": "Bioenergie" }, { "type": "bar", "stack": "five", - "data": [10, 10], - "name": "Konventionell" + "data": [10], + "name": "Speicher" } ], "title": {"text": "installed capacity of different types"} diff --git a/digiplan/map/charts/capacity_square.json b/digiplan/map/charts/capacity_square.json deleted file mode 100644 index 5c693d20..00000000 --- a/digiplan/map/charts/capacity_square.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "grid": { - "top": "24%", - "right": "30%", - "bottom": "5%" - }, - "legend": { - "data": [ - "Wind", - "Freiflächen-PV", - "Aufdach-PV", - "Bioenergie", - "Konventionell" - ], - "orient": "vertical", - "right": "1%", - "bottom": "5%" - }, - "color": ["#1F82C0","#F6B93B","#FFD660","#98D47E","#CFCFCF"], - "xAxis": { - "type": "category", - "boundaryGap": true, - "data": ["Status Quo", "2045"], - "axisTick": "alignWithLabel" - }, - "yAxis": { - "type": "value", - "show": true, - "name": "MW", - "nameLocation": "end", - "nameTextStyle": "Roboto" - }, - "series": [ - { - "type": "bar", - "stack": "five", - "data": [36, 10], - "name": "Wind" - }, - { - "type": "bar", - "stack": "five", - "data": [36, 10], - "name": "Freiflächen-PV" - }, - { - "type": "bar", - "stack": "five", - "data": [10, 10], - "name": "Aufdach-PV" - }, - { - "type": "bar", - "stack": "five", - "data": [10, 10], - "name": "Bioenergie" - }, - { - "type": "bar", - "stack": "five", - "data": [10, 10], - "name": "Konventionell" - } - ], - "title": {"text": "installed capacity per km² in different types"} -} diff --git a/digiplan/map/charts/decentralized_centralized_heat.json b/digiplan/map/charts/decentralized_centralized_heat.json index 493bf9e1..84cf4117 100644 --- a/digiplan/map/charts/decentralized_centralized_heat.json +++ b/digiplan/map/charts/decentralized_centralized_heat.json @@ -1,18 +1,9 @@ { + "grid": { + "bottom": "28%" + }, "legend": { - "data": [ - "Kohleofen", - "Holzofen", - "Gasheizkessel", - "Solarthermiekollektor", - "Wärmepumpe", - "Thermische Energiespeicher", - "Große Gasheizkessel", - "Große Wärmepumpen", - "Power to Heat", - "Große Solarthermiekollektoren" - ], - "bottom": "12" + "bottom": "0" }, "brush": { "toolbox": ["rect", "polygon", "lineX", "lineY", "keep", "clear"], @@ -27,7 +18,7 @@ } }, "yAxis": { - "data": ["Status Quo", "Mein Szenario", "Ziel Szenario"], + "data": ["Ziel", "Mein Szenario", "Status Quo"], "axisLine": { "onZero": true }, "splitLine": { "show": false }, "splitArea": { "show": false } @@ -36,7 +27,7 @@ "type": "value", "show": true, "position": "bottom", - "name": "TWh", + "name": "GWh", "nameLocation": "end", "nameTextStyle": "Roboto", "width": "76", @@ -50,7 +41,7 @@ "type": "bar", "barWidth": "16", "stack": "five", - "color": "#604F4F", + "color": "#FFF7EC", "emphasis": { "itemStyle": { "shadowBlur": 10, @@ -63,7 +54,7 @@ "name": "Holzofen", "type": "bar", "stack": "five", - "color": "#610B0B", + "color": "#FEE8C8", "emphasis": { "itemStyle": { "shadowBlur": 10, @@ -76,7 +67,7 @@ "name": "Gasheizkessel", "type": "bar", "stack": "five", - "color": "#B1BEC6", + "color": "#FDD49E", "emphasis": { "itemStyle": { "shadowBlur": 10, @@ -89,7 +80,7 @@ "name": "Wärmepumpe", "type": "bar", "stack": "five", - "color": "#E6772E", + "color": "#FDBB84", "emphasis": { "itemStyle": { "shadowBlur": 10, @@ -99,23 +90,10 @@ "data": [136, 134, 130] }, { - "name": "Thermische Energiespeicher", - "type": "bar", - "stack": "five", - "color": "#DF3A01", - "emphasis": { - "itemStyle": { - "shadowBlur": 10, - "shadowColor": "rgba(0,0,0,0.3)" - } - }, - "data": [280, 150, 100] - }, - { - "name": "Solarthermiekollektor", + "name": "Solarthermie", "type": "bar", "stack": "five", - "color": "#FFD660", + "color": "#EF6548", "emphasis": { "itemStyle": { "shadowBlur": 10, @@ -125,10 +103,11 @@ "data": [412, 254, 100] }, { - "name": "Große Gasheizkessel", + "name": "FW: Gasheizkessel", "type": "bar", + "barWidth": "16", "stack": "four", - "color": "#B1BEC6", + "color": "#D7301F", "emphasis": { "itemStyle": { "shadowBlur": 10, @@ -138,10 +117,10 @@ "data": [300, 344, 380] }, { - "name": "Große Wärmepumpen", + "name": "FW: Wärmepumpen", "type": "bar", "stack": "four", - "color": "#E6772E", + "color": "#B30000", "emphasis": { "itemStyle": { "shadowBlur": 10, @@ -155,7 +134,7 @@ "type": "bar", "barWidth": "35", "stack": "four", - "color": "#D7DF01", + "color": "#7F0000", "emphasis": { "itemStyle": { "shadowBlur": 10, @@ -165,10 +144,10 @@ "data": [280, 300, 350] }, { - "name": "Große Solarthermiekollektoren", + "name": "FW: Solarthermie", "type": "bar", "stack": "four", - "color": "#FFD660", + "color": "#3F0000", "emphasis": { "itemStyle": { "shadowBlur": 10, diff --git a/digiplan/map/charts/detailed_overview.json b/digiplan/map/charts/detailed_overview.json index 26c3b3af..09490896 100644 --- a/digiplan/map/charts/detailed_overview.json +++ b/digiplan/map/charts/detailed_overview.json @@ -1,18 +1,4 @@ { - "legend": { - "data": [ - "Konventionell", - "Wind", - "Freiflächen-PV", - "Aufdach-PV", - "Bioenergie", - "GHD", - "Haushalt", - "Industrie", - "Sonstiges" - ], - "bottom": "12" - }, "brush": { "toolbox": ["rect", "polygon", "lineX", "lineY", "keep", "clear"], "xAxisIndex": 0 @@ -30,11 +16,11 @@ "top": "10%", "left": "3%", "right": "15%", - "bottom": "10%", + "bottom": "20%", "containLabel": true }, "yAxis": { - "data": ["Status Quo", "Mein Szenario", "Ziel Szenario"], + "data": ["Ziel 2045", "Mein Szenario", "Status Quo"], "axisLine": { "onZero": true }, "splitLine": { "show": false }, "splitArea": { "show": false } @@ -53,76 +39,77 @@ }, "series": [ { - "name": "Konventionell", + "name": "Windenergie", "type": "bar", - "barWidth": "16", - "stack": "five", - "color": "#CFCFCF", + "stack": "production", + "color": "#6A89CC", "emphasis": { "itemStyle": { "shadowBlur": 10, "shadowColor": "rgba(0,0,0,0.3)" } }, - "data": [132, 334, 700] + "data": [302, 234, 230] }, { - "name": "Wind", + "name": "Freiflächen-PV", "type": "bar", - "stack": "five", - "color": "#1F82C0", + "stack": "production", + "color": "#EFAD25", "emphasis": { "itemStyle": { "shadowBlur": 10, "shadowColor": "rgba(0,0,0,0.3)" } }, - "data": [302, 234, 230] + "data": [282, 234, 100] }, { - "name": "Freiflächen-PV", + "name": "Aufdach-PV", "type": "bar", - "stack": "five", - "color": "#F6B93B", + "stack": "production", + "color": "#FFD660", "emphasis": { "itemStyle": { "shadowBlur": 10, "shadowColor": "rgba(0,0,0,0.3)" } }, - "data": [282, 234, 100] + "data": [312, 254, 100] }, { - "name": "Aufdach-PV", + "name": "Bioenergie", "type": "bar", - "stack": "five", - "color": "#FFD660", + "stack": "production", + "color": "#52C41A", "emphasis": { "itemStyle": { "shadowBlur": 10, "shadowColor": "rgba(0,0,0,0.3)" } }, - "data": [312, 254, 100] + "data": [136, 134, 130] }, { - "name": "Bioenergie", + "name": "Wasserkraft", "type": "bar", - "stack": "five", - "color": "#98D47E", + "barWidth": "16", + "stack": "production", + "color": "#A9BDE8", "emphasis": { "itemStyle": { "shadowBlur": 10, "shadowColor": "rgba(0,0,0,0.3)" } }, - "data": [136, 134, 130] + "data": [132, 334, 700] }, { - "name": "GHD", + "name": "Stromverbrauch GHD", "type": "bar", - "stack": "four", - "color": "#F5F5DC", + "barWidth": "16", + "stack": "demand", + "color": "#D3D3D3", "emphasis": { "itemStyle": { "shadowBlur": 10, @@ -132,10 +119,10 @@ "data": [300, 344, 380] }, { - "name": "Haushalte", + "name": "Stromverbrauch Haushalte", "type": "bar", - "stack": "four", - "color": "#A8DADC", + "stack": "demand", + "color": "#969696", "emphasis": { "itemStyle": { "shadowBlur": 10, @@ -145,11 +132,11 @@ "data": [254, 244, 380] }, { - "name": "Industrie", + "name": "Stromverbrauch Industrie", "type": "bar", "barWidth": "35", - "stack": "four", - "color": "#C27BA0", + "stack": "demand", + "color": "#616161", "emphasis": { "itemStyle": { "shadowBlur": 10, @@ -159,10 +146,10 @@ "data": [280, 300, 350] }, { - "name": "Sonstiges", + "name": "BEV", "type": "bar", - "stack": "four", - "color": "#B0BEC5", + "stack": "demand", + "color": "#2F2F2F", "emphasis": { "itemStyle": { "shadowBlur": 10, diff --git a/digiplan/map/charts/elecricity_demand_capita.json b/digiplan/map/charts/elecricity_demand_capita.json deleted file mode 100644 index 29479a39..00000000 --- a/digiplan/map/charts/elecricity_demand_capita.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "grid": { - "top": "24%", - "right": "30%", - "bottom": "5%" - }, - "legend": { - "data": [ - "GHD", - "Haushalte", - "Industrie", - "Sonstiges" - ], - "orient": "vertical", - "right": "1%", - "bottom": "5%" - }, - "color": ["#F5F5DC", "#A8DADC", "#C27BA0", "#B0BEC5"], - "xAxis": { - "type": "category", - "boundaryGap": true, - "data": ["Status Quo", "2045"], - "axisTick": "alignWithLabel" - }, - "yAxis": { - "type": "value", - "show": true, - "name": "GWh", - "nameLocation": "end", - "nameTextStyle": "Roboto" - }, - "series": [ - { - "type": "bar", - "stack": "five", - "data": [36, 10], - "name": "GHD" - }, - { - "type": "bar", - "stack": "five", - "data": [36, 10], - "name": "Haushalte" - }, - { - "type": "bar", - "stack": "five", - "data": [10, 10], - "name": "Industrie" - }, - { - "type": "bar", - "stack": "five", - "data": [10, 10], - "name": "Sonstiges" - } - ], - "title": { "text": "Electricity Demand per Capita" } - } diff --git a/digiplan/map/charts/electricity_demand.json b/digiplan/map/charts/electricity_demand.json index ac44ebe4..cb66df8b 100644 --- a/digiplan/map/charts/electricity_demand.json +++ b/digiplan/map/charts/electricity_demand.json @@ -2,24 +2,23 @@ "grid": { "top": "24%", "right": "30%", - "bottom": "5%" + "bottom": "25%" }, "legend": { "data": [ - "GHD", "Haushalte", - "Industrie", - "Sonstiges" + "GHD", + "Industrie" ], "orient": "vertical", "right": "1%", - "bottom": "5%" + "bottom": "35%" }, - "color": ["#F5F5DC", "#A8DADC", "#C27BA0", "#B0BEC5"], + "color": ["#6F9BB2", "#98C1D7", "#376984"], "xAxis": { "type": "category", "boundaryGap": true, - "data": ["Status Quo", "2045"], + "data": ["Status Quo"], "axisTick": "alignWithLabel" }, "yAxis": { @@ -34,25 +33,19 @@ "type": "bar", "stack": "five", "data": [36, 10], - "name": "GHD" + "name": "Haushalte" }, { "type": "bar", "stack": "five", "data": [36, 10], - "name": "Haushalte" + "name": "GHD" }, { "type": "bar", "stack": "five", "data": [10, 10], "name": "Industrie" - }, - { - "type": "bar", - "stack": "five", - "data": [10, 10], - "name": "Sonstiges" } ], "title": { "text": "Electricity Demand" } diff --git a/digiplan/map/charts/electricity_ghg.json b/digiplan/map/charts/electricity_ghg.json index f14a49a5..be8cf73f 100644 --- a/digiplan/map/charts/electricity_ghg.json +++ b/digiplan/map/charts/electricity_ghg.json @@ -1,4 +1,7 @@ { + "grid": { + "bottom": "25%" + }, "xAxis": { "type": "value", "show": true, @@ -24,7 +27,7 @@ "type": "bar", "barWidth": "16", "stack": "total", - "color": "#F5F5DC", + "color": "#98C1D7", "label": { "show": false }, @@ -37,7 +40,7 @@ "name": "Haushalte", "type": "bar", "stack": "total", - "color": "#A8DADC", + "color": "#6F9BB2", "label": { "show": false }, @@ -50,7 +53,7 @@ "name": "Industrie", "type": "bar", "stack": "total", - "color": "#C27BA0", + "color": "#376984", "label": { "show": false }, @@ -63,7 +66,7 @@ "name": "Sonstiges", "type": "bar", "stack": "total", - "color": "#B0BEC5", + "color": "#0D425F", "label": { "show": false }, diff --git a/digiplan/map/charts/electricity_overview.json b/digiplan/map/charts/electricity_overview.json index 2bcbe680..09490896 100644 --- a/digiplan/map/charts/electricity_overview.json +++ b/digiplan/map/charts/electricity_overview.json @@ -1,4 +1,30 @@ { + "brush": { + "toolbox": ["rect", "polygon", "lineX", "lineY", "keep", "clear"], + "xAxisIndex": 0 + }, + "toolbox": { + "feature": { + "magicType": { + "type": ["stack"] + }, + "dataView": {} + } + }, + "tooltip": {}, + "grid": { + "top": "10%", + "left": "3%", + "right": "15%", + "bottom": "20%", + "containLabel": true + }, + "yAxis": { + "data": ["Ziel 2045", "Mein Szenario", "Status Quo"], + "axisLine": { "onZero": true }, + "splitLine": { "show": false }, + "splitArea": { "show": false } + }, "xAxis": { "type": "value", "show": true, @@ -11,131 +37,126 @@ "fontWeight": "300", "fontSize": "14" }, - "yAxis": { - "type": "category", - "data": ["Bedarf", "Ziel", "Mein Szeanrio", "Status Quo"], - "axisTick": { - "show": false - } - }, "series": [ { - "name": "Wind", + "name": "Windenergie", "type": "bar", - "barWidth": "16", - "stack": "total", - "color": "#1F82C0", - "label": { - "show": false - }, + "stack": "production", + "color": "#6A89CC", "emphasis": { - "focus": "series" + "itemStyle": { + "shadowBlur": 10, + "shadowColor": "rgba(0,0,0,0.3)" + } }, - "data": [0, 502, 400, 334] + "data": [302, 234, 230] }, { - "name": "Freiflächen - PV", + "name": "Freiflächen-PV", "type": "bar", - "stack": "total", - "color": "#F6B93B", - "label": { - "show": false - }, + "stack": "production", + "color": "#EFAD25", "emphasis": { - "focus": "series" + "itemStyle": { + "shadowBlur": 10, + "shadowColor": "rgba(0,0,0,0.3)" + } }, - "data": [0, 382, 300, 234] + "data": [282, 234, 100] }, { - "name": "Aufdach - PV", + "name": "Aufdach-PV", "type": "bar", - "stack": "total", + "stack": "production", "color": "#FFD660", - "label": { - "show": false - }, "emphasis": { - "focus": "series" + "itemStyle": { + "shadowBlur": 10, + "shadowColor": "rgba(0,0,0,0.3)" + } }, - "data": [0, 312, 280, 254] + "data": [312, 254, 100] }, { "name": "Bioenergie", "type": "bar", - "stack": "total", - "color": "#98D47E", - "label": { - "show": false - }, + "stack": "production", + "color": "#52C41A", "emphasis": { - "focus": "series" + "itemStyle": { + "shadowBlur": 10, + "shadowColor": "rgba(0,0,0,0.3)" + } }, - "data": [0, 136, 135, 134] + "data": [136, 134, 130] }, { - "name": "Konventionell", + "name": "Wasserkraft", "type": "bar", - "stack": "total", - "color": "#1A1A1A", - "label": { - "show": false - }, + "barWidth": "16", + "stack": "production", + "color": "#A9BDE8", "emphasis": { - "focus": "series" + "itemStyle": { + "shadowBlur": 10, + "shadowColor": "rgba(0,0,0,0.3)" + } }, - "data": [0, 132, 200, 534] + "data": [132, 334, 700] }, { - "name": "GHD", + "name": "Stromverbrauch GHD", "type": "bar", - "stack": "total", - "color": "#F5F5DC", - "label": { - "show": false - }, + "barWidth": "16", + "stack": "demand", + "color": "#D3D3D3", "emphasis": { - "focus": "series" + "itemStyle": { + "shadowBlur": 10, + "shadowColor": "rgba(0,0,0,0.3)" + } }, - "data": [400, 0, 0, 0] + "data": [300, 344, 380] }, { - "name": "Haushalte", + "name": "Stromverbrauch Haushalte", "type": "bar", - "stack": "total", - "color": "#A8DADC", - "label": { - "show": false - }, + "stack": "demand", + "color": "#969696", "emphasis": { - "focus": "series" + "itemStyle": { + "shadowBlur": 10, + "shadowColor": "rgba(0,0,0,0.3)" + } }, - "data": [360, 0, 0, 0] + "data": [254, 244, 380] }, { - "name": "Industrie", + "name": "Stromverbrauch Industrie", "type": "bar", - "stack": "total", - "color": "#C27BA0", - "label": { - "show": false - }, + "barWidth": "35", + "stack": "demand", + "color": "#616161", "emphasis": { - "focus": "series" + "itemStyle": { + "shadowBlur": 10, + "shadowColor": "rgba(0,0,0,0.3)" + } }, - "data": [300, 0, 0, 0] + "data": [280, 300, 350] }, { - "name": "Sonstiges", + "name": "BEV", "type": "bar", - "stack": "total", - "color": "#B0BEC5", - "label": { - "show": false - }, + "stack": "demand", + "color": "#2F2F2F", "emphasis": { - "focus": "series" + "itemStyle": { + "shadowBlur": 10, + "shadowColor": "rgba(0,0,0,0.3)" + } }, - "data": [350, 0, 0, 0] + "data": [145, 144, 180] } ] } diff --git a/digiplan/map/charts/ghg_history.json b/digiplan/map/charts/ghg_history.json new file mode 100644 index 00000000..ffcc1ca9 --- /dev/null +++ b/digiplan/map/charts/ghg_history.json @@ -0,0 +1,229 @@ +{ + "legend": { + "show": true + }, + "tooltip": { + "trigger": "item", + "formatter": "{a}: {c} kt CO₂-Äq." + }, + "yAxis": { + "name": "kt CO₂-Äquivalent" + }, + "xAxis": [ + { + "inverse": false, + "splitLine": { + "show": true + }, + "axisTick": { + "length": 0, + "lineStyle": { + "color": "#ccc" + } + }, + "axisLine": { + "lineStyle": { + "color": "#ccc" + } + }, + "data": [ + "-", + "-" + ] + }, + { + "name": "", + "nameLocation": "start", + "nameTextStyle": { + "fontWeight": "bold" + }, + "position": "bottom", + "offset": 60, + "axisLine": { + "onZero": false, + "show": false + }, + "axisTick": { + "length": 30, + "inside": true, + "lineStyle": { + "color": "#ccc" + } + }, + "axisLabel": { + "inside": true, + "fontWeight": "bold" + }, + "inverse": false, + "data": [ + "1990", + "2019" + ] + }, + { + "name": "", + "nameLocation": "start", + "nameTextStyle": { + "fontWeight": "bold" + }, + "position": "bottom", + "offset": 30, + "axisLine": { + "onZero": false, + "show": false + }, + "axisTick": { + "length": 30, + "inside": true, + "lineStyle": { + "color": "#ccc" + } + }, + "axisLabel": { + "inside": true + }, + "inverse": false, + "data": [ + "Sachsen-Anhalt", + "ABW", + "Sachsen-Anhalt", + "ABW" + ] + } + ], + "series": [ + { + "name": "Energiewirtschaft", + "stack": "st", + "type": "bar", + "color": "#E6772E", + "barWidth":"30", + "barGap": "250%", + "data": [ + 15680, + 11571 + ] + }, + { + "name": "Industrie", + "stack": "st", + "type": "bar", + "color": "#FA9FB5", + "barWidth":"30", + "data": [ + 20519, + 9011 + ] + }, + { + "name": "Verkehr", + "stack": "st", + "type": "bar", + "color": "#6C567B", + "barWidth":"30", + "data": [ + 3779, + 4007 + ] + }, + { + "name": "Gebäude", + "stack": "st", + "type": "bar", + "color": "#A8DADC", + "barWidth":"30", + "data": [ + 13943, + 3983 + ] + }, + { + "name": "Landwirtschaft", + "stack": "st", + "type": "bar", + "color": "#87D068", + "barWidth":"30", + "data": [ + 3275, + 2113 + ] + }, + { + "name": "Abfall und Sonst.", + "stack": "st", + "type": "bar", + "color": "#F5F5DC", + "barWidth":"30", + "data": [ + 1458, + 947 + ] + }, + { + "name": "Energiewirtschaft", + "stack": "abw", + "type": "bar", + "color": "#E6772E", + "barWidth":"30", + "data": [ + 930, + 538 + ] + }, + { + "name": "Industrie", + "stack": "abw", + "type": "bar", + "color": "#FA9FB5", + "barWidth":"30", + "data": [ + 4848, + 2129 + ] + }, + { + "name": "Verkehr", + "stack": "abw", + "type": "bar", + "color": "#6C567B", + "barWidth":"30", + "data": [ + 698, + 672 + ] + }, + { + "name": "Gebäude", + "stack": "abw", + "type": "bar", + "color": "#A8DADC", + "barWidth":"30", + "data": [ + 2434, + 695 + ] + }, + { + "name": "Landwirtschaft", + "type": "bar", + "stack": "abw", + "color": "#87D068", + "barWidth":"30", + "data": [ + 501, + 368 + ] + }, + { + "name": "Abfall und Sonst.", + "type": "bar", + "stack": "abw", + "color": "#F5F5DC", + "barWidth":"30", + "data": [ + 262, + 157 + ] + } + ] +} diff --git a/digiplan/map/charts/ghg_overview.json b/digiplan/map/charts/ghg_overview.json index e951585c..1d201e95 100644 --- a/digiplan/map/charts/ghg_overview.json +++ b/digiplan/map/charts/ghg_overview.json @@ -1,4 +1,7 @@ { + "grid": { + "bottom": "20%" + }, "backgroundColor": "#FFFFFF", "yAxis": { "show": true, @@ -26,7 +29,7 @@ "type": "bar", "barWidth": "16", "stack": "total", - "color": "#F2F2F2", + "color": "#D8E2E7", "label": { "show": false }, @@ -40,7 +43,7 @@ "type": "bar", "barWidth": "25", "stack": "total", - "color": "#F5F5DC", + "color": "#98C1D7", "label": { "show": false }, @@ -54,7 +57,7 @@ "type": "bar", "barWidth": "25", "stack": "total", - "color": "#74A9CF", + "color": "#6F9BB2", "label": { "show": false }, @@ -68,7 +71,7 @@ "type": "bar", "barWidth": "25", "stack": "total", - "color": "#FA9FB5", + "color": "#376984", "label": { "show": false }, @@ -82,7 +85,7 @@ "type": "bar", "barWidth": "25", "stack": "total", - "color": "#FEC44F", + "color": "#0D425F", "label": { "show": false }, @@ -96,7 +99,7 @@ "type": "bar", "barWidth": "25", "stack": "total", - "color": "#8C96C6", + "color": "#072130", "label": { "show": false }, diff --git a/digiplan/map/charts/ghg_reduction.json b/digiplan/map/charts/ghg_reduction.json new file mode 100644 index 00000000..0bf4e251 --- /dev/null +++ b/digiplan/map/charts/ghg_reduction.json @@ -0,0 +1,386 @@ +{ + "backgroundColor":"#FFFFFF", + "fontStyle":"Roboto", + "fontSize":"14", + "tooltip": {"trigger": "item"}, + "legend":{ + "show":true, + "bottom":"0" + }, + "grid":{ + "top":"10%", + "left":"3%", + "right":"15%", + "bottom":"15%", + "containLabel":true + }, + "xAxis":{ + "type":"category", + "splitLine":{ + "show":false + }, + "data":[ + "Emissionen 2019", + "Reduktion", + "2045" + ] + }, + "yAxis":{ + "type":"value" + }, + "series":[ + { + "name":"", + "type":"bar", + "stack":"Total", + "itemStyle":{ + "borderColor":"transparent", + "color":"transparent" + }, + "emphasis":{ + "itemStyle":{ + "borderColor":"transparent", + "color":"transparent" + } + }, + "data":[ + 0, + 3057, + 0 + ] + }, + { + "name":"Energiewirtschaft", + "type":"bar", + "barWidth":"30", + "stack":"Total", + "color":"#E6772E", + "label":{ + "show":false, + "position":"inside" + }, + "data":[ + 538, + 0, + 0 + ] + }, + { + "name":"Industrie", + "type":"bar", + "barWidth":"30", + "stack":"Total", + "color":"#FA9FB5", + "label":{ + "show":false, + "position":"inside" + }, + "data":[ + 2129, + 0, + 0 + ] + }, + { + "name":"Verkehr", + "type":"bar", + "barWidth":"30", + "stack":"Total", + "color":"#6C567B", + "label":{ + "show":false, + "position":"inside" + }, + "data":[ + 672, + 0, + 0 + ] + }, + { + "name":"Gebäude", + "type":"bar", + "barWidth":"30", + "stack":"Total", + "color":"#A8DADC", + "label":{ + "show":false, + "position":"inside" + }, + "data":[ + 695, + 0, + 0 + ] + }, + { + "name":"Landwirtschaft", + "type":"bar", + "barWidth":"30", + "stack":"Total", + "color":"#87D068", + "label":{ + "show":false, + "position":"inside" + }, + "data":[ + 368, + 0, + 0 + ] + }, + { + "name":"Abfall und Sonst.", + "type":"bar", + "barWidth":"30", + "color":"#F5F5DC", + "stack":"Total", + "label":{ + "show":false, + "position":"inside" + }, + "data":[ + 157, + 0, + 0 + ] + }, + { + "name":"Importe", + "type":"bar", + "barWidth":"30", + "stack":"Total", + "color":"#D9B38C", + "label":{ + "show":false, + "position":"inside", + "color":"#666" + }, + "legend":false, + "data":[ + 0, + 500, + 0 + ] + }, + { + "name":"Regionale Erzeugung", + "type":"bar", + "barWidth":"30", + "stack":"Total", + "color":"#48BF91", + "label":{ + "show":false, + "position":"inside", + "color":"#666" + }, + "data":[ + 0, + 1000, + 0 + ] + }, + { + "name":{ + "name":"Referenzlinie" + }, + "type":"line", + "polyline":true, + "symbol":"none", + "symbolSize":"0", + "symbolRotate":"-90", + "lineStyle":{ + "type":"dashed" + }, + "color":"#808B96", + "label":{ + "show":false + }, + "data":[ + 4557, + 4557, + 4557 + ] + }, + { + "name":"", + "type":"lines", + "coordinateSystem":"cartesian2d", + "emphasis":{ + "label":{ + "show":false + } + }, + "polyline":false, + "symbol":[ + "none", + "triangle" + ], + "symbolSize":[ + 60, + 60 + ], + "z":1, + "lineStyle":{ + "color":"#ccc", + "width":30, + "opacity":0.15, + "type":"solid" + }, + "data":[ + { + "coords":[ + [ + 0, + 4557 + ], + [ + 2, + 200 + ] + ] + } + ] + }, + { + "name":"", + "type":"lines", + "coordinateSystem":"cartesian2d", + "emphasis":{ + "label":{ + "show":false + } + }, + "polyline":false, + "symbol":[ + "none", + "triangle" + ], + "symbolSize":[ + 30, + 30 + ], + "lineStyle":{ + "color":"#ccc", + "width":16, + "opacity":1, + "type":"solid" + }, + "data":[ + { + "coords":[ + [ + 1, + 2000 + ], + [ + 1, + 1000 + ] + ] + } + ] + } + ], + "graphic":[ + { + "type":"group", + "left":"40%", + "top":"3%", + "children":[ + + { + "type":"text", + "z":100, + "left":"center", + "top":"middle", + "style":{ + "fill":"#333", + "width":150, + "overflow":"break", + "text":"Reduktion durch", + "font":"14px Roboto" + } + } + ] + }, + { + "type":"group", + "left":"60%", + "top":"22%", + "children":[ + { + "type":"rect", + "z":100, + "left":"center", + "top":"middle", + "shape":{ + "width":160, + "height":30 + }, + "style":{ + "fill":"#48BF91", + "stroke":"#555", + "lineWidth":0, + "shadowBlur":8, + "shadowOffsetX":3, + "shadowOffsetY":3, + "shadowColor":"rgba(0,0,0,0.2)" + } + }, + { + "type":"text", + "z":100, + "left":"center", + "top":"middle", + "style":{ + "fill":"#fff", + "width":160, + "overflow":"break", + "text":"Regionale Erzeugung", + "font":"14px Roboto" + } + } + ] + }, + { + "type":"group", + "left":"60%", + "top":"32%", + "children":[ + { + "type":"rect", + "z":100, + "left":"center", + "top":"middle", + "shape":{ + "width":160, + "height":30 + }, + "style":{ + "fill":"#D9B38C", + "stroke":"#555", + "lineWidth":0, + "shadowBlur":8, + "shadowOffsetX":3, + "shadowOffsetY":3, + "shadowColor":"rgba(0,0,0,0.2)" + } + }, + { + "type":"text", + "z":100, + "left":"center", + "top":"middle", + "style":{ + "fill":"#fff", + "width":160, + "overflow":"break", + "text":"Importe", + "font":"14px Roboto" + } + } + ] + } + ] +} diff --git a/digiplan/map/charts/heat_demand.json b/digiplan/map/charts/heat_demand.json index 409b33e4..852a33a3 100644 --- a/digiplan/map/charts/heat_demand.json +++ b/digiplan/map/charts/heat_demand.json @@ -2,24 +2,23 @@ "grid": { "top": "24%", "right": "30%", - "bottom": "5%" + "bottom": "28%" }, "legend": { "data": [ - "GHD", "Haushalte", - "Industrie", - "Sonstiges" + "GHD", + "Industrie" ], "orient": "vertical", "right": "1%", - "bottom": "5%" + "bottom": "35%" }, - "color": ["#F5F5DC", "#A8DADC", "#C27BA0", "#B0BEC5"], + "color": ["#6F9BB2", "#98C1D7", "#376984"], "xAxis": { "type": "category", "boundaryGap": true, - "data": ["Status Quo", "2045"], + "data": ["Status Quo"], "axisTick": "alignWithLabel" }, "yAxis": { @@ -34,25 +33,19 @@ "type": "bar", "stack": "five", "data": [36, 10], - "name": "GHD" + "name": "Haushalte" }, { "type": "bar", "stack": "five", "data": [36, 10], - "name": "Haushalte" + "name": "GHD" }, { "type": "bar", "stack": "five", "data": [10, 10], "name": "Industrie" - }, - { - "type": "bar", - "stack": "five", - "data": [10, 10], - "name": "Sonstiges" } ], "title": { "text": "Heat Demand" } diff --git a/digiplan/map/charts/heat_demand_capita.json b/digiplan/map/charts/heat_demand_capita.json deleted file mode 100644 index bbce9c90..00000000 --- a/digiplan/map/charts/heat_demand_capita.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "grid": { - "top": "24%", - "right": "30%", - "bottom": "5%" - }, - "legend": { - "data": [ - "GHD", - "Haushalte", - "Industrie", - "Sonstiges" - ], - "orient": "vertical", - "right": "1%", - "bottom": "5%" - }, - "color": ["#F5F5DC", "#A8DADC", "#C27BA0", "#B0BEC5"], - "xAxis": { - "type": "category", - "boundaryGap": true, - "data": ["Status Quo", "2045"], - "axisTick": "alignWithLabel" - }, - "yAxis": { - "type": "value", - "show": true, - "name": "GWh", - "nameLocation": "end", - "nameTextStyle": "Roboto" - }, - "series": [ - { - "type": "bar", - "stack": "five", - "data": [36, 10], - "name": "GHD" - }, - { - "type": "bar", - "stack": "five", - "data": [36, 10], - "name": "Haushalte" - }, - { - "type": "bar", - "stack": "five", - "data": [10, 10], - "name": "Industrie" - }, - { - "type": "bar", - "stack": "five", - "data": [10, 10], - "name": "Sonstiges" - } - ], - "title": { "text": "Heat Demand per Capita" } - } diff --git a/digiplan/map/charts/mobility_ghg.json b/digiplan/map/charts/mobility_ghg.json index 5440bfba..ae017195 100644 --- a/digiplan/map/charts/mobility_ghg.json +++ b/digiplan/map/charts/mobility_ghg.json @@ -1,4 +1,7 @@ { + "grid": { + "bottom": "20%" + }, "xAxis": { "type": "value", "show": true, @@ -24,7 +27,7 @@ "type": "bar", "barWidth": "16", "stack": "total", - "color": "#C8D8E4", + "color": "#D8E2E7", "label": { "show": false }, @@ -37,7 +40,7 @@ "name": "Konventionell", "type": "bar", "stack": "total", - "color": "#647078", + "color": "#A1B3BC", "label": { "show": false }, @@ -50,7 +53,7 @@ "name": "Erneuerbare Energien", "type": "bar", "stack": "total", - "color": "#A8E7BA", + "color": "#06DFA7", "label": { "show": false }, diff --git a/digiplan/map/charts/mobility_overview.json b/digiplan/map/charts/mobility_overview.json index 52899cc2..9d86c5e7 100644 --- a/digiplan/map/charts/mobility_overview.json +++ b/digiplan/map/charts/mobility_overview.json @@ -1,4 +1,7 @@ { + "grid": { + "bottom": "20%" + }, "xAxis": { "type": "value", "show": true, @@ -24,7 +27,7 @@ "type": "bar", "barWidth": "16", "stack": "total", - "color": "#647078", + "color": "#DCD9F0", "label": { "show": false }, @@ -37,7 +40,7 @@ "name": "Benzin", "type": "bar", "stack": "total", - "color": "#866E18", + "color": "#B5ADE0", "label": { "show": false }, @@ -50,7 +53,7 @@ "name": "Hybrid", "type": "bar", "stack": "total", - "color": "#8FDCE1", + "color": "#897BD9", "label": { "show": false }, @@ -63,7 +66,7 @@ "name": "E-Auto", "type": "bar", "stack": "total", - "color": "#98D47E", + "color": "#6956D6", "label": { "show": false }, diff --git a/digiplan/map/charts/overview_heat.json b/digiplan/map/charts/overview_heat.json index d7a14c0d..9236797d 100644 --- a/digiplan/map/charts/overview_heat.json +++ b/digiplan/map/charts/overview_heat.json @@ -1,9 +1,12 @@ { + "grid": { + "bottom": "20%" + }, "xAxis": { "type": "value", "show": true, "position": "bottom", - "name": "TWh", + "name": "GWh", "nameLocation": "end", "nameTextStyle": "Roboto", "width": "76", @@ -13,7 +16,7 @@ }, "yAxis": { "type": "category", - "data": ["Ziel", "Mein Szeanrio", "Status Quo"], + "data": ["Ziel", "Mein Szenario", "Status Quo"], "axisTick": { "show": false } @@ -24,7 +27,7 @@ "type": "bar", "barWidth": "16", "stack": "total", - "color": "#F5F5DC", + "color": "#B3B3B3", "label": { "show": false }, @@ -37,7 +40,7 @@ "name": "Haushalte", "type": "bar", "stack": "total", - "color": "#A8DADC", + "color": "#7C7C7C", "label": { "show": false }, @@ -50,7 +53,7 @@ "name": "Industrie", "type": "bar", "stack": "total", - "color": "#C27BA0", + "color": "#4F4F4F", "label": { "show": false }, @@ -58,19 +61,6 @@ "focus": "series" }, "data": [200, 250, 350] - }, - { - "name": "Sonstiges", - "type": "bar", - "stack": "total", - "color": "#B0BEC5", - "label": { - "show": false - }, - "emphasis": { - "focus": "series" - }, - "data": [150, 150, 150] } ] } diff --git a/digiplan/map/charts/population.json b/digiplan/map/charts/population.json index 49c6b8ce..d66f354b 100644 --- a/digiplan/map/charts/population.json +++ b/digiplan/map/charts/population.json @@ -29,5 +29,5 @@ "name": "Population" } ], - "title": {"text": "population in different years"} + "title": {"text": "Population per year"} } diff --git a/digiplan/map/charts/population_density.json b/digiplan/map/charts/population_density.json deleted file mode 100644 index a51eb078..00000000 --- a/digiplan/map/charts/population_density.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "tooltip": { - "trigger": "item", - "axisPointer": { - "type": "cross" - } - }, - "grid": { - "top": "24%" - }, - "xAxis": { - "type": "time", - "xAxisLabel":{ - "formatter": "{yyyy}" - } - }, - "yAxis": { - "type": "value", - "show": true, - "name": "Density per km²", - "nameLocation": "end", - "nameTextStyle": "Roboto" - }, - "series": [ - { - "type": "line", - "barWidth": "16", - "data": [5, 20, 36, 10, 10], - "name": "Population per km²" - } - ], - "title": {"text": "population density in different years"} -} diff --git a/digiplan/map/charts/renewable_electricity_production.json b/digiplan/map/charts/renewable_electricity_production.json index c50d970e..261fe90c 100644 --- a/digiplan/map/charts/renewable_electricity_production.json +++ b/digiplan/map/charts/renewable_electricity_production.json @@ -2,7 +2,7 @@ "grid": { "top": "24%", "right": "30%", - "bottom": "5%" + "bottom": "25%" }, "legend": { "data": [ @@ -16,7 +16,7 @@ "right": "1%", "bottom": "5%" }, - "color": ["#1F82C0", "#F6B93B", "#FFD660", "#98D47E", "#CFCFCF"], + "color": ["#6A89CC", "#EFAD25", "#FFD660", "#52C41A", "#A1B3BC"], "xAxis": { "type": "category", "boundaryGap": true, diff --git a/digiplan/map/charts/renewable_electricity_production_capita.json b/digiplan/map/charts/renewable_electricity_production_capita.json deleted file mode 100644 index 743a8398..00000000 --- a/digiplan/map/charts/renewable_electricity_production_capita.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "grid": { - "top": "24%", - "right": "30%", - "bottom": "5%" - }, - "legend": { - "data": [ - "Wind", - "Freiflächen-PV", - "Aufdach-PV", - "Bioenergie", - "Konventionell" - ], - "orient": "vertical", - "right": "1%", - "bottom": "5%" - }, - "color": ["#1F82C0", "#F6B93B", "#FFD660", "#98D47E", "#CFCFCF"], - "xAxis": { - "type": "category", - "boundaryGap": true, - "data": ["Status Quo", "2045"], - "axisTick": "alignWithLabel" - }, - "yAxis": { - "type": "value", - "show": true, - "name": "MWh", - "nameLocation": "end", - "nameTextStyle": "Roboto" - }, - "series": [ - { - "type": "bar", - "stack": "five", - "data": [36, 10], - "name": "Wind" - }, - { - "type": "bar", - "stack": "five", - "data": [36, 10], - "name": "Freiflächen-PV" - }, - { - "type": "bar", - "stack": "five", - "data": [10, 10], - "name": "Aufdach-PV" - }, - { - "type": "bar", - "stack": "five", - "data": [10, 10], - "name": "Bioenergie" - }, - { - "type": "bar", - "stack": "five", - "data": [10, 10], - "name": "Konventionell" - } - ], - "title": { "text": "Renewable Electricity Production per Capita" } - } diff --git a/digiplan/map/charts/renewable_electricity_production_square.json b/digiplan/map/charts/renewable_electricity_production_square.json deleted file mode 100644 index 1f2a7ef5..00000000 --- a/digiplan/map/charts/renewable_electricity_production_square.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "grid": { - "top": "24%", - "right": "30%", - "bottom": "5%" - }, - "legend": { - "data": [ - "Wind", - "Freiflächen-PV", - "Aufdach-PV", - "Bioenergie", - "Konventionell" - ], - "orient": "vertical", - "right": "1%", - "bottom": "5%" - }, - "color": ["#1F82C0", "#F6B93B", "#FFD660", "#98D47E", "#CFCFCF"], - "xAxis": { - "type": "category", - "boundaryGap": true, - "data": ["Status Quo", "2045"], - "axisTick": "alignWithLabel" - }, - "yAxis": { - "type": "value", - "show": true, - "name": "MWh", - "nameLocation": "end", - "nameTextStyle": "Roboto" - }, - "series": [ - { - "type": "bar", - "stack": "five", - "data": [36, 10], - "name": "Wind" - }, - { - "type": "bar", - "stack": "five", - "data": [36, 10], - "name": "Freiflächen-PV" - }, - { - "type": "bar", - "stack": "five", - "data": [10, 10], - "name": "Aufdach-PV" - }, - { - "type": "bar", - "stack": "five", - "data": [10, 10], - "name": "Bioenergie" - }, - { - "type": "bar", - "stack": "five", - "data": [10, 10], - "name": "Konventionell" - } - ], - "title": { "text": "Renewable Electricity Production per km²" } - } diff --git a/digiplan/map/charts/wind_turbines.json b/digiplan/map/charts/wind_turbines.json index d855af87..6bcc0477 100644 --- a/digiplan/map/charts/wind_turbines.json +++ b/digiplan/map/charts/wind_turbines.json @@ -9,10 +9,10 @@ "top": "24%" }, "xAxis": { - "type": "time", - "xAxisLabel":{ - "formatter": "{yyyy}" - } + "type": "category", + "boundaryGap": true, + "data": ["Status Quo"], + "axisTick": "alignWithLabel" }, "yAxis": { "type": "value", @@ -23,9 +23,9 @@ }, "series": [ { - "type": "line", + "type": "bar", "barWidth": "16", - "data": [5, 20, 36, 10, 10], + "data": [5], "name": "Wind Turbines" } ], diff --git a/digiplan/map/charts/wind_turbines_square.json b/digiplan/map/charts/wind_turbines_square.json deleted file mode 100644 index 0c231384..00000000 --- a/digiplan/map/charts/wind_turbines_square.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "tooltip": { - "trigger": "item", - "axisPointer": { - "type": "cross" - } - }, - "grid": { - "top": "24%" - }, - "xAxis": { - "type": "time", - "xAxisLabel":{ - "formatter": "{yyyy}" - } - }, - "yAxis": { - "type": "value", - "show": true, - "name": "Wind Turbines", - "nameLocation": "end", - "nameTextStyle": "Roboto" - }, - "series": [ - { - "type": "line", - "barWidth": "16", - "data": [5, 20, 36, 10, 10], - "name": "Wind Turbines" - } - ], - "title": { "text": "number of wind turbines per km²" } -} diff --git a/digiplan/map/choropleths.py b/digiplan/map/choropleths.py index 828cfdad..d9bbd884 100644 --- a/digiplan/map/choropleths.py +++ b/digiplan/map/choropleths.py @@ -4,6 +4,7 @@ from collections.abc import Callable from typing import Optional, Union +import pandas as pd from django.conf import settings from django.http.response import JsonResponse @@ -73,50 +74,156 @@ def render(self) -> JsonResponse: values = self.get_values_per_feature() paint_properties = self.get_paint_properties() paint_properties["fill-color"] = self.get_fill_color(values) - return JsonResponse({"values": self.get_values_per_feature(), "paintProperties": paint_properties}) + return JsonResponse({"values": values, "paintProperties": paint_properties}) -class RenewableElectricityProductionChoropleth(Choropleth): # noqa: D101 +class EnergyShareChoropleth(Choropleth): # noqa: D101 def get_values_per_feature(self) -> dict[int, float]: # noqa: D102 - return calculations.capacity_per_municipality() + return calculations.energy_shares_per_municipality().sum(axis=1).to_dict() -class CapacityChoropleth(Choropleth): # noqa: D101 +class EnergyChoropleth(Choropleth): # noqa: D101 + def get_values_per_feature(self) -> dict[int, float]: # noqa: D102 + return calculations.energies_per_municipality().sum(axis=1).to_dict() + + +class Energy2045Choropleth(Choropleth): # noqa: D101 + def get_values_per_feature(self) -> dict[int, float]: # noqa: D102 + return calculations.energies_per_municipality_2045(self.map_state["simulation_id"]).sum(axis=1).to_dict() + + +class EnergyCapitaChoropleth(Choropleth): # noqa: D101 + def get_values_per_feature(self) -> dict[int, float]: # noqa: D102 + energies = calculations.energies_per_municipality() + energies_per_capita = calculations.calculate_capita_for_value(energies) * 1e3 + return energies_per_capita.sum(axis=1).to_dict() + + +class EnergyCapita2045Choropleth(Choropleth): # noqa: D101 + def get_values_per_feature(self) -> dict[int, float]: # noqa: D102 + energies = calculations.energies_per_municipality_2045(self.map_state["simulation_id"]) + energies_per_capita = calculations.calculate_capita_for_value(energies) * 1e3 + return energies_per_capita.sum(axis=1).to_dict() + + +class EnergySquareChoropleth(Choropleth): # noqa: D101 + def get_values_per_feature(self) -> dict[int, float]: # noqa: D102 + energies = calculations.energies_per_municipality() + energies_per_square = calculations.calculate_square_for_value(energies) * 1e3 + return energies_per_square.sum(axis=1).to_dict() + + +class EnergySquare2045Choropleth(Choropleth): # noqa: D101 def get_values_per_feature(self) -> dict[int, float]: # noqa: D102 - return calculations.capacity_per_municipality() + energies = calculations.energies_per_municipality_2045(self.map_state["simulation_id"]) + energies_per_square = calculations.calculate_square_for_value(energies) * 1e3 + return energies_per_square.sum(axis=1).to_dict() + + +class CapacityChoropleth(Choropleth): # noqa: D101 + def get_values_per_feature(self) -> pd.DataFrame: # noqa: D102 + capacities = calculations.capacities_per_municipality().sum(axis=1) + return capacities.to_dict() class CapacitySquareChoropleth(Choropleth): # noqa: D101 def get_values_per_feature(self) -> dict[int, float]: # noqa: D102 - return calculations.capacity_square_per_municipality() + capacities = calculations.capacities_per_municipality() + capacities_square = calculations.calculate_square_for_value(capacities) + return capacities_square.sum(axis=1).to_dict() class PopulationChoropleth(Choropleth): # noqa: D101 def get_values_per_feature(self) -> dict[int, float]: # noqa: D102 - return models.Population.population_per_municipality() + return models.Population.quantity_per_municipality_per_year().sum(axis=1).to_dict() class PopulationDensityChoropleth(Choropleth): # noqa: D101 def get_values_per_feature(self) -> dict[int, float]: # noqa: D102 - return models.Population.density_per_municipality() + population = models.Population.quantity_per_municipality_per_year() + population_square = calculations.calculate_square_for_value(population) + return population_square.sum(axis=1).to_dict() + + +class EmployeesChoropleth(Choropleth): # noqa: D101 + def get_values_per_feature(self) -> dict[int, float]: # noqa: D102 + return calculations.employment_per_municipality().to_dict() + + +class CompaniesChoropleth(Choropleth): # noqa: D101 + def get_values_per_feature(self) -> dict[int, float]: # noqa: D102 + return calculations.companies_per_municipality().to_dict() class WindTurbinesChoropleth(Choropleth): # noqa: D101 def get_values_per_feature(self) -> dict[int, float]: # noqa: D102 - return models.WindTurbine.quantity_per_municipality() + return models.WindTurbine.quantity_per_municipality().to_dict() class WindTurbinesSquareChoropleth(Choropleth): # noqa: D101 def get_values_per_feature(self) -> dict[int, float]: # noqa: D102 - return models.WindTurbine.quantity_per_square() + wind_turbines = models.WindTurbine.quantity_per_municipality() + wind_turbines_square = calculations.calculate_square_for_value(wind_turbines) + return wind_turbines_square.to_dict() + + +class ElectricityDemandChoropleth(Choropleth): # noqa: D101 + def get_values_per_feature(self) -> dict[int, float]: # noqa: D102 + return calculations.electricity_demand_per_municipality().sum(axis=1).to_dict() + + +class ElectricityDemandCapitaChoropleth(Choropleth): # noqa: D101 + def get_values_per_feature(self) -> dict[int, float]: # noqa: D102 + capita_demand = ( + calculations.calculate_capita_for_value(calculations.electricity_demand_per_municipality()).sum(axis=1) + * 1e6 + ) + return capita_demand.to_dict() + + +class HeatDemandChoropleth(Choropleth): # noqa: D101 + def get_values_per_feature(self) -> dict[int, float]: # noqa: D102 + return calculations.heat_demand_per_municipality().sum(axis=1).to_dict() + + +class HeatDemandCapitaChoropleth(Choropleth): # noqa: D101 + def get_values_per_feature(self) -> dict[int, float]: # noqa: D102 + capita_demand = ( + calculations.calculate_capita_for_value(calculations.heat_demand_per_municipality()).sum(axis=1) * 1e6 + ) + return capita_demand.to_dict() + + +class BatteriesChoropleth(Choropleth): # noqa: D101 + def get_values_per_feature(self) -> dict[int, float]: # noqa: D102 + return calculations.batteries_per_municipality().to_dict() + + +class BatteriesCapacityChoropleth(Choropleth): # noqa: D101 + def get_values_per_feature(self) -> dict[int, float]: # noqa: D102 + return calculations.battery_capacities_per_municipality().to_dict() CHOROPLETHS: dict[str, Union[Callable, type(Choropleth)]] = { - "capacity": CapacityChoropleth, - "capacity_square": CapacitySquareChoropleth, - "population": PopulationChoropleth, - "population_density": PopulationDensityChoropleth, - "wind_turbines": WindTurbinesChoropleth, - "wind_turbines_square": WindTurbinesSquareChoropleth, - "renewable_electricity_production": RenewableElectricityProductionChoropleth, + "population_statusquo": PopulationChoropleth, + "population_density_statusquo": PopulationDensityChoropleth, + "employees_statusquo": EmployeesChoropleth, + "companies_statusquo": CompaniesChoropleth, + "energy_statusquo": EnergyChoropleth, + "energy_2045": Energy2045Choropleth, + "energy_share_statusquo": EnergyShareChoropleth, + "energy_capita_statusquo": EnergyCapitaChoropleth, + "energy_capita_2045": EnergyCapita2045Choropleth, + "energy_square_statusquo": EnergySquareChoropleth, + "energy_square_2045": EnergySquare2045Choropleth, + "capacity_statusquo": CapacityChoropleth, + "capacity_square_statusquo": CapacitySquareChoropleth, + "wind_turbines_statusquo": WindTurbinesChoropleth, + "wind_turbines_square_statusquo": WindTurbinesSquareChoropleth, + "electricity_demand_statusquo": ElectricityDemandChoropleth, + "electricity_demand_capita_statusquo": ElectricityDemandCapitaChoropleth, + "heat_demand_statusquo": HeatDemandChoropleth, + "heat_demand_capita_statusquo": HeatDemandCapitaChoropleth, + "batteries_statusquo": BatteriesChoropleth, + "batteries_capacity_statusquo": BatteriesCapacityChoropleth, } diff --git a/digiplan/map/config.py b/digiplan/map/config.py index d56960a0..6c77dc1f 100644 --- a/digiplan/map/config.py +++ b/digiplan/map/config.py @@ -1,17 +1,18 @@ """Configuration for map app.""" import json import pathlib +from pathlib import Path +import pandas as pd from django.conf import settings from django.utils.translation import gettext_lazy as _ from digiplan import __version__ -from . import utils +from . import datapackage, utils # DIRECTORIES MAP_DIR = settings.APPS_DIR.path("map") -POPUPS_DIR = MAP_DIR.path("popups") CHARTS_DIR = MAP_DIR.path("charts") SCENARIOS_DIR = settings.DATA_DIR.path("scenarios") @@ -19,8 +20,12 @@ ENERGY_SETTINGS_PANEL_FILE = settings.APPS_DIR.path("static/config/energy_settings_panel.json") HEAT_SETTINGS_PANEL_FILE = settings.APPS_DIR.path("static/config/heat_settings_panel.json") TRAFFIC_SETTINGS_PANEL_FILE = settings.APPS_DIR.path("static/config/traffic_settings_panel.json") +ADDITIONAL_ENERGY_SETTINGS_FILE = settings.DATA_DIR.path("digipipe/settings/energy_settings_panel.json") +ADDITIONAL_HEAT_SETTINGS_FILE = settings.DATA_DIR.path("digipipe/settings/heat_settings_panel.json") +ADDITIONAL_TRAFFIC_SETTINGS_FILE = settings.DATA_DIR.path("digipipe/settings/traffic_settings_panel.json") SETTINGS_DEPENDENCY_MAP_FILE = settings.APPS_DIR.path("static/config/settings_dependency_map.json") DEPENDENCY_PARAMETERS_FILE = settings.APPS_DIR.path("static/config/dependency_parameters.json") +TECHNOLOGY_DATA_FILE = settings.DIGIPIPE_DIR.path("scalars").path("technology_data.json") # FILTERS FILTER_DEFINITION = {} @@ -30,18 +35,92 @@ ENERGY_SETTINGS_PANEL = utils.get_translated_json_from_file(ENERGY_SETTINGS_PANEL_FILE) HEAT_SETTINGS_PANEL = utils.get_translated_json_from_file(HEAT_SETTINGS_PANEL_FILE) TRAFFIC_SETTINGS_PANEL = utils.get_translated_json_from_file(TRAFFIC_SETTINGS_PANEL_FILE) +ADDITIONAL_ENERGY_SETTINGS = utils.get_translated_json_from_file(ADDITIONAL_ENERGY_SETTINGS_FILE) +ADDITIONAL_HEAT_SETTINGS = utils.get_translated_json_from_file(ADDITIONAL_HEAT_SETTINGS_FILE) +ADDITIONAL_TRAFFIC_SETTINGS = utils.get_translated_json_from_file(ADDITIONAL_TRAFFIC_SETTINGS_FILE) SETTINGS_DEPENDENCY_MAP = utils.get_translated_json_from_file(SETTINGS_DEPENDENCY_MAP_FILE) DEPENDENCY_PARAMETERS = utils.get_translated_json_from_file(DEPENDENCY_PARAMETERS_FILE) +TECHNOLOGY_DATA = utils.get_translated_json_from_file(TECHNOLOGY_DATA_FILE) + + +def get_all_settings() -> dict: + """ + Concatenate all Settings. + + Returns + ------- + dict + one dict with all settings concatenated + """ + all_settings = {} + for setting_dict in [ + ENERGY_SETTINGS_PANEL, + HEAT_SETTINGS_PANEL, + TRAFFIC_SETTINGS_PANEL, + ADDITIONAL_ENERGY_SETTINGS, + ADDITIONAL_HEAT_SETTINGS, + ADDITIONAL_TRAFFIC_SETTINGS, + ]: + all_settings.update(setting_dict) + return all_settings + + +def get_slider_marks() -> dict: + """ + get all status quo values and future scenario values for all settings. + + Returns + ------- + dict + one dict with all values in correct format for usage + """ + all_settings = get_all_settings().items() + slider_marks = {} + for param_name, param_data in all_settings: + if "status_quo" in param_data: + if param_name in slider_marks: + slider_marks[param_name].append(("Heute", param_data["status_quo"])) + else: + slider_marks[param_name] = [("Heute", param_data["status_quo"])] + if "future_scenario" in param_data: + if param_name in slider_marks: + slider_marks[param_name].append(("2045", param_data["future_scenario"])) + else: + slider_marks[param_name] = [("2045", param_data["future_scenario"])] + return slider_marks + + +def get_slider_per_sector() -> dict: + """ + get demand per sector. + + Returns + ------- + dict + demand per sector for each slider + """ + sector_dict = { + "s_v_1": {"hh": 0, "ind": 0, "cts": 0}, + "w_v_1": {"hh": 0, "ind": 0, "cts": 0}, + "w_d_wp_1": {"hh": 0, "ind": 0, "cts": 0}, + } + demand = {"s_v_1": "power_demand", "w_v_1": "heat_demand", "w_d_wp_1": "heat_demand_dec"} + sectors = ["hh", "ind", "cts"] + for key, value in demand.items(): + for sector in sectors: + file = f"demand_{sector}_{value}.csv" + path = Path(settings.DATA_DIR, "digipipe/scalars", file) + reader = pd.read_csv(path) + sector_dict[key][sector] = reader["2022"].sum() + return sector_dict # STORE STORE_COLD_INIT = { "version": __version__, - "slider_marks": { - param_name: [("Status Quo", param_data["status_quo"])] - for param_name, param_data in ENERGY_SETTINGS_PANEL.items() - if "status_quo" in param_data - }, + "slider_marks": get_slider_marks(), + "slider_max": datapackage.get_potential_values(), + "slider_per_sector": get_slider_per_sector(), "allowedSwitches": ["wind_distance"], "detailTab": {"showPotentialLayers": True}, "staticState": 0, @@ -92,18 +171,26 @@ def init_sources() -> dict[str, dict]: # SIMULATION SIMULATION_RENEWABLES = { - "ABW-solar-pv_ground": _("Freiflächen-PV"), - "ABW-solar-pv_rooftop": _("Aufdach-PV"), - "ABW-wind-onshore": _("Wind"), + "ABW-solar-pv_ground": _("Outdoor PV"), + "ABW-solar-pv_rooftop": _("Roof-mounted PV"), + "ABW-wind-onshore": _("Wind turbine"), "ABW-hydro-ror": _("Hydro"), "ABW-biomass": _("Biomass"), } SIMULATION_DEMANDS = { + # electricty demands "ABW-electricity-bev_charging": _("BEV"), "ABW-electricity-demand_hh": _("Electricity Household Demand"), "ABW-electricity-demand_cts": _("Electricity CTS Demand"), "ABW-electricity-demand_ind": _("Electricity Industry Demand"), + "electricity_heat_demand_hh": _("Electricity Household Heat Demand"), + "electricity_heat_demand_cts": _("Electricity CTS Heat Demand"), + "electricity_heat_demand_ind": _("Electricity Industry Heat Demand"), + # heat demands + "heat-demand-cts": _("CTS Heat Demand"), + "heat-demand-hh": _("Household Heat Demand"), + "heat-demand-ind": _("Industry Heat Demand"), } SIMULATION_NAME_MAPPING = {} | SIMULATION_RENEWABLES | SIMULATION_DEMANDS diff --git a/digiplan/map/datapackage.py b/digiplan/map/datapackage.py new file mode 100644 index 00000000..b1502ec9 --- /dev/null +++ b/digiplan/map/datapackage.py @@ -0,0 +1,142 @@ +"""Read functionality for digipipe datapackage.""" +import json +from collections import defaultdict +from pathlib import Path +from typing import Optional + +import pandas as pd +from django.conf import settings +from django_oemof.settings import OEMOF_DIR + +from config.settings.base import DATA_DIR + + +def get_employment() -> pd.DataFrame: + """Return employment data.""" + employment_filename = settings.DIGIPIPE_DIR.path("scalars").path("employment.csv") + return pd.read_csv(employment_filename, index_col=0) + + +def get_batteries() -> pd.DataFrame: + """Return battery data.""" + battery_filename = settings.DIGIPIPE_DIR.path("scalars").path("bnetza_mastr_storage_stats_muns.csv") + return pd.read_csv(battery_filename) + + +def get_power_demand(sector: Optional[str] = None) -> dict[str, pd.DataFrame]: + """Return power demand for given sector or all sectors.""" + sectors = (sector,) if sector else ("hh", "cts", "ind") + demand = {} + for sec in sectors: + demand_filename = settings.DIGIPIPE_DIR.path("scalars").path(f"demand_{sec}_power_demand.csv") + demand[sec] = pd.read_csv(demand_filename) + return demand + + +def get_summed_heat_demand_per_municipality( + sector: Optional[str] = None, + distribution: Optional[str] = None, +) -> dict[str, dict[str, pd.DataFrame]]: + """Return heat demand for given sector and distribution.""" + sectors = (sector,) if sector else ("hh", "cts", "ind") + distributions = (distribution,) if distribution else ("cen", "dec") + demand = defaultdict(dict) + for sec in sectors: + for dist in distributions: + demand_filename = settings.DIGIPIPE_DIR.path("scalars").path( + f"demand_{sec}_heat_demand_{dist}.csv", + ) + demand[sec][dist] = pd.read_csv(demand_filename) + return demand + + +def get_heat_demand( + sector: Optional[str] = None, + distribution: Optional[str] = None, +) -> dict[str, dict[str, pd.DataFrame]]: + """Return heat demand for given sector and distribution.""" + sectors = (sector,) if sector else ("hh", "cts", "ind") + distributions = (distribution,) if distribution else ("central", "decentral") + demand = defaultdict(dict) + for sec in sectors: + for dist in distributions: + demand_filename = ( + OEMOF_DIR / settings.OEMOF_SCENARIO / "data" / "sequences" / f"heat_{dist}-demand_{sec}_profile.csv" + ) + demand[sec][dist] = pd.read_csv(demand_filename, sep=";")[f"ABW-heat_{dist}-demand_{sec}-profile"] + return demand + + +def get_thermal_efficiency(component: str) -> float: + """Return thermal efficiency from given component from oemof scenario.""" + component_filename = OEMOF_DIR / settings.OEMOF_SCENARIO / "data" / "elements" / f"{component}.csv" + component_df = pd.read_csv(component_filename, sep=";") + if component_df["type"][0] in ("extraction", "backpressure"): + return float(pd.read_csv(component_filename, sep=";")["thermal_efficiency"][0]) + + if "efficiency" in component_df.columns and isinstance(component_df["efficiency"][0], float): + return component_df["efficiency"][0] + + if "heatpump" in component: + component = "efficiency" + sequence_filename = OEMOF_DIR / settings.OEMOF_SCENARIO / "data" / "sequences" / f"{component}_profile.csv" + return pd.read_csv(sequence_filename, sep=";").iloc[:, 1] + + +def get_potential_values(*, per_municipality: bool = False) -> dict: + """ + Calculate max_values for sliders. + + Parameters + ---------- + per_municipality: bool + If set to True, potentials are not aggregated, but given per municipality + + Returns + ------- + dict + dictionary with each slider / switch and respective max_value + """ + scalars = { + "wind": "potentialarea_wind_area_stats_muns.csv", + "pv_ground": "potentialarea_pv_ground_area_stats_muns.csv", + "pv_roof": "potentialarea_pv_roof_wo_historic_area_stats_muns.csv", + } + + areas = { + "wind": { + "s_w_3": "stp_2018_vreg", + "s_w_4_1": "stp_2027_vr", + "s_w_4_2": "stp_2027_repowering", + "s_w_5_1": "stp_2027_search_area_open_area", + "s_w_5_2": "stp_2027_search_area_forest_area", + }, + "pv_ground": {"s_pv_ff_3": "road_railway_region", "s_pv_ff_4": "agriculture_lfa-off_region"}, + "pv_roof": {"s_pv_d_3": None}, + } + + tech_data = json.load(Path.open(Path(settings.DIGIPIPE_DIR, "scalars/technology_data.json"))) + + potentials = {} + for profile in areas: + path = Path(DATA_DIR, "digipipe/scalars", scalars[profile]) + reader = pd.read_csv(path) + for key, value in areas[profile].items(): + if key == "s_pv_d_3": + pv_roof_potential = reader[ + [f"installable_power_{orient}" for orient in ["south", "east", "west", "flat"]] + ].sum(axis=1) + if per_municipality: + potentials = pv_roof_potential + else: + potentials[key] = pv_roof_potential.sum() + else: + if per_municipality: + potentials[key] = reader[value] + else: + potentials[key] = reader[value].sum() + if profile == "wind": + potentials[key] = potentials[key] * tech_data["power_density"]["wind"] + if profile == "pv_ground": + potentials[key] = potentials[key] * tech_data["power_density"]["pv_ground"] + return potentials diff --git a/digiplan/map/forms.py b/digiplan/map/forms.py index cdeeb630..996a8213 100644 --- a/digiplan/map/forms.py +++ b/digiplan/map/forms.py @@ -1,19 +1,10 @@ from itertools import count # noqa: D100 -from django.db.models import Max, Min -from django.forms import ( - BooleanField, - Form, - IntegerField, - MultipleChoiceField, - MultiValueField, - TextInput, - renderers, -) +from django.forms import BooleanField, Form, IntegerField, TextInput, renderers from django.utils.safestring import mark_safe from django_mapengine import legend -from . import models +from . import charts from .widgets import SwitchWidget @@ -44,47 +35,16 @@ def __init__(self, layer: legend.LegendLayer, *args, **kwargs) -> None: # noqa: super().__init__(*args, **kwargs) self.layer = layer - if hasattr(layer.model, "filters"): - self.has_filters = True - for filter_ in layer.layer.model.filters: - if filter_.type == models.LayerFilterType.Range: - filter_min = layer.layer.model.vector_tiles.aggregate(Min(filter_.name))[f"{filter_.name}__min"] - filter_max = layer.layer.model.vector_tiles.aggregate(Max(filter_.name))[f"{filter_.name}__max"] - self.fields[filter_.name] = MultiValueField( - label=getattr(layer.layer.model, filter_.name).field.verbose_name, - fields=[IntegerField(), IntegerField()], - widget=TextInput( - attrs={ - "class": "js-slider", - "data-type": "double", - "data-min": filter_min, - "data-max": filter_max, - "data-from": filter_min, - "data-to": filter_max, - "data-grid": True, - }, - ), - ) - elif filter_.type == models.LayerFilterType.Dropdown: - filter_values = ( - layer.layer.model.vector_tiles.values_list(filter_.name, flat=True) - .order_by(filter_.name) - .distinct() - ) - self.fields[filter_.name] = MultipleChoiceField( - choices=[(value, value) for value in filter_values], - ) - else: - raise ValueError(f"Unknown filter type '{filter_.type}'") - class PanelForm(TemplateForm): # noqa: D101 - def __init__(self, parameters, **kwargs) -> None: # noqa: D107, ANN001 + def __init__(self, parameters, additional_parameters=None, **kwargs) -> None: # noqa: D107, ANN001 super().__init__(**kwargs) - self.fields = {item["name"]: item["field"] for item in self.generate_fields(parameters)} + self.fields = {item["name"]: item["field"] for item in self.generate_fields(parameters, additional_parameters)} @staticmethod - def generate_fields(parameters): # noqa: ANN001, ANN205, D102 + def generate_fields(parameters, additional_parameters=None): # noqa: ANN001, ANN205, D102 + if additional_parameters is not None: + charts.merge_dicts(parameters, additional_parameters) for name, item in parameters.items(): if item["type"] == "slider": attrs = { @@ -101,7 +61,12 @@ def generate_fields(parameters): # noqa: ANN001, ANN205, D102 if "step" in item: attrs["data-step"] = item["step"] - field = IntegerField(label=item["label"], widget=TextInput(attrs=attrs), help_text=item["tooltip"]) + field = IntegerField( + label=item["label"], + widget=TextInput(attrs=attrs), + help_text=item["tooltip"], + required=item.get("required", True), + ) yield {"name": name, "field": field} elif item["type"] == "switch": attrs = { diff --git a/digiplan/map/hooks.py b/digiplan/map/hooks.py index d7b90c08..72b7a520 100644 --- a/digiplan/map/hooks.py +++ b/digiplan/map/hooks.py @@ -1,10 +1,12 @@ """Module to implement hooks for django-oemof.""" +import logging +import math + import pandas as pd -from django.conf import settings from django.http import HttpRequest -from digiplan.map import config, forms +from digiplan.map import config, datapackage, forms def read_parameters(scenario: str, parameters: dict, request: HttpRequest) -> dict: # noqa: ARG001 @@ -60,17 +62,67 @@ def adapt_electricity_demand(scenario: str, data: dict, request: HttpRequest) -> dict Parameters for oemof with adapted demands """ + del data["s_v_1"] year = "2045" if scenario == "scenario_2045" else "2022" - for sector, slider in (("hh", "s_v_2"), ("cts", "s_v_3"), ("ind", "s_v_4")): - demand_filename = settings.DATA_DIR.path("scenarios").path(f"demand_{sector}_power_demand.csv") - demand = pd.read_csv(demand_filename) + for sector, slider in (("hh", "s_v_3"), ("cts", "s_v_4"), ("ind", "s_v_5")): + demand = datapackage.get_power_demand(sector)[sector] + logging.info(f"Adapting electricity demand at {sector=}.") data[f"ABW-electricity-demand_{sector}"] = {"amount": float(demand[year].sum()) * data.pop(slider) / 100} return data -def adapt_heat_demand(scenario: str, data: dict, request: HttpRequest) -> dict: # noqa: ARG001 +def adapt_heat_capacities(distribution: str, remaining_energy: pd.Series) -> dict: + """Adapt heat settings for remaining energy.""" + # TODO (Hendrik): Read values from datapackage # noqa: TD003 + heat_shares = { + "central": { + "ABW-wood-extchp_central": 0, + "ABW-biogas-bpchp_central": 0, + "ABW-ch4-bpchp_central": 0.5, + "ABW-ch4-extchp_central": 0.5, + "ABW-solar-thermalcollector_central": 0, + "ABW-ch4-boiler_central": 0, + }, + "decentral": { + "ABW-wood-extchp_decentral": 0.15, + "ABW-biogas-bpchp_decentral": 0.15, + "ABW-ch4-bpchp_decentral": 0.15, + "ABW-ch4-extchp_decentral": 0.30, + "ABW-solar-thermalcollector_decentral": 0, + "ABW-ch4-boiler_decentral": 0.15, + "ABW-wood-oven": 0.1, + }, + } + + remaining_energy_sum = remaining_energy.sum() + data = {} + for component, share in heat_shares[distribution].items(): + if share == 0: + continue + efficiency = datapackage.get_thermal_efficiency(component[4:]) + capacity = math.ceil((remaining_energy / efficiency).max() * share) + if capacity == 0: + continue + energy = remaining_energy_sum * share + if "extchp" in component or "bpchp" in component: + parameter = "input_parameters" + energy = energy / efficiency + else: + parameter = "output_parameters" + logging.info(f"Adapting capacity and energy for {component} at {distribution=}.") + data[component] = { + "capacity": capacity, + parameter: { + "summed_min": energy / capacity, + "summed_max": energy / capacity, + }, + } + return data + + +def adapt_heat_settings(scenario: str, data: dict, request: HttpRequest) -> dict: # noqa: ARG001 """ - Read settings and adapt related heat demands. + Read settings and adapt related heat demands and capacities. Parameters ---------- @@ -84,21 +136,82 @@ def adapt_heat_demand(scenario: str, data: dict, request: HttpRequest) -> dict: Returns ------- dict - Parameters for oemof with adapted heat demands + Parameters for oemof with adapted heat demands and capacities """ - year = "2045" if scenario == "scenario_2045" else "2022" - for sector, slider in (("hh", "w_v_3"), ("cts", "w_v_4"), ("ind", "w_v_5")): - percentage = data.pop(slider) - for location in ("central", "decentral"): - demand_filename = settings.DATA_DIR.path("scenarios").path(f"demand_{sector}_heat_demand_{location}.csv") - demand = pd.read_csv(demand_filename) - data[f"ABW-heat_{location}-demand_{sector}"] = { - "amount": float(demand[year].sum()) * percentage / 100, + demand_sliders = {"hh": "w_v_3", "cts": "w_v_4", "ind": "w_v_5"} + hp_sliders = {"hh": "w_d_wp_3", "cts": "w_d_wp_4", "ind": "w_d_wp_5"} + + heat_demand_per_municipality = datapackage.get_summed_heat_demand_per_municipality() + heat_demand = datapackage.get_heat_demand() + + for distribution in ("central", "decentral"): + demand = {} + hp_energy = {} + + # Calculate demands per sector + for sector in ("hh", "cts", "ind"): + summed_demand = int( # Convert to int, otherwise int64 is used + heat_demand_per_municipality[sector][distribution[:3]]["2045"].sum(), + ) + demand[sector] = heat_demand[sector][distribution] * summed_demand + percentage = ( + data.pop(demand_sliders[sector]) if distribution == "decentral" else data.get(demand_sliders[sector]) + ) + # DEMAND + logging.info(f"Adapting heat demand at {distribution=} and {sector=}.") + data[f"ABW-heat_{distribution}-demand_{sector}"] = { + "amount": summed_demand * percentage / 100, } + + # HP contribution per sector: + if distribution == "decentral": + hp_share = data.pop(hp_sliders[sector]) / 100 + hp_energy[sector] = demand[sector] * hp_share + else: + if sector == "hh": # noqa: PLR5501 + hp_share = data.pop("w_z_wp_3") / 100 + hp_energy[sector] = demand[sector] * hp_share + else: + hp_energy[sector] = demand[sector] * 0 + + # HP Capacity and Energies + hp_energy_total = pd.concat(hp_energy.values(), axis=1).sum(axis=1) + hp_energy_sum = hp_energy_total.sum() + capacity = math.ceil(hp_energy_total.max()) + logging.info(f"Adapting capacity and energy for heatpump at {distribution=}.") + data[f"ABW-electricity-heatpump_{distribution}"] = { + "capacity": capacity, + "output_parameters": { + "summed_min": hp_energy_sum / capacity, + "summed_max": hp_energy_sum / capacity, + }, + } + + total_demand = pd.concat(demand.values(), axis=1).sum(axis=1) + remaining_energy = total_demand - hp_energy_total + + # HEAT capacities + data.update(adapt_heat_capacities(distribution, remaining_energy)) + + # STORAGES + storage_sliders = {"decentral": "w_d_s_1", "central": "w_z_s_1"} + avg_demand_per_day = total_demand.sum() / 365 + logging.info(f"Adapting capacity for storage at {distribution=}.") + data[f"ABW-heat_{distribution}-storage"] = { + "capacity": float(avg_demand_per_day * data.pop(storage_sliders[distribution]) / 100), + } + + # Remove unnecessary heat settings + del data["w_v_1"] + del data["w_d_wp_1"] + del data["w_z_wp_1"] + del data["w_d_s_3"] + del data["w_z_s_3"] + return data -def adapt_capacities(scenario: str, data: dict, request: HttpRequest) -> dict: # noqa: ARG001 +def adapt_renewable_capacities(scenario: str, data: dict, request: HttpRequest) -> dict: # noqa: ARG001 """ Read renewable capacities from user input and adapt ES parameters accordingly. @@ -116,26 +229,23 @@ def adapt_capacities(scenario: str, data: dict, request: HttpRequest) -> dict: dict Adapted parameters dict with set up capacities """ + # ELECTRICITY data["ABW-wind-onshore"] = {"capacity": data.pop("s_w_1")} data["ABW-solar-pv_ground"] = {"capacity": data.pop("s_pv_ff_1")} data["ABW-solar-pv_rooftop"] = {"capacity": data.pop("s_pv_d_1")} - # TODO(Hendrik): Slider not yet implemented - # https://github.com/rl-institut-private/digiplan/issues/229 - data["ABW-hydro-ror"] = {"capacity": data.pop("ror")} - data["ABW-electricity-heatpump_decentral"] = {"capacity": data.pop("w_d_wp_1")} - data["ABW-electricity-heatpump_central"] = {"capacity": data.pop("w_z_wp_1")} - - # TODO(Hendrik): Get values either from static file or from sliders - # https://github.com/rl-institut-private/digipipe/issues/119 - data["ABW-biogas-bpchp_central"] = {"capacity": 100} - data["ABW-biogas-bpchp_decentral"] = {"capacity": 100} - data["ABW-wood-extchp_central"] = {"capacity": 100} - data["ABW-wood-extchp_decentral"] = {"capacity": 100} - data["ABW-ch4-bpchp_central"] = {"capacity": 100} - data["ABW-ch4-bpchp_decentral"] = {"capacity": 100} - data["ABW-ch4-extchp_central"] = {"capacity": 100} - data["ABW-ch4-extchp_decentral"] = {"capacity": 100} - data["ABW-ch4-gt"] = {"capacity": 100} - data["ABW-biogas-biogas_upgrading_plant"] = {"capacity": 100} - data["ABW-biomass-biogas_plant"] = {"capacity": 100} + data["ABW-hydro-ror"] = {"capacity": data.pop("s_h_1")} + data["ABW-electricity-large_scale_battery"] = {"capacity": data.pop("s_s_g_1")} + + # Remove unnecessary renewable sliders: + del data["s_w_3"] + del data["s_w_4"] + del data["s_w_4_1"] + del data["s_w_4_2"] + del data["s_w_5"] + del data["s_w_5_1"] + del data["s_w_5_2"] + del data["s_pv_ff_3"] + del data["s_pv_ff_4"] + del data["s_pv_d_3"] + del data["s_pv_d_4"] return data diff --git a/digiplan/map/managers.py b/digiplan/map/managers.py index 72f36748..74f2c6d7 100644 --- a/digiplan/map/managers.py +++ b/digiplan/map/managers.py @@ -1,3 +1,7 @@ +"""Module to hold MVT managers.""" +from typing import Optional + +import django.db.models from django.contrib.gis.db import models from django.contrib.gis.db.models.functions import Transform from django.contrib.gis.geos import Polygon @@ -8,48 +12,74 @@ # pylint: disable=W0223 -class AsMVTGeom(models.functions.GeomOutputGeoFunc): +class AsMVTGeom(models.functions.GeomOutputGeoFunc): # noqa: D101 function = "ST_AsMVTGeom" geom_param_pos = (0, 1) # pylint: disable=W0223 -class X(models.functions.Func): +class X(models.functions.Func): # noqa: D101 function = "ST_X" # pylint: disable=W0223 -class Y(models.functions.Func): +class Y(models.functions.Func): # noqa: D101 function = "ST_Y" class MVTManager(models.Manager): - def __init__(self, *args, geo_col="geom", columns=None, **kwargs): + """Manager to get MVTs from model geometry using postgres MVT abilities.""" + + def __init__( + self, + *args, # noqa: ANN002 + geo_col: str = "geom", + columns: Optional[list[str]] = None, + **kwargs, + ) -> None: + """Init.""" super().__init__(*args, **kwargs) self.geo_col = geo_col self.columns = columns - def get_mvt_query(self, x, y, z, filters=None): + def get_mvt_query(self, x: int, y: int, z: int, filters: Optional[dict] = None) -> tuple: + """Build MVT query; might be overwritten in child class.""" filters = filters or {} return self._build_mvt_query(x, y, z, filters) - def get_columns(self): + def get_columns(self) -> list[str]: + """Return columns to use as features in MVT.""" return self.columns or self._get_non_geom_columns() # pylint: disable=W0613,R0913 - def _filter_query(self, query, x, y, z, filters): + def _filter_query( # noqa: PLR0913 + self, + query: django.db.models.QuerySet, + x: int, # noqa: ARG002 + y: int, # noqa: ARG002 + z: int, # noqa: ARG002 + filters: dict, + ) -> django.db.models.QuerySet: + """ + Filter queryset for given filters. + + Might be overwritten in child class + """ return query.filter(**filters) - def _get_mvt_geom_query(self, x, y, z): + def _get_mvt_geom_query(self, x: int, y: int, z: int) -> django.db.models.QuerySet: + """Intersect bbox from given coordinates and return related MVT.""" bbox = Polygon.from_bbox(tile_edges(x, y, z)) bbox.srid = 4326 - query = self.annotate(mvt_geom=AsMVTGeom(Transform(self.geo_col, 3857), Transform(bbox, 3857), 4096, 0, False)) + query = self.annotate( + mvt_geom=AsMVTGeom(Transform(self.geo_col, 3857), Transform(bbox, 3857), 4096, 0, False), # noqa: FBT003 + ) intersect = {f"{self.geo_col}__intersects": bbox} return query.filter(**intersect) - def _build_mvt_query(self, x, y, z, filters): + def _build_mvt_query(self, x: int, y: int, z: int, filters: dict) -> str: """ - Creates MVT query + Create MVT query. Parameters ---------- @@ -84,9 +114,9 @@ def _build_mvt_query(self, x, y, z, filters): with connection.cursor() as cursor: return cursor.mogrify(sql, params).decode("utf-8") - def _get_non_geom_columns(self): + def _get_non_geom_columns(self) -> list[str]: """ - Retrieves all table columns that are NOT the defined geometry column + Retrieve all table columns that are NOT the defined geometry column. Returns ------- @@ -94,7 +124,7 @@ def _get_non_geom_columns(self): List of column names (excluding geom) """ columns = [] - for field in self.model._meta.get_fields(): + for field in self.model._meta.get_fields(): # noqa: SLF001 if hasattr(field, "get_attname_column"): column_name = field.get_attname_column()[1] if column_name != self.geo_col: @@ -103,41 +133,32 @@ def _get_non_geom_columns(self): class RegionMVTManager(MVTManager): - def get_queryset(self): - return super().get_queryset().annotate(bbox=models.functions.AsGeoJSON(models.functions.Envelope("geom"))) - + """Manager which adds bbox to layer features to better show regions in frontend.""" -class DistrictMVTManager(MVTManager): - def get_queryset(self): - return ( - super() - .get_queryset() - .annotate(bbox=models.functions.AsGeoJSON(models.functions.Envelope("geom"))) - .annotate(state_name=models.F("state__name")) - ) + def get_queryset(self) -> django.db.models.QuerySet: + """Annotate bbox to queryset.""" + return super().get_queryset().annotate(bbox=models.functions.AsGeoJSON(models.functions.Envelope("geom"))) class StaticMVTManager(MVTManager): + """Manager which does nothing?.""" + # pylint: disable=R0913 - def _filter_query(self, query, x, y, z, filters): + def _filter_query( # noqa: PLR0913 + self, + query: django.db.models.QuerySet, + x: int, + y: int, + z: int, + filters: dict, + ) -> django.db.models.QuerySet: query = super()._filter_query(query, x, y, z, filters) return query class LabelMVTManager(MVTManager): - def get_queryset(self): - return super().get_queryset().annotate(geom_label=models.functions.Centroid("geom")) + """Manager which adds centroid of geom to place label.""" - -class ClusterMVTManager(MVTManager): - def get_queryset(self): - return ( - super() - .get_queryset() - .annotate(center=models.functions.Centroid("geom")) - .annotate(state_name=models.F("district__state__name")) - .annotate(district_name=models.F("district__name")) - .annotate( - lat=X("center", output_field=models.DecimalField()), lon=Y("center", output_field=models.DecimalField()) - ) - ) + def get_queryset(self) -> django.db.models.QuerySet: + """Return queryset with added centroid.""" + return super().get_queryset().annotate(geom_label=models.functions.Centroid("geom")) diff --git a/digiplan/map/map_config.py b/digiplan/map/map_config.py index 2e95855d..9f149c63 100644 --- a/digiplan/map/map_config.py +++ b/digiplan/map/map_config.py @@ -1,17 +1,74 @@ """Actual map setup is done here.""" +import dataclasses from django.utils.translation import gettext_lazy as _ from django_mapengine import legend + +@dataclasses.dataclass +class SymbolLegendLayer(legend.LegendLayer): + """Adds symbol field.""" + + symbol: str = "rectangle" + + # TODO(Josi): Add real descriptions for layer info buttons # https://github.com/rl-institut-private/digiplan/issues/249 LEGEND = { _("Renewables"): [ - legend.LegendLayer(_("Wind turbine"), _("Wind turbine layer"), layer_id="wind", color="#6A89CC"), - legend.LegendLayer(_("Roof-mounted PV"), _("PV roof layer"), layer_id="pvroof", color="#FFD660"), - legend.LegendLayer(_("Outdoor PV"), _("Hydro layer"), layer_id="pvground", color="#F6B93B"), - legend.LegendLayer(_("Hydro"), _("Hydro layer"), layer_id="hydro", color="#9CC4D9"), - legend.LegendLayer(_("Biomass"), _("Wind turbine layer"), layer_id="biomass", color="#52C41A"), - legend.LegendLayer(_("Combustion"), _("Wind turbine layer"), layer_id="combustion", color="#1A1A1A"), + SymbolLegendLayer( + _("Wind turbine"), + _("Wind turbine layer"), + layer_id="wind", + color="#6A89CC", + symbol="circle", + ), + SymbolLegendLayer( + _("Roof-mounted PV"), + _("PV roof layer"), + layer_id="pvroof", + color="#FFD660", + symbol="circle", + ), + SymbolLegendLayer(_("Outdoor PV"), _("Hydro layer"), layer_id="pvground", color="#EFAD25", symbol="circle"), + SymbolLegendLayer(_("Hydro"), _("Hydro layer"), layer_id="hydro", color="#A9BDE8", symbol="circle"), + SymbolLegendLayer(_("Biomass"), _("Wind turbine layer"), layer_id="biomass", color="#52C41A", symbol="circle"), + SymbolLegendLayer( + _("Combustion"), + _("Wind turbine layer"), + layer_id="combustion", + color="#E6772E", + symbol="circle", + ), + SymbolLegendLayer(_("GSGK"), _("Wind turbine layer"), layer_id="gsgk", color="#C27BA0", symbol="circle"), + SymbolLegendLayer(_("Storage"), _("Wind turbine layer"), layer_id="storage", color="#8D2D5F", symbol="circle"), + ], + _("Settlements Infrastructure"): [ + legend.LegendLayer(_("Settlement 0m"), _("Aviation layer"), layer_id="settlement-0m"), + legend.LegendLayer(_("Industry"), _("Aviation layer"), layer_id="industry"), + legend.LegendLayer(_("Road Railway 500m"), _("Aviation layer"), layer_id="road_railway-500m_region"), + legend.LegendLayer(_("Road"), _("Aviation layer"), layer_id="road_default"), + legend.LegendLayer(_("Railway"), _("Aviation layer"), layer_id="railway"), + legend.LegendLayer(_("Aviation"), _("Aviation layer"), layer_id="aviation"), + legend.LegendLayer(_("Air Traffic"), _("Air traffic layer"), layer_id="air_traffic"), + legend.LegendLayer(_("Military"), _("Aviation layer"), layer_id="military"), + legend.LegendLayer(_("Grid"), _("Aviation layer"), layer_id="grid"), + ], + _("Nature Landscape"): [ + legend.LegendLayer(_("Nature Conservation Area"), _("layer info"), layer_id="nature_conservation_area"), + legend.LegendLayer(_("Fauna Flora Habitat"), _("layer info"), layer_id="fauna_flora_habitat"), + legend.LegendLayer(_("Special Protection Area"), _("layer info"), layer_id="special_protection_area"), + legend.LegendLayer(_("Biosphere Reserve"), _("Biosphere Reserve layer"), layer_id="biosphere_reserve"), + legend.LegendLayer(_("Landscape Protection Area"), _("layer info"), layer_id="landscape_protection_area"), + legend.LegendLayer(_("Forest"), _("layer info"), layer_id="forest"), + legend.LegendLayer( + _("Drinking Water Protection Area"), + _("layer info"), + layer_id="drinking_water_protection_area", + ), + legend.LegendLayer(_("Water"), _("layer info"), layer_id="water"), + legend.LegendLayer(_("Floodplain"), _("layer info"), layer_id="floodplain"), + legend.LegendLayer(_("Soil Quality High"), _("layer info"), layer_id="soil_quality_high"), + legend.LegendLayer(_("Soil Quality Low"), _("layer info"), layer_id="soil_quality_low"), ], } diff --git a/digiplan/map/migrations/0019_airtraffic.py b/digiplan/map/migrations/0019_airtraffic.py new file mode 100644 index 00000000..74c765e0 --- /dev/null +++ b/digiplan/map/migrations/0019_airtraffic.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2.19 on 2023-06-26 10:12 + +import django.contrib.gis.db.models.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('map', '0018_auto_20230307_1404'), + ] + + operations = [ + migrations.CreateModel( + name='AirTraffic', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(srid=4326)), + ], + ), + ] diff --git a/digiplan/map/migrations/0020_aviation.py b/digiplan/map/migrations/0020_aviation.py new file mode 100644 index 00000000..3d8fc19c --- /dev/null +++ b/digiplan/map/migrations/0020_aviation.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2.19 on 2023-06-26 10:30 + +import django.contrib.gis.db.models.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('map', '0019_airtraffic'), + ] + + operations = [ + migrations.CreateModel( + name='Aviation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(srid=4326)), + ], + ), + ] diff --git a/digiplan/map/migrations/0021_biospherereserve.py b/digiplan/map/migrations/0021_biospherereserve.py new file mode 100644 index 00000000..5e2d9132 --- /dev/null +++ b/digiplan/map/migrations/0021_biospherereserve.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2.19 on 2023-06-26 10:41 + +import django.contrib.gis.db.models.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('map', '0020_aviation'), + ] + + operations = [ + migrations.CreateModel( + name='BiosphereReserve', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(srid=4326)), + ], + ), + ] diff --git a/digiplan/map/migrations/0022_drinkingwaterarea_faunaflorahabitat_floodplain_forest_grid_gsgk_industry_landscapeprotectionarea_les.py b/digiplan/map/migrations/0022_drinkingwaterarea_faunaflorahabitat_floodplain_forest_grid_gsgk_industry_landscapeprotectionarea_les.py new file mode 100644 index 00000000..c7a1986b --- /dev/null +++ b/digiplan/map/migrations/0022_drinkingwaterarea_faunaflorahabitat_floodplain_forest_grid_gsgk_industry_landscapeprotectionarea_les.py @@ -0,0 +1,282 @@ +# Generated by Django 3.2.19 on 2023-06-26 14:27 + +import django.contrib.gis.db.models.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('map', '0021_biospherereserve'), + ] + + operations = [ + migrations.CreateModel( + name='DrinkingWaterArea', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(srid=4326)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='FaunaFloraHabitat', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(srid=4326)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Floodplain', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(srid=4326)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Forest', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(srid=4326)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Grid', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(srid=4326)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='GSGK', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('geom', django.contrib.gis.db.models.fields.PointField(srid=4326)), + ('name', models.CharField(max_length=255, null=True)), + ('zip_code', models.CharField(max_length=50, null=True)), + ('geometry_approximated', models.BooleanField()), + ('unit_count', models.BigIntegerField(null=True)), + ('capacity_net', models.FloatField(null=True)), + ('feedin_type', models.CharField(max_length=50, null=True)), + ('mun_id', models.IntegerField(null=True)), + ], + options={ + 'verbose_name': 'GSGK', + 'verbose_name_plural': 'GSGK', + }, + ), + migrations.CreateModel( + name='Industry', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(srid=4326)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='LandscapeProtectionArea', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(srid=4326)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='LessFavouredAreasAgricultural', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(srid=4326)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Military', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(srid=4326)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='NatureConservationArea', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(srid=4326)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='PotentialareaPVAgricultureLFAOff', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(srid=4326)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='PotentialareaPVRoadRailway', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(srid=4326)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='PotentialareaWindSTP2018Vreg', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(srid=4326)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='PotentialareaWindSTP2027Repowering', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(srid=4326)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='PotentialareaWindSTP2027SearchAreaForestArea', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(srid=4326)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='PotentialareaWindSTP2027SearchAreaOpenArea', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(srid=4326)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='PotentialareaWindSTP2027VR', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(srid=4326)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Railway', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(srid=4326)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Road', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(srid=4326)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='RoadRailway500m', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(srid=4326)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Settlement0m', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(srid=4326)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='SoilQualityHigh', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(srid=4326)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='SoilQualityLow', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(srid=4326)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='SpecialProtectionArea', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(srid=4326)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Water', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(srid=4326)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/digiplan/map/migrations/0023_storage.py b/digiplan/map/migrations/0023_storage.py new file mode 100644 index 00000000..f58a64ac --- /dev/null +++ b/digiplan/map/migrations/0023_storage.py @@ -0,0 +1,31 @@ +# Generated by Django 3.2.19 on 2023-06-26 15:11 + +import django.contrib.gis.db.models.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('map', '0022_drinkingwaterarea_faunaflorahabitat_floodplain_forest_grid_gsgk_industry_landscapeprotectionarea_les'), + ] + + operations = [ + migrations.CreateModel( + name='Storage', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('geom', django.contrib.gis.db.models.fields.PointField(srid=4326)), + ('name', models.CharField(max_length=255, null=True)), + ('zip_code', models.CharField(max_length=50, null=True)), + ('geometry_approximated', models.BooleanField()), + ('unit_count', models.BigIntegerField(null=True)), + ('capacity_net', models.FloatField(null=True)), + ('mun_id', models.IntegerField(null=True)), + ], + options={ + 'verbose_name': 'Storage', + 'verbose_name_plural': 'Storages', + }, + ), + ] diff --git a/digiplan/map/migrations/0024_auto_20230706_1135.py b/digiplan/map/migrations/0024_auto_20230706_1135.py new file mode 100644 index 00000000..d9b24eea --- /dev/null +++ b/digiplan/map/migrations/0024_auto_20230706_1135.py @@ -0,0 +1,54 @@ +# Generated by Django 3.2.20 on 2023-07-06 11:35 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('map', '0023_storage'), + ] + + operations = [ + migrations.AlterField( + model_name='biomass', + name='mun_id', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='map.municipality'), + ), + migrations.AlterField( + model_name='combustion', + name='mun_id', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='map.municipality'), + ), + migrations.AlterField( + model_name='gsgk', + name='mun_id', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='map.municipality'), + ), + migrations.AlterField( + model_name='hydro', + name='mun_id', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='map.municipality'), + ), + migrations.AlterField( + model_name='pvground', + name='mun_id', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='map.municipality'), + ), + migrations.AlterField( + model_name='pvroof', + name='mun_id', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='map.municipality'), + ), + migrations.AlterField( + model_name='storage', + name='mun_id', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='map.municipality'), + ), + migrations.AlterField( + model_name='windturbine', + name='mun_id', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='map.municipality'), + ), + ] diff --git a/digiplan/map/models.py b/digiplan/map/models.py index 0ca68269..f5b368d2 100644 --- a/digiplan/map/models.py +++ b/digiplan/map/models.py @@ -1,7 +1,7 @@ """Digiplan models.""" -from typing import Optional +import pandas as pd from django.contrib.gis.db import models from django.db.models import Sum from django.utils.translation import gettext_lazy as _ @@ -80,139 +80,48 @@ class Meta: # noqa: D106 verbose_name_plural = _("Population") @classmethod - def quantity(cls, year: int, mun_id: Optional[int] = None) -> int: + def quantity_per_municipality_per_year(cls) -> pd.DataFrame: """ - Calculate population in 2022 (either for municipality or for whole region). - - Parameters - ---------- - year: int - Year to filter population for - mun_id: Optional[int] - If given, population for given municipality are calculated. If not, for whole region. + Return population in 2022 per municipality and year. Returns ------- - int - Value of population - """ - if mun_id is not None: - return cls.objects.filter(year=year, municipality__id=mun_id).aggregate(sum=Sum("value"))["sum"] - return cls.objects.filter(year=year).aggregate(sum=Sum("value"))["sum"] - - @classmethod - def population_history(cls, mun_id: int) -> models.QuerySet: - """ - Get chart for population per municipality in different years. - - Parameters - ---------- - mun_id: int - Related municipality - - Returns - ------- - models.QuerySet - containing list of year/value pairs - """ - return cls.objects.filter(municipality__id=mun_id).values_list("year", "value") - - @classmethod - def population_per_municipality(cls) -> dict[int, int]: - """ - Calculate population per municipality. - - Returns - ------- - dict[int, int] - Population per municipality - """ - return {row.municipality_id: row.value for row in cls.objects.filter(year=2022)} - - @classmethod - def density(cls, year: int, mun_id: Optional[int] = None) -> float: - """ - Calculate population denisty in given year per km² (either for municipality or for whole region). - - Parameters - ---------- - year: int - Year to filter population for - mun_id: Optional[int] - If given, population per km² for given municipality are calculated. If not, for whole region. - - Returns - ------- - float - Value of population density - """ - population = cls.quantity(year, mun_id=mun_id) - - if mun_id is not None: - density = population / Municipality.objects.get(pk=mun_id).area - else: - density = population / Municipality.area_whole_region() - return density - - @classmethod - def density_history(cls, mun_id: int) -> dict: + pd.DataFrame + Population per municipality (index) and year (column) """ - Get chart for population density for the given municipality in different years. + population_per_year = ( + pd.DataFrame.from_records(cls.objects.all().values("municipality__id", "year", "value")) # noqa: PD010 + .set_index("municipality__id") + .pivot(columns="year") + ) + population_per_year.columns = population_per_year.columns.droplevel(0) + return population_per_year - Parameters - ---------- - mun_id: int - Related municipality - Returns - ------- - dict - Chart data to use in JS - """ - density_history = [] - population_history = cls.objects.filter(municipality_id=mun_id).values_list("year", "value") +class RenewableModel(models.Model): + """Base class for renewable cluster models.""" - for year, value in population_history: - density = value / Municipality.objects.get(pk=mun_id).area - density_history.append((year, density)) + geom = models.PointField(srid=4326) + name = models.CharField(max_length=255, null=True) + geometry_approximated = models.BooleanField() + unit_count = models.BigIntegerField(null=True) + capacity_net = models.FloatField(null=True) + zip_code = models.CharField(max_length=50, null=True) - return density_history + mun_id = models.ForeignKey(Municipality, on_delete=models.DO_NOTHING, null=True) - @classmethod - def density_per_municipality(cls) -> dict[int, int]: - """ - Calculate population per municipality. + objects = models.Manager() - Returns - ------- - dict[int, int] - Population per municipality - """ - density = cls.population_per_municipality() - for mun_id in density: - density[mun_id] = density[mun_id] / Municipality.objects.get(pk=mun_id).area - return density + class Meta: # noqa: D106 + abstract = True -class WindTurbine(models.Model): +class WindTurbine(RenewableModel): """Model holding wind turbines.""" - geom = models.PointField(srid=4326) - name = models.CharField(max_length=255, null=True) name_park = models.CharField(max_length=255, null=True) - geometry_approximated = models.BooleanField() - unit_count = models.BigIntegerField(null=True) - capacity_net = models.FloatField(null=True) hub_height = models.FloatField(null=True) - zip_code = models.CharField(max_length=50, null=True) rotor_diameter = models.FloatField(null=True) - mun_id = models.IntegerField(null=True) - - objects = models.Manager() - vector_tiles = StaticMVTManager( - geo_col="geom", - columns=["id", "name", "unit_count", "capacity_net", "geometry_approximated", "mun_id"], - ) data_file = "bnetza_mastr_wind_agg_region" layer = "bnetza_mastr_wind" @@ -238,138 +147,24 @@ def __str__(self) -> str: return self.name @classmethod - def quantity(cls, municipality_id: Optional[int] = None) -> int: - """ - Calculate number of windturbines (either for municipality or for whole region). - - Parameters - ---------- - municipality_id: Optional[int] - If given, number of windturbines for given municipality are calculated. If not, for whole region. - - Returns - ------- - int - Sum of windturbines - """ - values = cls.quantity_per_municipality() - windturbines = 0 - - if municipality_id is not None: - windturbines = values[municipality_id] - else: - for index in values: - windturbines += values[index] - return windturbines - - @classmethod - def quantity_per_municipality(cls) -> dict[int, int]: + def quantity_per_municipality(cls) -> pd.DataFrame: """ Calculate number of wind turbines per municipality. Returns ------- - dict[int, int] + dpd.DataFrame wind turbines per municipality """ - windturbines = {} - municipalities = Municipality.objects.all() - - for mun in municipalities: - res_windturbine = cls.objects.filter(mun_id=mun.id).aggregate(Sum("unit_count"))["unit_count__sum"] - if res_windturbine is None: - res_windturbine = 0 - windturbines[mun.id] = res_windturbine - return windturbines - - @classmethod - def wind_turbines_history(cls, municipality_id: int) -> dict: # noqa: ARG003 - """ - Get chart for wind turbines. + queryset = cls.objects.values("mun_id").annotate(units=Sum("unit_count")).values("mun_id", "units") + wind_turbines = pd.DataFrame.from_records(queryset).set_index("mun_id") + return wind_turbines["units"].reindex(Municipality.objects.all().values_list("id", flat=True), fill_value=0) - Parameters - ---------- - municipality_id: int - Related municipality - - Returns - ------- - dict - Chart data to use in JS - """ - return [(2023, 2), (2046, 3), (2050, 4)] - - @classmethod - def quantity_per_square(cls, municipality_id: Optional[int] = None) -> float: - """ - Calculate number of windturbines per km² (either for municipality or for whole region). - Parameters - ---------- - municipality_id: Optional[int] - If given, number of windturbines per km² for given municipality are calculated. If not, for whole region. - - Returns - ------- - float - Sum of windturbines per km² - """ - windturbines = cls.quantity(municipality_id) - - if municipality_id is not None: - return windturbines / Municipality.objects.get(pk=municipality_id).area - return windturbines / Municipality.area_whole_region() - - @classmethod - def wind_turbines_per_area_history(cls, municipality_id: int) -> dict: # noqa: ARG003 - """ - Get chart for wind turbines per km². - - Parameters - ---------- - municipality_id: int - Related municipality - - Returns - ------- - dict - Chart data to use in JS - """ - return [(2023, 2), (2046, 3), (2050, 4)] - - @classmethod - def quantity_per_mun_and_area(cls) -> dict[int, int]: - """ - Calculate windturbines per km² per municipality. - - Returns - ------- - dict[int, int] - windturbines per km² per municipality - """ - windtubines = cls.quantity_per_municipality() - for index in windtubines: - windtubines[index] = windtubines[index] / Municipality.objects.get(pk=index).area - return windtubines - - -class PVroof(models.Model): +class PVroof(RenewableModel): """Model holding PV roof.""" - geom = models.PointField(srid=4326) - name = models.CharField(max_length=255, null=True) - zip_code = models.CharField(max_length=50, null=True) - geometry_approximated = models.BooleanField() - unit_count = models.BigIntegerField(null=True) - capacity_net = models.FloatField(null=True) power_limitation = models.CharField(max_length=50, null=True) - mun_id = models.IntegerField(null=True) - - objects = models.Manager() - vector_tiles = StaticMVTManager( - geo_col="geom", - columns=["id", "name", "unit_count", "capacity_net", "geometry_approximated", "mun_id"], - ) data_file = "bnetza_mastr_pv_roof_agg_region" layer = "bnetza_mastr_pv_roof" @@ -394,23 +189,10 @@ def __str__(self) -> str: return self.name -class PVground(models.Model): +class PVground(RenewableModel): """Model holding PV on ground.""" - geom = models.PointField(srid=4326) - name = models.CharField(max_length=255, null=True) - zip_code = models.CharField(max_length=50, null=True) - geometry_approximated = models.BooleanField() - unit_count = models.BigIntegerField(null=True) - capacity_net = models.FloatField(null=True) power_limitation = models.CharField(max_length=50, null=True) - mun_id = models.IntegerField(null=True) - - objects = models.Manager() - vector_tiles = StaticMVTManager( - geo_col="geom", - columns=["id", "name", "unit_count", "capacity_net", "geometry_approximated", "mun_id"], - ) data_file = "bnetza_mastr_pv_ground_agg_region" layer = "bnetza_mastr_pv_ground" @@ -431,23 +213,10 @@ class Meta: # noqa: D106 verbose_name_plural = _("Outdoor PVs") -class Hydro(models.Model): +class Hydro(RenewableModel): """Hydro model.""" - geom = models.PointField(srid=4326) - name = models.CharField(max_length=255, null=True) - zip_code = models.CharField(max_length=50, null=True) - geometry_approximated = models.BooleanField() - unit_count = models.BigIntegerField(null=True) - capacity_net = models.FloatField(null=True) water_origin = models.CharField(max_length=255, null=True) - mun_id = models.IntegerField(null=True) - - objects = models.Manager() - vector_tiles = StaticMVTManager( - geo_col="geom", - columns=["id", "name", "unit_count", "capacity_net", "geometry_approximated", "mun_id"], - ) data_file = "bnetza_mastr_hydro_agg_region" layer = "bnetza_mastr_hydro" @@ -468,23 +237,10 @@ class Meta: # noqa: D106 verbose_name_plural = _("Hydro") -class Biomass(models.Model): +class Biomass(RenewableModel): """Biomass model.""" - geom = models.PointField(srid=4326) - name = models.CharField(max_length=255, null=True) - zip_code = models.CharField(max_length=50, null=True) - geometry_approximated = models.BooleanField() - unit_count = models.BigIntegerField(null=True) - capacity_net = models.FloatField(null=True) fuel_type = models.CharField(max_length=50, null=True) - mun_id = models.IntegerField(null=True) - - objects = models.Manager() - vector_tiles = StaticMVTManager( - geo_col="geom", - columns=["id", "name", "unit_count", "capacity_net", "geometry_approximated", "mun_id"], - ) data_file = "bnetza_mastr_biomass_agg_region" layer = "bnetza_mastr_biomass" @@ -505,23 +261,10 @@ class Meta: # noqa: D106 verbose_name_plural = _("Biomass") -class Combustion(models.Model): +class Combustion(RenewableModel): """Combustion model.""" - geom = models.PointField(srid=4326) - name = models.CharField(max_length=255, null=True) name_block = models.CharField(max_length=255, null=True) - zip_code = models.CharField(max_length=50, null=True) - geometry_approximated = models.BooleanField() - unit_count = models.BigIntegerField(null=True) - capacity_net = models.FloatField(null=True) - mun_id = models.IntegerField(null=True) - - objects = models.Manager() - vector_tiles = StaticMVTManager( - geo_col="geom", - columns=["id", "name", "unit_count", "capacity_net", "geometry_approximated", "mun_id"], - ) data_file = "bnetza_mastr_combustion_agg_region" layer = "bnetza_mastr_combustion" @@ -542,4 +285,200 @@ class Meta: # noqa: D106 verbose_name_plural = _("Combustion") -RENEWABLES = (WindTurbine, PVroof, PVground, Hydro, Biomass) +class GSGK(RenewableModel): + """GSGK model.""" + + feedin_type = models.CharField(max_length=50, null=True) + + data_file = "bnetza_mastr_gsgk_agg_region" + layer = "bnetza_mastr_gsgk" + + mapping = { + "geom": "POINT", + "name": "name", + "zip_code": "zip_code", + "geometry_approximated": "geometry_approximated", + "unit_count": "unit_count", + "capacity_net": "capacity_net", + "feedin_type": "feedin_type", + "mun_id": "municipality_id", + } + + class Meta: # noqa: D106 + verbose_name = _("GSGK") + verbose_name_plural = _("GSGK") + + +class Storage(RenewableModel): + """Storage model.""" + + data_file = "bnetza_mastr_storage_agg_region" + layer = "bnetza_mastr_storage" + + mapping = { + "geom": "POINT", + "name": "name", + "zip_code": "zip_code", + "geometry_approximated": "geometry_approximated", + "unit_count": "unit_count", + "capacity_net": "capacity_net", + "mun_id": "municipality_id", + } + + class Meta: # noqa: D106 + verbose_name = _("Storage") + verbose_name_plural = _("Storages") + + +class StaticRegionModel(models.Model): + """Base class for static region models.""" + + geom = models.MultiPolygonField(srid=4326) + + objects = models.Manager() + vector_tiles = StaticMVTManager(columns=[]) + + mapping = {"geom": "MULTIPOLYGON"} + + class Meta: # noqa: D106 + abstract = True + + +class AirTraffic(StaticRegionModel): # noqa: D101 + data_file = "air_traffic_control_system_region" + layer = "air_traffic_control_system" + + +class Aviation(StaticRegionModel): # noqa: D101 + data_file = "aviation_region" + layer = "aviation" + + +class BiosphereReserve(StaticRegionModel): # noqa: D101 + data_file = "biosphere_reserve_region" + layer = "biosphere_reserve" + + +class DrinkingWaterArea(StaticRegionModel): # noqa: D101 + data_file = "drinking_water_protection_area_region" + layer = "drinking_water_protection_area" + + +class FaunaFloraHabitat(StaticRegionModel): # noqa: D101 + data_file = "fauna_flora_habitat_region" + layer = "fauna_flora_habitat" + + +class Floodplain(StaticRegionModel): # noqa: D101 + data_file = "floodplain_region" + layer = "floodplain" + + +class Forest(StaticRegionModel): # noqa: D101 + data_file = "forest_region" + layer = "forest" + + +class Grid(StaticRegionModel): # noqa: D101 + data_file = "grid_region" + layer = "grid" + + +class Industry(StaticRegionModel): # noqa: D101 + data_file = "industry_region" + layer = "industry" + + +class LandscapeProtectionArea(StaticRegionModel): # noqa: D101 + data_file = "landscape_protection_area_region" + layer = "landscape_protection_area" + + +class LessFavouredAreasAgricultural(StaticRegionModel): # noqa: D101 + data_file = "less_favoured_areas_agricultural_region" + layer = "less_favoured_areas_agricultural" + + +class Military(StaticRegionModel): # noqa: D101 + data_file = "military_region" + layer = "military" + + +class NatureConservationArea(StaticRegionModel): # noqa: D101 + data_file = "nature_conservation_area_region" + layer = "nature_conservation_area" + + +class Railway(StaticRegionModel): # noqa: D101 + data_file = "railway_region" + layer = "railway" + + +class RoadRailway500m(StaticRegionModel): # noqa: D101 + data_file = "road_railway-500m_region" + layer = "road_railway-500m" + + +class Road(StaticRegionModel): # noqa: D101 + data_file = "road_region" + layer = "road" + + +class Settlement0m(StaticRegionModel): # noqa: D101 + data_file = "settlement-0m_region" + layer = "settlement-0m" + + +class SoilQualityHigh(StaticRegionModel): # noqa: D101 + data_file = "soil_quality_high_region" + layer = "soil_quality_high" + + +class SoilQualityLow(StaticRegionModel): # noqa: D101 + data_file = "soil_quality_low_region" + layer = "soil_quality_low" + + +class SpecialProtectionArea(StaticRegionModel): # noqa: D101 + data_file = "special_protection_area_region" + layer = "special_protection_area" + + +class Water(StaticRegionModel): # noqa: D101 + data_file = "water_region" + layer = "water" + + +class PotentialareaPVAgricultureLFAOff(StaticRegionModel): # noqa: D101 + data_file = "potentialarea_pv_agriculture_lfa-off_region" + layer = "potentialarea_pv_agriculture_lfa-off_region" + + +class PotentialareaPVRoadRailway(StaticRegionModel): # noqa: D101 + data_file = "potentialarea_pv_road_railway_region" + layer = "potentialarea_pv_road_railway_region" + + +class PotentialareaWindSTP2018Vreg(StaticRegionModel): # noqa: D101 + data_file = "potentialarea_wind_stp_2018_vreg" + layer = "potentialarea_wind_stp_2018_vreg" + + +class PotentialareaWindSTP2027Repowering(StaticRegionModel): # noqa: D101 + data_file = "potentialarea_wind_stp_2027_repowering" + layer = "potentialarea_wind_stp_2027_repowering" + + +class PotentialareaWindSTP2027SearchAreaForestArea(StaticRegionModel): # noqa: D101 + data_file = "potentialarea_wind_stp_2027_search_area_forest_area" + layer = "potentialarea_wind_stp_2027_search_area_forest_area" + + +class PotentialareaWindSTP2027SearchAreaOpenArea(StaticRegionModel): # noqa: D101 + data_file = "potentialarea_wind_stp_2027_search_area_open_area" + layer = "potentialarea_wind_stp_2027_search_area_open_area" + + +class PotentialareaWindSTP2027VR(StaticRegionModel): # noqa: D101 + data_file = "potentialarea_wind_stp_2027_vr" + layer = "potentialarea_wind_stp_2027_vr" diff --git a/digiplan/map/popups.py b/digiplan/map/popups.py index e15696b4..62498976 100644 --- a/digiplan/map/popups.py +++ b/digiplan/map/popups.py @@ -1,22 +1,43 @@ """Provide popups for digiplan.""" import abc -import json -import pathlib +from collections import namedtuple from collections.abc import Iterable from typing import Optional, Union +import pandas as pd +from django.db.models import F +from django.utils.translation import gettext_lazy as _ from django_mapengine import popups from django_oemof import results from oemof.tabular.postprocessing import core -from . import calculations, charts, config, models +from . import calculations, charts, models + +Source = namedtuple("Source", ("name", "url")) class RegionPopup(popups.ChartPopup): """Popup containing values for municipality and region in header.""" + lookup: Optional[str] = None + title: str = None + description: str = None unit: str = None + sources: Optional[list[Source]] = None + + def __init__( + self, + lookup: str, + selected_id: int, + map_state: Optional[dict] = None, + template: Optional[str] = None, + ) -> None: + """Initialize parent popup class and adds initialization of detailed data.""" + if self.lookup: + lookup = self.lookup + super().__init__(lookup, selected_id, map_state, template) + self.detailed_data = self.get_detailed_data() def get_context_data(self) -> dict: """ @@ -27,16 +48,15 @@ def get_context_data(self) -> dict: dict context dict including region and municipality data """ - with pathlib.Path(config.POPUPS_DIR.path(f"{self.lookup}.json")).open("r", encoding="utf-8") as data_json: - data = json.load(data_json) - - data["id"] = self.selected_id - data["data"]["region_value"] = self.get_region_value() - data["data"]["municipality_value"] = self.get_municipality_value() - data["data"]["unit"] = self.unit - data["municipality"] = models.Municipality.objects.get(pk=self.selected_id) - - return data + return { + "id": self.selected_id, + "title": self.title, + "description": self.description, + "unit": self.unit, + "region_value": self.get_region_value(), + "municipality_value": self.get_municipality_value(), + "municipality": models.Municipality.objects.get(pk=self.selected_id), + } def get_chart_options(self) -> dict: """ @@ -51,16 +71,29 @@ def get_chart_options(self) -> dict: return charts.create_chart(self.lookup, chart_data) @abc.abstractmethod + def get_detailed_data(self) -> pd.DataFrame: + """ + Return detailed data for each municipality and technology/component. + + Municipality IDs are stored in index, components/technologies/etc. are stored in columns + """ + def get_region_value(self) -> float: - """Must be overwritten.""" + """Return aggregated data of all municipalities and technologies.""" + return self.detailed_data.sum().sum() - @abc.abstractmethod def get_municipality_value(self) -> Optional[float]: - """Must be overwritten.""" + """Return aggregated data for all technologies for given municipality ID.""" + if self.selected_id not in self.detailed_data.index: + return 0 + return self.detailed_data.loc[self.selected_id].sum() - @abc.abstractmethod def get_chart_data(self) -> Iterable: - """Must be overwritten.""" + """Return data for given municipality ID.""" + if self.selected_id not in self.detailed_data.index: + msg = "No chart data available for given ID" + raise KeyError(msg) + return self.detailed_data.loc[self.selected_id] class SimulationPopup(RegionPopup, abc.ABC): @@ -94,107 +127,469 @@ def __init__( self.result = list(results.get_results(self.simulation_id, [self.calculation]).values())[0] +class ClusterPopup(popups.Popup): + """Popup for clusters.""" + + def __init__(self, lookup: str, selected_id: int, **kwargs) -> None: # noqa: ARG002 + """Initialize popup with default cluster template.""" + self.model_lookup = lookup + super().__init__(lookup="cluster", selected_id=selected_id) + + def get_context_data(self) -> dict: + """Return cluster data as context data.""" + model = { + "wind": models.WindTurbine, + "pvroof": models.PVroof, + "pvground": models.PVground, + "hydro": models.Hydro, + "biomass": models.Biomass, + "combustion": models.Combustion, + "gsgk": models.GSGK, + "storage": models.Storage, + }[self.model_lookup] + # TODO(Hendrik Huyskens): Add mapping + # https://github.com/rl-institut-private/digiplan/issues/153 + default_attributes = { + "name": "Name", + "mun_name": "Gemeindename", + "zip_code": "Postleitzahl", + "geometry_approximated": "Geschätzt", + "unit_count": "Anzahl", + "capacity_net": "Kapazität", + } + instance = model.objects.annotate(mun_name=F("mun_id__name")).get(pk=self.selected_id) + return { + "title": model._meta.verbose_name, # noqa: SLF001 + "data": {name: getattr(instance, key) for key, name in default_attributes.items()}, + } + + class CapacityPopup(RegionPopup): """Popup to show capacities.""" - def get_region_value(self) -> float: # noqa: D102 - return calculations.capacity() + lookup = "capacity" - def get_municipality_value(self) -> float: # noqa: D102 - return calculations.capacity(self.selected_id) - - def get_chart_data(self) -> Iterable: # noqa: D102 - return calculations.capacity_comparison(self.selected_id) + def get_detailed_data(self) -> pd.DataFrame: # noqa: D102 + return calculations.capacities_per_municipality() class CapacitySquarePopup(RegionPopup): """Popup to show capacities per km².""" - def get_region_value(self) -> float: # noqa: D102 - return calculations.capacity_square + lookup = "capacity" + + def get_detailed_data(self) -> pd.DataFrame: + """Return capacities per square kilometer.""" + capacities = calculations.capacities_per_municipality() + return calculations.calculate_square_for_value(capacities) - def get_municipality_value(self) -> float: # noqa: D102 - return calculations.capacity_square(self.selected_id) + def get_chart_options(self) -> dict: + """Overwrite title and unit.""" + chart_options = super().get_chart_options() + chart_options["title"]["text"] = _("Installed capacities per square meter") + chart_options["yAxis"]["name"] = _("MW/km²") + return chart_options - def get_chart_data(self) -> Iterable: # noqa: D102 - return calculations.capacity_square_comparison(self.selected_id) +class EnergyPopup(RegionPopup): + """Popup to show energies.""" -class PopulationPopup(RegionPopup): - """Popup to show Population.""" + lookup = "capacity" + title = _("Energies") + + def get_detailed_data(self) -> pd.DataFrame: # noqa: D102 + return calculations.energies_per_municipality() + + def get_chart_options(self) -> dict: + """Overwrite title and unit.""" + chart_options = super().get_chart_options() + chart_options["title"]["text"] = _("Energies per technology") + chart_options["yAxis"]["name"] = _("GWh") + return chart_options + + +class Energy2045Popup(RegionPopup): + """Popup to show energies.""" + + lookup = "capacity" + title = _("Energies") + + def get_detailed_data(self) -> pd.DataFrame: # noqa: D102 + return calculations.energies_per_municipality_2045(self.map_state["simulation_id"]) + + def get_chart_options(self) -> dict: + """Overwrite title and unit.""" + chart_options = super().get_chart_options() + chart_options["title"]["text"] = _("Energies per technology") + chart_options["yAxis"]["name"] = _("GWh") + chart_options["xAxis"]["data"] = ["Status Quo", "Mein Szenario"] + return chart_options + + def get_chart_data(self) -> Iterable: + """Create capacity chart data for SQ and future scenario.""" + status_quo_data = calculations.energies_per_municipality().loc[self.selected_id] + future_data = super().get_chart_data() + return list(zip(status_quo_data, future_data)) + + +class EnergySharePopup(RegionPopup): + """Popup to show energy shares.""" + + lookup = "capacity" + title = _("Energie Shares") + + def get_detailed_data(self) -> pd.DataFrame: # noqa: D102 + return calculations.energy_shares_per_municipality() + + def get_chart_options(self) -> dict: + """Overwrite title and unit.""" + chart_options = super().get_chart_options() + chart_options["title"]["text"] = _("Energy shares per technology") + chart_options["yAxis"]["name"] = _("%") + return chart_options + + +class EnergyCapitaPopup(RegionPopup): + """Popup to show energy shares per population.""" + + lookup = "capacity" + title = _("Gewonnene Energie pro EW") + + def get_detailed_data(self) -> pd.DataFrame: # noqa: D102 + return calculations.calculate_capita_for_value(calculations.energies_per_municipality()) * 1e3 + + def get_chart_options(self) -> dict: + """Overwrite title and unit.""" + chart_options = super().get_chart_options() + chart_options["title"]["text"] = _("Energy per capita per technology") + chart_options["yAxis"]["name"] = _("MWh") + return chart_options + + +class EnergyCapita2045Popup(RegionPopup): + """Popup to show energies.""" + + lookup = "capacity" + title = _("Gewonnene Energie pro EW") + + def get_detailed_data(self) -> pd.DataFrame: # noqa: D102 + return calculations.calculate_capita_for_value( + calculations.energies_per_municipality_2045(self.map_state["simulation_id"]), + ) + + def get_chart_options(self) -> dict: + """Overwrite title and unit.""" + chart_options = super().get_chart_options() + chart_options["title"]["text"] = _("Energies per capita per technology") + chart_options["yAxis"]["name"] = _("GWh") + chart_options["xAxis"]["data"] = ["Status Quo", "Mein Szenario"] + return chart_options + + def get_chart_data(self) -> Iterable: + """Create capacity chart data for SQ and future scenario.""" + status_quo_data = calculations.calculate_capita_for_value(calculations.energies_per_municipality()).loc[ + self.selected_id + ] + future_data = super().get_chart_data() + return list(zip(status_quo_data, future_data)) + + +class EnergySquarePopup(RegionPopup): + """Popup to show energy shares per km².""" + + lookup = "capacity" + title = _("Gewonnene Energie pro km²") + + def get_detailed_data(self) -> pd.DataFrame: # noqa: D102 + return calculations.calculate_square_for_value(calculations.energies_per_municipality()) * 1e3 + + def get_chart_options(self) -> dict: + """Overwrite title and unit.""" + chart_options = super().get_chart_options() + chart_options["title"]["text"] = _("Energie pro km²") + chart_options["yAxis"]["name"] = _("MWh") + return chart_options - def get_region_value(self) -> float: # noqa: D102 - return models.Population.quantity(2022) - def get_municipality_value(self) -> float: # noqa: D102 - return models.Population.quantity(2022, self.selected_id) +class EnergySquare2045Popup(RegionPopup): + """Popup to show energies.""" + + lookup = "capacity" + title = _("Gewonnene Energie pro km²") + + def get_detailed_data(self) -> pd.DataFrame: # noqa: D102 + return calculations.calculate_square_for_value( + calculations.energies_per_municipality_2045(self.map_state["simulation_id"]), + ) + + def get_chart_options(self) -> dict: + """Overwrite title and unit.""" + chart_options = super().get_chart_options() + chart_options["title"]["text"] = _("Gewonnene Energie pro km²") + chart_options["yAxis"]["name"] = _("MWh") + chart_options["xAxis"]["data"] = ["Status Quo", "Mein Szenario"] + return chart_options def get_chart_data(self) -> Iterable: - return models.Population.population_history(self.selected_id) + """Create capacity chart data for SQ and future scenario.""" + status_quo_data = calculations.calculate_square_for_value(calculations.energies_per_municipality()).loc[ + self.selected_id + ] + future_data = super().get_chart_data() + return list(zip(status_quo_data, future_data)) + + +class PopulationPopup(RegionPopup): + """Popup to show Population.""" + + lookup = "population" + + def get_detailed_data(self) -> pd.DataFrame: + """Return population data.""" + return models.Population.quantity_per_municipality_per_year() class PopulationDensityPopup(RegionPopup): """Popup to show Population Density.""" - def get_region_value(self) -> float: # noqa: D102 - return models.Population.density(2022) + lookup = "population" + + def get_detailed_data(self) -> pd.DataFrame: + """Return population data squared.""" + population = models.Population.quantity_per_municipality_per_year() + return calculations.calculate_square_for_value(population) + + def get_chart_options(self) -> dict: + """Overwrite title and unit.""" + chart_options = super().get_chart_options() + chart_options["title"]["text"] = _("Population density per year") + chart_options["yAxis"]["name"] = _("Pop/km²") + return chart_options + - def get_municipality_value(self) -> float: # noqa: D102 - return models.Population.density(2022, self.selected_id) +class EmployeesPopup(RegionPopup): + """Popup to show employees.""" - def get_chart_data(self) -> Iterable: # noqa: D102 - return models.Population.density_history(self.selected_id) + lookup = "wind_turbines" + title = _("Beschäftigte") + def get_detailed_data(self) -> pd.DataFrame: # noqa: D102 + return calculations.employment_per_municipality() -class RenewableElectricityProductionPopup(SimulationPopup): - """Popup to show renewable electricity production values.""" + def get_chart_data(self) -> Iterable: + """Return single value for employeess in current municipality.""" + return [int(self.detailed_data.loc[self.selected_id])] - unit = "MWh" - calculation = calculations.electricity_production + def get_chart_options(self) -> dict: + """Overwrite title and unit.""" + chart_options = super().get_chart_options() + chart_options["title"]["text"] = _("Beschäftigte") + chart_options["yAxis"]["name"] = _("") + del chart_options["series"][0]["name"] + return chart_options - def get_region_value(self) -> float: # noqa: D102 - return self.result.sum() / 1000 - def get_municipality_value(self) -> Optional[float]: # noqa: D102 - return None +class CompaniesPopup(RegionPopup): + """Popup to show companies.""" - def get_chart_data(self) -> Iterable: # noqa: D102 - self.result.index = self.result.index.map(lambda x: config.SIMULATION_NAME_MAPPING[x[0]]) - return self.result + lookup = "wind_turbines" + title = _("Betriebe") + + def get_detailed_data(self) -> pd.DataFrame: # noqa: D102 + return calculations.companies_per_municipality() + + def get_chart_data(self) -> Iterable: + """Return single value for companies in current municipality.""" + return [int(self.detailed_data.loc[self.selected_id])] + + def get_chart_options(self) -> dict: + """Overwrite title and unit.""" + chart_options = super().get_chart_options() + chart_options["title"]["text"] = _("Betriebe") + chart_options["yAxis"]["name"] = _("") + del chart_options["series"][0]["name"] + return chart_options class NumberWindturbinesPopup(RegionPopup): """Popup to show the number of wind turbines.""" - def get_region_value(self) -> float: # noqa: D102 - return models.WindTurbine.quantity() + lookup = "wind_turbines" + title = _("Number of wind turbines") + description = _("Description for number of wind turbines") + unit = "" - def get_municipality_value(self) -> float: # noqa: D102 - return models.WindTurbine.quantity(self.selected_id) + def get_detailed_data(self) -> pd.DataFrame: + """Return quantity of wind turbines per municipality (index).""" + return models.WindTurbine.quantity_per_municipality() - def get_chart_data(self) -> Iterable: # noqa: D102 - return models.WindTurbine.wind_turbines_history(self.selected_id) + def get_chart_data(self) -> Iterable: + """Return single value for wind turbines in current municipality.""" + return [int(self.detailed_data.loc[self.selected_id])] class NumberWindturbinesSquarePopup(RegionPopup): """Popup to show the number of wind turbines per km².""" - def get_region_value(self) -> float: # noqa: D102 - return models.WindTurbine.quantity_per_square() + lookup = "wind_turbines" + + def get_detailed_data(self) -> pd.DataFrame: + """Return quantity of wind turbines per municipality (index).""" + wind_turbines = models.WindTurbine.quantity_per_municipality() + return calculations.calculate_square_for_value(wind_turbines) + + def get_chart_options(self) -> dict: + """Overwrite title and unit in chart options.""" + chart_options = super().get_chart_options() + chart_options["title"]["text"] = _("Wind turbines per square meter") + chart_options["yAxis"]["name"] = _("WT/km²") + return chart_options + + def get_chart_data(self) -> Iterable: + """Return single value for wind turbines in current municipality.""" + return [float(self.detailed_data.loc[self.selected_id])] + + +class ElectricityDemandPopup(RegionPopup): + """Popup to show electricity demand.""" + + lookup = "electricity_demand" + title = _("Strombedarf") + + def get_detailed_data(self) -> pd.DataFrame: # noqa: D102 + return calculations.electricity_demand_per_municipality() + + def get_chart_options(self) -> dict: + """Overwrite title and unit.""" + chart_options = super().get_chart_options() + chart_options["title"]["text"] = _("Strombedarf") + chart_options["yAxis"]["name"] = _("ǴWh") + return chart_options - def get_municipality_value(self) -> float: # noqa: D102 - return models.WindTurbine.quantity_per_square(self.selected_id) - def get_chart_data(self) -> Iterable: # noqa: D102 - return models.WindTurbine.wind_turbines_per_area_history(self.selected_id) +class ElectricityDemandCapitaPopup(RegionPopup): + """Popup to show electricity demand capita.""" + + lookup = "electricity_demand" + title = _("Strombedarf je EinwohnerIn") + + def get_detailed_data(self) -> pd.DataFrame: # noqa: D102 + return calculations.calculate_capita_for_value(calculations.electricity_demand_per_municipality()) + + def get_chart_options(self) -> dict: + """Overwrite title and unit.""" + chart_options = super().get_chart_options() + chart_options["title"]["text"] = _("Strombedarf je EinwohnerIn") + chart_options["yAxis"]["name"] = _("kWh") + return chart_options + + +class HeatDemandPopup(RegionPopup): + """Popup to show heat demand.""" + + lookup = "heat_demand" + title = _("Wärmebedarf") + + def get_detailed_data(self) -> pd.DataFrame: # noqa: D102 + return calculations.electricity_demand_per_municipality() + + def get_chart_options(self) -> dict: + """Overwrite title and unit.""" + chart_options = super().get_chart_options() + chart_options["title"]["text"] = _("Wärmebedarf") + chart_options["yAxis"]["name"] = _("GWh") + return chart_options + + +class HeatDemandCapitaPopup(RegionPopup): + """Popup to show heat demand capita.""" + + lookup = "heat_demand" + title = _("Wärmebedarf je EinwohnerIn") + + def get_detailed_data(self) -> pd.DataFrame: # noqa: D102 + return calculations.calculate_capita_for_value(calculations.electricity_demand_per_municipality()) + + def get_chart_options(self) -> dict: + """Overwrite title and unit.""" + chart_options = super().get_chart_options() + chart_options["title"]["text"] = _("Wärmebedarf je EinwohnerIn") + chart_options["yAxis"]["name"] = _("kWh") + return chart_options + + +class BatteriesPopup(RegionPopup): + """Popup to show battery count.""" + + lookup = "wind_turbines" + title = _("Anzahl Batteriespeicher") + + def get_detailed_data(self) -> pd.DataFrame: # noqa: D102 + return calculations.batteries_per_municipality() + + def get_chart_data(self) -> Iterable: + """Return single value for employeess in current municipality.""" + return [int(self.detailed_data.loc[self.selected_id])] + + def get_chart_options(self) -> dict: + """Overwrite title and unit.""" + chart_options = super().get_chart_options() + chart_options["title"]["text"] = _("Anzahl Batteriespeicher") + chart_options["yAxis"]["name"] = _("#") + del chart_options["series"][0]["name"] + return chart_options + + +class BatteriesCapacityPopup(RegionPopup): + """Popup to show battery count.""" + + lookup = "wind_turbines" + title = _("Kapazität Batteriespeicher") + + def get_detailed_data(self) -> pd.DataFrame: # noqa: D102 + return calculations.battery_capacities_per_municipality() + + def get_chart_data(self) -> Iterable: + """Return single value for employeess in current municipality.""" + return [int(self.detailed_data.loc[self.selected_id])] + + def get_chart_options(self) -> dict: + """Overwrite title and unit.""" + chart_options = super().get_chart_options() + chart_options["title"]["text"] = _("Kapazität Batteriespeicher") + chart_options["yAxis"]["name"] = _("MWh") + del chart_options["series"][0]["name"] + return chart_options POPUPS: dict[str, type(popups.Popup)] = { - "capacity": CapacityPopup, - "capacity_square": CapacitySquarePopup, - "population": PopulationPopup, - "population_density": PopulationDensityPopup, - "renewable_electricity_production": RenewableElectricityProductionPopup, - "wind_turbines": NumberWindturbinesPopup, - "wind_turbines_square": NumberWindturbinesSquarePopup, + "wind": ClusterPopup, + "pvroof": ClusterPopup, + "pvground": ClusterPopup, + "hydro": ClusterPopup, + "biomass": ClusterPopup, + "combustion": ClusterPopup, + "gsgk": ClusterPopup, + "storage": ClusterPopup, + "population_statusquo": PopulationPopup, + "population_density_statusquo": PopulationDensityPopup, + "employees_statusquo": EmployeesPopup, + "companies_statusquo": CompaniesPopup, + "energy_statusquo": EnergyPopup, + "energy_2045": Energy2045Popup, + "energy_share_statusquo": EnergySharePopup, + "energy_capita_statusquo": EnergyCapitaPopup, + "energy_capita_2045": EnergyCapita2045Popup, + "energy_square_statusquo": EnergySquarePopup, + "energy_square_2045": EnergySquare2045Popup, + "capacity_statusquo": CapacityPopup, + "capacity_square_statusquo": CapacitySquarePopup, + "wind_turbines_statusquo": NumberWindturbinesPopup, + "wind_turbines_square_statusquo": NumberWindturbinesSquarePopup, + "electricity_demand_statusquo": ElectricityDemandPopup, + "electricity_demand_capita_statusquo": ElectricityDemandCapitaPopup, + "heat_demand_statusquo": HeatDemandPopup, + "heat_demand_capita_statusquo": HeatDemandCapitaPopup, + "batteries_statusquo": BatteriesPopup, + "batteries_capacity_statusquo": BatteriesCapacityPopup, } diff --git a/digiplan/map/popups/capacity.json b/digiplan/map/popups/capacity.json deleted file mode 100644 index d682ccf2..00000000 --- a/digiplan/map/popups/capacity.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "description": "something about the installed capacity of the renewable energy in this region and / or municipality", - "id": "14", - "data": { - "unit": "MW", - "year": null, - "municipality_value": null, - "region_title": "ABW region", - "region_value": null - }, - "municipality": null, - "sources": [ ], - "title": "installed capacity", - "chart": [] -} diff --git a/digiplan/map/popups/capacity_square.json b/digiplan/map/popups/capacity_square.json deleted file mode 100644 index a515e26a..00000000 --- a/digiplan/map/popups/capacity_square.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "description": "something about the installed capacity of the renewable energy per km² in this region and / or municipality", - "id": "14", - "data": { - "unit": "installed capacity per km²", - "year": null, - "municipality_value": null, - "region_title": "ABW region", - "region_value": null - }, - "municipality": null, - "sources": [ ], - "title": "installed capacity per km²" - } diff --git a/digiplan/map/popups/population.json b/digiplan/map/popups/population.json deleted file mode 100644 index 177262e9..00000000 --- a/digiplan/map/popups/population.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "description": "The population in 2022 of Z\u00f6rbig was 9,311 inhabitants. The entire ABW region had 370,190 inhabitants at that time. The following chart shows a forecast of the population development for the years 2022, 2030, and 2045.", - "id": "14", - "data": { - "unit": "population", - "year": null, - "municipality_value": null, - "region_title": "ABW region", - "region_value": null - }, - "municipality": null, - "sources": [ ], - "title": "population" -} diff --git a/digiplan/map/popups/population_density.json b/digiplan/map/popups/population_density.json deleted file mode 100644 index 8ca0a162..00000000 --- a/digiplan/map/popups/population_density.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "description": "something about the population density in this region and / or municipality", - "id": "14", - "data": { - "unit": "population per km²", - "year": null, - "municipality_value": null, - "region_title": "ABW region", - "region_value": null - }, - "municipality": null, - "sources": [ ], - "title": "Population Density" - } diff --git a/digiplan/map/popups/renewable_electricity_production.json b/digiplan/map/popups/renewable_electricity_production.json deleted file mode 100644 index d39affba..00000000 --- a/digiplan/map/popups/renewable_electricity_production.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "description": "something about the installed capacity of the renewable energy in this region and / or municipality", - "id": "14", - "data": { - "unit": "GWh", - "year": null, - "municipality_value": null, - "region_title": "ABW region", - "region_value": null - }, - "municipality": null, - "sources": [ ], - "title": "Renewable electricity production" -} diff --git a/digiplan/map/popups/wind_turbines.json b/digiplan/map/popups/wind_turbines.json deleted file mode 100644 index d54c1a52..00000000 --- a/digiplan/map/popups/wind_turbines.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "description": "Wind Turbines in this municipality in comparision to the whole region.", - "id": "14", - "data": { - "unit": "wind turbines", - "year": null, - "municipality_value": null, - "region_title": "ABW region", - "region_value": null - }, - "municipality": null, - "sources": [ ], - "title": "wind turbines" -} diff --git a/digiplan/map/popups/wind_turbines_square.json b/digiplan/map/popups/wind_turbines_square.json deleted file mode 100644 index c330360d..00000000 --- a/digiplan/map/popups/wind_turbines_square.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "description": "Wind Turbines per km² in this municipality in comparision to the whole region.", - "id": "14", - "data": { - "unit": "wind turbines", - "year": null, - "municipality_value": null, - "region_title": "ABW region", - "region_value": null - }, - "municipality": null, - "sources": [ ], - "title": "wind turbines" -} diff --git a/digiplan/map/templatetags/helper_tags.py b/digiplan/map/templatetags/helper_tags.py index 7a88a21b..17dd6ef4 100644 --- a/digiplan/map/templatetags/helper_tags.py +++ b/digiplan/map/templatetags/helper_tags.py @@ -1,8 +1,11 @@ +"""Template tags for django templating.""" + from django import template register = template.Library() @register.filter -def dict_lookup_filter(dictionary, key): +def dict_lookup_filter(dictionary: dict, key: str): # noqa: ANN201 + """Lookup key in dictionary.""" return dictionary.get(key, "") diff --git a/digiplan/map/urls.py b/digiplan/map/urls.py index d9b9ccb9..2a3d8b28 100644 --- a/digiplan/map/urls.py +++ b/digiplan/map/urls.py @@ -11,4 +11,5 @@ path("", views.MapGLView.as_view(), name="map"), path("choropleth//", views.get_choropleth, name="choropleth"), path("popup//", views.get_popup, name="popup"), + path("charts", views.get_charts, name="charts"), ] diff --git a/digiplan/map/utils.py b/digiplan/map/utils.py index f1043959..e8b30fd5 100644 --- a/digiplan/map/utils.py +++ b/digiplan/map/utils.py @@ -1,3 +1,5 @@ +"""Module for smaller helper functions.""" + import json import pathlib @@ -6,9 +8,9 @@ from django.template.context import make_context -def get_translated_json_from_file(json_filename: str, request: HttpRequest = None): +def get_translated_json_from_file(json_filename: str, request: HttpRequest = None) -> dict: """ - Renders JSON using translations + Render JSON using translations. Parameters ---------- @@ -28,3 +30,30 @@ def get_translated_json_from_file(json_filename: str, request: HttpRequest = Non c = make_context({}, request) translated_json_string = t.render(c) return json.loads(translated_json_string) + + +def merge_dicts(dict1: dict, dict2: dict) -> dict: + """ + Recursively merge two dictionaries. + + Parameters + ---------- + dict1: dict + Containing the first chart structure. Objects will be first. + dict2: dict + Containing the second chart structure. Objects will be last and + if they have the same name as ones from dict1 they overwrite the ones in first. + + Returns + ------- + dict + First chart modified and appended by second chart. + """ + for key in dict2: + if key in dict1 and isinstance(dict1[key], dict) and isinstance(dict2[key], dict): + merge_dicts(dict1[key], dict2[key]) + elif key in dict1 and isinstance(dict1[key], list) and isinstance(dict2[key], list): + dict1[key].extend(dict2[key]) + else: + dict1[key] = dict2[key] + return dict1 diff --git a/digiplan/map/views.py b/digiplan/map/views.py index 9e3768c9..066af62e 100644 --- a/digiplan/map/views.py +++ b/digiplan/map/views.py @@ -48,10 +48,17 @@ def get_context_data(self, **kwargs) -> dict: context = super().get_context_data(**kwargs) context["panels"] = [ - forms.EnergyPanelForm(utils.get_translated_json_from_file(config.ENERGY_SETTINGS_PANEL_FILE, self.request)), - forms.HeatPanelForm(utils.get_translated_json_from_file(config.HEAT_SETTINGS_PANEL_FILE, self.request)), + forms.EnergyPanelForm( + utils.get_translated_json_from_file(config.ENERGY_SETTINGS_PANEL_FILE, self.request), + additional_parameters=utils.get_translated_json_from_file(config.ADDITIONAL_ENERGY_SETTINGS_FILE), + ), + forms.HeatPanelForm( + utils.get_translated_json_from_file(config.HEAT_SETTINGS_PANEL_FILE, self.request), + additional_parameters=utils.get_translated_json_from_file(config.ADDITIONAL_HEAT_SETTINGS_FILE), + ), forms.TrafficPanelForm( utils.get_translated_json_from_file(config.TRAFFIC_SETTINGS_PANEL_FILE, self.request), + additional_parameters=utils.get_translated_json_from_file(config.ADDITIONAL_TRAFFIC_SETTINGS_FILE), ), ] @@ -68,14 +75,16 @@ def get_context_data(self, **kwargs) -> dict: } context["sources"] = categorized_sources context["store_cold_init"] = config.STORE_COLD_INIT - context["detailed_overview"] = charts.create_chart("detailed_overview") - context["ghg_overview"] = charts.create_chart("ghg_overview") - context["electricity_overview"] = charts.create_chart("electricity_overview") - context["electricity_ghg"] = charts.create_chart("electricity_ghg") - context["mobility_overview"] = charts.create_chart("mobility_overview") - context["mobility_ghg"] = charts.create_chart("mobility_ghg") - context["overview_heat"] = charts.create_chart("overview_heat") - context["decentralized_centralized_heat"] = charts.create_chart("decentralized_centralized_heat") + context["detailed_overview"] = charts.Chart("detailed_overview").render() + context["ghg_overview"] = charts.Chart("ghg_overview").render() + context["electricity_overview"] = charts.Chart("electricity_overview").render() + context["electricity_ghg"] = charts.Chart("electricity_ghg").render() + context["mobility_overview"] = charts.Chart("mobility_overview").render() + context["mobility_ghg"] = charts.Chart("mobility_ghg").render() + context["overview_heat"] = charts.Chart("overview_heat").render() + context["decentralized_centralized_heat"] = charts.Chart("decentralized_centralized_heat").render() + context["ghg_history"] = charts.Chart("ghg_history").render() + context["ghg_reduction"] = charts.Chart("ghg_reduction").render() return context @@ -99,7 +108,7 @@ def get_popup(request: HttpRequest, lookup: str, region: int) -> response.JsonRe containing HTML to render popup and chart options to be used in E-Chart. """ map_state = request.GET.dict() - popup = popups.POPUPS[lookup](lookup, region, map_state) + popup = popups.POPUPS[lookup](lookup, region, map_state=map_state) return popup.render() @@ -124,3 +133,27 @@ def get_choropleth(request: HttpRequest, lookup: str, layer_id: str) -> response """ map_state = request.GET.dict() return choropleths.CHOROPLETHS[lookup](lookup, map_state) + + +def get_charts(request: HttpRequest) -> response.JsonResponse: + """ + Return all result charts at once. + + Parameters + ---------- + request: HttpRequest + request holding simulation ID in map_state dict + + Returns + ------- + JsonResponse + holding dict with `div_id` as keys and chart options as values. + `div_id` is used in frontend to detect chart container. + """ + lookups = request.GET.getlist("charts[]") + simulation_id = None + if "map_state[simulation_id]" in request.GET.dict(): + simulation_id = int(request.GET.dict()["map_state[simulation_id]"]) + return response.JsonResponse( + {lookup: charts.CHARTS[lookup](simulation_id=simulation_id).render() for lookup in lookups}, + ) diff --git a/digiplan/map/widgets.py b/digiplan/map/widgets.py index c42d20a8..af0a1993 100644 --- a/digiplan/map/widgets.py +++ b/digiplan/map/widgets.py @@ -1,67 +1,15 @@ +"""Module holds widgets for digiplan.""" + from django.forms.widgets import Widget -from django.utils.safestring import mark_safe class SliderWidget(Widget): + """Widget to render sliders.""" + template_name = "widgets/slider.html" class SwitchWidget(Widget): - template_name = "widgets/switch.html" - - -# pylint: disable=R0903 -class JsonWidget: - def __init__(self, json): - self.json = json - - def __convert_to_html(self, data, level=0): - html = "" - if isinstance(data, dict): - html += ( - f'
' - if level > 0 - else "
" - ) - for key, value in data.items(): - html += f"{key}: {self.__convert_to_html(value, level+1)}" - html += "
" - elif isinstance(data, list): - html += f'
' - for item in data: - html += f"{self.__convert_to_html(item, level+1)}" - html += "
" - else: - html += f"{data}
" - return html - - def render(self): - header = "" - if self.json["title"] != "": - header += f'

{self.json["title"]}

' - if self.json["description"] != "": - header += f'

{self.json["description"]}

' - return mark_safe(header + self.__convert_to_html(data=self.json)) # noqa: S703,S308 + """Widget to render switches.""" - -class BoxWidget(Widget): - template_name = "widgets/box.html" - - def __init__(self, attrs=None): - default_attrs = {"class": "box-widget"} - if attrs: - default_attrs.update(attrs) - super().__init__(default_attrs) - - -class TitleWidget(Widget): - template_name = "widgets/title.html" - - def __init__(self, attrs=None): - default_attrs = {"class": "title-widget"} - if attrs: - default_attrs.update(attrs) - super().__init__(default_attrs) + template_name = "widgets/switch.html" diff --git a/digiplan/static/config/energy_settings_panel.json b/digiplan/static/config/energy_settings_panel.json index ef31e9a7..32e0dee6 100644 --- a/digiplan/static/config/energy_settings_panel.json +++ b/digiplan/static/config/energy_settings_panel.json @@ -37,23 +37,33 @@ "tooltip": "{% trans 'Priority areas repowering.' %}", "type": "switch" }, - "s_w_5": { + "s_w_5": { "class": "js-sidepanel-switch", "label": "{% trans 'Free extension in the search area.' %}", "tooltip": "{% trans 'Free extension in the search area.' %}", "type": "switch" }, "s_w_5_1": { - "class": "js-sidepanel-switch", + "class": "js-slider", "label": "{% trans 'Use of open country search area.' %}", "tooltip": "{% trans 'Use of open country search area.' %}", - "type": "switch" + "max": 100, + "min": 0, + "start": 0, + "step": 5, + "type": "slider", + "required": false }, "s_w_5_2": { - "class": "js-sidepanel-switch", + "class": "js-slider", "label": "{% trans 'Use of forest search space.' %}", "tooltip": "{% trans 'Use of forest search space.' %}", - "type": "switch" + "max": 100, + "min": 0, + "start": 0, + "step": 5, + "type": "slider", + "required": false }, "s_pv_ff_1": { "class": "js-slider js-slider-panel js-power-mix", @@ -135,18 +145,6 @@ "tooltip": "{% trans 'Run-of-river power plants' %}", "type": "slider" }, - "s_b_1": { - "class": "js-slider js-slider-panel js-power-mix", - "color": "#FA9FB5", - "label": "{% trans 'Bioenergy (MW)' %}", - "max": 30, - "min": 0, - "start": 5, - "status_quo": 15, - "step": 5, - "tooltip": "{% trans 'Biomass and biogas plants' %}", - "type": "slider" - }, "s_v_1": { "class": "js-slider js-slider-panel", "grid": false, @@ -193,13 +191,13 @@ }, "s_s_g_1": { "class": "js-slider js-slider-panel js-power-mix", - "label": "{% trans 'Large scale batteries (GWh)' %}", + "label": "{% trans 'Large scale batteries (MWh)' %}", "max": 20, "min": 0, "start": 2, "status_quo": 3, "step": 0.1, - "tooltip": "{% trans 'Large scale batteries (GWh)' %}", + "tooltip": "{% trans 'Large scale batteries (MWh)' %}", "type": "slider", "sidepanel": true }, diff --git a/digiplan/static/config/heat_settings_panel.json b/digiplan/static/config/heat_settings_panel.json index 575d8228..e9768828 100644 --- a/digiplan/static/config/heat_settings_panel.json +++ b/digiplan/static/config/heat_settings_panel.json @@ -60,7 +60,7 @@ }, "w_z_wp_3": { "class": "js-slider", - "label": "{% trans 'Households (%)' %}", + "label": "{% trans 'Share (%)' %}", "max": 200, "min": 50, "start": 50, diff --git a/digiplan/static/config/layer_styles.json b/digiplan/static/config/layer_styles.json index 46a66e99..ef438143 100644 --- a/digiplan/static/config/layer_styles.json +++ b/digiplan/static/config/layer_styles.json @@ -158,7 +158,7 @@ "type": "circle", "filter": ["has", "point_count"], "paint": { - "circle-color": "#F6B93B", + "circle-color": "#EFAD25", "circle-radius": [ "step", ["get", "point_count"], @@ -195,7 +195,7 @@ "type": "circle", "filter": ["has", "point_count"], "paint": { - "circle-color": "#9CC4D9", + "circle-color": "#A9BDE8", "circle-radius": [ "step", ["get", "point_count"], @@ -253,6 +253,86 @@ "text-size": 12 } }, + "gsgk": { + "type": "symbol", + "filter": ["!", ["has", "point_count"]], + "layout": { + "icon-image": "gsgk", + "icon-size": 0.4, + "icon-allow-overlap": true + }, + "paint": { + "icon-color": "black" + } + }, + "gsgk_cluster": { + "type": "circle", + "filter": ["has", "point_count"], + "paint": { + "circle-color": "#C27BA0", + "circle-radius": [ + "step", + ["get", "point_count"], + 20, + 15, + 30, + 50, + 40 + ] + } + }, + "gsgk_cluster_count": { + "type": "symbol", + "filter": ["has", "point_count"], + "layout": { + "text-field": "{point_count_abbreviated}", + "text-font": ["DIN Offc Pro Medium", "Arial Unicode MS Bold"], + "text-size": 12 + }, + "paint": { + "text-color": "white" + } + }, + "storage": { + "type": "symbol", + "filter": ["!", ["has", "point_count"]], + "layout": { + "icon-image": "storage", + "icon-size": 0.4, + "icon-allow-overlap": true + }, + "paint": { + "icon-color": "black" + } + }, + "storage_cluster": { + "type": "circle", + "filter": ["has", "point_count"], + "paint": { + "circle-color": "#8D2D5F", + "circle-radius": [ + "step", + ["get", "point_count"], + 20, + 15, + 30, + 50, + 40 + ] + } + }, + "storage_cluster_count": { + "type": "symbol", + "filter": ["has", "point_count"], + "layout": { + "text-field": "{point_count_abbreviated}", + "text-font": ["DIN Offc Pro Medium", "Arial Unicode MS Bold"], + "text-size": 12 + }, + "paint": { + "text-color": "white" + } + }, "combustion": { "type": "symbol", "filter": ["!", ["has", "point_count"]], @@ -269,7 +349,7 @@ "type": "circle", "filter": ["has", "point_count"], "paint": { - "circle-color": "#1A1A1A", + "circle-color": "#E6772E", "circle-radius": [ "step", ["get", "point_count"], @@ -318,5 +398,173 @@ ], "fill-opacity": 0.8 } + }, + "potentialarea_pv_road_railway_region": { + "type": "fill", + "paint": { + "fill-color": "#ffd744" + } + }, + "potentialarea_pv_agriculture_lfa-off_region": { + "type": "fill", + "paint": { + "fill-color": "#c19800" + } + }, + "potentialarea_wind_stp_2018_vreg": { + "type": "fill", + "paint": { + "fill-color": "#6A89CC" + } + }, + "potentialarea_wind_stp_2027_vr": { + "type": "fill", + "paint": { + "fill-color": "#6A89CC" + } + }, + "potentialarea_wind_stp_2027_repowering": { + "type": "fill", + "paint": { + "fill-color": "#273f73" + } + }, + "potentialarea_wind_stp_2027_search_area_open_area": { + "type": "fill", + "paint": { + "fill-color": "#6A89CC" + } + }, + "potentialarea_wind_stp_2027_search_area_forest_area": { + "type": "fill", + "paint": { + "fill-color": "#273f73" + } + }, + "air_traffic": { + "type": "fill", + "paint": { + "fill-color": "#969696" + } + }, + "aviation": { + "type": "fill", + "paint": { + "fill-color": "#CCCCCC" + } + }, + "biosphere_reserve": { + "type": "fill", + "paint": { + "fill-color": "#238443" + } + }, + "drinking_water_protection_area": { + "type": "fill", + "paint": { + "fill-color": "#6BAED6" + } + }, + "fauna_flora_habitat": { + "type": "fill", + "paint": { + "fill-color": "#78C679" + } + }, + "floodplain": { + "type": "fill", + "paint": { + "fill-color": "#08306B" + } + }, + "forest": { + "type": "fill", + "paint": { + "fill-color": "#004529" + } + }, + "grid": { + "type": "fill", + "paint": { + "fill-color": "#252525" + } + }, + "industry": { + "type": "fill", + "paint": { + "fill-color": "#BCBDDC" + } + }, + "landscape_protection_area": { + "type": "fill", + "paint": { + "fill-color": "#006837" + } + }, + "less_favoured_areas_agricultural": { + "type": "fill", + "paint": { + "fill-color": "#66c2a4" + } + }, + "military": { + "type": "fill", + "paint": { + "fill-color": "#636363" + } + }, + "nature_conservation_area": { + "type": "fill", + "paint": { + "fill-color": "#ADDD8E" + } + }, + "railway": { + "type": "fill", + "paint": { + "fill-color": "#54278F" + } + }, + "road_default": { + "type": "fill", + "paint": { + "fill-color": "#756BB1" + } + }, + "road_railway-500m_region": { + "type": "fill", + "paint": { + "fill-color": "#9E9AC8" + } + }, + "settlement-0m": { + "type": "fill", + "paint": { + "fill-color": "#DADAEB" + } + }, + "soil_quality_low": { + "type": "fill", + "paint": { + "fill-color": "#A63603" + } + }, + "soil_quality_high": { + "type": "fill", + "paint": { + "fill-color": "#FD8D3C" + } + }, + "special_protection_area": { + "type": "fill", + "paint": { + "fill-color": "#41AB5D" + } + }, + "water": { + "type": "fill", + "paint": { + "fill-color": "#2171B5" + } } } diff --git a/digiplan/static/images/icons/i_GSGK.svg b/digiplan/static/images/icons/i_GSGK.svg new file mode 100644 index 00000000..3ec31cd7 --- /dev/null +++ b/digiplan/static/images/icons/i_GSGK.svg @@ -0,0 +1,10 @@ + + + + + + + diff --git a/digiplan/static/images/icons/i_battery.svg b/digiplan/static/images/icons/i_battery.svg new file mode 100644 index 00000000..df1c8f74 --- /dev/null +++ b/digiplan/static/images/icons/i_battery.svg @@ -0,0 +1,7 @@ + + + + + diff --git a/digiplan/static/images/icons/map_battery.png b/digiplan/static/images/icons/map_battery.png new file mode 100644 index 00000000..e84ced97 Binary files /dev/null and b/digiplan/static/images/icons/map_battery.png differ diff --git a/digiplan/static/images/icons/map_gsgk.png b/digiplan/static/images/icons/map_gsgk.png new file mode 100644 index 00000000..4699507a Binary files /dev/null and b/digiplan/static/images/icons/map_gsgk.png differ diff --git a/digiplan/static/images/map-outdoor-pv.png b/digiplan/static/images/map-outdoor-pv.png new file mode 100644 index 00000000..54b15811 Binary files /dev/null and b/digiplan/static/images/map-outdoor-pv.png differ diff --git a/digiplan/static/images/map-roof-pv.png b/digiplan/static/images/map-roof-pv.png new file mode 100644 index 00000000..1b2f6c33 Binary files /dev/null and b/digiplan/static/images/map-roof-pv.png differ diff --git a/digiplan/static/images/map-wind.png b/digiplan/static/images/map-wind.png new file mode 100644 index 00000000..d8d87f81 Binary files /dev/null and b/digiplan/static/images/map-wind.png differ diff --git a/digiplan/static/images/map_clusters.png b/digiplan/static/images/map_clusters.png new file mode 100644 index 00000000..08172d46 Binary files /dev/null and b/digiplan/static/images/map_clusters.png differ diff --git a/digiplan/static/images/settings.png b/digiplan/static/images/settings.png new file mode 100644 index 00000000..0ca99d9d Binary files /dev/null and b/digiplan/static/images/settings.png differ diff --git a/digiplan/static/images/sidebar.png b/digiplan/static/images/sidebar.png new file mode 100644 index 00000000..cdd518cf Binary files /dev/null and b/digiplan/static/images/sidebar.png differ diff --git a/digiplan/static/js/charts.js b/digiplan/static/js/charts.js index ea060a65..09e036eb 100644 --- a/digiplan/static/js/charts.js +++ b/digiplan/static/js/charts.js @@ -8,12 +8,6 @@ const renewable_share_scenario_chart = echarts.init(renewable_share_scenario_div const co2_emissions_scenario_div = document.getElementById("co2_emissions_scenario_chart"); const co2_emissions_scenario_chart = echarts.init(co2_emissions_scenario_div); -// Sidebar, initialize charts -const chart3Element = document.getElementById("chart3"); -const chart3 = echarts.init(chart3Element); -const chart4Element = document.getElementById("chart4"); -const chart4 = echarts.init(chart4Element); - // Results view, initiliaze charts const detailed_overview_chart = echarts.init(document.getElementById("detailed_overview_chart")); const ghg_overview_chart = echarts.init(document.getElementById("ghg_overview_chart")); @@ -23,7 +17,8 @@ const mobility_overview_chart = echarts.init(document.getElementById("mobility_o const mobility_THG_chart = echarts.init(document.getElementById("mobility_THG_chart")); const overview_heat_chart = echarts.init(document.getElementById("overview_heat_chart")); const decentralized_centralized_heat_chart = echarts.init(document.getElementById("decentralized_centralized_heat_chart")); - +const ghg_history_chart = echarts.init(document.getElementById("ghg_history_chart")); +const ghg_reduction_chart = echarts.init(document.getElementById("ghg_reduction_chart")); PubSub.subscribe(eventTopics.MENU_CHANGED, resizeCharts); @@ -31,7 +26,7 @@ PubSub.subscribe(eventTopics.MENU_CHANGED, resizeCharts); const chart_tooltip = { trigger: 'axis', axisPointer: { - type: 'shadow' + type: 'shadow' } }; const chart_bar_width_sm = 16; @@ -52,20 +47,20 @@ const chart_grid_results = { }; const chart_text_style = { fontFamily: "Roboto", - fontSize: 14, - fontWeight: 300, - color: '#002C50' + fontSize: 10, + //fontWeight: 300, + //color: '#002C50' }; const chart_legend = { show: true, - bottom: '15', - itemWidth: 14, - itemHeight: 14 + bottom: '0', + itemWidth: 10, + itemHeight: 10 }; // Goal variables -const renewable_share_goal_value = 90; -const co2_emissions_goal_value = 30; +// const renewable_share_goal_value = 90; +// const co2_emissions_goal_value = 30; // CHARTS const renewable_share_goal = { @@ -74,10 +69,11 @@ const renewable_share_goal = { textStyle: chart_text_style, xAxis: { type: 'category', - data: ['2021', '2045'], + data: ['2021', 'Szenario'], axisTick: { - show: false - } + show: false, + }, + axisLabel: {fontSize: 9}, //, rotate: 20}, }, yAxis: { show: true, @@ -92,7 +88,7 @@ const renewable_share_goal = { { value: 30, itemStyle: { - color: '#C3D1DC' + color: '#D8E2E7' } }, { @@ -102,20 +98,20 @@ const renewable_share_goal = { } }, ], - markLine: { - silent: true, - lineStyle: { - color: '#00BC8C', - type: 'solid' - }, - symbol: 'none', - data: [{ - yAxis: renewable_share_goal_value, - label: { - show: false - } - }] - } + // markLine: { + // silent: true, + // lineStyle: { + // color: '#06DFA7', + // type: 'solid' + // }, + // symbol: 'none', + // data: [{ + // yAxis: renewable_share_goal_value, + // label: { + // show: false + // } + // }] + // } }, ], }; @@ -124,12 +120,14 @@ const co2_emissions_goal = { grid: chart_grid_goal, tooltip: chart_tooltip, textStyle: chart_text_style, + legend: chart_legend, xAxis: { type: 'category', - data: ['2021', '2045'], + data: ['Szenario'], axisTick: { show: false - } + }, + axisLabel: {fontSize: 9}, //, rotate: 20}, }, yAxis: { show: true, @@ -139,36 +137,34 @@ const co2_emissions_goal = { series: [ { type: 'bar', + name: 'Regional', + stack: 'reduc', + color: '#06DFA7', barWidth: chart_bar_width_sm, - data: [ - { - value: 90, - itemStyle: { - color: '#C3D1DC' - } - }, - { - value: 30, - itemStyle: { - color: '#E6A100' - } - }, - ], - markLine: { - silent: true, - lineStyle: { - color: '#BE880B', - type: 'solid' - }, - symbol: 'none', - data: [{ - yAxis: co2_emissions_goal_value, - label: { - show: false - } - }] - } + data: [50], }, + { + type: 'bar', + name: 'Import', + stack: 'reduc', + color: '#E8986B', + barWidth: chart_bar_width_sm, + data: [20], + }, + // markLine: { + // silent: true, + // lineStyle: { + // color: '#E8986B', + // type: 'solid' + // }, + // symbol: 'none', + // data: [{ + // yAxis: co2_emissions_goal_value, + // label: { + // show: false + // } + // }] + // } ], }; @@ -178,10 +174,11 @@ const renewable_share_scenario = { textStyle: chart_text_style, xAxis: { type: 'category', - data: ['2021', '2045'], + data: ['2021', 'Szenario'], axisTick: { - show: false - } + show: false, + }, + axisLabel: {fontSize: 9}, //, rotate: 20}, }, yAxis: { show: true, @@ -196,7 +193,7 @@ const renewable_share_scenario = { { value: 30, itemStyle: { - color: '#C3D1DC' + color: '#D8E2E7' } }, { @@ -206,20 +203,20 @@ const renewable_share_scenario = { } }, ], - markLine: { - silent: true, - lineStyle: { - color: '#00BC8C', - type: 'solid' - }, - symbol: 'none', - data: [{ - yAxis: renewable_share_goal_value, - label: { - show: false - } - }] - } + // markLine: { + // silent: true, + // lineStyle: { + // color: '#06DFA7', + // type: 'solid' + // }, + // symbol: 'none', + // data: [{ + // yAxis: renewable_share_goal_value, + // label: { + // show: false + // } + // }] + // } }, ], }; @@ -228,12 +225,14 @@ const co2_emissions_scenario = { grid: chart_grid_goal, tooltip: chart_tooltip, textStyle: chart_text_style, + legend: chart_legend, xAxis: { type: 'category', - data: ['2021', '2045'], + data: ['Szenario'], axisTick: { show: false - } + }, + axisLabel: {fontSize: 9}, //, rotate: 20}, }, yAxis: { show: true, @@ -243,90 +242,35 @@ const co2_emissions_scenario = { series: [ { type: 'bar', + name: 'Regional', + stack: 'reduc', + color: '#06DFA7', barWidth: chart_bar_width_sm, - data: [ - { - value: 90, - itemStyle: { - color: '#C3D1DC' - } - }, - { - value: 30, - itemStyle: { - color: '#E6A100' - } - }, - ], - markLine: { - silent: true, - lineStyle: { - color: '#BE880B', - type: 'solid' - }, - symbol: 'none', - data: [{ - yAxis: co2_emissions_goal_value, - label: { - show: false - } - }] - } + data: [50], }, - ], -}; - -const option = { - textStyle: chart_text_style, - title: { - text: 'Anteil Erneuerbare \nEnergien (%)', - }, - tooltip: { - trigger: 'axis' - }, - legend: { - bottom: 10, - data: ['2021', '2035'] - }, - toolbox: { - show: false - }, - calculable: true, - xAxis: [ { - type: 'category', - data: ['Jahre'], - axisTick: { - show: false - } - } - ], - yAxis: [ - { - type: 'value', - max: 100 - } - ], - series: [ - { - name: '2021', type: 'bar', - color: '#C3D1DC', - barWidth: '32', - data: [ - 60 - ], + name: 'Import', + stack: 'reduc', + color: '#E8986B', + barWidth: chart_bar_width_sm, + data: [20], }, - { - name: '2035', - type: 'bar', - color: '#06DFA7', - barWidth: '32', - data: [ - 80 - ] - } - ] + // markLine: { + // silent: true, + // lineStyle: { + // color: '#E8986B', + // type: 'solid' + // }, + // symbol: 'none', + // data: [{ + // yAxis: co2_emissions_goal_value, + // label: { + // show: false + // } + // }] + // } + ], }; // get options for result view charts @@ -338,6 +282,8 @@ const mobility_overview_option = JSON.parse(document.getElementById("mobility_ov const mobility_ghg_option = JSON.parse(document.getElementById("mobility_ghg").textContent); const overview_heat_option = JSON.parse(document.getElementById("overview_heat").textContent); const decentralized_centralized_heat_option = JSON.parse(document.getElementById("decentralized_centralized_heat").textContent); +const ghg_history_option = JSON.parse(document.getElementById("ghg_history").textContent); +const ghg_reduction_option = JSON.parse(document.getElementById("ghg_reduction").textContent); function resizeCharts() { setTimeout(function () { @@ -345,8 +291,6 @@ function resizeCharts() { co2_emissions_goal_chart.resize(); renewable_share_scenario_chart.resize(); co2_emissions_scenario_chart.resize(); - chart3.resize(); - chart4.resize(); detailed_overview_chart.resize(); ghg_overview_chart.resize(); electricity_overview_chart.resize(); @@ -355,6 +299,8 @@ function resizeCharts() { mobility_THG_chart.resize(); overview_heat_chart.resize(); decentralized_centralized_heat_chart.resize(); + ghg_history_chart.resize(); + ghg_reduction_chart.resize(); }, 200); } @@ -364,10 +310,6 @@ co2_emissions_goal_chart.setOption(co2_emissions_goal); renewable_share_scenario_chart.setOption(renewable_share_scenario); co2_emissions_scenario_chart.setOption(co2_emissions_scenario); -// Sidebar, setOptions -chart3.setOption(option); -chart4.setOption(option); - // Results, setOptions detailed_overview_chart.setOption(detailed_overview_option); ghg_overview_chart.setOption(ghg_overview_option); @@ -377,6 +319,8 @@ mobility_overview_chart.setOption(mobility_overview_option); mobility_THG_chart.setOption(mobility_ghg_option); overview_heat_chart.setOption(overview_heat_option); decentralized_centralized_heat_chart.setOption(decentralized_centralized_heat_option); +ghg_history_chart.setOption(ghg_history_option); +ghg_reduction_chart.setOption(ghg_reduction_option); resizeCharts(); @@ -386,6 +330,13 @@ document.addEventListener("show.bs.tab", resizeCharts); function createChart(div_id, options) { const chartElement = document.getElementById(div_id); - const chart = echarts.init(chartElement, null, {renderer: 'svg'}); + let chart; + if (echarts.getInstanceByDom(chartElement)) { + chart = echarts.getInstanceByDom(chartElement); + chart.clear(); + } else { + chart = echarts.init(chartElement, null, {renderer: 'svg'}); + } chart.setOption(options); + chart.resize(); } diff --git a/digiplan/static/js/elements.js b/digiplan/static/js/elements.js index da6482aa..e68f681b 100644 --- a/digiplan/static/js/elements.js +++ b/digiplan/static/js/elements.js @@ -1,2 +1,46 @@ -export const resultsDropdown = document.getElementById("result_views"); +export const statusquoDropdown = document.getElementById("situation_today"); +export const futureDropdown = document.getElementById("result_views"); export const resultsTabs = document.getElementById("results-tabs"); + +// Show onboarding modal on start +document.addEventListener('DOMContentLoaded', (event) => { + var myModal = new bootstrap.Modal(document.getElementById('onboardingModal'), {}); + myModal.show(); +}); + +// Prevent continuous cycle of modal carousel +document.addEventListener("DOMContentLoaded", function() { + var carouselEl = document.querySelector('#carouselExampleIndicators'); + var carousel = new bootstrap.Carousel(carouselEl, { + wrap: false + }); + + var prevButton = document.querySelector('.carousel-control-prev'); + var nextButton = document.querySelector('.carousel-control-next'); + + prevButton.addEventListener('click', function (event) { + event.preventDefault(); + carousel.prev(); + }); + + nextButton.addEventListener('click', function (event) { + event.preventDefault(); + carousel.next(); + }); + + carouselEl.addEventListener('slid.bs.carousel', function () { + const carouselItems = carouselEl.querySelectorAll('.carousel-item'); + const currentIndex = Array.prototype.indexOf.call(carouselItems, carouselEl.querySelector('.carousel-item.active')); + if (currentIndex === 0) { + prevButton.classList.add('transparent'); + } else { + prevButton.classList.remove('transparent'); + } + + if (currentIndex === carouselItems.length - 1) { + nextButton.classList.add('transparent'); + } else { + nextButton.classList.remove('transparent'); + } + }); + }); diff --git a/digiplan/static/js/event-topics.js b/digiplan/static/js/event-topics.js index 1ac3d6bd..db3806f1 100644 --- a/digiplan/static/js/event-topics.js +++ b/digiplan/static/js/event-topics.js @@ -18,6 +18,10 @@ const eventTopics = { POWER_PANEL_SLIDER_CHANGE: "POWER_PANEL_SLIDER_CHANGE", PANEL_SLIDER_CHANGE: "PANEL_SLIDER_CHANGE", MORE_LABEL_CLICK: "MORE_LABEL_CLICK", + PV_CONTROL_ACTIVATED: "PV_CONTROL_ACTIVATED", + PV_CONTROL_DEACTIVATED: "PV_CONTROL_DEACTIVATED", + WIND_CONTROL_ACTIVATED: "WIND_CONTROL_ACTIVATED", + WIND_CONTROL_DEACTIVATED: "WIND_CONTROL_DEACTIVATED", MENU_CHANGED: "MENU_CHANGED", MENU_STATUS_QUO_SELECTED: "MENU_STATUS_QUO_CLICKED", diff --git a/digiplan/static/js/intro_tour.js b/digiplan/static/js/intro_tour.js index d146909b..89655a80 100644 --- a/digiplan/static/js/intro_tour.js +++ b/digiplan/static/js/intro_tour.js @@ -1,3 +1,5 @@ +const onbaordingCloseBtn = document.getElementById("close-onboarding"); + const tour = new Shepherd.Tour({ // jshint ignore:line useModalOverlay: true, defaultStepOptions: { @@ -88,4 +90,6 @@ tour.addStep({ id: 'creating' }); -tour.start(); +onbaordingCloseBtn.addEventListener("click", function() { + tour.start(); +}); diff --git a/digiplan/static/js/menu.js b/digiplan/static/js/menu.js index 60ebb45f..96846909 100644 --- a/digiplan/static/js/menu.js +++ b/digiplan/static/js/menu.js @@ -1,4 +1,4 @@ -import {resultsDropdown, resultsTabs} from "./elements.js"; +import {statusquoDropdown, resultsTabs} from "./elements.js"; const menuNextBtn = document.getElementById("menu_next_btn"); const menuPreviousBtn = document.getElementById("menu_previous_btn"); @@ -24,10 +24,12 @@ chartTab.addEventListener("click", function () { PubSub.subscribe(eventTopics.MENU_STATUS_QUO_SELECTED, setMapChartViewVisibility); PubSub.subscribe(eventTopics.MENU_STATUS_QUO_SELECTED, showMapView); +PubSub.subscribe(eventTopics.MENU_STATUS_QUO_SELECTED, hidePotentialLayers); PubSub.subscribe(eventTopics.MENU_SETTINGS_SELECTED, setMapChartViewVisibility); PubSub.subscribe(eventTopics.MENU_SETTINGS_SELECTED, showMapView); PubSub.subscribe(eventTopics.MENU_SETTINGS_SELECTED, deactivateChoropleth); PubSub.subscribe(eventTopics.MENU_RESULTS_SELECTED, setMapChartViewVisibility); +PubSub.subscribe(eventTopics.MENU_RESULTS_SELECTED, hidePotentialLayers); PubSub.subscribe(eventTopics.MAP_VIEW_SELECTED, setResultsView); PubSub.subscribe(eventTopics.CHART_VIEW_SELECTED, setResultsView); @@ -90,10 +92,10 @@ function setMapChartViewVisibility(msg) { function setResultsView(msg) { if (msg === eventTopics.CHART_VIEW_SELECTED) { - resultsDropdown.parentElement.setAttribute("style", "display: none !important"); + statusquoDropdown.parentElement.setAttribute("style", "display: none !important"); resultsTabs.parentElement.setAttribute("style", ""); } else { - resultsDropdown.parentElement.setAttribute("style", ""); + statusquoDropdown.parentElement.setAttribute("style", ""); resultsTabs.parentElement.setAttribute("style", "display: none !important"); } return logMessage(msg); diff --git a/digiplan/static/js/results.js b/digiplan/static/js/results.js index 5f5b8aab..01368948 100644 --- a/digiplan/static/js/results.js +++ b/digiplan/static/js/results.js @@ -1,19 +1,28 @@ -import {resultsDropdown} from "./elements.js"; +import {statusquoDropdown, futureDropdown} from "./elements.js"; const imageResults = document.getElementById("info_tooltip_results"); const simulation_spinner = document.getElementById("simulation_spinner"); const SIMULATION_CHECK_TIME = 5000; +const resultCharts = { + "detailed_overview": "detailed_overview_chart", + "electricity_overview": "electricity_overview_chart" +}; + // Setup // Disable settings form submit $('#settings').submit(false); -resultsDropdown.addEventListener("change", function() { - PubSub.publish(mapEvent.CHOROPLETH_SELECTED, resultsDropdown.value); - imageResults.title = resultsDropdown.options[resultsDropdown.selectedIndex].title; +statusquoDropdown.addEventListener("change", function() { + PubSub.publish(mapEvent.CHOROPLETH_SELECTED, statusquoDropdown.value); + imageResults.title = statusquoDropdown.options[statusquoDropdown.selectedIndex].title; +}); +futureDropdown.addEventListener("change", function() { + PubSub.publish(mapEvent.CHOROPLETH_SELECTED, futureDropdown.value); + imageResults.title = futureDropdown.options[futureDropdown.selectedIndex].title; }); @@ -23,6 +32,10 @@ PubSub.subscribe(eventTopics.MENU_RESULTS_SELECTED, showSimulationSpinner); PubSub.subscribe(eventTopics.SIMULATION_STARTED, checkResultsPeriodically); PubSub.subscribe(eventTopics.SIMULATION_FINISHED, showResults); PubSub.subscribe(eventTopics.SIMULATION_FINISHED, hideSimulationSpinner); +PubSub.subscribe(eventTopics.SIMULATION_FINISHED, showResultCharts); +PubSub.subscribe(mapEvent.CHOROPLETH_SELECTED, showRegionChart); +// for testing: +PubSub.subscribe(eventTopics.CHART_VIEW_SELECTED, showResultCharts); // Subscriber Functions @@ -97,3 +110,36 @@ function hideSimulationSpinner(msg) { simulation_spinner.hidden = true; return logMessage(msg); } + +function showRegionChart(msg, lookup) { + const region_lookup = `${lookup}_region`; + let charts = {}; + if (region_lookup.includes("2045")) { + charts[region_lookup] = "region_chart_2045"; + } else { + charts[region_lookup] = "region_chart_statusquo"; + } + showCharts(charts); + return logMessage(msg); +} + +function showResultCharts(msg) { + showCharts(resultCharts); + return logMessage(msg); +} + +function showCharts(charts={}) { + $.ajax({ + url : "/charts", + type : "GET", + data : { + "charts": Object.keys(charts), + "map_state": map_store.cold.state + }, + success : function(chart_options) { + for (const chart in charts) { + createChart(charts[chart], chart_options[chart]); + } + }, + }); +} diff --git a/digiplan/static/js/sliders.js b/digiplan/static/js/sliders.js index f9b4bf59..49e36fbc 100644 --- a/digiplan/static/js/sliders.js +++ b/digiplan/static/js/sliders.js @@ -9,6 +9,26 @@ const sliderMoreLabels = document.querySelectorAll(".c-slider__label--more > .bu const powerMixInfoBanner = document.getElementById("js-power-mix"); +const potentialPVLayers = ["potentialarea_pv_agriculture_lfa-off_region", "potentialarea_pv_road_railway_region"]; +const potentialWindLayers = [ + "potentialarea_wind_stp_2018_vreg", + "potentialarea_wind_stp_2027_repowering", + "potentialarea_wind_stp_2027_search_area_forest_area", + "potentialarea_wind_stp_2027_search_area_open_area", + "potentialarea_wind_stp_2027_vr" +]; +const potentialWindSwitches = document.querySelectorAll("#id_s_w_3, #id_s_w_4, #id_s_w_4_1, #id_s_w_4_2, #id_s_w_5, #id_s_w_5_1, #id_s_w_5_2"); + +const sectorSlider = document.querySelectorAll("#id_s_v_3, #id_s_v_4, #id_s_v_5, #id_w_d_wp_3, #id_w_d_wp_4, #id_w_d_wp_5, #id_w_v_3, #id_w_v_4, #id_w_v_5"); + +const sliderDependencies = { + "id_s_s_g_1": "id_s_s_g_3", + "id_w_z_wp_1": "id_w_z_wp_3", + "id_w_d_s_1": "id_w_d_s_3", + "id_w_z_s_1": "id_w_z_s_3", + "id_v_iv_1": "id_v_iv_3" +}; + // Setup // Order matters. Start with the most specific, and end with most general sliders. @@ -30,18 +50,99 @@ Array.from(Object.keys(SETTINGS_DEPENDENCY_MAP)).forEach(dependent_name => { }); $(".js-slider.js-slider-panel.js-power-mix").ionRangeSlider({ onChange: function (data) { - const msg = eventTopics.POWER_PANEL_SLIDER_CHANGE; - PubSub.publish(msg, data); + PubSub.publish(eventTopics.POWER_PANEL_SLIDER_CHANGE, data); + } + } +); +$(potentialWindSwitches).on("change", function () { + PubSub.publish(eventTopics.WIND_CONTROL_ACTIVATED); +}); +$(".js-slider.js-slider-panel").ionRangeSlider({ + onChange: function (data) { + PubSub.publish(eventTopics.PANEL_SLIDER_CHANGE, data); + } + } +); +$(sectorSlider).ionRangeSlider({ + onChange: function (data) { + calculate_slider_value(data); } } ); + $(".js-slider.js-slider-panel").ionRangeSlider({ onChange: function (data) { - const msg = eventTopics.PANEL_SLIDER_CHANGE; - PubSub.publish(msg, data); + PubSub.publish(eventTopics.PANEL_SLIDER_CHANGE, data); + } + } +); +$(".form-check-input").on( + 'click', function (data) { + toggleFormFields(data.target.id); + } +); +$("#id_s_w_5_1").ionRangeSlider({ + onChange: function (data) { + calculate_max_wind(); + } + } +); +$("#id_s_w_5_2").ionRangeSlider({ + onChange: function (data) { + calculate_max_wind(); + } + } +); +$("#id_s_pv_ff_3").ionRangeSlider({ + onChange: function (data) { + calculate_max_pv_ff(); + } + } +); +$("#id_s_pv_ff_4").ionRangeSlider({ + onChange: function (data) { + calculate_max_pv_ff(); } } ); +$("#id_s_pv_d_3").ionRangeSlider({ + onChange: function (data) { + let new_max = Math.round(Math.round(store.cold.slider_max.s_pv_d_3) * (data.from/100)); + $(`#id_s_pv_d_1`).data("ionRangeSlider").update({max:new_max}); + } + } +); +$("#id_w_z_wp_3").ionRangeSlider({ + onChange: function (data) { + $(`#id_w_z_wp_1`).data("ionRangeSlider").update({from:data.from}); + } + } +); +$("#id_w_d_s_3").ionRangeSlider({ + onChange: function (data) { + $(`#id_w_d_s_1`).data("ionRangeSlider").update({from:data.from}); + } + } +); +$("#id_w_z_s_3").ionRangeSlider({ + onChange: function (data) { + $(`#id_w_z_s_1`).data("ionRangeSlider").update({from:data.from}); + } + } +); +$("#id_v_iv_3").ionRangeSlider({ + onChange: function (data) { + $(`#id_v_iv_1`).data("ionRangeSlider").update({from:data.from}); + } + } +); +$("#id_s_s_g_3").ionRangeSlider({ + onChange: function (data) { + $(`#id_s_s_g_1`).data("ionRangeSlider").update({from:data.from}); + } + } +); + $(".js-slider").ionRangeSlider(); Array.from(sliderMoreLabels).forEach(moreLabel => { @@ -66,17 +167,66 @@ subscribeToEvents( [eventTopics.POWER_PANEL_SLIDER_CHANGE, eventTopics.PANEL_SLIDER_CHANGE], showActivePanelSliderOnPanelSliderChange ); +subscribeToEvents( + [eventTopics.POWER_PANEL_SLIDER_CHANGE, eventTopics.PANEL_SLIDER_CHANGE], + hidePotentialLayers +); +subscribeToEvents( + [eventTopics.POWER_PANEL_SLIDER_CHANGE, eventTopics.PANEL_SLIDER_CHANGE], + checkMainPanelSlider +); PubSub.subscribe(eventTopics.MORE_LABEL_CLICK, showOrHideSidepanelsOnMoreLabelClick); +PubSub.subscribe(eventTopics.MORE_LABEL_CLICK, showOrHidePotentialLayersOnMoreLabelClick); PubSub.subscribe(eventTopics.DEPENDENCY_PANEL_SLIDER_CHANGE, (msg, payload) => { const {dependent, dependency, data} = payload; const value = DEPENDENCY_PARAMETERS[dependency][dependent][data.from]; const dependentDataElement = $("#id_" + dependent).data("ionRangeSlider"); dependentDataElement.update({max: value}); }); +PubSub.subscribe(eventTopics.PV_CONTROL_ACTIVATED, showPVLayers); +PubSub.subscribe(eventTopics.WIND_CONTROL_ACTIVATED, showWindLayers); // Subscriber Functions +function checkMainPanelSlider(msg, data) { + if (sliderDependencies.hasOwnProperty(data.input[0].id)) { + let target = sliderDependencies[data.input[0].id]; + $('#' + target).data("ionRangeSlider").update({from:data.from}); + } + if (data.input[0].id === "id_s_v_1") { + $(`#id_s_v_3`).data("ionRangeSlider").update({from:data.from}); + $(`#id_s_v_4`).data("ionRangeSlider").update({from:data.from}); + $(`#id_s_v_5`).data("ionRangeSlider").update({from:data.from}); + } + if (data.input[0].id === "id_w_d_wp_1") { + $(`#id_w_d_wp_3`).data("ionRangeSlider").update({from:data.from}); + $(`#id_w_d_wp_4`).data("ionRangeSlider").update({from:data.from}); + $(`#id_w_d_wp_5`).data("ionRangeSlider").update({from:data.from}); + } + if (data.input[0].id === "id_w_v_1") { + $(`#id_w_v_3`).data("ionRangeSlider").update({from:data.from}); + $(`#id_w_v_4`).data("ionRangeSlider").update({from:data.from}); + $(`#id_w_v_5`).data("ionRangeSlider").update({from:data.from}); + } +} + +function showOrHidePotentialLayersOnMoreLabelClick(msg, moreLabel) { + const classes = ["active", "active-sidepanel"]; + const show = moreLabel.classList.contains(classes[0]); + hidePotentialLayers(); + if (show) { + const sliderLabel = moreLabel.getElementsByTagName("input")[0]; + if (sliderLabel.id === "id_s_pv_ff_1") { + PubSub.publish(eventTopics.PV_CONTROL_ACTIVATED); + } + if (sliderLabel.id === "id_s_w_1") { + PubSub.publish(eventTopics.WIND_CONTROL_ACTIVATED); + } + } + return logMessage(msg); +} + function showOrHideSidepanelsOnMoreLabelClick(msg, moreLabel) { const classes = ["active", "active-sidepanel"]; const hide = moreLabel.classList.contains(classes[0]) && moreLabel.classList.contains(classes[1]); @@ -124,6 +274,165 @@ function updateSliderMarks(msg) { return logMessage(msg); } +function showPVLayers(msg) { + hidePotentialLayers(); + for (const layer of potentialPVLayers) { + map.setLayoutProperty(layer, "visibility", "visible"); + } + return logMessage(msg); +} + +function calculate_slider_value(data) { + if (data.input[0].id === "id_s_v_3" || data.input[0].id === "id_s_v_4" || data.input[0].id === "id_s_v_5") { + let factor_hh = $("#id_s_v_3").data("ionRangeSlider").result.from; + let factor_ind = $("#id_s_v_5").data("ionRangeSlider").result.from; + let factor_cts = $("#id_s_v_4").data("ionRangeSlider").result.from; + let demand_hh = store.cold.slider_per_sector.s_v_1.hh; + let demand_ind = store.cold.slider_per_sector.s_v_1.ind; + let demand_cts = store.cold.slider_per_sector.s_v_1.cts; + let new_val = (factor_hh * demand_hh + factor_ind * demand_ind + factor_cts * demand_cts) / (demand_hh + demand_ind + demand_cts); + $(`#id_s_v_1`).data("ionRangeSlider").update({from:new_val}); + } + if (data.input[0].id === "id_w_d_wp_3" || data.input[0].id === "id_w_d_wp_4" || data.input[0].id === "id_w_d_wp_5") { + let factor_hh = $("#id_w_d_wp_3").data("ionRangeSlider").result.from; + let factor_ind = $("#id_w_d_wp_5").data("ionRangeSlider").result.from; + let factor_cts = $("#id_w_d_wp_4").data("ionRangeSlider").result.from; + let demand_hh = store.cold.slider_per_sector.w_d_wp_1.hh; + let demand_ind = store.cold.slider_per_sector.w_d_wp_1.ind; + let demand_cts = store.cold.slider_per_sector.w_d_wp_1.cts; + let new_val = (factor_hh * demand_hh + factor_ind * demand_ind + factor_cts * demand_cts) / (demand_hh + demand_ind + demand_cts); + $(`#id_w_d_wp_1`).data("ionRangeSlider").update({from:new_val}); + } + if (data.input[0].id === "id_w_v_3" || data.input[0].id === "id_w_v_4" || data.input[0].id === "id_w_v_5") { + let factor_hh = $("#id_w_v_3").data("ionRangeSlider").result.from; + let factor_ind = $("#id_w_v_5").data("ionRangeSlider").result.from; + let factor_cts = $("#id_w_v_4").data("ionRangeSlider").result.from; + let demand_hh = store.cold.slider_per_sector.w_d_wp_1.hh; + let demand_ind = store.cold.slider_per_sector.w_d_wp_1.ind; + let demand_cts = store.cold.slider_per_sector.w_d_wp_1.cts; + let new_val = (factor_hh * demand_hh + factor_ind * demand_ind + factor_cts * demand_cts) / (demand_hh + demand_ind + demand_cts); + $(`#id_w_v_1`).data("ionRangeSlider").update({from:new_val}); + } +} + +function calculate_max_wind() { + let slider_one = $("#id_s_w_5_1").data("ionRangeSlider").result.from / 100; + let slider_two = $("#id_s_w_5_2").data("ionRangeSlider").result.from / 100; + let new_max = slider_one * Math.round(store.cold.slider_max.s_w_5_1) + slider_two * Math.round(store.cold.slider_max.s_w_5_2); + $(`#id_s_w_1`).data("ionRangeSlider").update({max:Math.round(new_max)}); +} + +function calculate_max_pv_ff() { + let slider_one = $("#id_s_pv_ff_3").data("ionRangeSlider").result.from / 100; + let slider_two = $("#id_s_pv_ff_4").data("ionRangeSlider").result.from / 100; + let new_max = slider_one * Math.round(store.cold.slider_max.s_pv_ff_3) + slider_two * Math.round(store.cold.slider_max.s_pv_ff_4); + $(`#id_s_pv_ff_1`).data("ionRangeSlider").update({max:Math.round(new_max)}); +} + +function toggleFormFields(formfield_id) { + if (formfield_id === "id_s_w_3") { + if (document.getElementById("id_s_w_4").checked === false && document.getElementById("id_s_w_5").checked === false) { + document.getElementById("id_s_w_3").checked = true; + } + else { + document.getElementById("id_s_w_4").checked = false; + document.getElementById("id_s_w_4_1").disabled = true; + document.getElementById("id_s_w_4_2").disabled = true; + document.getElementById("id_s_w_5").checked = false; + $(`#id_s_w_5_1`).data("ionRangeSlider").update({block:true}); + $(`#id_s_w_5_2`).data("ionRangeSlider").update({block:true}); + $(`#id_s_w_1`).data("ionRangeSlider").update({max:Math.round(store.cold.slider_max.s_w_3)}); + } + } + if (formfield_id === "id_s_w_4") { + if (document.getElementById("id_s_w_3").checked === false && document.getElementById("id_s_w_5").checked === false) { + document.getElementById("id_s_w_4").checked = true; + } + else { + if (document.getElementById("id_s_w_4_1").checked === true && document.getElementById("id_s_w_4_2").checked === true) { + $(`#id_s_w_1`).data("ionRangeSlider").update({max:Math.round(store.cold.slider_max.s_w_4_1) + Math.round(store.cold.slider_max.s_w_4_2)}); + } + if (document.getElementById("id_s_w_4_1").checked === false && document.getElementById("id_s_w_4_2").checked === true) { + $(`#id_s_w_1`).data("ionRangeSlider").update({max:Math.round(store.cold.slider_max.s_w_4_2)}); + } + if (document.getElementById("id_s_w_4_1").checked === true && document.getElementById("id_s_w_4_2").checked === false) { + $(`#id_s_w_1`).data("ionRangeSlider").update({max:Math.round(store.cold.slider_max.s_w_4_1)}); + } + document.getElementById("id_s_w_3").checked = false; + document.getElementById("id_s_w_4_1").disabled = false; + document.getElementById("id_s_w_4_2").disabled = false; + document.getElementById("id_s_w_5").checked = false; + $(`#id_s_w_5_1`).data("ionRangeSlider").update({block: true}); + $(`#id_s_w_5_2`).data("ionRangeSlider").update({block: true}); + } + } + if (formfield_id === "id_s_w_4_1") { + if (document.getElementById("id_s_w_4_2").checked === false) { + document.getElementById("id_s_w_4_1").checked = true; + $(`#id_s_w_1`).data("ionRangeSlider").update({max:Math.round(store.cold.slider_max.s_w_4_1)}); + } + if (document.getElementById("id_s_w_4_1").checked === true && document.getElementById("id_s_w_4_2").checked === true) { + $(`#id_s_w_1`).data("ionRangeSlider").update({max:Math.round(store.cold.slider_max.s_w_4_1) + Math.round(store.cold.slider_max.s_w_4_2)}); + } + if (document.getElementById("id_s_w_4_1").checked === false && document.getElementById("id_s_w_4_2").checked === true) { + $(`#id_s_w_1`).data("ionRangeSlider").update({max:Math.round(store.cold.slider_max.s_w_4_2)}); + } + } + if (formfield_id === "id_s_w_4_2") { + if (document.getElementById("id_s_w_4_1").checked === false) { + document.getElementById("id_s_w_4_2").checked = true; + $(`#id_s_w_1`).data("ionRangeSlider").update({max:Math.round(store.cold.slider_max.s_w_4_2)}); + } + if (document.getElementById("id_s_w_4_1").checked === true && document.getElementById("id_s_w_4_2").checked === true) { + $(`#id_s_w_1`).data("ionRangeSlider").update({max:Math.round(store.cold.slider_max.s_w_4_1) + Math.round(store.cold.slider_max.s_w_4_2)}); + } + if (document.getElementById("id_s_w_4_1").checked === true && document.getElementById("id_s_w_4_2").checked === false) { + $(`#id_s_w_1`).data("ionRangeSlider").update({max:Math.round(store.cold.slider_max.s_w_4_1)}); + } + } + if (formfield_id === "id_s_w_5") { + if (document.getElementById("id_s_w_3").checked === false && document.getElementById("id_s_w_4").checked === false) { + document.getElementById("id_s_w_5").checked = true; + } + else { + document.getElementById("id_s_w_3").checked = false; + document.getElementById("id_s_w_4").checked = false; + document.getElementById("id_s_w_4_1").disabled = true; + document.getElementById("id_s_w_4_2").disabled = true; + $(`#id_s_w_5_1`).data("ionRangeSlider").update({block:false}); + $(`#id_s_w_5_2`).data("ionRangeSlider").update({block:false}); + calculate_max_wind(); + } + } +} + +function showWindLayers(msg) { + hidePotentialLayers(); + if (document.getElementById("id_s_w_3").checked) { + map.setLayoutProperty("potentialarea_wind_stp_2018_vreg", "visibility", "visible"); + } + if (document.getElementById("id_s_w_4").checked) { + if (document.getElementById("id_s_w_4_1").checked) { + map.setLayoutProperty("potentialarea_wind_stp_2027_vr", "visibility", "visible"); + } + if (document.getElementById("id_s_w_4_2").checked) { + map.setLayoutProperty("potentialarea_wind_stp_2027_repowering", "visibility", "visible"); + } + } + if (document.getElementById("id_s_w_5").checked) { + map.setLayoutProperty("potentialarea_wind_stp_2027_search_area_open_area", "visibility", "visible"); + map.setLayoutProperty("potentialarea_wind_stp_2027_search_area_forest_area", "visibility", "visible"); + } + return logMessage(msg); +} + +function hidePotentialLayers(msg) { + for (const layer of potentialPVLayers.concat(potentialWindLayers)) { + map.setLayoutProperty(layer, "visibility", "none"); + } + return logMessage(msg); +} + // Helper Functions @@ -183,3 +492,17 @@ function addMarks(data, marks) { data.slider.append(html); } + + +$(document).ready(function () { + document.getElementById("id_s_w_4_1").checked = true; + document.getElementById("id_s_w_4_1").disabled = true; + document.getElementById("id_s_w_4_2").disabled = true; + document.getElementById("id_s_w_3").checked = true; + $(`#id_s_w_5_1`).data("ionRangeSlider").update({from:13, block:true}); + $(`#id_s_w_5_2`).data("ionRangeSlider").update({from:13, block:true}); + document.getElementById("id_s_w_5_1").disabled = true; + document.getElementById("id_s_w_5_2").disabled = true; + $(`#id_s_h_1`).data("ionRangeSlider").update({block:true}); + calculate_max_pv_ff(); +}); diff --git a/digiplan/static/scss/base/_variables.scss b/digiplan/static/scss/base/_variables.scss index f0465110..5012ba11 100644 --- a/digiplan/static/scss/base/_variables.scss +++ b/digiplan/static/scss/base/_variables.scss @@ -16,6 +16,11 @@ $c-color-conventional: #A6BDDB; $c-color-pvground: #FA9FB5; $c-color-pvroof: #FEC44F; +$windlight: #6A89CC; +$winddark: #273f73; +$pvlight: #ffd744; +$pvdark: #c19800; + // TYPOGRAPHY $c-letter-spacing: 0.03rem; $font-size-xlarge: 1.953rem; // 31.25px @@ -55,9 +60,10 @@ $map-padding-x-small: 2rem; $map-layers-btn-size: 3rem; $map-items-padding: 1rem; $map-layers-box-legend-color-size: 0.875rem; -$map-layers-box-height: 65vh; +$map-layers-box-height: 62vh; $map-layers-box-width: 18rem; $map-layers-box-z-index: 200; +$map-legend-box-height: 8.5rem; $map-legend-box-padding-bottom: 2rem; // Map controls diff --git a/digiplan/static/scss/components/_charts.scss b/digiplan/static/scss/components/_charts.scss index f9c1baad..84ce074c 100644 --- a/digiplan/static/scss/components/_charts.scss +++ b/digiplan/static/scss/components/_charts.scss @@ -1,17 +1,9 @@ -#chart1, -#chart2 { - width: 40%; - height: 260px; -} - -#chart3, -#chart4 { +#region_chart_statusquo, +#region_chart_future { width: 100%; height: 300px; } - - .power-mix { @extend .position-relative; @extend .d-flex; diff --git a/digiplan/static/scss/components/_map.scss b/digiplan/static/scss/components/_map.scss index d691e46e..851ef8aa 100644 --- a/digiplan/static/scss/components/_map.scss +++ b/digiplan/static/scss/components/_map.scss @@ -62,8 +62,9 @@ @extend .bg-white; @extend .border; @extend .p-2; + @extend .overflow-auto; width: $map-layers-box-width; - height: $map-layers-box-height; + height: calc(100vh - #{$top-nav-height} - #{$steps-height} - #{$map-legend-box-height} - #{$map-legend-box-padding-bottom} - (2 * #{$map-items-padding})); top: calc(#{$steps-height} + #{$map-items-padding}); right: $map-items-padding; z-index: $map-layers-box-z-index; @@ -80,6 +81,10 @@ @extend .text-uppercase; @extend .fw-bold; @extend .mb-1; + + &:not(:first-of-type) { + @extend .pt-2; + } } } @@ -89,17 +94,27 @@ @extend .justify-content-between; &__legend { + @extend .d-flex; + @extend .flex-row; + @extend .flex-nowrap; + &-color { @extend .d-inline-block; @include translateY(2px); + flex: 0 0 1rem; width: 1rem; height: 1rem; - background-color: blue; + + &--rounded { + @extend .rounded-circle; + } } &-text { @extend .d-inline-block; @extend .fs-7; + @extend .fw-light; + @extend .ps-1; } } &__control { @@ -123,7 +138,6 @@ @extend .fs-8; background-color: rgba(255, 255, 255, 0.8); width: calc(100% - 7rem); - height: 10rem; right: $map-padding-x-small; bottom: 5rem; z-index: $map-layers-box-z-index; @@ -131,7 +145,6 @@ @include media-breakpoint-up(sm) { font-size: $font-size-small !important; width: $map-layers-box-width; - height: calc(100vh - #{$top-nav-height} - #{$steps-height} - #{$map-layers-box-height} - (2 * #{#{$map-items-padding}}) - #{$map-legend-box-padding-bottom}); right: $map-items-padding; bottom: 2rem; } @@ -177,13 +190,17 @@ } } -@media only screen and (max-height: 700px) { - .map__layers-box { - height: 55vh; - } +#legend.legend { + height: $map-legend-box-height; .legend { - height: calc(100vh - #{$top-nav-height} - #{$steps-height} - 55vh - (2 * #{#{$map-items-padding}}) - #{$map-legend-box-padding-bottom}); + &__title { + @extend .d-none; + } + + &__detail { + @extend .pb-1; + } } } diff --git a/digiplan/static/scss/components/_modals.scss b/digiplan/static/scss/components/_modals.scss index e6db2545..8c0b2d9f 100644 --- a/digiplan/static/scss/components/_modals.scss +++ b/digiplan/static/scss/components/_modals.scss @@ -25,3 +25,46 @@ @extend .py-4; } } + +.onboarding-modal { + .carousel-item { + &__content { + @extend .col-8; + @extend .bg-white; + @extend .ps-5; + @extend .pe-4; + } + } + + img { + height: 100%; + object-fit: cover; + } + + .carousel-control-next, + .carousel-control-prev { + @extend .position-relative; + opacity: 1 !important; + background-color: $gray-700 !important; + + &.transparent { + @extend .bg-white; + @extend .text-white; + pointer-events: none; + } + } + + .carousel-control-prev { + background-color: $gray-200 !important; + color: $gray-800 !important; + } + + .btn, .modal-footer button { + opacity: 1 !important; + } + + .modal-body { + height: 40rem; + overflow: hidden; + } +} diff --git a/digiplan/static/scss/components/_popup.scss b/digiplan/static/scss/components/_popup.scss index 0e80a235..b60a4813 100644 --- a/digiplan/static/scss/components/_popup.scss +++ b/digiplan/static/scss/components/_popup.scss @@ -84,6 +84,26 @@ } } + .popup--cluster { + table { + @extend .table; + @extend .table-sm; + @extend .mb-0; + + thead th { + @extend .border-bottom; + @extend .border-primary; + @extend .fs-7; + @extend .p-1; + } + + tr th, + tr td { + @extend .ps-2; + } + } + } + .maplibregl-popup-close-button, .mapboxgl-popup-close-button { @extend .fs-3; diff --git a/digiplan/static/scss/layouts/_panel.scss b/digiplan/static/scss/layouts/_panel.scss index 07446459..a551c62f 100644 --- a/digiplan/static/scss/layouts/_panel.scss +++ b/digiplan/static/scss/layouts/_panel.scss @@ -151,7 +151,8 @@ } .c-slider.active { - @extend .px-3; + @extend .ps-2; + @extend .pe-3; @extend .bg-light; } @@ -273,7 +274,7 @@ @extend .bg-light; @extend .p-3; @extend .overflow-auto; - width: 18rem; + width: 20rem; left: $panel-width-sm; padding-top: $padding-large !important; @@ -291,8 +292,100 @@ @extend .d-flex; @extend .flex-row; @extend .justify-content-between; - @extend .fs-7; + @extend .fs-6; @extend .fw-bold; + @extend .mb-1; + } + + &__indication { + @extend .text-secondary; + @extend .fs-7; + } + + .sidepanel__block { + @extend .border-bottom; + @extend .pb-2; + @extend .pt-2; + + .c-slider { + @extend .d-flex; + @extend .flex-row; + @extend .justify-content-between; + @extend .pb-0; + + &:first-of-type .form-switch .form-check-input { + width: 2.25rem; + height: 1.25rem; + } + + &:not(:first-of-type) { + @extend .fw-light; + @extend .ps-2; + } + + &.s_w_5_1, + &.s_w_5_2 { + @extend .d-block; + } + } + + &--wind-light { + .form-check.form-switch .form-check-input:checked, + .irs.irs--flat .irs-bar, .irs.irs--flat .irs-handle > i:first-child, .irs.irs--flat .irs-single { + background-color: $windlight; + border-color: $windlight; + } + + .irs.irs--flat .irs-single:before { + border-top-color: $windlight; + } + } + + &--wind-dark { + .form-check.form-switch .form-check-input:checked, + .irs.irs--flat .irs-bar, .irs.irs--flat .irs-handle > i:first-child, .irs.irs--flat .irs-single { + background-color: $winddark; + border-color: $winddark; + } + + .irs.irs--flat .irs-single:before { + border-top-color: $winddark; + } + } + + &--pv-light { + .irs.irs--flat .irs-bar, .irs.irs--flat .irs-handle > i:first-child, .irs.irs--flat .irs-single { + background-color: $pvlight; + border-color: $pvlight; + } + + .irs.irs--flat .irs-single:before { + border-top-color: $pvlight; + } + } + + &--pv-dark { + .irs.irs--flat .irs-bar, .irs.irs--flat .irs-handle > i:first-child, .irs.irs--flat .irs-single { + background-color: $pvdark; + border-color: $pvdark; + } + + .irs.irs--flat .irs-single:before { + border-top-color: $pvdark; + } + } + } + + .sidepanel__block.sidepanel__block--slider { + .c-slider { + @extend .d-block; + } + } + + &__results { + @extend .pb-1; + @extend .pt-3; + @extend .fs-7; } &__close { diff --git a/digiplan/templates/components/map.html b/digiplan/templates/components/map.html index 230ab600..51590d31 100644 --- a/digiplan/templates/components/map.html +++ b/digiplan/templates/components/map.html @@ -1,5 +1,7 @@ {% load static i18n %} +{% include "components/onboarding.html" %} +
+
+ + +
+ + diff --git a/digiplan/templates/components/panel_1_today.html b/digiplan/templates/components/panel_1_today.html index 3428d23c..5203d216 100644 --- a/digiplan/templates/components/panel_1_today.html +++ b/digiplan/templates/components/panel_1_today.html @@ -1,9 +1,9 @@ {% load static i18n %}
-
- {% translate "Goals 2045" %} -
+ + +
@@ -15,7 +15,7 @@
- {% translate "CO2-Emissions (Mt)" %} + {% translate "CO2-Reduktion ggü. 2019 (%)" %}
@@ -36,62 +36,66 @@

Info Icon @@ -99,8 +103,7 @@

-
-
+

diff --git a/digiplan/templates/components/panel_3_results.html b/digiplan/templates/components/panel_3_results.html index dae926bd..ef52abca 100644 --- a/digiplan/templates/components/panel_3_results.html +++ b/digiplan/templates/components/panel_3_results.html @@ -12,7 +12,7 @@
- {% translate "CO2-Emissions (Mt)" %} + {% translate "CO2-Reduktion ggü. 2019 (%)" %}
@@ -42,14 +42,14 @@

title="{% translate "Annual balance sheet share of renewable energy (wind energy, photovoltaic, hydroelectric power) in the electricity demand of the sectors agriculture, GHG (trade, commerce, services), households and industry in percent. The demand is the net electricity consumption, i.e. transmission losses and power plant consumption are not taken into account. 49.7% of the electricity demand in Germany was covered by renewable energies in 2022." %}"> {% translate "Renewable Energy Share of Demand" %} (%) - - - @@ -91,6 +91,11 @@

Info Icon

+
+
+
+
+

{% translate "View" %}

@@ -100,7 +105,7 @@

- {% translate "General" %} + {% translate "Overview" %} +

diff --git a/digiplan/templates/components/results_view.html b/digiplan/templates/components/results_view.html index b617a725..ddb6611e 100644 --- a/digiplan/templates/components/results_view.html +++ b/digiplan/templates/components/results_view.html @@ -32,19 +32,21 @@

{% translate "General" %}

-

{% translate "General" %}

+

{% translate "Electricity" %}

{% translate "Overview" %}
-
+
+ {% translate "Hier folgt ein Beschreibungstext..." noop %} +
{% translate "CO2 Emissions" %}
-
+ {% translate "Hier folgt ein Beschreibungstext..." noop %}
@@ -55,34 +57,68 @@

{% translate "Heat" %}

-
{% translate "Overview" %}
+
{% translate "Endenergiebedarf" %}
-
+
+ {% translate "Hier folgt ein Beschreibungstext..." noop %} +
-
{% translate "CO2 Emissions" %}
+
{% translate "Beheizungsstruktur" %}
-
+ {% translate "Hier folgt ein Beschreibungstext..." noop %}
-

{% translate "General" %}

+

{% translate "Motorized individual traffic" %}

-
{% translate "Overview" %}
+
{% translate "Vehicles" %}
-
+
+ {% translate "Zugelassene Fahrzeuge nach Antriebsart." noop %} +
{% translate "CO2 Emissions" %}
-
+
+ 1 t CO2 + = + XXX km {% translate "Distance" %} + + + + + +
+
+
+
+
+
+
+

{% translate "Abschätzung CO2-Emissionen" %}

+
+
+
+
{% translate "Sachsen-Anhalt und Abschätzung Region ABW" %}
+
+
+ {% translate "Die THG-Emissionen für die Region ABW werden näherungsweise anhand unterschiedlicher Kriterien und Datenquellen aus den Gesamtemissionen für Sachsen-Anhalt (THG-Bericht 2021) von 1990 und 2019 disaggregiert. Die sektorale Unterteilung orientiert sich an diesem Bericht.
Der energiebedingte Sektor stellt den größten Emissionsverursacher dar und gliedert sich hier in die Untersektoren Energiewirtschaft, Industrie, Verkehr sowie \"Sonstige Energie\" (insbesondere Gebäude)." noop %} +
+
+
+
{% translate "Abschätzung der Reduktion in der Region ABW" %}
+
+
+ {% translate "Hier folgt ein Beschreibungstext..." noop %}
diff --git a/digiplan/templates/forms/layer.html b/digiplan/templates/forms/layer.html index 6ce91bf6..7eb40594 100644 --- a/digiplan/templates/forms/layer.html +++ b/digiplan/templates/forms/layer.html @@ -4,7 +4,7 @@
-
+
diff --git a/digiplan/templates/forms/panel_energy.html b/digiplan/templates/forms/panel_energy.html index 7badd2a0..c422f0fc 100644 --- a/digiplan/templates/forms/panel_energy.html +++ b/digiplan/templates/forms/panel_energy.html @@ -4,50 +4,97 @@

{% trans "Generation" %}

{% include "widgets/slider.html" with field=form.s_w_1 %} -
-

+

+
{% trans "Detail settings" %} {% include "widgets/sidepanel_close_btn.html" %} -

{% trans "Set the use of wind potential areas." %}

- {% include "widgets/slider.html" with field=form.s_w_3 %} - {% include "widgets/slider.html" with field=form.s_w_4 %} - {% include "widgets/slider.html" with field=form.s_w_4_1 %} - {% include "widgets/slider.html" with field=form.s_w_4_2 %} - {% include "widgets/slider.html" with field=form.s_w_5 %} - {% include "widgets/slider.html" with field=form.s_w_5_1 %} - {% include "widgets/slider.html" with field=form.s_w_5_2 %} -

{% trans "With your settings, a maximum total output of 13 MW and a yield of 7 GWh can be realized.
The goals for Saxony-Anhalt from the Wind Energy Area Requirement Act are:
- for 2027 (1.4%) achieved.
- for 2032 (2.2%) achieved.

Tip: In the menu on the right you can display the systems that exist today." %}

+
+

+ {% trans "Set the use of wind potential areas." %}

+
+ + {% include "widgets/slider.html" with field=form.s_w_3 %} + +
+
+ + {% include "widgets/slider.html" with field=form.s_w_4 %} + + + {% include "widgets/slider.html" with field=form.s_w_4_1 %} + + + {% include "widgets/slider.html" with field=form.s_w_4_2 %} + +
+
+ + {% include "widgets/slider.html" with field=form.s_w_5 %} + + + {% include "widgets/slider.html" with field=form.s_w_5_1 %} + + + {% include "widgets/slider.html" with field=form.s_w_5_2 %} + +
+
+

{% trans "With your settings, a maximum total output of 13 MW and a yield of 7 GWh can be realized.
The goals for Saxony-Anhalt from the Wind Energy Area Requirement Act are:
- for 2027 (1.4%) achieved.
- for 2032 (2.2%) achieved.

Tip: In the menu on the right you can display the systems that exist today." %}

+
{% include "widgets/slider.html" with field=form.s_pv_ff_1 %} -
-

+

+
{% trans "Detail settings" %} {% include "widgets/sidepanel_close_btn.html" %} -

{% trans "Set the use of potential areas for ground-mounted PV." %}

- {% include "widgets/slider.html" with field=form.s_pv_ff_3 %} - {% include "widgets/slider.html" with field=form.s_pv_ff_4 %} -

{% trans "Mit Ihren Einstellungen kann eine maximale Gesamtleistung von 18 MW und ein Ertrag von 3 GWh realisiert werden.

Tipp: Im rechten Menü können Sie die heute bestehenden Anlagen einblenden." %}

+
+

+ {% trans "Set the use of potential areas for ground-mounted PV." %} +

+
+ + {% include "widgets/slider.html" with field=form.s_pv_ff_3 %} + +
+
+ + {% include "widgets/slider.html" with field=form.s_pv_ff_4 %} + +
+
+

{% trans "With your settings, a maximum total output of 18 MW and a yield of 3 GWh can be realized.

Tip: In the right-hand menu, you can display the systems that currently exist." %}

+

{% include "widgets/slider.html" with field=form.s_pv_d_1 %} -
-

+

+
{% trans "Detail settings" %} {% include "widgets/sidepanel_close_btn.html" %} -

{% trans "Set how much roof area should be used for photovoltaics." %}

- {% include "widgets/slider.html" with field=form.s_pv_d_3 %} - {% include "widgets/slider.html" with field=form.s_pv_d_4 %} -

{% trans "Mit Ihren Einstellungen kann eine maximale Gesamtleistung von 4 MW und ein Ertrag von 2 GWh realisiert werden. Die Aufteilung der Ausrichtung ist wie folgt:
- Süddächer: 3 %
- Westdächer: 3 %
- Norddächer: 3 %
- Ostdächer: 8 %
Denkmalgeschützte Gebäude werden nicht verwendet.

Tipp: Im rechten Menü können Sie die heute bestehenden Anlagen einblenden." %}

+
+

+ {% trans "Set how much roof area should be used for photovoltaics." %}

+
+ + {% include "widgets/slider.html" with field=form.s_pv_d_3 %} + +
+
+ + {% include "widgets/slider.html" with field=form.s_pv_d_4 %} + +
+
+

{% trans "With your settings, a maximum total output of 4 MW and a yield of 2 GWh can be realized. The distribution of the orientation is as follows:
- South roofs: 3%
- West roofs: 3%
- North roofs: 3%
- East roofs: 8%
Buildings under monument protection are not used.

Tip: In the menu on the right you can show the systems that exist today." %}

+
{% include "widgets/slider.html" with field=form.s_h_1 %} -{% include "widgets/slider.html" with field=form.s_b_1 %} -

{% trans "Consumption" %}

@@ -60,7 +107,7 @@

{% trans "Consumption" %}

{% include "widgets/slider.html" with field=form.s_v_3 %} {% include "widgets/slider.html" with field=form.s_v_4 %} {% include "widgets/slider.html" with field=form.s_v_5 %} -

{% trans "Mit Ihren Einstellungen beträgt der Stromverbrauch 42 % des heutigen Stromverbrauchs." %}

+

{% trans "With your settings, the power consumption is 42% of today's power consumption." %}

@@ -74,6 +121,6 @@

{% trans "Batteries" %}

{% include "widgets/sidepanel_close_btn.html" %}

{% trans "Set how much of the daily (on average) fed-in energy from wind energy and free-field PV should be able to be temporarily stored using large batteries." %}

{% include "widgets/slider.html" with field=form.s_s_g_3 %} -

{% trans "Mit Ihren Einstellungen können 73 % zwischengespeichert werden. Das entspricht einer Speicherkapazität von 17 GWh." %}

+

{% trans "With your settings, 73% can be cached. That corresponds to a storage capacity of 17 GWh." %}

diff --git a/digiplan/templates/forms/panel_heat.html b/digiplan/templates/forms/panel_heat.html index 882f37c2..625f692e 100644 --- a/digiplan/templates/forms/panel_heat.html +++ b/digiplan/templates/forms/panel_heat.html @@ -9,7 +9,7 @@

{% trans "Generation" %}

{% include "widgets/slider.html" with field=form.w_d_wp_3 %} {% include "widgets/slider.html" with field=form.w_d_wp_4 %} {% include "widgets/slider.html" with field=form.w_d_wp_5 %} -

{% trans "Mit Ihren Einstellungen beträgt der Anteil der mittels Wärmepumpen bereitgestellten Energiemenge 87 %. Die übrige Energie wird bereitgestellt durch:
- Direktelektrische Heizung: 10 %
- Bioenergie: 5 %
- Solarthermie: 20 %

Hinweis: Fernwärmesysteme werden links im Menü separat konfiguriert." %}

+

{% trans "With your settings, the proportion of energy provided by heat pumps is 87%. The remaining energy is provided by:
- Direct electric heating: 10%
- Bioenergy: 5%
- Solar thermal: 20%

Note: District heating systems are configured separately in the menu on the left." %}

@@ -20,7 +20,7 @@

{% trans "Generation" %}

{% include "widgets/sidepanel_close_btn.html" %}

{% trans "Set the share of heat pumps for all consumers with district heating connection. This includes households, tertiary and industrial customers in the Dessau-Roßlau, Bitterfeld-Wolfen, Köthen and Wittenberg district heating networks." %}

{% include "widgets/slider.html" with field=form.w_z_wp_3 %} -

{% trans "Mit Ihren Einstellungen beträgt der Anteil der mittels Wärmepumpen bereitgestellten Energiemenge 54 %. Die übrige Energie wird bereitgestellt durch:
- Direktelektrische Heizung: 12 %
- Bioenergie: 6 %
- Solarthermie: 52 %

Hinweis: Dezentrale Verbraucher werden links im Menü separat konfiguriert." %}

+

{% trans "With your settings, the proportion of energy provided by heat pumps is 54%. The remaining energy is provided by:
- Direct electric heating: 12%
- Bioenergy: 6%
- Solar thermal: 52%

Note: Decentralized consumers are configured separately in the menu on the left." %}

@@ -37,7 +37,7 @@

{% trans "Consumption" %}

{% include "widgets/slider.html" with field=form.w_v_3 %} {% include "widgets/slider.html" with field=form.w_v_4 %} {% include "widgets/slider.html" with field=form.w_v_5 %} -

{% trans "Mit Ihren Einstellungen beträgt der Wärmeverbrauch 27 % des heutigen Wärmeverbrauchs." %}

+

{% trans "With your settings, the heat consumption is 27% of today's heat consumption." %}

@@ -52,7 +52,7 @@

{% trans "Storage" %}

{% include "widgets/sidepanel_close_btn.html" %}

{% trans "Set how much of the daily heat requirement should be able to be temporarily stored in hot water tanks." %}

{% include "widgets/slider.html" with field=form.w_d_s_3 %} -

{% trans "Mit Ihren Einstellungen können 32 % des täglichen Wärmeverbrauchs zwischengespeichert werden. Das entspricht einer Speicherkapazität von 3 GWh" %}

+

{% trans "With your settings, 32% of the daily heat consumption can be temporarily stored. That corresponds to a storage capacity of 3 GWh" %}

@@ -63,6 +63,6 @@

{% trans "Storage" %}

{% include "widgets/sidepanel_close_btn.html" %}

{% trans "Set how much of the daily heat requirement should be able to be temporarily stored in hot water tanks." %}

{% include "widgets/slider.html" with field=form.w_z_s_3 %} -

{% trans "Mit Ihren Einstellungen können 34 % des täglichen Wärmeverbrauchs zwischengespeichert werden. Das entspricht einer Speicherkapazität von 5 GWh" %}

+

{% trans "With your settings, 57% of the daily heat consumption can be temporarily stored. That corresponds to a storage capacity of 5 GWh" %}

diff --git a/digiplan/templates/forms/panel_traffic.html b/digiplan/templates/forms/panel_traffic.html index 75673283..186ab0f9 100644 --- a/digiplan/templates/forms/panel_traffic.html +++ b/digiplan/templates/forms/panel_traffic.html @@ -7,6 +7,6 @@ {% include "widgets/sidepanel_close_btn.html" %}

{% trans "Set the share of electric vehicles in motorized private transport." %}

{% include "widgets/slider.html" with field=form.v_iv_3 %} -

{% trans "Mit Ihren Einstellungen sind 22 % der privaten Fahrzeuge (Kleinwagen, Mittelklasse, Oberklasse) elektrisch. Davon sind
- 10 % batterieelektrisch (BEV) und
- 4 % Plugin-Hybrid-Fahrzeuge." %}

+

{% trans "With your settings, 22% of private vehicles (compact, mid-size, full-size) are electric. Of these,
- 50% are battery electric (BEV) and
- 50% are plug-in hybrid vehicles." %}

diff --git a/digiplan/templates/map.html b/digiplan/templates/map.html index 5bfe9526..fcd6ca00 100644 --- a/digiplan/templates/map.html +++ b/digiplan/templates/map.html @@ -101,6 +101,8 @@ {{ mobility_ghg|json_script:"mobility_ghg" }} {{ overview_heat|json_script:"overview_heat" }} {{ decentralized_centralized_heat|json_script:"decentralized_centralized_heat" }} + {{ ghg_history|json_script:"ghg_history" }} + {{ ghg_reduction|json_script:"ghg_reduction" }} {{ settings_parameters|json_script:"settings_parameters" }} {{ settings_dependency_map|json_script:"settings_dependency_map" }} {{ dependency_parameters|json_script:"dependency_parameters" }} diff --git a/digiplan/templates/popups/base.html b/digiplan/templates/popups/base.html index bb9b2a20..7673b79a 100644 --- a/digiplan/templates/popups/base.html +++ b/digiplan/templates/popups/base.html @@ -8,11 +8,11 @@