Initial implementation: Leaflet + DWD radar + TomTom traffic
This commit is contained in:
295
MMM-CombinedMap.js
Normal file
295
MMM-CombinedMap.js
Normal file
@@ -0,0 +1,295 @@
|
||||
Module.register("MMM-CombinedMap", {
|
||||
defaults: {
|
||||
// Map settings
|
||||
lat: 49.8833,
|
||||
lng: 8.8333,
|
||||
zoom: 7,
|
||||
width: "300px",
|
||||
height: "300px",
|
||||
|
||||
// Marker
|
||||
showMarker: true,
|
||||
markerColor: "#3388ff",
|
||||
|
||||
// Layers
|
||||
showTraffic: true,
|
||||
showRadar: true,
|
||||
|
||||
// TomTom API key (get free at developer.tomtom.com)
|
||||
tomtomApiKey: "",
|
||||
|
||||
// Animation settings
|
||||
animationSpeed: 500, // ms per frame
|
||||
radarOpacity: 0.7,
|
||||
trafficOpacity: 0.7,
|
||||
|
||||
// Update interval (ms) - how often to refresh radar data
|
||||
updateInterval: 5 * 60 * 1000, // 5 minutes
|
||||
},
|
||||
|
||||
getStyles: function() {
|
||||
return [
|
||||
"MMM-CombinedMap.css",
|
||||
"https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
];
|
||||
},
|
||||
|
||||
getScripts: function() {
|
||||
return [
|
||||
"https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||
];
|
||||
},
|
||||
|
||||
start: function() {
|
||||
Log.info("Starting module: " + this.name);
|
||||
this.loaded = false;
|
||||
this.map = null;
|
||||
this.radarLayers = [];
|
||||
this.currentRadarIndex = 0;
|
||||
this.animationTimer = null;
|
||||
this.radarTimestamps = [];
|
||||
},
|
||||
|
||||
getDom: function() {
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.id = "mmm-combinedmap-" + this.identifier;
|
||||
wrapper.className = "mmm-combinedmap-wrapper";
|
||||
wrapper.style.width = this.config.width;
|
||||
wrapper.style.height = this.config.height;
|
||||
|
||||
if (!this.loaded) {
|
||||
wrapper.innerHTML = "Loading map...";
|
||||
// Initialize map after DOM is ready
|
||||
setTimeout(() => this.initMap(wrapper), 100);
|
||||
}
|
||||
|
||||
return wrapper;
|
||||
},
|
||||
|
||||
initMap: function(container) {
|
||||
if (this.map) return;
|
||||
|
||||
const mapId = "mmm-combinedmap-" + this.identifier;
|
||||
const mapContainer = document.getElementById(mapId);
|
||||
if (!mapContainer) return;
|
||||
|
||||
mapContainer.innerHTML = "";
|
||||
|
||||
// Create map
|
||||
this.map = L.map(mapId, {
|
||||
zoomControl: false,
|
||||
attributionControl: false,
|
||||
dragging: false,
|
||||
scrollWheelZoom: false,
|
||||
doubleClickZoom: false,
|
||||
boxZoom: false,
|
||||
keyboard: false,
|
||||
touchZoom: false
|
||||
}).setView([this.config.lat, this.config.lng], this.config.zoom);
|
||||
|
||||
// Dark base map (CartoDB Dark Matter)
|
||||
L.tileLayer("https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png", {
|
||||
maxZoom: 19,
|
||||
opacity: 1
|
||||
}).addTo(this.map);
|
||||
|
||||
// Traffic layer (TomTom)
|
||||
if (this.config.showTraffic && this.config.tomtomApiKey) {
|
||||
this.addTrafficLayer();
|
||||
}
|
||||
|
||||
// Radar layer (DWD)
|
||||
if (this.config.showRadar) {
|
||||
this.initRadarLayers();
|
||||
}
|
||||
|
||||
// Marker
|
||||
if (this.config.showMarker) {
|
||||
const markerIcon = L.divIcon({
|
||||
className: "mmm-combinedmap-marker",
|
||||
html: `<div style="background-color: ${this.config.markerColor}; width: 12px; height: 12px; border-radius: 50%; border: 2px solid white; box-shadow: 0 0 4px rgba(0,0,0,0.5);"></div>`,
|
||||
iconSize: [16, 16],
|
||||
iconAnchor: [8, 8]
|
||||
});
|
||||
L.marker([this.config.lat, this.config.lng], { icon: markerIcon }).addTo(this.map);
|
||||
}
|
||||
|
||||
this.loaded = true;
|
||||
|
||||
// Set up periodic refresh
|
||||
setInterval(() => this.refreshRadar(), this.config.updateInterval);
|
||||
},
|
||||
|
||||
addTrafficLayer: function() {
|
||||
// TomTom Traffic Flow tiles
|
||||
const trafficLayer = L.tileLayer(
|
||||
`https://api.tomtom.com/traffic/map/4/tile/flow/relative0/{z}/{x}/{y}.png?key=${this.config.tomtomApiKey}&tileSize=256`,
|
||||
{
|
||||
maxZoom: 18,
|
||||
opacity: this.config.trafficOpacity
|
||||
}
|
||||
);
|
||||
trafficLayer.addTo(this.map);
|
||||
|
||||
// Also add traffic incidents overlay
|
||||
const incidentsLayer = L.tileLayer(
|
||||
`https://api.tomtom.com/traffic/map/4/tile/incidents/s3/{z}/{x}/{y}.png?key=${this.config.tomtomApiKey}&tileSize=256`,
|
||||
{
|
||||
maxZoom: 18,
|
||||
opacity: this.config.trafficOpacity
|
||||
}
|
||||
);
|
||||
incidentsLayer.addTo(this.map);
|
||||
},
|
||||
|
||||
initRadarLayers: function() {
|
||||
// Fetch available radar timestamps from DWD
|
||||
this.fetchRadarTimestamps();
|
||||
},
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
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);
|
||||
t.setMinutes(Math.floor(t.getMinutes() / 5) * 5, 0, 0);
|
||||
timestamps.push(t);
|
||||
}
|
||||
|
||||
this.radarTimestamps = timestamps;
|
||||
this.createRadarLayers();
|
||||
},
|
||||
|
||||
createRadarLayers: function() {
|
||||
// Clear existing layers
|
||||
this.radarLayers.forEach(layer => {
|
||||
if (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
|
||||
|
||||
const now = new Date();
|
||||
|
||||
this.radarTimestamps.forEach((timestamp, index) => {
|
||||
const isForecast = timestamp > now;
|
||||
const timeStr = timestamp.toISOString();
|
||||
|
||||
// DWD WMS layer - using Niederschlagsradar (precipitation radar)
|
||||
const layer = L.tileLayer.wms("https://maps.dwd.de/geoserver/dwd/wms", {
|
||||
layers: isForecast ? "dwd:RV-Produkt" : "dwd:Niederschlagsradar",
|
||||
format: "image/png",
|
||||
transparent: true,
|
||||
opacity: 0, // Start hidden
|
||||
time: timeStr,
|
||||
styles: "",
|
||||
version: "1.3.0",
|
||||
crs: L.CRS.EPSG3857
|
||||
});
|
||||
|
||||
layer.addTo(this.map);
|
||||
|
||||
this.radarLayers.push({
|
||||
leafletLayer: layer,
|
||||
timestamp: timestamp,
|
||||
isForecast: isForecast
|
||||
});
|
||||
});
|
||||
|
||||
// Start animation
|
||||
if (this.radarLayers.length > 0) {
|
||||
this.startAnimation();
|
||||
}
|
||||
},
|
||||
|
||||
startAnimation: function() {
|
||||
if (this.animationTimer) {
|
||||
clearInterval(this.animationTimer);
|
||||
}
|
||||
|
||||
// Show first frame
|
||||
this.showRadarFrame(0);
|
||||
|
||||
this.animationTimer = setInterval(() => {
|
||||
this.currentRadarIndex = (this.currentRadarIndex + 1) % this.radarLayers.length;
|
||||
this.showRadarFrame(this.currentRadarIndex);
|
||||
}, this.config.animationSpeed);
|
||||
},
|
||||
|
||||
showRadarFrame: function(index) {
|
||||
this.radarLayers.forEach((layer, i) => {
|
||||
if (i === index) {
|
||||
layer.leafletLayer.setOpacity(this.config.radarOpacity);
|
||||
} else {
|
||||
layer.leafletLayer.setOpacity(0);
|
||||
}
|
||||
});
|
||||
|
||||
// Update timestamp display if needed
|
||||
this.updateTimestampDisplay(this.radarLayers[index]);
|
||||
},
|
||||
|
||||
updateTimestampDisplay: function(layer) {
|
||||
const container = document.getElementById("mmm-combinedmap-" + this.identifier);
|
||||
if (!container) return;
|
||||
|
||||
let timestampEl = container.querySelector(".radar-timestamp");
|
||||
if (!timestampEl) {
|
||||
timestampEl = document.createElement("div");
|
||||
timestampEl.className = "radar-timestamp";
|
||||
container.appendChild(timestampEl);
|
||||
}
|
||||
|
||||
const time = layer.timestamp;
|
||||
const now = new Date();
|
||||
const diffMinutes = Math.round((time - now) / 60000);
|
||||
|
||||
let label;
|
||||
if (diffMinutes === 0) {
|
||||
label = "Now";
|
||||
} else if (diffMinutes > 0) {
|
||||
label = `+${diffMinutes} min`;
|
||||
} else {
|
||||
label = `${diffMinutes} min`;
|
||||
}
|
||||
|
||||
timestampEl.textContent = label;
|
||||
timestampEl.classList.toggle("forecast", layer.isForecast);
|
||||
},
|
||||
|
||||
refreshRadar: function() {
|
||||
Log.info("MMM-CombinedMap: Refreshing radar data");
|
||||
this.fetchRadarTimestamps();
|
||||
},
|
||||
|
||||
suspend: function() {
|
||||
if (this.animationTimer) {
|
||||
clearInterval(this.animationTimer);
|
||||
this.animationTimer = null;
|
||||
}
|
||||
},
|
||||
|
||||
resume: function() {
|
||||
if (this.radarLayers.length > 0) {
|
||||
this.startAnimation();
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user