From 2f1c42a1b4013a3c00fab862443fa699fbca773f Mon Sep 17 00:00:00 2001 From: Clawd Date: Wed, 11 Feb 2026 13:19:28 +0000 Subject: [PATCH] Fix DWD WMS integration: proper time dimension parsing, better animation --- MMM-CombinedMap.js | 158 +++++++++++++++++++++++++++++++++------------ 1 file changed, 117 insertions(+), 41 deletions(-) diff --git a/MMM-CombinedMap.js b/MMM-CombinedMap.js index 1949c91..1422f4d 100644 --- a/MMM-CombinedMap.js +++ b/MMM-CombinedMap.js @@ -1,15 +1,15 @@ Module.register("MMM-CombinedMap", { defaults: { // Map settings - lat: 49.8833, - lng: 8.8333, + lat: 49.8666, + lng: 8.8505, zoom: 7, - width: "300px", + width: "450px", height: "300px", // Marker showMarker: true, - markerColor: "#3388ff", + markerColor: "#00cc00", // Layers showTraffic: true, @@ -19,10 +19,15 @@ Module.register("MMM-CombinedMap", { tomtomApiKey: "", // Animation settings - animationSpeed: 500, // ms per frame + animationSpeed: 800, // ms per frame radarOpacity: 0.7, trafficOpacity: 0.7, + // Radar frames + historyFrames: 12, // 1 hour of history (5 min intervals) + forecastFrames: 24, // 2 hours of forecast + extraDelayLastFrame: 2000, // Extra delay on last frame + // Update interval (ms) - how often to refresh radar data updateInterval: 5 * 60 * 1000, // 5 minutes }, @@ -47,7 +52,7 @@ Module.register("MMM-CombinedMap", { this.radarLayers = []; this.currentRadarIndex = 0; this.animationTimer = null; - this.radarTimestamps = []; + this.availableTimestamps = []; }, getDom: function() { @@ -100,7 +105,7 @@ Module.register("MMM-CombinedMap", { // Radar layer (DWD) if (this.config.showRadar) { - this.initRadarLayers(); + this.fetchRadarCapabilities(); } // Marker @@ -142,65 +147,125 @@ Module.register("MMM-CombinedMap", { incidentsLayer.addTo(this.map); }, - initRadarLayers: function() { - // Fetch available radar timestamps from DWD - this.fetchRadarTimestamps(); + fetchRadarCapabilities: function() { + // Parse WMS GetCapabilities to find available time range + const capsUrl = "https://maps.dwd.de/geoserver/dwd/wms?service=WMS&version=1.3.0&request=GetCapabilities"; + + fetch(capsUrl) + .then(response => response.text()) + .then(xml => { + const parser = new DOMParser(); + const doc = parser.parseFromString(xml, "text/xml"); + + // Find Niederschlagsradar layer and its time dimension + const layers = doc.querySelectorAll("Layer"); + for (const layer of layers) { + const nameEl = layer.querySelector("Name"); + if (nameEl && nameEl.textContent === "Niederschlagsradar") { + const dimEl = layer.querySelector("Dimension[name='time']"); + if (dimEl) { + this.parseTimeDimension(dimEl.textContent); + } + break; + } + } + }) + .catch(err => { + Log.error("MMM-CombinedMap: Failed to fetch WMS capabilities:", err); + // Fallback: generate timestamps based on current time + this.generateFallbackTimestamps(); + }); }, - fetchRadarTimestamps: function() { - // DWD WMS GetCapabilities to find available times - // For simplicity, we'll generate timestamps for the last 2 hours + 2 hours forecast - // DWD updates every 5 minutes + parseTimeDimension: function(dimStr) { + // Format: "2026-02-10T09:35:00.000Z/2026-02-11T15:15:00.000Z/PT5M" + const parts = dimStr.split("/"); + if (parts.length !== 3) { + this.generateFallbackTimestamps(); + return; + } + + const startTime = new Date(parts[0]); + const endTime = new Date(parts[1]); + const interval = 5 * 60 * 1000; // PT5M = 5 minutes const now = new Date(); const timestamps = []; - // Past 2 hours (every 5 minutes = 24 frames) - for (let i = 24; i >= 0; i--) { - const t = new Date(now.getTime() - i * 5 * 60 * 1000); - // Round to nearest 5 minutes + // Get frames from history (before now) + const historyStart = Math.max( + startTime.getTime(), + now.getTime() - this.config.historyFrames * interval + ); + + // Get frames into forecast (after now) + const forecastEnd = Math.min( + endTime.getTime(), + now.getTime() + this.config.forecastFrames * interval + ); + + // Round to 5-minute intervals + let t = Math.floor(historyStart / interval) * interval; + while (t <= forecastEnd) { + if (t >= startTime.getTime() && t <= endTime.getTime()) { + timestamps.push(new Date(t)); + } + t += interval; + } + + this.availableTimestamps = timestamps; + this.createRadarLayers(); + }, + + generateFallbackTimestamps: function() { + // Fallback: generate timestamps without checking capabilities + const now = new Date(); + const interval = 5 * 60 * 1000; + const timestamps = []; + + // Past hour + for (let i = this.config.historyFrames; i >= 0; i--) { + const t = new Date(now.getTime() - i * interval); t.setMinutes(Math.floor(t.getMinutes() / 5) * 5, 0, 0); timestamps.push(t); } - // Future 2 hours (nowcast) - every 5 minutes = 24 frames - for (let i = 1; i <= 24; i++) { - const t = new Date(now.getTime() + i * 5 * 60 * 1000); + // Future 2 hours + for (let i = 1; i <= this.config.forecastFrames; i++) { + const t = new Date(now.getTime() + i * interval); t.setMinutes(Math.floor(t.getMinutes() / 5) * 5, 0, 0); timestamps.push(t); } - this.radarTimestamps = timestamps; + this.availableTimestamps = timestamps; this.createRadarLayers(); }, createRadarLayers: function() { // Clear existing layers this.radarLayers.forEach(layer => { - if (this.map.hasLayer(layer.leafletLayer)) { + if (this.map && this.map.hasLayer(layer.leafletLayer)) { this.map.removeLayer(layer.leafletLayer); } }); this.radarLayers = []; - // DWD WMS for radar - // Using RV (Radar Vorhersage/Forecast) product for nowcast - // and standard radar for past + if (!this.availableTimestamps.length) return; const now = new Date(); - this.radarTimestamps.forEach((timestamp, index) => { + this.availableTimestamps.forEach((timestamp, index) => { const isForecast = timestamp > now; - const timeStr = timestamp.toISOString(); + const timeStr = timestamp.toISOString().replace(/\.\d{3}Z$/, ".000Z"); - // DWD WMS layer - using Niederschlagsradar (precipitation radar) + // DWD WMS layer const layer = L.tileLayer.wms("https://maps.dwd.de/geoserver/dwd/wms", { - layers: isForecast ? "dwd:RV-Produkt" : "dwd:Niederschlagsradar", + layers: "dwd:Niederschlagsradar", format: "image/png", transparent: true, opacity: 0, // Start hidden time: timeStr, - styles: "", + styles: "niederschlagsradar", version: "1.3.0", crs: L.CRS.EPSG3857 }); @@ -210,7 +275,8 @@ Module.register("MMM-CombinedMap", { this.radarLayers.push({ leafletLayer: layer, timestamp: timestamp, - isForecast: isForecast + isForecast: isForecast, + isLast: index === this.availableTimestamps.length - 1 }); }); @@ -225,13 +291,23 @@ Module.register("MMM-CombinedMap", { clearInterval(this.animationTimer); } - // Show first frame + this.currentRadarIndex = 0; this.showRadarFrame(0); - this.animationTimer = setInterval(() => { - this.currentRadarIndex = (this.currentRadarIndex + 1) % this.radarLayers.length; - this.showRadarFrame(this.currentRadarIndex); - }, this.config.animationSpeed); + const animate = () => { + const currentLayer = this.radarLayers[this.currentRadarIndex]; + const delay = currentLayer && currentLayer.isLast + ? this.config.animationSpeed + this.config.extraDelayLastFrame + : this.config.animationSpeed; + + this.animationTimer = setTimeout(() => { + this.currentRadarIndex = (this.currentRadarIndex + 1) % this.radarLayers.length; + this.showRadarFrame(this.currentRadarIndex); + animate(); + }, delay); + }; + + animate(); }, showRadarFrame: function(index) { @@ -243,7 +319,7 @@ Module.register("MMM-CombinedMap", { } }); - // Update timestamp display if needed + // Update timestamp display this.updateTimestampDisplay(this.radarLayers[index]); }, @@ -263,7 +339,7 @@ Module.register("MMM-CombinedMap", { const diffMinutes = Math.round((time - now) / 60000); let label; - if (diffMinutes === 0) { + if (Math.abs(diffMinutes) <= 2) { label = "Now"; } else if (diffMinutes > 0) { label = `+${diffMinutes} min`; @@ -277,12 +353,12 @@ Module.register("MMM-CombinedMap", { refreshRadar: function() { Log.info("MMM-CombinedMap: Refreshing radar data"); - this.fetchRadarTimestamps(); + this.fetchRadarCapabilities(); }, suspend: function() { if (this.animationTimer) { - clearInterval(this.animationTimer); + clearTimeout(this.animationTimer); this.animationTimer = null; } },