Skip to content

Commit 1a0ce4b

Browse files
MrSuttonmannclaude
andcommitted
Cull aircraft markers + trails to map viewport and zoom
Markers and trails outside the padded viewport, or below a per-zoom altitude floor, are now suppressed. Selected aircraft remain exempt so Follow / detail panel keep working off-screen. Re-runs on moveend and zoomend so pan/zoom updates without waiting for the next tick. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent e168c2a commit 1a0ce4b

3 files changed

Lines changed: 74 additions & 6 deletions

File tree

app/static/map_controls.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import { applyFollowState, setFollow } from './detail_panel.js';
88
import { applyLabelsVisibility } from './labels.js';
9-
import { applyTrailsVisibility } from './trails.js';
9+
import { applyTrailsVisibility, applyViewportVisibility } from './trails.js';
1010
import { escapeHtml } from './format.js';
1111
import { lucide } from './icons_lib.js';
1212
import { getUnitSystem, setUnitSystem, uconv } from './units.js';
@@ -326,6 +326,14 @@ export function initMapControls() {
326326
.addEventListener('click', () => setCompact(!state.compactMode));
327327
applyCompactMode();
328328

329+
// Re-evaluate aircraft viewport / altitude-floor gates on pan/zoom
330+
// so planes appear/vanish without waiting for the next snapshot tick.
331+
// Trails follow because they read the same hiddenByViewport flag.
332+
state.map.on('moveend zoomend', () => {
333+
applyViewportVisibility();
334+
applyTrailsVisibility();
335+
});
336+
329337
// Airports overlay — refresh on pan/zoom when visible.
330338
state.map.on('moveend', () => { if (state.showAirports) scheduleAirportRefresh(); });
331339
applyAirportsToggle();

app/static/trails.js

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ export function applyTrailsVisibility() {
264264
resetTrailState(entry);
265265
continue;
266266
}
267-
if (entry.hiddenByFilter) {
267+
if (entry.hiddenByFilter || entry.hiddenByViewport) {
268268
resetTrailState(entry);
269269
continue;
270270
}
@@ -282,6 +282,17 @@ export function applyTrailsVisibility() {
282282
state.syncOverlay(state.trailsProxy, state.showTrails);
283283
}
284284

285+
// Reconcile marker presence on the map against both gates
286+
// (hiddenByFilter, hiddenByViewport). Either one being true keeps the
287+
// marker off the map; both false puts it on. Trail polylines clear
288+
// independently in resetTrailState.
289+
function reconcileMarker(entry) {
290+
const shouldHide = entry.hiddenByFilter || entry.hiddenByViewport;
291+
const onMap = state.map.hasLayer(entry.marker);
292+
if (shouldHide && onMap) state.map.removeLayer(entry.marker);
293+
else if (!shouldHide && !onMap) state.map.addLayer(entry.marker);
294+
}
295+
285296
// Show/hide markers + trails for aircraft that don't match the active
286297
// list filters. The currently-selected aircraft is exempt — hiding a
287298
// plane the user is actively looking at would orphan the detail panel.
@@ -293,8 +304,8 @@ export function applyFilterVisibility() {
293304
if (active.size === 0 && state.showPeer) {
294305
for (const entry of state.aircraft.values()) {
295306
if (entry.hiddenByFilter) {
296-
state.map.addLayer(entry.marker);
297307
entry.hiddenByFilter = false;
308+
reconcileMarker(entry);
298309
}
299310
}
300311
return;
@@ -304,12 +315,56 @@ export function applyFilterVisibility() {
304315
const visible = icao === selIcao ||
305316
((!isPeer || state.showPeer) && matchesActiveFilters(entry.data));
306317
if (!visible && !entry.hiddenByFilter) {
307-
state.map.removeLayer(entry.marker);
308-
resetTrailState(entry);
309318
entry.hiddenByFilter = true;
319+
resetTrailState(entry);
320+
reconcileMarker(entry);
310321
} else if (visible && entry.hiddenByFilter) {
311-
state.map.addLayer(entry.marker);
312322
entry.hiddenByFilter = false;
323+
reconcileMarker(entry);
324+
}
325+
}
326+
}
327+
328+
// Per-zoom altitude floor (feet). At wide zooms a hundred GA planes at
329+
// 1500 ft just clutter — only the high cruisers are interesting. Once
330+
// you're zoomed in to metro scale, show everything including ground
331+
// traffic. Aircraft with no altitude reported are always shown rather
332+
// than guessing.
333+
export function minAltitudeFtForZoom(zoom) {
334+
if (zoom >= 8) return 0;
335+
if (zoom >= 6) return 5000;
336+
if (zoom >= 4) return 15000;
337+
return 25000;
338+
}
339+
340+
// Hide markers + trails for aircraft outside the current map viewport
341+
// (with a 20% pad so things don't pop in/out at the edges of a pan)
342+
// or below the per-zoom altitude floor. The currently-selected aircraft
343+
// is exempt — Follow / detail-panel must keep working even when the
344+
// plane is off-screen. Called per tick and on map moveend / zoomend.
345+
export function applyViewportVisibility() {
346+
const bounds = state.map.getBounds().pad(0.2);
347+
const minAltFt = minAltitudeFtForZoom(state.map.getZoom());
348+
const selIcao = state.selectedIcao;
349+
for (const [icao, entry] of state.aircraft) {
350+
if (icao === selIcao) {
351+
if (entry.hiddenByViewport) {
352+
entry.hiddenByViewport = false;
353+
reconcileMarker(entry);
354+
}
355+
continue;
356+
}
357+
const inside = bounds.contains(entry.marker.getLatLng());
358+
const alt = entry.data?.altitude;
359+
const altOk = alt == null || alt >= minAltFt;
360+
const visible = inside && altOk;
361+
if (!visible && !entry.hiddenByViewport) {
362+
entry.hiddenByViewport = true;
363+
resetTrailState(entry);
364+
reconcileMarker(entry);
365+
} else if (visible && entry.hiddenByViewport) {
366+
entry.hiddenByViewport = false;
367+
reconcileMarker(entry);
313368
}
314369
}
315370
}

app/static/update_loop.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { GO_AROUND_COOLDOWN_MS } from './state.js';
1010
import {
1111
applyFilterVisibility,
1212
applyTrailsVisibility,
13+
applyViewportVisibility,
1314
mergeClientTrail,
1415
peekListItem,
1516
setHoverHalo,
@@ -182,6 +183,10 @@ export function update(snap) {
182183
// doesn't redraw a trail for a now-hidden plane.
183184
applyFilterVisibility();
184185

186+
// Same gate but spatial: drop markers for aircraft outside the
187+
// current viewport / below the per-zoom altitude floor.
188+
applyViewportVisibility();
189+
185190
// Single pass across all aircraft now that each has its fresh
186191
// snapshot data merged — applyTrailsVisibility picks which source
187192
// to render from (server window vs full client history).

0 commit comments

Comments
 (0)