From 1aebd7f1ccf1a7240bcd228f6bf58c15a96eac98 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Tue, 31 Mar 2026 10:26:20 -0500 Subject: [PATCH] Optimize map performance: reduce payload 75%, faster rendering - Remove factors from heatmap scores, fetch on-demand via point_detail - Pre-encode initial scores JSON once instead of per-render - Don't store scores in socket assigns (reduce memory) - Pre-calculate RGBA color lookup table for canvas rendering - Batch fillStyle changes, hoist math out of inner loop - Show topbar immediately on data requests (no 300ms delay) --- assets/js/propagation_map_hook.js | 33 ++++++++++++++++++++------ lib/microwaveprop_web/live/map_live.ex | 7 +++++- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/assets/js/propagation_map_hook.js b/assets/js/propagation_map_hook.js index 6c984b19..d570eea3 100644 --- a/assets/js/propagation_map_hook.js +++ b/assets/js/propagation_map_hook.js @@ -264,14 +264,21 @@ export const PropagationMap = { } }) - // Click on map to show factor breakdown + range circle + // Click on map to request factor detail from server this.map.on("click", (e) => { - const detail = this.lookupPoint(e.latlng.lat, e.latlng.lng) + const basic = this.lookupPoint(e.latlng.lat, e.latlng.lng) + if (basic) { + this.showRangeCircles(basic) + this.pushEvent("point_detail", { lat: e.latlng.lat, lon: e.latlng.lng }) + } + }) + + this.handleEvent("point_detail", (detail) => { if (detail) { - this.showRangeCircles(detail) + const merged = { ...detail, ...this.bandInfo } this.detailPopup .setLatLng([detail.lat, detail.lon]) - .setContent(buildPopupHTML(detail)) + .setContent(buildPopupHTML(merged)) .openOn(this.map) } }) @@ -327,6 +334,13 @@ export const PropagationMap = { this.gridData.set(key, s) }) + // Pre-calculate RGBA strings for scores 0-100 + const colorCache = new Array(101) + for (let i = 0; i <= 100; i++) { + const c = this.scoreColorRGB(i) + colorCache[i] = `rgba(${c.r},${c.g},${c.b},0.55)` + } + const self = this this.scoreOverlay = L.GridLayer.extend({ createTile(coords) { @@ -345,15 +359,20 @@ export const PropagationMap = { const lonPerPx = (se.lng - nw.lng) / size.x const cellPxX = Math.max(1, Math.ceil(step / lonPerPx)) const cellPxY = Math.max(1, Math.ceil(step / Math.abs(latPerPx))) + const absLatPerPx = Math.abs(latPerPx) + let lastColor = null for (let py = 0; py < size.y; py += cellPxY) { + const lat = nw.lat - py * absLatPerPx for (let px = 0; px < size.x; px += cellPxX) { - const lat = nw.lat - py * Math.abs(latPerPx) const lon = nw.lng + px * lonPerPx const score = self.interpolateScore(lat, lon, step) if (score !== null) { - const color = self.scoreColorRGB(score) - ctx.fillStyle = `rgba(${color.r},${color.g},${color.b},0.55)` + const color = colorCache[Math.max(0, Math.min(100, Math.round(score)))] + if (color !== lastColor) { + ctx.fillStyle = color + lastColor = color + } ctx.fillRect(px, py, cellPxX, cellPxY) } } diff --git a/lib/microwaveprop_web/live/map_live.ex b/lib/microwaveprop_web/live/map_live.ex index 10d96342..5ab3c106 100644 --- a/lib/microwaveprop_web/live/map_live.ex +++ b/lib/microwaveprop_web/live/map_live.ex @@ -60,6 +60,11 @@ defmodule MicrowavepropWeb.MapLive do {:noreply, socket} end + def handle_event("point_detail", %{"lat" => lat, "lon" => lon}, socket) do + detail = Propagation.point_detail(socket.assigns.selected_band, lat, lon) + {:noreply, push_event(socket, "point_detail", detail || %{})} + end + def handle_event("map_bounds", bounds, socket) do scores = Propagation.latest_scores(socket.assigns.selected_band, bounds) @@ -121,7 +126,7 @@ defmodule MicrowavepropWeb.MapLive do id="propagation-map" phx-hook="PropagationMap" phx-update="ignore" - data-scores={Jason.encode!(@scores)} + data-scores={@initial_scores_json} data-band-info={Jason.encode!(band_info(@selected_band))} class="absolute inset-0 z-0" >