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)
This commit is contained in:
Graham McIntire 2026-03-31 10:26:20 -05:00
parent 2f2f3388cd
commit 1aebd7f1cc
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
2 changed files with 32 additions and 8 deletions

View file

@ -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)
}
}

View file

@ -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"
>