@@ -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}
0 commit comments