Fix DWD WMS integration: proper time dimension parsing, better animation
This commit is contained in:
@@ -1,15 +1,15 @@
|
|||||||
Module.register("MMM-CombinedMap", {
|
Module.register("MMM-CombinedMap", {
|
||||||
defaults: {
|
defaults: {
|
||||||
// Map settings
|
// Map settings
|
||||||
lat: 49.8833,
|
lat: 49.8666,
|
||||||
lng: 8.8333,
|
lng: 8.8505,
|
||||||
zoom: 7,
|
zoom: 7,
|
||||||
width: "300px",
|
width: "450px",
|
||||||
height: "300px",
|
height: "300px",
|
||||||
|
|
||||||
// Marker
|
// Marker
|
||||||
showMarker: true,
|
showMarker: true,
|
||||||
markerColor: "#3388ff",
|
markerColor: "#00cc00",
|
||||||
|
|
||||||
// Layers
|
// Layers
|
||||||
showTraffic: true,
|
showTraffic: true,
|
||||||
@@ -19,10 +19,15 @@ Module.register("MMM-CombinedMap", {
|
|||||||
tomtomApiKey: "",
|
tomtomApiKey: "",
|
||||||
|
|
||||||
// Animation settings
|
// Animation settings
|
||||||
animationSpeed: 500, // ms per frame
|
animationSpeed: 800, // ms per frame
|
||||||
radarOpacity: 0.7,
|
radarOpacity: 0.7,
|
||||||
trafficOpacity: 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
|
// Update interval (ms) - how often to refresh radar data
|
||||||
updateInterval: 5 * 60 * 1000, // 5 minutes
|
updateInterval: 5 * 60 * 1000, // 5 minutes
|
||||||
},
|
},
|
||||||
@@ -47,7 +52,7 @@ Module.register("MMM-CombinedMap", {
|
|||||||
this.radarLayers = [];
|
this.radarLayers = [];
|
||||||
this.currentRadarIndex = 0;
|
this.currentRadarIndex = 0;
|
||||||
this.animationTimer = null;
|
this.animationTimer = null;
|
||||||
this.radarTimestamps = [];
|
this.availableTimestamps = [];
|
||||||
},
|
},
|
||||||
|
|
||||||
getDom: function() {
|
getDom: function() {
|
||||||
@@ -100,7 +105,7 @@ Module.register("MMM-CombinedMap", {
|
|||||||
|
|
||||||
// Radar layer (DWD)
|
// Radar layer (DWD)
|
||||||
if (this.config.showRadar) {
|
if (this.config.showRadar) {
|
||||||
this.initRadarLayers();
|
this.fetchRadarCapabilities();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Marker
|
// Marker
|
||||||
@@ -142,65 +147,125 @@ Module.register("MMM-CombinedMap", {
|
|||||||
incidentsLayer.addTo(this.map);
|
incidentsLayer.addTo(this.map);
|
||||||
},
|
},
|
||||||
|
|
||||||
initRadarLayers: function() {
|
fetchRadarCapabilities: function() {
|
||||||
// Fetch available radar timestamps from DWD
|
// Parse WMS GetCapabilities to find available time range
|
||||||
this.fetchRadarTimestamps();
|
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() {
|
parseTimeDimension: function(dimStr) {
|
||||||
// DWD WMS GetCapabilities to find available times
|
// Format: "2026-02-10T09:35:00.000Z/2026-02-11T15:15:00.000Z/PT5M"
|
||||||
// For simplicity, we'll generate timestamps for the last 2 hours + 2 hours forecast
|
const parts = dimStr.split("/");
|
||||||
// DWD updates every 5 minutes
|
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 now = new Date();
|
||||||
const timestamps = [];
|
const timestamps = [];
|
||||||
|
|
||||||
// Past 2 hours (every 5 minutes = 24 frames)
|
// Get frames from history (before now)
|
||||||
for (let i = 24; i >= 0; i--) {
|
const historyStart = Math.max(
|
||||||
const t = new Date(now.getTime() - i * 5 * 60 * 1000);
|
startTime.getTime(),
|
||||||
// Round to nearest 5 minutes
|
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);
|
t.setMinutes(Math.floor(t.getMinutes() / 5) * 5, 0, 0);
|
||||||
timestamps.push(t);
|
timestamps.push(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Future 2 hours (nowcast) - every 5 minutes = 24 frames
|
// Future 2 hours
|
||||||
for (let i = 1; i <= 24; i++) {
|
for (let i = 1; i <= this.config.forecastFrames; i++) {
|
||||||
const t = new Date(now.getTime() + i * 5 * 60 * 1000);
|
const t = new Date(now.getTime() + i * interval);
|
||||||
t.setMinutes(Math.floor(t.getMinutes() / 5) * 5, 0, 0);
|
t.setMinutes(Math.floor(t.getMinutes() / 5) * 5, 0, 0);
|
||||||
timestamps.push(t);
|
timestamps.push(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.radarTimestamps = timestamps;
|
this.availableTimestamps = timestamps;
|
||||||
this.createRadarLayers();
|
this.createRadarLayers();
|
||||||
},
|
},
|
||||||
|
|
||||||
createRadarLayers: function() {
|
createRadarLayers: function() {
|
||||||
// Clear existing layers
|
// Clear existing layers
|
||||||
this.radarLayers.forEach(layer => {
|
this.radarLayers.forEach(layer => {
|
||||||
if (this.map.hasLayer(layer.leafletLayer)) {
|
if (this.map && this.map.hasLayer(layer.leafletLayer)) {
|
||||||
this.map.removeLayer(layer.leafletLayer);
|
this.map.removeLayer(layer.leafletLayer);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.radarLayers = [];
|
this.radarLayers = [];
|
||||||
|
|
||||||
// DWD WMS for radar
|
if (!this.availableTimestamps.length) return;
|
||||||
// Using RV (Radar Vorhersage/Forecast) product for nowcast
|
|
||||||
// and standard radar for past
|
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
this.radarTimestamps.forEach((timestamp, index) => {
|
this.availableTimestamps.forEach((timestamp, index) => {
|
||||||
const isForecast = timestamp > now;
|
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", {
|
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",
|
format: "image/png",
|
||||||
transparent: true,
|
transparent: true,
|
||||||
opacity: 0, // Start hidden
|
opacity: 0, // Start hidden
|
||||||
time: timeStr,
|
time: timeStr,
|
||||||
styles: "",
|
styles: "niederschlagsradar",
|
||||||
version: "1.3.0",
|
version: "1.3.0",
|
||||||
crs: L.CRS.EPSG3857
|
crs: L.CRS.EPSG3857
|
||||||
});
|
});
|
||||||
@@ -210,7 +275,8 @@ Module.register("MMM-CombinedMap", {
|
|||||||
this.radarLayers.push({
|
this.radarLayers.push({
|
||||||
leafletLayer: layer,
|
leafletLayer: layer,
|
||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
isForecast: isForecast
|
isForecast: isForecast,
|
||||||
|
isLast: index === this.availableTimestamps.length - 1
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -225,13 +291,23 @@ Module.register("MMM-CombinedMap", {
|
|||||||
clearInterval(this.animationTimer);
|
clearInterval(this.animationTimer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show first frame
|
this.currentRadarIndex = 0;
|
||||||
this.showRadarFrame(0);
|
this.showRadarFrame(0);
|
||||||
|
|
||||||
this.animationTimer = setInterval(() => {
|
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.currentRadarIndex = (this.currentRadarIndex + 1) % this.radarLayers.length;
|
||||||
this.showRadarFrame(this.currentRadarIndex);
|
this.showRadarFrame(this.currentRadarIndex);
|
||||||
}, this.config.animationSpeed);
|
animate();
|
||||||
|
}, delay);
|
||||||
|
};
|
||||||
|
|
||||||
|
animate();
|
||||||
},
|
},
|
||||||
|
|
||||||
showRadarFrame: function(index) {
|
showRadarFrame: function(index) {
|
||||||
@@ -243,7 +319,7 @@ Module.register("MMM-CombinedMap", {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update timestamp display if needed
|
// Update timestamp display
|
||||||
this.updateTimestampDisplay(this.radarLayers[index]);
|
this.updateTimestampDisplay(this.radarLayers[index]);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -263,7 +339,7 @@ Module.register("MMM-CombinedMap", {
|
|||||||
const diffMinutes = Math.round((time - now) / 60000);
|
const diffMinutes = Math.round((time - now) / 60000);
|
||||||
|
|
||||||
let label;
|
let label;
|
||||||
if (diffMinutes === 0) {
|
if (Math.abs(diffMinutes) <= 2) {
|
||||||
label = "Now";
|
label = "Now";
|
||||||
} else if (diffMinutes > 0) {
|
} else if (diffMinutes > 0) {
|
||||||
label = `+${diffMinutes} min`;
|
label = `+${diffMinutes} min`;
|
||||||
@@ -277,12 +353,12 @@ Module.register("MMM-CombinedMap", {
|
|||||||
|
|
||||||
refreshRadar: function() {
|
refreshRadar: function() {
|
||||||
Log.info("MMM-CombinedMap: Refreshing radar data");
|
Log.info("MMM-CombinedMap: Refreshing radar data");
|
||||||
this.fetchRadarTimestamps();
|
this.fetchRadarCapabilities();
|
||||||
},
|
},
|
||||||
|
|
||||||
suspend: function() {
|
suspend: function() {
|
||||||
if (this.animationTimer) {
|
if (this.animationTimer) {
|
||||||
clearInterval(this.animationTimer);
|
clearTimeout(this.animationTimer);
|
||||||
this.animationTimer = null;
|
this.animationTimer = null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user