From 3d37c31ab6284fec1ea9a047985f8bb2b22a2639 Mon Sep 17 00:00:00 2001 From: Clawd Date: Wed, 11 Feb 2026 14:43:46 +0000 Subject: [PATCH] Add visual timeline with moving indicator, now marker, and time labels --- MMM-CombinedMap.css | 114 +++++++++++++++++++++++++++++++++++++------- MMM-CombinedMap.js | 110 +++++++++++++++++++++++++++++++++++------- 2 files changed, 191 insertions(+), 33 deletions(-) diff --git a/MMM-CombinedMap.css b/MMM-CombinedMap.css index 3798c3a..da4d9a2 100644 --- a/MMM-CombinedMap.css +++ b/MMM-CombinedMap.css @@ -49,31 +49,111 @@ border: none !important; } -/* Radar timestamp display */ -.radar-timestamp { +/* Radar timeline display */ +.radar-timeline { position: absolute; - bottom: 8px; - left: 8px; - background: rgba(0, 0, 0, 0.7); - color: #fff; - padding: 4px 10px; - border-radius: 4px; - font-size: 12px; - font-family: "Roboto Condensed", sans-serif; + bottom: 12px; + left: 12px; + right: 12px; + height: 24px; z-index: 1000; pointer-events: none; } -.radar-timestamp.forecast { - background: rgba(0, 100, 180, 0.8); +.timeline-track { + position: absolute; + top: 10px; + left: 30px; + right: 30px; + height: 4px; + background: rgba(255, 255, 255, 0.2); + border-radius: 2px; + overflow: hidden; } -/* Forecast indicator - subtle arrow */ -.radar-timestamp.forecast::before { - content: "▶ "; +.timeline-past { + position: absolute; + left: 0; + top: 0; + width: 33%; /* ~1h of 3h total */ + height: 100%; + background: rgba(150, 150, 150, 0.5); +} + +.timeline-future { + position: absolute; + right: 0; + top: 0; + width: 67%; /* ~2h of 3h total */ + height: 100%; + background: rgba(0, 120, 200, 0.4); +} + +.timeline-now { + position: absolute; + top: 0; + transform: translateX(-50%); + color: #fff; font-size: 8px; - opacity: 0.8; - vertical-align: middle; + text-align: center; + left: 33%; /* Will be updated by JS */ + margin-left: 30px; + width: calc(100% - 60px); + left: 0; +} + +.timeline-now span { + position: absolute; + left: 33%; + transform: translateX(-50%); +} + +.timeline-indicator { + position: absolute; + top: 6px; + width: 12px; + height: 12px; + background: #fff; + border: 2px solid #0af; + border-radius: 50%; + transform: translateX(-50%); + box-shadow: 0 0 6px rgba(0, 170, 255, 0.8); + transition: left 0.2s ease-out; + /* Position is set by JS */ +} + +.timeline-time { + position: absolute; + top: -2px; + left: 50%; + transform: translateX(-50%); + background: rgba(0, 0, 0, 0.7); + color: #fff; + padding: 2px 8px; + border-radius: 3px; + font-size: 11px; + font-family: "Roboto Condensed", sans-serif; + white-space: nowrap; +} + +.timeline-time.forecast { + background: rgba(0, 100, 180, 0.9); +} + +.timeline-label { + position: absolute; + top: 6px; + color: rgba(255, 255, 255, 0.6); + font-size: 9px; + font-family: "Roboto Condensed", sans-serif; +} + +.timeline-start { + left: 0; +} + +.timeline-end { + right: 0; } /* Legend (optional, can be enabled) */ diff --git a/MMM-CombinedMap.js b/MMM-CombinedMap.js index a4f38bd..ffe52a5 100644 --- a/MMM-CombinedMap.js +++ b/MMM-CombinedMap.js @@ -351,28 +351,106 @@ Module.register("MMM-CombinedMap", { 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); + // Create or get timeline element + let timelineEl = container.querySelector(".radar-timeline"); + if (!timelineEl) { + timelineEl = this.createTimelineElement(); + container.appendChild(timelineEl); } - const time = layer.timestamp; + // Calculate position (0-100%) + const currentIndex = this.radarLayers.indexOf(layer); + const totalFrames = this.radarLayers.length; + const position = (currentIndex / (totalFrames - 1)) * 100; + + // Find "now" position const now = new Date(); - const diffMinutes = Math.round((time - now) / 60000); + let nowIndex = 0; + for (let i = 0; i < this.radarLayers.length; i++) { + if (this.radarLayers[i].timestamp <= now) { + nowIndex = i; + } + } + const nowPosition = (nowIndex / (totalFrames - 1)) * 100; - let label; - if (Math.abs(diffMinutes) <= 2) { - label = "Now"; - } else if (diffMinutes > 0) { - label = `+${diffMinutes} min`; - } else { - label = `${diffMinutes} min`; + // Update indicator position (30px offset on each side for labels) + const indicator = timelineEl.querySelector(".timeline-indicator"); + const trackWidth = timelineEl.offsetWidth - 60; // 30px margin each side + if (indicator && trackWidth > 0) { + const pixelPos = 30 + (position / 100) * trackWidth; + indicator.style.left = pixelPos + "px"; } - timestampEl.textContent = label; - timestampEl.classList.toggle("forecast", layer.isForecast); + // Update "now" marker position + const nowMarker = timelineEl.querySelector(".timeline-now span"); + if (nowMarker && trackWidth > 0) { + const nowPixelPos = (nowPosition / 100) * 100; + nowMarker.style.left = nowPixelPos + "%"; + } + + // Update time label + const timeLabel = timelineEl.querySelector(".timeline-time"); + if (timeLabel) { + const diffMinutes = Math.round((layer.timestamp - now) / 60000); + if (Math.abs(diffMinutes) <= 2) { + timeLabel.textContent = "Now"; + } else if (diffMinutes > 0) { + timeLabel.textContent = "+" + diffMinutes + "m"; + } else { + timeLabel.textContent = diffMinutes + "m"; + } + timeLabel.classList.toggle("forecast", layer.isForecast); + } + }, + + createTimelineElement: function() { + const timeline = document.createElement("div"); + timeline.className = "radar-timeline"; + + // Track (the line) + const track = document.createElement("div"); + track.className = "timeline-track"; + + // Past section (darker) + const past = document.createElement("div"); + past.className = "timeline-past"; + + // Future section (highlighted) + const future = document.createElement("div"); + future.className = "timeline-future"; + + // "Now" marker + const nowMarker = document.createElement("div"); + nowMarker.className = "timeline-now"; + nowMarker.innerHTML = ""; + + // Moving indicator + const indicator = document.createElement("div"); + indicator.className = "timeline-indicator"; + + // Time label + const timeLabel = document.createElement("div"); + timeLabel.className = "timeline-time"; + + // Labels for start/end + const startLabel = document.createElement("div"); + startLabel.className = "timeline-label timeline-start"; + startLabel.textContent = "-1h"; + + const endLabel = document.createElement("div"); + endLabel.className = "timeline-label timeline-end"; + endLabel.textContent = "+2h"; + + track.appendChild(past); + track.appendChild(future); + timeline.appendChild(track); + timeline.appendChild(nowMarker); + timeline.appendChild(indicator); + timeline.appendChild(timeLabel); + timeline.appendChild(startLabel); + timeline.appendChild(endLabel); + + return timeline; }, refreshRadar: function() {