perf: abort stale overlay fetches, skip same-cell rebuilds, update markers in place

moveend aborts in-flight overlay JSON and skips work when the 0.01° cell
is unchanged. Live aircraft/trains/vessels diff-update by id. Camera popup
HTML is built on open. Alert/perimeter polygons paint on canvas.
This commit is contained in:
Sirius DevOps 2026-08-27 21:23:25 -04:00
parent 435f63e473
commit 734c310d2c

View file

@ -1650,6 +1650,16 @@ let vesselsGroup = null, vesselsOn = false;
let stormsGroup = null, stormsOn = true;
let overlayReq = {ac:0, trains:0, vessels:0, alerts:0, perim:0, incidents:0, storms:0};
let moveDebounce = null;
let overlayAbort = null;
let lastCell = '';
function bboxCell() {
if (!map) return '';
return currentBBox().split(',').map(n => Number(n).toFixed(2)).join(',') + '@' + map.getZoom();
}
function overlayFetch(url) {
return fetch(url, overlayAbort ? { signal: overlayAbort.signal } : {});
}
function isAbort(e) { return e && e.name === 'AbortError'; }
const pointCanvas = () => L.canvas({ padding: 0.5 });
async function initMap() {
@ -1733,6 +1743,11 @@ async function initMap() {
if (camPopupOpen) return; // only the popup's own autopan now
if (moveDebounce) clearTimeout(moveDebounce);
moveDebounce = setTimeout(() => {
const cell = bboxCell();
if (cell === lastCell) return;
lastCell = cell;
if (overlayAbort) overlayAbort.abort();
overlayAbort = new AbortController();
if (firesOn) loadFires();
if (camsOn) loadCams();
if (blipsOn) loadBlips();
@ -1977,7 +1992,7 @@ async function loadFires() {
let url = `${API}/api/fires?bbox=${currentBBox()}&limit=2000&format=heat`;
const since = sinceToISO(firesSince);
if (since) url += `&since=${encodeURIComponent(since)}`;
const r = await fetch(url);
const r = await overlayFetch(url);
const fires = await r.json();
if (req !== fireReq) return; // superseded by a newer pan/zoom
if (firesHeat) map.removeLayer(firesHeat);
@ -1994,6 +2009,7 @@ async function loadFires() {
`${fires.length.toLocaleString()} fire hotspots in view` +
(since ? ` · since ${since.slice(0,16).replace('T',' ')}Z` : '');
} catch(e) {
if (isAbort(e)) return;
document.getElementById('map-hint').textContent = `Fires load failed: ${e.message || e}`;
console.error('Fires load failed', e);
}
@ -2068,7 +2084,7 @@ async function loadCams() {
}
const req = ++camReq;
try {
const r = await fetch(`${API}/api/cameras?bbox=${currentBBox()}&limit=2000`);
const r = await overlayFetch(`${API}/api/cameras?bbox=${currentBBox()}&limit=2000`);
const cams = await r.json();
if (req !== camReq) return; // superseded by a newer pan/zoom
if (camsGroup) map.removeLayer(camsGroup);
@ -2092,7 +2108,7 @@ async function loadCams() {
iconSize: [12, 12], iconAnchor: [6, 6],
});
camsGroup.addLayer(L.marker([c.lat, c.lon], { icon })
.bindPopup(`<div class="cam-pop">` +
.bindPopup(() => `<div class="cam-pop">` +
`<b>${esc(c.location_name || 'Open camera')}</b>` +
`${c.id ? camThumb(c) : '<div class="thumb placeholder">no snapshot</div>'}` +
`<table>` +
@ -2113,6 +2129,7 @@ async function loadCams() {
document.getElementById('map-hint').textContent =
`${cams.length.toLocaleString()} open cameras in view`;
} catch(e) {
if (isAbort(e)) return;
document.getElementById('map-hint').textContent = `Cameras load failed: ${e.message || e}`;
console.error('Cameras load failed', e);
}
@ -2148,7 +2165,7 @@ async function loadBlips() {
let url = `${API}/api/events?bbox=${currentBBox()}&has_coords=true&limit=500`;
const since = sinceToISO(blipsSince);
if (since) url += `&since=${encodeURIComponent(since)}`;
const r = await fetch(url);
const r = await overlayFetch(url);
const evs = (await r.json()).filter(ev => ev.source_type !== 'camera');
if (req !== blipReq) return; // superseded by a newer pan/zoom
if (blipsGroup) map.removeLayer(blipsGroup);
@ -2178,6 +2195,7 @@ async function loadBlips() {
`${evs.length.toLocaleString()} event blips in view`;
}
} catch(e) {
if (isAbort(e)) return;
document.getElementById('map-hint').textContent = `Blips load failed: ${e.message || e}`;
console.error('Blips load failed', e);
}
@ -2289,33 +2307,69 @@ function feedIcon(feed, color, heading) {
}
return ic;
}
function makePointMarker(p, colorFn, feed, renderer) {
if (p.lat == null || p.lon == null) return null;
const col = sanitizeColor(colorFn(p), '#35e0ff');
if (feed) {
const heading = Number(p.heading);
const icon = feedIcon(feed, col, Number.isNaN(heading) ? null : heading);
return L.marker([p.lat, p.lon], { icon }).bindPopup(() => pointPopup(p));
}
return L.circleMarker([p.lat, p.lon], {
radius: 5, color: col, fillColor: col, fillOpacity: 0.9, weight: 1,
renderer,
}).bindPopup(() => pointPopup(p));
}
function renderPoints(existing, points, colorFn, cluster, feed) {
if (existing) map.removeLayer(existing);
const zoom = map.getZoom();
const useCluster = cluster && (zoom < 7 || points.length > 200);
const group = useCluster
? L.markerClusterGroup({ maxClusterRadius: 48, showCoverageOnHover: false, spiderfyOnMaxZoom: true, chunkedLoading: true })
: L.layerGroup();
const canReuse = existing && map.hasLayer(existing)
&& !!existing._osintCluster === !!useCluster
&& existing._osintById;
if (!canReuse) {
if (existing) map.removeLayer(existing);
const group = useCluster
? L.markerClusterGroup({ maxClusterRadius: 48, showCoverageOnHover: false, spiderfyOnMaxZoom: true, chunkedLoading: true })
: L.layerGroup();
group._osintCluster = !!useCluster;
group._osintById = new Map();
const renderer = pointCanvas();
points.forEach(p => {
const m = makePointMarker(p, colorFn, feed, renderer);
if (!m) return;
group.addLayer(m);
if (p.id != null) group._osintById.set(String(p.id), m);
});
group.addTo(map);
return group;
}
const group = existing;
const byId = group._osintById;
const next = new Set();
const renderer = pointCanvas();
points.forEach(p => {
if (p.lat == null || p.lon == null) return;
if (p.lat == null || p.lon == null || p.id == null) return;
const id = String(p.id);
next.add(id);
const col = sanitizeColor(colorFn(p), '#35e0ff');
if (feed) {
const heading = Number(p.heading);
const icon = feedIcon(feed, col, Number.isNaN(heading) ? null : heading);
const m = L.marker([p.lat, p.lon], { icon }).bindPopup(pointPopup(p));
group.addLayer(m);
const m = byId.get(id);
if (m) {
m.setLatLng([p.lat, p.lon]);
if (feed) m.setIcon(feedIcon(feed, col, p.heading));
else if (m.setStyle) m.setStyle({ color: col, fillColor: col });
} else {
const heading = Number(p.heading);
const m = L.circleMarker([p.lat, p.lon], {
radius: 5, color: col, fillColor: col, fillOpacity: 0.9, weight: 1,
renderer,
}).bindPopup(pointPopup(p));
if (!Number.isNaN(heading)) m.setStyle({ className: 'hdg' });
group.addLayer(m);
const nm = makePointMarker(p, colorFn, feed, renderer);
if (!nm) return;
group.addLayer(nm);
byId.set(id, nm);
}
});
byId.forEach((m, id) => {
if (!next.has(id)) {
group.removeLayer(m);
byId.delete(id);
}
});
group.addTo(map);
return group;
}
async function toggleRadar() {
@ -2332,7 +2386,7 @@ async function loadRadar() {
if (!map) return;
try {
if (!radarMeta) {
const r = await fetch(`${API}/api/map/radar`);
const r = await overlayFetch(`${API}/api/map/radar`);
radarMeta = await r.json();
addExtraAttrib('<a href="https://www.rainviewer.com/api.html">Weather data by RainViewer</a>');
addExtraAttrib('Iowa Environmental Mesonet');
@ -2351,6 +2405,7 @@ async function loadRadar() {
radarLayer = L.tileLayer(url, { opacity: radarOpacity, maxZoom: 12, maxNativeZoom: useIem ? 18 : 7, attribution: '' }).addTo(map);
}
} catch (e) {
if (isAbort(e)) return;
console.error('Radar load failed', e);
document.getElementById('lp-radar-count').textContent = 'err';
}
@ -2383,12 +2438,13 @@ async function loadWxAlerts() {
}
const req = ++overlayReq.alerts;
try {
const r = await fetch(`${API}/api/weather-alerts?bbox=${currentBBox()}`);
const r = await overlayFetch(`${API}/api/weather-alerts?bbox=${currentBBox()}`);
const fc = await r.json();
if (req !== overlayReq.alerts) return;
wxAlertsGroup = dropLayer(wxAlertsGroup);
const feats = fc.features || [];
wxAlertsGroup = L.geoJSON(fc, {
renderer: L.canvas({ padding: 0.5 }),
style: (f) => ({
color: severityColor((f.properties || {}).severity),
weight: 2, fillOpacity: 0.18,
@ -2401,6 +2457,7 @@ async function loadWxAlerts() {
document.getElementById('lp-alerts-count').textContent = feats.length.toLocaleString();
addExtraAttrib('NWS / IEM storm-based warnings');
} catch (e) {
if (isAbort(e)) return;
console.error('Alerts load failed', e);
document.getElementById('lp-alerts-count').textContent = 'err';
}
@ -2419,12 +2476,13 @@ async function loadPerimeters() {
}
const req = ++overlayReq.perim;
try {
const r = await fetch(`${API}/api/fire-perimeters?bbox=${currentBBox()}`);
const r = await overlayFetch(`${API}/api/fire-perimeters?bbox=${currentBBox()}`);
const fc = await r.json();
if (req !== overlayReq.perim) return;
perimGroup = dropLayer(perimGroup);
const feats = fc.features || [];
perimGroup = L.geoJSON(fc, {
renderer: L.canvas({ padding: 0.5 }),
style: (f) => {
const acres = Number((f.properties || {}).poly_GISAcres || (f.properties || {}).attr_IncidentSize || 0);
return { color: acres > 10000 ? '#ef4444' : '#fb923c', weight: 2, fillOpacity: 0.25, fillColor: '#fb923c' };
@ -2439,6 +2497,7 @@ async function loadPerimeters() {
document.getElementById('lp-perim-count').textContent = feats.length.toLocaleString();
addExtraAttrib('NIFC WFIGS');
} catch (e) {
if (isAbort(e)) return;
console.error('Perimeters load failed', e);
document.getElementById('lp-perim-count').textContent = 'err';
}
@ -2457,13 +2516,14 @@ async function loadIncidents() {
}
const req = ++overlayReq.incidents;
try {
const r = await fetch(`${API}/api/fire-incidents?bbox=${currentBBox()}`);
const r = await overlayFetch(`${API}/api/fire-incidents?bbox=${currentBBox()}`);
const pts = await r.json();
if (req !== overlayReq.incidents) return;
incidentsGroup = renderPoints(incidentsGroup, pts, () => '#ef4444', false);
document.getElementById('lp-incidents-count').textContent = pts.length.toLocaleString();
addExtraAttrib('NIFC WFIGS');
} catch (e) {
if (isAbort(e)) return;
console.error('Incidents load failed', e);
document.getElementById('lp-incidents-count').textContent = 'err';
}
@ -2481,13 +2541,14 @@ async function loadAircraft() {
}
const req = ++overlayReq.ac;
try {
const r = await fetch(`${API}/api/aircraft?bbox=${currentBBox()}`);
const r = await overlayFetch(`${API}/api/aircraft?bbox=${currentBBox()}`);
const pts = await r.json();
if (req !== overlayReq.ac) return;
acGroup = renderPoints(acGroup, Array.isArray(pts) ? pts : [], p => altColor((p.extra || {}).alt_baro), true, 'ac');
document.getElementById('lp-ac-count').textContent = (pts.length || 0).toLocaleString();
addExtraAttrib('<a href="https://www.adsb.lol/docs/open-data/api">ADSB.lol</a> ODbL');
} catch (e) {
if (isAbort(e)) return;
console.error('Aircraft load failed', e);
document.getElementById('lp-ac-count').textContent = 'err';
}
@ -2501,13 +2562,14 @@ async function loadTrains() {
if (!map) return;
const req = ++overlayReq.trains;
try {
const r = await fetch(`${API}/api/trains?bbox=${currentBBox()}`);
const r = await overlayFetch(`${API}/api/trains?bbox=${currentBBox()}`);
const pts = await r.json();
if (req !== overlayReq.trains) return;
trainsGroup = renderPoints(trainsGroup, Array.isArray(pts) ? pts : [], p => (p.extra || {}).iconColor || '#c084fc', false, 'train');
document.getElementById('lp-trains-count').textContent = (pts.length || 0).toLocaleString();
addExtraAttrib('<a href="https://amtraker.com/about">Amtraker</a>');
} catch (e) {
if (isAbort(e)) return;
console.error('Trains load failed', e);
document.getElementById('lp-trains-count').textContent = 'err';
}
@ -2525,7 +2587,7 @@ async function loadVessels() {
}
const req = ++overlayReq.vessels;
try {
const r = await fetch(`${API}/api/vessels?bbox=${currentBBox()}`);
const r = await overlayFetch(`${API}/api/vessels?bbox=${currentBBox()}`);
const pts = await r.json();
if (req !== overlayReq.vessels) return;
vesselsGroup = renderPoints(vesselsGroup, Array.isArray(pts) ? pts : [], p => {
@ -2535,6 +2597,7 @@ async function loadVessels() {
document.getElementById('lp-vessels-count').textContent = (pts.length || 0).toLocaleString();
addExtraAttrib('AISStream');
} catch (e) {
if (isAbort(e)) return;
console.error('Vessels load failed', e);
document.getElementById('lp-vessels-count').textContent = 'err';
}
@ -2548,13 +2611,14 @@ async function loadStorms() {
if (!map) return;
const req = ++overlayReq.storms;
try {
const r = await fetch(`${API}/api/storms`);
const r = await overlayFetch(`${API}/api/storms`);
const pts = await r.json();
if (req !== overlayReq.storms) return;
stormsGroup = renderPoints(stormsGroup, Array.isArray(pts) ? pts : [], () => '#f472b6', false);
document.getElementById('lp-storms-count').textContent = (pts.length || 0).toLocaleString();
addExtraAttrib('NHC');
} catch (e) {
if (isAbort(e)) return;
console.error('Storms load failed', e);
document.getElementById('lp-storms-count').textContent = 'err';
}