Fix DWD WMS integration: proper time dimension parsing, better animation

This commit is contained in:
Clawd
2026-02-11 13:19:28 +00:00
parent 794bd70552
commit 2f1c42a1b4

View File

@@ -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;
}
},