feat: implement horizontal distance calculations and densify elevated paths; enhance flight layer rendering with improved trail visibility and color mapping

This commit is contained in:
Kewonit
2026-02-15 11:21:36 +05:30
parent 3b431e2c8d
commit 27a3ae042a

View File

@ -57,6 +57,23 @@ function smoothStep(t: number): number {
return t * t * (3 - 2 * t); return t * t * (3 - 2 * t);
} }
function horizontalDistanceFromLngLat(
aLng: number,
aLat: number,
bLng: number,
bLat: number,
): number {
const avgLatRad = ((aLat + bLat) * 0.5 * Math.PI) / 180;
const metersPerDegLon = 111_320 * Math.max(0.2, Math.cos(avgLatRad));
const dx = (bLng - aLng) * metersPerDegLon;
const dy = (bLat - aLat) * 111_320;
return Math.hypot(dx, dy);
}
function horizontalDistanceMeters(a: Snapshot, b: Snapshot): number {
return horizontalDistanceFromLngLat(a.lng, a.lat, b.lng, b.lat);
}
type ElevatedPoint = [number, number, number]; type ElevatedPoint = [number, number, number];
function smoothElevatedPath( function smoothElevatedPath(
@ -91,6 +108,30 @@ function smoothElevatedPath(
return current; return current;
} }
function densifyElevatedPath(
points: ElevatedPoint[],
subdivisions: number = 2,
): ElevatedPoint[] {
if (points.length < 2 || subdivisions <= 1) return points;
const out: ElevatedPoint[] = [];
for (let i = 0; i < points.length - 1; i++) {
const a = points[i];
const b = points[i + 1];
out.push(a);
for (let j = 1; j < subdivisions; j++) {
const t = j / subdivisions;
out.push([
a[0] + (b[0] - a[0]) * t,
a[1] + (b[1] - a[1]) * t,
a[2] + (b[2] - a[2]) * t,
]);
}
}
out.push(points[points.length - 1]);
return out;
}
function smoothNumericSeries(values: number[]): number[] { function smoothNumericSeries(values: number[]): number[] {
if (values.length < 3) return values; if (values.length < 3) return values;
const out = [...values]; const out = [...values];
@ -396,6 +437,7 @@ export function FlightLayers({
const currentFlights = flightsRef.current; const currentFlights = flightsRef.current;
const currentTrails = trailsRef.current; const currentTrails = trailsRef.current;
const trailByIcao = new Map(currentTrails.map((t) => [t.icao24, t]));
const altColors = showAltColorsRef.current; const altColors = showAltColorsRef.current;
const defaultColor: [number, number, number, number] = [ const defaultColor: [number, number, number, number] = [
180, 220, 255, 200, 180, 220, 255, 200,
@ -460,6 +502,65 @@ export function FlightLayers({
interpolatedMap.set(f.icao24, f); interpolatedMap.set(f.icao24, f);
} }
const pitchByIcao = new Map<string, number>();
for (const f of interpolated) {
const curr = currSnapshotsRef.current.get(f.icao24);
const prev = prevSnapshotsRef.current.get(f.icao24);
const trendTrail = trailByIcao.get(f.icao24);
const trendPitch =
trendTrail && trendTrail.path.length >= 2
? (() => {
const end = trendTrail.path.length - 1;
const start = Math.max(0, end - 7);
const startAlt =
trendTrail.altitudes[start] ??
trendTrail.altitudes[end] ??
f.baroAltitude ??
0;
const endAlt =
trendTrail.altitudes[end] ?? f.baroAltitude ?? startAlt;
const [sLng, sLat] = trendTrail.path[start];
const [eLng, eLat] = trendTrail.path[end];
const horizontalMeters = horizontalDistanceFromLngLat(
sLng,
sLat,
eLng,
eLat,
);
if (horizontalMeters < 1) return 0;
return (
(-Math.atan2(endAlt - startAlt, horizontalMeters) * 180) /
Math.PI
);
})()
: 0;
const risePitch =
curr && prev
? (() => {
const horizontalMeters = horizontalDistanceMeters(prev, curr);
if (horizontalMeters < 1) return 0;
const deltaAltitudeMeters = curr.alt - prev.alt;
return (
(-Math.atan2(deltaAltitudeMeters, horizontalMeters) * 180) /
Math.PI
);
})()
: 0;
const speed = f.velocity ?? 0;
const verticalRate = f.verticalRate ?? 0;
const kinematicPitch =
speed > 0 ? (-Math.atan2(verticalRate, speed) * 180) / Math.PI : 0;
const blendedPitch =
trendPitch * 0.5 + risePitch * 0.38 + kinematicPitch * 0.12;
const amplifiedPitch = blendedPitch * 1.55;
const clampedPitch = Math.max(-40, Math.min(40, amplifiedPitch));
pitchByIcao.set(f.icao24, clampedPitch);
}
const layers = []; const layers = [];
if (showShadowsRef.current) { if (showShadowsRef.current) {
@ -486,6 +587,63 @@ export function FlightLayers({
const handledIds = new Set<string>(); const handledIds = new Set<string>();
const trailData: TrailEntry[] = []; const trailData: TrailEntry[] = [];
const buildVisibleTrailPoints = (
trail: TrailEntry,
animFlight: FlightState | undefined,
): ElevatedPoint[] => {
const historyPoints = Math.max(
2,
Math.round(trailDistanceRef.current),
);
const pathSlice =
trail.path.length > historyPoints
? trail.path.slice(trail.path.length - historyPoints)
: trail.path;
const altitudeSlice =
trail.altitudes.length > historyPoints
? trail.altitudes.slice(trail.altitudes.length - historyPoints)
: trail.altitudes;
const smoothPathSlice = smoothPlanarPath(pathSlice);
const altitudeMeters = smoothNumericSeries(
altitudeSlice.map(
(a) => a ?? trail.baroAltitude ?? animFlight?.baroAltitude ?? 0,
),
);
const basePath = smoothPathSlice.map((p, i) => [
p[0],
p[1],
Math.max(0, altitudeMeters[i] ?? trail.baroAltitude ?? 0),
]) as ElevatedPoint[];
const denseBasePath = densifyElevatedPath(basePath, 2);
if (
animFlight &&
animFlight.longitude != null &&
animFlight.latitude != null &&
denseBasePath.length > 1
) {
const clipped = trimPathAheadOfAircraft(denseBasePath, [
animFlight.longitude,
animFlight.latitude,
Math.max(0, animFlight.baroAltitude ?? 0),
]);
const smoothed =
clipped.length < 4 ? clipped : smoothElevatedPath(clipped);
return smoothed.map((p) => [p[0], p[1], Math.max(0, p[2])]);
}
const smoothed =
denseBasePath.length < 4
? denseBasePath
: smoothElevatedPath(denseBasePath);
return smoothed.map((p) => [p[0], p[1], Math.max(0, p[2])]);
};
for (const f of interpolated) { for (const f of interpolated) {
if (f.longitude == null || f.latitude == null) continue; if (f.longitude == null || f.latitude == null) continue;
@ -519,83 +677,40 @@ export function FlightLayers({
new PathLayer<TrailEntry>({ new PathLayer<TrailEntry>({
id: "flight-trails", id: "flight-trails",
data: trailData, data: trailData,
updateTriggers: { getPath: elapsed }, updateTriggers: {
getPath: [elapsed, trailDistanceRef.current],
getColor: [elapsed, altColors, trailDistanceRef.current],
},
getPath: (d) => { getPath: (d) => {
const historyPoints = Math.max(
2,
Math.round(trailDistanceRef.current),
);
const pathSlice =
d.path.length > historyPoints
? d.path.slice(d.path.length - historyPoints)
: d.path;
const altitudeSlice =
d.altitudes.length > historyPoints
? d.altitudes.slice(d.altitudes.length - historyPoints)
: d.altitudes;
const smoothPathSlice = smoothPlanarPath(pathSlice);
const altitudeMeters = smoothNumericSeries(
altitudeSlice.map((a) =>
altitudeToElevation(a ?? d.baroAltitude),
),
);
const animFlight = interpolatedMap.get(d.icao24); const animFlight = interpolatedMap.get(d.icao24);
const basePath = smoothPathSlice.map((p, i) => { const visiblePoints = buildVisibleTrailPoints(d, animFlight);
const pointAlt = return visiblePoints.map(
altitudeMeters[i] ?? altitudeToElevation(d.baroAltitude); (p) =>
const trailAlt = Math.max( [
0, p[0],
pointAlt - TRAIL_BELOW_AIRCRAFT_METERS, p[1],
); Math.max(
return [p[0], p[1], trailAlt] as [number, number, number]; 0,
}); altitudeToElevation(p[2]) - TRAIL_BELOW_AIRCRAFT_METERS,
if ( ),
animFlight && ] as [number, number, number],
animFlight.longitude != null && );
animFlight.latitude != null &&
basePath.length > 1
) {
const ax = animFlight.longitude;
const ay = animFlight.latitude;
const currentAlt = Math.max(
0,
altitudeToElevation(animFlight.baroAltitude) -
TRAIL_BELOW_AIRCRAFT_METERS,
);
const clipped = trimPathAheadOfAircraft(basePath, [
ax,
ay,
currentAlt,
]);
if (clipped.length < 4) return clipped;
return smoothElevatedPath(clipped);
}
if (basePath.length < 4) return basePath;
return smoothElevatedPath(basePath);
}, },
getColor: (d) => { getColor: (d) => {
const historyPoints = Math.max( const animFlight = interpolatedMap.get(d.icao24);
2, const visiblePoints = buildVisibleTrailPoints(d, animFlight);
Math.round(trailDistanceRef.current), const len = visiblePoints.length;
); return visiblePoints.map((point, i) => {
const visibleLen = Math.min(d.path.length, historyPoints);
const len =
visibleLen < 4
? visibleLen
: visibleLen * 2 ** TRAIL_SMOOTHING_ITERATIONS;
const base = altColors
? altitudeToColor(d.baroAltitude)
: defaultColor;
return Array.from({ length: len }, (_, i) => {
const tVal = len > 1 ? i / (len - 1) : 1; const tVal = len > 1 ? i / (len - 1) : 1;
const fade = Math.pow(tVal, 2.4); const fade = Math.pow(tVal, 1.65);
const base = altColors
? altitudeToColor(point[2])
: defaultColor;
return [ return [
Math.min(255, base[0] + 22), base[0],
Math.min(255, base[1] + 22), base[1],
Math.min(255, base[2] + 22), base[2],
Math.round(20 + fade * 200), Math.round(70 + fade * 150),
]; ];
}) as [number, number, number, number][]; }) as [number, number, number, number][];
}, },
@ -620,9 +735,7 @@ export function FlightLayers({
altitudeToElevation(d.baroAltitude), altitudeToElevation(d.baroAltitude),
], ],
getOrientation: (d) => { getOrientation: (d) => {
const vr = d.verticalRate ?? 0; const pitch = pitchByIcao.get(d.icao24) ?? 0;
const v = d.velocity ?? 0;
const pitch = v > 0 ? (-Math.atan2(vr, v) * 180) / Math.PI : 0;
const yaw = -(d.trueTrack ?? 0); const yaw = -(d.trueTrack ?? 0);
return [pitch, yaw, 90]; return [pitch, yaw, 90];
}, },