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: `
`, 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(); } } });