From ec0dd21ef0038d4dff35fbc8036d8b084550d7f1 Mon Sep 17 00:00:00 2001 From: Hendrik Huyskens Date: Tue, 23 Apr 2024 17:42:28 +0200 Subject: [PATCH] Implement multiple basemaps --- CHANGELOG.md | 4 + django_mapengine/apps.py | 11 +- django_mapengine/layers.py | 27 +++ django_mapengine/setup.py | 30 +++ django_mapengine/sources.py | 21 +- .../images/layer_ctrl_default.svg | 115 +++++++++++ .../images/layer_ctrl_satellite.svg | 189 ++++++++++++++++++ .../static/django_mapengine/js/basemaps.js | 34 ++-- .../static/django_mapengine/js/layer.js | 26 +++ .../static/django_mapengine/js/map.js | 23 --- .../templates/django_mapengine/map.html | 2 - .../django_mapengine/map_basemaps.html | 18 +- .../templates/django_mapengine/map_json.html | 1 + django_mapengine/views.py | 2 + 14 files changed, 443 insertions(+), 60 deletions(-) create mode 100644 django_mapengine/static/django_mapengine/images/layer_ctrl_default.svg create mode 100644 django_mapengine/static/django_mapengine/images/layer_ctrl_satellite.svg diff --git a/CHANGELOG.md b/CHANGELOG.md index 64e8cb6..9e1704b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and the versioning aim to respect [Semantic Versioning](http://semver.org/spec/v Here is a template for new release sections +## [Unreleased] +### Added +- multiple basemaps possible + ## [1.4.1] - 2024-04-08 ### Added - initial cluster docs diff --git a/django_mapengine/apps.py b/django_mapengine/apps.py index 0d45e18..bfbca24 100644 --- a/django_mapengine/apps.py +++ b/django_mapengine/apps.py @@ -8,7 +8,7 @@ from appconf import AppConf from django.conf import settings -from . import choropleth +from . import choropleth, setup env = environ.Env() @@ -66,6 +66,15 @@ class MapEngineConf(AppConf): # MVTS and CLUSTERS + BASEMAPS = [ + setup.MaptilerBasemap( + "satellite", + source_id="satellite", + type="raster", + image="django_mapengine/images/layer_ctrl_satellite.svg", + description="Satellite basemap view", + ) + ] API_MVTS = {} API_CLUSTERS = [] diff --git a/django_mapengine/layers.py b/django_mapengine/layers.py index 9e2b261..3e44292 100644 --- a/django_mapengine/layers.py +++ b/django_mapengine/layers.py @@ -14,6 +14,27 @@ from django_mapengine import setup +@dataclass +class BasemapLayer: + """Default map layer used in maplibre.""" + + # pylint:disable=C0103 + id: str # noqa: A003 + source: str + type: str # noqa: A003 + + def get_layer(self): + """ + Build dict from layer settings and style. + + Returns + ------- + dict + to be used as layer in maplibre. + """ + return {"id": self.id, "source": self.source, "type": self.type} + + @dataclass class MapLayer: """Default map layer used in maplibre.""" @@ -163,6 +184,12 @@ def get_map_layers(self) -> Iterable[MapLayer]: ) +def get_basemap_layers() -> Iterable[BasemapLayer]: + """Return basemap layers""" + for basemap in settings.MAP_ENGINE_BASEMAPS: + yield BasemapLayer(id=basemap.layer_id, source=basemap.layer_id, type=basemap.type) + + def get_region_layers() -> Iterable[MapLayer]: """ Return map layers for region-based models. diff --git a/django_mapengine/setup.py b/django_mapengine/setup.py index 8294936..406d161 100644 --- a/django_mapengine/setup.py +++ b/django_mapengine/setup.py @@ -13,6 +13,35 @@ Zoom = namedtuple("MinMax", ("min", "max")) +@dataclass +class MaptilerBasemap: + """ + Base class for a basemap + + This is used to: + - prepare basemap layers + - prepare basemap sources + """ + + layer_id: str + source_id: str + description: str + image: str + type: str = "vector" # noqa: A003 + format: str = "jpg" + + def as_dict(self): + """Return maptilerBasemap as dict""" + return { + "layer_id": self.layer_id, + "source_id": self.source_id, + "description": self.description, + "image": self.image, + "type": self.type, + "format": self.format, + } + + @dataclass class ModelAPI: """ @@ -45,6 +74,7 @@ def model(self) -> "Model": @dataclass class ClusterAPI(ModelAPI): """Exists only to distinguish between "normal" and clustered API""" + properties: list = field(default_factory=lambda: []) diff --git a/django_mapengine/sources.py b/django_mapengine/sources.py index 8d2195b..9ec19c4 100644 --- a/django_mapengine/sources.py +++ b/django_mapengine/sources.py @@ -154,7 +154,7 @@ def get_cluster_sources() -> Iterable[MapSource]: ) -def get_satellite_source() -> MapSource: +def get_satellite_sources() -> Iterable[MapSource]: """ Return source for satellite basemap @@ -163,14 +163,15 @@ def get_satellite_source() -> MapSource: MapSource of satellite raster """ - return MapSource( - "satellite", - type="raster", - tiles=[ - "https://api.maptiler.com/tiles/satellite-v2/" - f"{{z}}/{{x}}/{{y}}.jpg?key={settings.MAP_ENGINE_TILING_SERVICE_TOKEN}", - ], - ) + for basemap in settings.MAP_ENGINE_BASEMAPS: + yield MapSource( + basemap.layer_id, + type=basemap.type, + tiles=[ + f"https://api.maptiler.com/maps/{basemap.source_id}/" + f"{{z}}/{{x}}/{{y}}.{basemap.format}?key={settings.MAP_ENGINE_TILING_SERVICE_TOKEN}", + ], + ) def get_all_sources() -> List[MapSource]: @@ -183,7 +184,7 @@ def get_all_sources() -> List[MapSource]: all map sources """ sources = list(get_region_sources()) - sources.append(get_satellite_source()) + sources.extend(get_satellite_sources()) sources.extend(get_static_sources()) sources.extend(get_cluster_sources()) return sources diff --git a/django_mapengine/static/django_mapengine/images/layer_ctrl_default.svg b/django_mapengine/static/django_mapengine/images/layer_ctrl_default.svg new file mode 100644 index 0000000..f00732f --- /dev/null +++ b/django_mapengine/static/django_mapengine/images/layer_ctrl_default.svg @@ -0,0 +1,115 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/django_mapengine/static/django_mapengine/images/layer_ctrl_satellite.svg b/django_mapengine/static/django_mapengine/images/layer_ctrl_satellite.svg new file mode 100644 index 0000000..3f0ff84 --- /dev/null +++ b/django_mapengine/static/django_mapengine/images/layer_ctrl_satellite.svg @@ -0,0 +1,189 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/django_mapengine/static/django_mapengine/js/basemaps.js b/django_mapengine/static/django_mapengine/js/basemaps.js index 923c8fd..942e066 100644 --- a/django_mapengine/static/django_mapengine/js/basemaps.js +++ b/django_mapengine/static/django_mapengine/js/basemaps.js @@ -1,30 +1,34 @@ -const basemaps = ["satellite"]; + +const toggleBasemapButton = document.getElementById("basemaps-control"); +toggleBasemapButton.onclick = toggleBasemapControl; + +const basemapButtons = document.getElementById("basemaps").querySelectorAll("button"); +const basemaps = Array.from(basemapButtons).map(function (basemapButton) {return basemapButton.id.slice("basemaps__".length);}); + +for (const basemapButton of basemapButtons) { + let basemap_layer = basemapButton.id.slice("basemaps__".length); + basemapButton.addEventListener("click", () => { + toggleBasemap(basemap_layer); + }); +} function toggleBasemap(basemap) { map_store.cold.basemap = basemap; - if (basemap === null) { - map_store.cold.basemapFocusElement = "basemaps__default"; - } else { - map_store.cold.basemapFocusElement = `basemaps__${basemap}`; - } - const legend = document.getElementById("legend"); for (const bm of basemaps) { - map.setLayoutProperty(bm, "visibility", "none"); + if (bm !== "default") { + map.setLayoutProperty(bm, "visibility", "none"); + } } - if (basemap !== null) { + if (basemap !== "default") { map.setLayoutProperty(basemap, "visibility", "visible"); } else { legend.hidden = true; - } } -// Toggle basemaps control -let toggleBasemapButton = document.getElementById("basemaps-control"); - function toggleBasemapControl() { const basemapControl = document.getElementById("basemaps"); @@ -33,8 +37,6 @@ function toggleBasemapControl() { } else { basemapControl.style.display = "flex"; - document.getElementById(map_store.cold.basemapFocusElement).focus(); + document.getElementById(`basemaps__${map_store.cold.basemap}`).focus(); } } - -toggleBasemapButton.onclick = toggleBasemapControl; diff --git a/django_mapengine/static/django_mapengine/js/layer.js b/django_mapengine/static/django_mapengine/js/layer.js index dc2d2a1..0822f49 100644 --- a/django_mapengine/static/django_mapengine/js/layer.js +++ b/django_mapengine/static/django_mapengine/js/layer.js @@ -22,12 +22,38 @@ map.on("load", function () { // Subscriptions +PubSub.subscribe(mapEvent.MAP_SOURCES_LOADED, add_basemap_layers); PubSub.subscribe(mapEvent.MAP_SOURCES_LOADED, add_layers); PubSub.subscribe(mapEvent.MAP_LAYER_SWITCH_CLICK, toggleLayer); // Subscriber Functions +function add_basemap_layers(msg) { + const layers = map.getStyle().layers; + // Find the index of the first symbol layer in the map style + let beforeLayer; + for (let i = 0; i < layers.length; i++) { + if (layers[i].type === "symbol") { + firstSymbolId = layers[i].id; + break; + } + } + const basemap_layers = JSON.parse(document.getElementById("mapengine_basemap_layers").textContent); + for (const basemap_layer of basemap_layers) { + map.addLayer( + { + id: basemap_layer.layer_id, + type: basemap_layer.type, + source: basemap_layer.layer_id, + }, + beforeLayer + ); + map.setLayoutProperty(basemap_layer.layer_id, "visibility", "none"); + } + return logMessage(msg); +} + function add_layers(msg) { const layers = JSON.parse(document.getElementById("mapengine_layers").textContent); diff --git a/django_mapengine/static/django_mapengine/js/map.js b/django_mapengine/static/django_mapengine/js/map.js index a60edee..885bf43 100644 --- a/django_mapengine/static/django_mapengine/js/map.js +++ b/django_mapengine/static/django_mapengine/js/map.js @@ -4,32 +4,9 @@ map.on("load", async function() { }); PubSub.subscribe(mapEvent.MAP_LOADED, add_sources); -PubSub.subscribe(mapEvent.MAP_LOADED, add_satellite); PubSub.subscribe(mapEvent.MAP_LOADED, add_images); -function add_satellite(msg) { - const layers = map.getStyle().layers; - // Find the index of the first symbol layer in the map style - let firstSymbolId; - for (let i = 0; i < layers.length; i++) { - if (layers[i].type === "symbol") { - firstSymbolId = layers[i].id; - break; - } - } - map.addLayer( - { - id: "satellite", - type: "raster", - source: "satellite" - }, - firstSymbolId - ); - map.setLayoutProperty("satellite", "visibility", "none"); - return logMessage(msg); -} - function add_sources(msg) { const sources = JSON.parse(document.getElementById("mapengine_sources").textContent); for (const source in sources) { diff --git a/django_mapengine/templates/django_mapengine/map.html b/django_mapengine/templates/django_mapengine/map.html index b6c9e68..111c6e4 100644 --- a/django_mapengine/templates/django_mapengine/map.html +++ b/django_mapengine/templates/django_mapengine/map.html @@ -1,7 +1,5 @@ {% load static %} -{% include "./map_basemaps.html" %} -
diff --git a/django_mapengine/templates/django_mapengine/map_basemaps.html b/django_mapengine/templates/django_mapengine/map_basemaps.html index 5d83d15..882d96f 100644 --- a/django_mapengine/templates/django_mapengine/map_basemaps.html +++ b/django_mapengine/templates/django_mapengine/map_basemaps.html @@ -9,15 +9,17 @@ diff --git a/django_mapengine/templates/django_mapengine/map_json.html b/django_mapengine/templates/django_mapengine/map_json.html index 16bb817..79075e9 100644 --- a/django_mapengine/templates/django_mapengine/map_json.html +++ b/django_mapengine/templates/django_mapengine/map_json.html @@ -1,6 +1,7 @@ {{ mapengine_setup|json_script:"mapengine_setup" }} {{ mapengine_sources|json_script:"mapengine_sources" }} +{{ mapengine_basemap_layers|json_script:"mapengine_basemap_layers" }} {{ mapengine_layers|json_script:"mapengine_layers" }} {{ mapengine_images|json_script:"mapengine_images" }} {{ mapengine_popups|json_script:"mapengine_popups" }} diff --git a/django_mapengine/views.py b/django_mapengine/views.py index feefb9e..3de40b1 100644 --- a/django_mapengine/views.py +++ b/django_mapengine/views.py @@ -46,6 +46,7 @@ def get_context_data(self, **kwargs) -> dict: "mapengine_sources": { source.name: source.get_source(self.request) for source in sources.get_all_sources() }, + "mapengine_basemap_layers": [basemap.as_dict() for basemap in settings.MAP_ENGINE_BASEMAPS], "mapengine_layers": [layer.get_layer() for layer in layers.get_all_layers()], "mapengine_layers_at_startup": settings.MAP_ENGINE_LAYERS_AT_STARTUP + settings.MAP_ENGINE_REGIONS, "mapengine_images": [image.as_dict() for image in settings.MAP_ENGINE_IMAGES], @@ -60,6 +61,7 @@ def get_context_data(self, **kwargs) -> dict: "cluster_layers": [cluster.layer_id for cluster in settings.MAP_ENGINE_API_CLUSTERS], "choropleths": {choropleth.name: choropleth.as_dict() for choropleth in settings.MAP_ENGINE_CHOROPLETHS}, "layer_switch_class": settings.MAP_ENGINE_LAYER_SWITCH_CLASS, + "basemap": "default", } context["mapengine_store_cold_init"] = store