From aec3ecae9a75dc13a33270604199704926c4c071 Mon Sep 17 00:00:00 2001 From: Sirius DevOps Date: Mon, 24 Aug 2026 18:02:56 -0400 Subject: [PATCH] Map: FIRMS heatmap + camera thumbnail popups + unified layer panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Vendored leaflet.heat 0.2.0; fires overlay is now a real heatmap (L.heatLayer) with intensity driven by brightness or VIIRS confidence (color mode selector) and a since= time filter (6h/24h/48h/7d/30d/all). - Camera markers now open popups with the live snapshot thumbnail proxied through GET /api/cameras/{id}/snapshot (backend TTL cache) + full metadata (vendor, device, discovery source, first/last seen, coords, source link); missing-snapshot cams show a placeholder instead of 404. - New collapsible Layers panel (dark neon) over the map: per-layer visibility toggles, opacity sliders (basemap/fires/cameras), live counts legend, and a fire heat-intensity ramp that swaps for the confidence legend in confidence mode. - Clamp currentBBox() to world bounds so /api/cameras doesn't 422 at low zoom (worldCopyJump spans multiple copies). - Skip overlay reloads on moveend while a popup is open — popup autopan no longer rebuilds the marker group and closes the popup. - Zoom control moved to top-right so it doesn't collide with the panel. --- app/static/index.html | 301 ++++++++++++++++++---- app/static/vendor/leaflet/leaflet.heat.js | 11 + 2 files changed, 266 insertions(+), 46 deletions(-) create mode 100644 app/static/vendor/leaflet/leaflet.heat.js diff --git a/app/static/index.html b/app/static/index.html index bffb987..88589d5 100644 --- a/app/static/index.html +++ b/app/static/index.html @@ -85,6 +85,59 @@ .leaflet-popup-content { font-size: 0.82rem; } .leaflet-popup-close-button { color: var(--muted) !important; } .leaflet-overlay-pane svg { filter: drop-shadow(0 0 3px rgba(0,0,0,0.6)); } + /* ── Map layer control panel (dark neon) ─────────────────── */ + #map-wrap { position: relative; } + #map { z-index: 0; } + #layer-panel { + position: absolute; top: 12px; left: 12px; z-index: 600; + width: 244px; max-height: calc(100% - 24px); overflow: hidden; + background: rgba(15,23,42,0.88); backdrop-filter: blur(6px); + border: 1px solid var(--border); border-radius: 10px; + box-shadow: 0 0 18px rgba(56,189,248,0.18), 0 0 2px rgba(56,189,248,0.5); + color: var(--text); font-size: 0.78rem; display: flex; flex-direction: column; + } + .lp-head { + display: flex; justify-content: space-between; align-items: center; + padding: 0.55rem 0.7rem; border-bottom: 1px solid var(--border); + font-size: 0.72rem; font-weight: 600; letter-spacing: 0.06em; + text-transform: uppercase; color: var(--accent); + text-shadow: 0 0 10px rgba(56,189,248,0.55); cursor: pointer; user-select: none; + } + .lp-head .lp-caret { color: var(--muted); font-size: 0.8rem; transition: transform .18s; } + #layer-panel.collapsed .lp-caret { transform: rotate(-90deg); } + .lp-body { padding: 0.55rem 0.7rem 0.65rem; display: flex; flex-direction: column; gap: 0.55rem; overflow-y: auto; } + #layer-panel.collapsed .lp-body { display: none; } + .lp-layer { display: flex; flex-direction: column; gap: 0.3rem; padding-bottom: 0.5rem; border-bottom: 1px dashed var(--border); } + .lp-layer:last-child { border-bottom: none; padding-bottom: 0; } + .lp-row { display: flex; justify-content: space-between; align-items: center; gap: 0.5rem; } + .lp-name { display: flex; align-items: center; gap: 0.42rem; font-weight: 600; cursor: pointer; text-transform: uppercase; letter-spacing: 0.04em; font-size: 0.7rem; } + .lp-name input[type="checkbox"] { accent-color: var(--accent); cursor: pointer; } + .lp-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; } + .lp-dot.base { background: #38bdf8; box-shadow: 0 0 7px #38bdf8; } + .lp-dot.fires { background: #fb923c; box-shadow: 0 0 7px #fb923c; } + .lp-dot.cams { background: #4ade80; box-shadow: 0 0 7px #4ade80; } + .lp-count { font-family: ui-monospace, monospace; font-size: 0.72rem; color: var(--accent); } + .lp-sub { display: flex; justify-content: space-between; align-items: center; gap: 0.5rem; } + .lp-opacity { display: flex; align-items: center; gap: 0.4rem; font-size: 0.68rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; } + .lp-opacity input[type="range"] { flex: 1; accent-color: var(--accent); height: 4px; cursor: pointer; } + .lp-opacity b { color: var(--text); font-family: ui-monospace, monospace; font-weight: 400; min-width: 2.4em; text-align: right; } + .lp-select { background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: 6px; padding: 0.25rem 0.4rem; font-size: 0.72rem; font-family: ui-monospace, monospace; color-scheme: dark; } + .lp-select:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 8px rgba(56,189,248,0.35); } + .lp-legend { border-top: 1px solid var(--border); padding-top: 0.5rem; display: flex; flex-direction: column; gap: 0.35rem; } + .lp-legend h4 { font-size: 0.62rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); margin: 0; } + .lp-ramp { height: 8px; border-radius: 4px; background: linear-gradient(90deg, #3b82f6, #f59e0b, #ef4444, #fde047); box-shadow: 0 0 8px rgba(251,146,60,0.35) inset; } + .lp-ramp-scale { display: flex; justify-content: space-between; font-size: 0.6rem; color: var(--muted); } + .lp-heat-legend { display: flex; align-items: center; justify-content: space-between; font-size: 0.68rem; color: var(--muted); gap: 0.5rem; } + .lp-heat-legend .sw { width: 10px; height: 10px; border-radius: 3px; display: inline-block; margin-right: 0.3rem; box-shadow: 0 0 6px currentColor; } + .lp-error { color: var(--red); font-size: 0.68rem; } + /* ── Camera popup thumbnail ───────────────────────────────── */ + .cam-pop { min-width: 210px; max-width: 260px; } + .cam-pop .thumb { width: 100%; height: 120px; object-fit: cover; border-radius: 6px; border: 1px solid var(--border); margin: 0.3rem 0; box-shadow: 0 0 10px rgba(56,189,248,0.25); background: #0b1220; } + .cam-pop .thumb.placeholder { display: flex; align-items: center; justify-content: center; color: var(--muted); font-size: 0.7rem; height: 80px; } + .cam-pop table { width: 100%; font-size: 0.72rem; border-collapse: collapse; } + .cam-pop td { padding: 0.12rem 0.2rem; vertical-align: top; } + .cam-pop td.k { color: var(--muted); text-transform: uppercase; font-size: 0.62rem; letter-spacing: 0.04em; white-space: nowrap; width: 34%; } + .cam-pop a { color: var(--accent); word-break: break-all; } @@ -198,10 +251,10 @@
- + @@ -231,6 +334,7 @@ +