perf: live-data deltas, payload caps, and map render polish

Cut fast-tier payload cost with zoom-aware sampling, row deltas, CCTV bbox columns, and MapLibre/motion polish; force viewport snapshot refetches so regional pans refill aircraft immediately.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
BigBodyCobain
2026-07-30 19:40:11 -06:00
co-authored by Cursor
parent d38c886af9
commit 5ae1e5b272
56 changed files with 2104 additions and 492 deletions
@@ -1,7 +1,7 @@
'use client';
import React, { useEffect, useState, useRef } from 'react';
import { Source, Layer, Marker } from 'react-map-gl/maplibre';
import { Source, Layer } from 'react-map-gl/maplibre';
import { API_BASE } from '@/lib/api';
interface Props {
@@ -20,7 +20,10 @@ export default function FishingDestinationRoute({ vesselLat, vesselLng, destinat
const prevDest = useRef('');
useEffect(() => {
if (!destination) { setDestCoords(null); return; }
if (!destination) {
setDestCoords(null);
return;
}
const query = destination.trim();
if (!query || query === prevDest.current) return;
prevDest.current = query;
@@ -43,7 +46,9 @@ export default function FishingDestinationRoute({ vesselLat, vesselLng, destinat
setDestCoords(null);
}
})();
return () => { cancelled = true; };
return () => {
cancelled = true;
};
}, [destination]);
if (!destCoords) return null;
@@ -56,7 +61,10 @@ export default function FishingDestinationRoute({ vesselLat, vesselLng, destinat
properties: { type: 'fishing-route' },
geometry: {
type: 'LineString',
coordinates: [[vesselLng, vesselLat], destCoords],
coordinates: [
[vesselLng, vesselLat],
destCoords,
],
},
},
{
@@ -71,49 +79,47 @@ export default function FishingDestinationRoute({ vesselLat, vesselLng, destinat
};
return (
<>
<Source id="fishing-dest-route" type="geojson" data={geojson}>
<Layer
id="fishing-dest-line"
type="line"
filter={['==', ['get', 'type'], 'fishing-route']}
paint={{
'line-color': '#0ea5e9',
'line-width': 2,
'line-opacity': 0.7,
'line-dasharray': [6, 4],
}}
/>
<Layer
id="fishing-dest-point"
type="circle"
filter={['==', ['get', 'type'], 'fishing-dest']}
paint={{
'circle-radius': 6,
'circle-color': 'rgba(14, 165, 233, 0.3)',
'circle-stroke-width': 2,
'circle-stroke-color': '#0ea5e9',
}}
/>
<Layer
id="fishing-dest-label"
type="symbol"
filter={['==', ['get', 'type'], 'fishing-dest']}
layout={{
'text-field': destLabel,
'text-font': ['Noto Sans Bold'],
'text-size': 11,
'text-offset': [0, 1.4],
'text-anchor': 'top',
'text-allow-overlap': true,
}}
paint={{
'text-color': '#0ea5e9',
'text-halo-color': 'rgba(0,0,0,0.9)',
'text-halo-width': 1.5,
}}
/>
</Source>
</>
<Source id="fishing-dest-route" type="geojson" data={geojson}>
<Layer
id="fishing-dest-line"
type="line"
filter={['==', ['get', 'type'], 'fishing-route']}
paint={{
'line-color': '#0ea5e9',
'line-width': 2,
'line-opacity': 0.7,
'line-dasharray': [6, 4],
}}
/>
<Layer
id="fishing-dest-point"
type="circle"
filter={['==', ['get', 'type'], 'fishing-dest']}
paint={{
'circle-radius': 6,
'circle-color': 'rgba(14, 165, 233, 0.3)',
'circle-stroke-width': 2,
'circle-stroke-color': '#0ea5e9',
}}
/>
<Layer
id="fishing-dest-label"
type="symbol"
filter={['==', ['get', 'type'], 'fishing-dest']}
layout={{
'text-field': destLabel,
'text-font': ['Noto Sans Bold'],
'text-size': 11,
'text-offset': [0, 1.4],
'text-anchor': 'top',
'text-allow-overlap': true,
}}
paint={{
'text-color': '#0ea5e9',
'text-halo-color': 'rgba(0,0,0,0.9)',
'text-halo-width': 1.5,
}}
/>
</Source>
);
}
@@ -0,0 +1,95 @@
import { interpolatePosition } from '@/utils/positioning';
import type { DynamicMapLayersResult } from '@/components/map/dynamicMapLayers.worker';
const UNBOUNDED_INTERP_SECONDS = Number.POSITIVE_INFINITY;
type InterpProps = {
baseLat?: number;
baseLng?: number;
spd?: number | null;
hdg?: number | null;
alt?: number | null;
kind?: 'flight' | 'ship';
};
function interpCoords(props: InterpProps, dtSeconds: number): [number, number] | null {
const lat = props.baseLat;
const lng = props.baseLng;
if (typeof lat !== 'number' || typeof lng !== 'number') return null;
const spd = props.spd;
if (!spd || spd <= 0 || dtSeconds <= 0) return [lng, lat];
if (props.kind === 'flight') {
if (props.alt != null && props.alt <= 100) return [lng, lat];
if (dtSeconds < 1) return [lng, lat];
}
const heading = props.hdg || 0;
const [newLat, newLng] = interpolatePosition(
lat,
lng,
heading,
spd,
dtSeconds,
0,
UNBOUNDED_INTERP_SECONDS,
);
return [newLng, newLat];
}
function applyInterpToCollection(
fc: GeoJSON.FeatureCollection | null,
dtSeconds: number,
): GeoJSON.FeatureCollection | null {
if (!fc?.features?.length || dtSeconds <= 0) return fc;
let changed = false;
const features = fc.features.map((feature) => {
const props = (feature.properties || {}) as InterpProps;
if (props.baseLat == null || props.baseLng == null) return feature;
if (!props.spd || props.spd <= 0) return feature;
const coords = interpCoords(props, dtSeconds);
if (!coords) return feature;
const geom = feature.geometry;
if (!geom || geom.type !== 'Point') return feature;
const [oldLng, oldLat] = geom.coordinates;
if (oldLng === coords[0] && oldLat === coords[1]) return feature;
changed = true;
return {
...feature,
geometry: {
type: 'Point' as const,
coordinates: coords,
},
};
});
if (!changed) return fc;
return { type: 'FeatureCollection', features };
}
/**
* Re-apply dead-reckoning between backend polls without rebuilding layer
* FeatureCollections in the dynamic map worker.
*
* Expects features stamped with baseLat/baseLng/spd/hdg(/alt/kind) by the worker.
*/
export function applyDynamicLayerInterp(
layers: DynamicMapLayersResult,
dtSeconds: number,
): DynamicMapLayersResult {
if (dtSeconds <= 0) return layers;
return {
commercialFlightsGeoJSON: applyInterpToCollection(layers.commercialFlightsGeoJSON, dtSeconds),
privateFlightsGeoJSON: applyInterpToCollection(layers.privateFlightsGeoJSON, dtSeconds),
privateJetsGeoJSON: applyInterpToCollection(layers.privateJetsGeoJSON, dtSeconds),
militaryFlightsGeoJSON: applyInterpToCollection(layers.militaryFlightsGeoJSON, dtSeconds),
trackedFlightsGeoJSON: applyInterpToCollection(layers.trackedFlightsGeoJSON, dtSeconds),
shipsGeoJSON: applyInterpToCollection(layers.shipsGeoJSON, dtSeconds),
meshtasticGeoJSON: layers.meshtasticGeoJSON,
aprsGeoJSON: layers.aprsGeoJSON,
};
}
@@ -1,6 +1,5 @@
/// <reference lib="webworker" />
import { interpolatePosition } from '@/utils/positioning';
import { classifyAircraft } from '@/utils/aircraftClassification';
import type { Flight, Ship, SigintSignal } from '@/types/dashboard';
import type { FlightLayerConfig } from '@/components/map/geoJSONBuilders';
@@ -99,8 +98,6 @@ const EMPTY_RESULT: DynamicMapLayersResult = {
aprsGeoJSON: null,
};
const UNBOUNDED_INTERP_SECONDS = Number.POSITIVE_INFINITY;
const TRACKED_GROUNDED_ICON_MAP: Record<string, string> = {
airliner: 'svgAirlinerGrey',
turboprop: 'svgTurbopropGrey',
@@ -213,38 +210,6 @@ function flightDisplayLabel(f: Flight): string {
return '';
}
function interpFlightPosition(f: Flight, dtSeconds: number): [number, number] {
if (!f.speed_knots || f.speed_knots <= 0 || dtSeconds <= 0) return [f.lng, f.lat];
if (f.alt != null && f.alt <= 100) return [f.lng, f.lat];
if (dtSeconds < 1) return [f.lng, f.lat];
const heading = f.true_track || f.heading || 0;
const [newLat, newLng] = interpolatePosition(
f.lat,
f.lng,
heading,
f.speed_knots,
dtSeconds,
0,
UNBOUNDED_INTERP_SECONDS,
);
return [newLng, newLat];
}
function interpShipPosition(s: Ship, dtSeconds: number): [number, number] {
if (typeof s.sog !== 'number' || !s.sog || s.sog <= 0 || dtSeconds <= 0) return [s.lng, s.lat];
const heading = (typeof s.cog === 'number' ? s.cog : 0) || s.heading || 0;
const [newLat, newLng] = interpolatePosition(
s.lat,
s.lng,
heading,
s.sog,
dtSeconds,
0,
UNBOUNDED_INTERP_SECONDS,
);
return [newLng, newLat];
}
function buildFlightLayerGeoJSONWorker(
flights: Flight[] | undefined,
config: FlightLayerConfig,
@@ -257,11 +222,14 @@ function buildFlightLayerGeoJSONWorker(
const { colorMap, groundedMap, typeLabel, idPrefix, milSpecialMap, useTrackHeading } = config;
const features: GeoJSON.Feature[] = [];
// Geometry is stamped at the reported position (dt=0). Main-thread
// applyDynamicLayerInterp dead-reckons between polls using baseLat/spd/hdg
// so we do not rebuild this FeatureCollection every interp tick.
void dtSeconds;
for (let i = 0; i < flights.length; i += 1) {
const f = flights[i];
if (f.lat == null || f.lng == null) continue;
const [iLng, iLat] = interpFlightPosition(f, dtSeconds);
if (!passesViewFilter(iLat, iLng, bounds, serverBboxScoped)) continue;
if (!passesViewFilter(f.lat, f.lng, bounds, serverBboxScoped)) continue;
if (f.icao24 && trackedIcaos.has(f.icao24.toLowerCase())) continue;
const acType = classifyAircraft(f.model, f.aircraft_category);
@@ -289,8 +257,14 @@ function buildFlightLayerGeoJSONWorker(
callsign: flightDisplayLabel(f),
rotation,
iconId,
kind: 'flight',
baseLat: f.lat,
baseLng: f.lng,
spd: f.speed_knots ?? null,
hdg: rotation,
alt: f.alt ?? null,
},
geometry: { type: 'Point', coordinates: [iLng, iLat] },
geometry: { type: 'Point', coordinates: [f.lng, f.lat] },
});
}
@@ -306,11 +280,11 @@ function buildTrackedFlightsGeoJSONWorker(
if (!flights?.length) return null;
const features: GeoJSON.Feature[] = [];
void dtSeconds;
for (let i = 0; i < flights.length; i += 1) {
const f = flights[i];
if (f.lat == null || f.lng == null) continue;
const [lng, lat] = interpFlightPosition(f, dtSeconds);
if (!passesViewFilter(lat, lng, bounds, serverBboxScoped)) continue;
if (!passesViewFilter(f.lat, f.lng, bounds, serverBboxScoped)) continue;
const alertColor = ('alert_color' in f ? f.alert_color : '') || 'white';
const acType = classifyAircraft(f.model, f.aircraft_category);
@@ -326,6 +300,7 @@ function buildTrackedFlightsGeoJSONWorker(
TRACKED_ICON_MAP.airliner[alertColor] ||
'svgAirlinerWhite';
const displayName = flightDisplayLabel(f);
const rotation = f.heading || 0;
features.push({
type: 'Feature',
@@ -333,10 +308,16 @@ function buildTrackedFlightsGeoJSONWorker(
id: f.icao24 || i,
type: 'tracked_flight',
callsign: String(displayName),
rotation: f.heading || 0,
rotation,
iconId,
kind: 'flight',
baseLat: f.lat,
baseLng: f.lng,
spd: f.speed_knots ?? null,
hdg: rotation,
alt: f.alt ?? null,
},
geometry: { type: 'Point', coordinates: [lng, lat] },
geometry: { type: 'Point', coordinates: [f.lng, f.lat] },
});
}
@@ -363,12 +344,12 @@ function buildShipsGeoJSONWorker(
return null;
}
void dtSeconds;
const features: GeoJSON.Feature[] = [];
for (let i = 0; i < ships.length; i += 1) {
const s = ships[i];
if (s.lat == null || s.lng == null) continue;
const [iLng, iLat] = interpShipPosition(s, dtSeconds);
if (!passesViewFilter(iLat, iLng, bounds, serverBboxScoped)) continue;
if (!passesViewFilter(s.lat, s.lng, bounds, serverBboxScoped)) continue;
if (s.type === 'carrier') continue;
const isTrackedYacht = Boolean(s.yacht_alert);
@@ -389,16 +370,22 @@ function buildShipsGeoJSONWorker(
else if (s.type === 'yacht' || isPassenger) iconId = 'svgShipWhite';
else if (isMilitary) iconId = 'svgShipAmber';
const rotation = s.heading || (typeof s.cog === 'number' ? s.cog : 0) || 0;
features.push({
type: 'Feature',
properties: {
id: s.mmsi || s.name || `ship-${i}`,
type: 'ship',
name: s.name,
rotation: s.heading || 0,
rotation,
iconId,
kind: 'ship',
baseLat: s.lat,
baseLng: s.lng,
spd: typeof s.sog === 'number' ? s.sog : null,
hdg: rotation,
},
geometry: { type: 'Point', coordinates: [iLng, iLat] },
geometry: { type: 'Point', coordinates: [s.lng, s.lat] },
});
}
@@ -12,8 +12,9 @@ const UNBOUNDED_INTERP_SECONDS = Number.POSITIVE_INFINITY;
* to smoothly animate entity positions between API updates.
*
* The interp functions read dtSeconds from a ref so their references stay stable.
* This prevents 7 GeoJSON useMemos from re-firing every tick — GeoJSON only rebuilds
* when source data actually changes (new API fetch), not on every interpolation tick.
* Dynamic flight/ship GeoJSON is rebuilt by the worker only when source data /
* filters / bounds change; applyDynamicLayerInterp then dead-reckons coordinates
* on each interpTick without another full worker rebuild.
*/
export function useInterpolation() {
const dataTimestamp = useRef(Date.now());
@@ -21,7 +22,7 @@ export function useInterpolation() {
const [interpTick, setInterpTick] = useState(0);
// Update dtSeconds on each tick and bump a lightweight counter so moving
// layers actually rebuild between backend refreshes.
// markers can advance between backend refreshes.
useEffect(() => {
const iv = setInterval(() => {
dtRef.current = (Date.now() - dataTimestamp.current) / 1000;