From ed259ada360d0fc472e83ca5bb3467d1825c0aa3 Mon Sep 17 00:00:00 2001 From: max-zinn Date: Thu, 12 Mar 2026 21:20:49 +0100 Subject: [PATCH 1/2] feat: Extrapolate aircraft positions based on last known location - Extending the mandatoryData of VatsimPilots by groundspeed and last_updated timestamp - Implement function on render to alter the pilots coordinates based on extrapolation function - Increase Rate of renderings --- app/components/map/layers/MapAircraftList.vue | 2 +- app/composables/render/aircraft/index.ts | 22 +++++++++++++++++-- app/composables/render/storage.ts | 5 ++++- app/types/data/vatsim.ts | 4 ++-- app/utils/server/vatsim/update.ts | 2 +- 5 files changed, 28 insertions(+), 7 deletions(-) diff --git a/app/components/map/layers/MapAircraftList.vue b/app/components/map/layers/MapAircraftList.vue index 615154833..7a64d2417 100644 --- a/app/components/map/layers/MapAircraftList.vue +++ b/app/components/map/layers/MapAircraftList.vue @@ -320,7 +320,7 @@ const debouncedUpdate = useThrottleFn(() => { tracks: showTracks.value, }); } -}, 1000, true); +}, 1000/20, true); useUpdateCallback(['mandatory', 'short', 'extent', updateRelatedSettings], () => { if (!init) return; diff --git a/app/composables/render/aircraft/index.ts b/app/composables/render/aircraft/index.ts index 6955f9b85..71d88395d 100644 --- a/app/composables/render/aircraft/index.ts +++ b/app/composables/render/aircraft/index.ts @@ -140,8 +140,8 @@ export async function setMapAircraft(settings: { } const coordinates = (isSelfFlight && dataStore.vatsim.selfCoordinate.value) - ? dataStore.vatsim.selfCoordinate.value.coordinate - : [aircraft.longitude, aircraft.latitude]; + ? extrapolateCoordinates(dataStore.vatsim.selfCoordinate.value.coordinate, aircraft.heading, aircraft.groundspeed, aircraft.last_updated) + : extrapolateCoordinates([aircraft.longitude, aircraft.latitude], aircraft.heading, aircraft.groundspeed, aircraft.last_updated); const heading = (isSelfFlight && dataStore.vatsim.selfCoordinate.value) ? dataStore.vatsim.selfCoordinate.value.heading @@ -211,3 +211,21 @@ export async function setMapAircraft(settings: { } } } + +function extrapolateCoordinates(coordinates: Coordinate, heading: number, groundspeed: number, last_updated: string): Coordinate { + const lastUpdateTime = new Date(last_updated).getTime(); + const currentTime = Date.now(); + const deltaTimeHours = (currentTime - lastUpdateTime) / (1000 * 60 * 60); + + const distanceNauticalMiles = groundspeed * deltaTimeHours; + const distanceDegrees = distanceNauticalMiles / 60; + + const headingRadians = degreesToRadians(heading); + + const [lon, lat] = coordinates; + + const deltaLat = distanceDegrees * Math.cos(headingRadians); + const deltaLon = distanceDegrees * Math.sin(headingRadians) / Math.cos(degreesToRadians(lat)); + + return [lon + deltaLon, lat + deltaLat]; +} diff --git a/app/composables/render/storage.ts b/app/composables/render/storage.ts index 3f8984793..7a819ba41 100644 --- a/app/composables/render/storage.ts +++ b/app/composables/render/storage.ts @@ -293,12 +293,13 @@ export function setVatsimMandatoryData(mandatoryData: VatsimMandatoryData) { if (hasActivePilotFilter()) mandatoryData.pilots = mandatoryData.pilots.filter(x => vatsim.data.pilots.value.some(y => y.cid === x[0])); vatsim.mandatoryData.value = { - pilots: mandatoryData.pilots.map(([cid, lon, lat, icon, heading]) => { + pilots: mandatoryData.pilots.map(([cid, lon, lat, icon, heading, groundspeed, last_updated]) => { const cidString = cid.toString(); if (data.keyedPilots.value?.[cidString]) { data.keyedPilots.value[cidString].longitude = lon; data.keyedPilots.value[cidString].latitude = lat; data.keyedPilots.value[cidString].heading = heading; + data.keyedPilots.value[cidString].groundspeed = groundspeed; } return { @@ -307,6 +308,8 @@ export function setVatsimMandatoryData(mandatoryData: VatsimMandatoryData) { latitude: lat, icon, heading, + groundspeed, + last_updated, }; }), controllers: mandatoryData.controllers.map(([cid, callsign, frequency, facility]) => ({ diff --git a/app/types/data/vatsim.ts b/app/types/data/vatsim.ts index 97e5a2107..767f83e87 100644 --- a/app/types/data/vatsim.ts +++ b/app/types/data/vatsim.ts @@ -214,13 +214,13 @@ export type VatsimMandatoryData = { timestamp: string; timestampNum: number; serverTime: number; - pilots: [cid: VatsimPilot['cid'], longitude: VatsimPilot['longitude'], latitude: VatsimPilot['latitude'], icon: AircraftIcon, heading: number][]; + pilots: [cid: VatsimPilot['cid'], longitude: VatsimPilot['longitude'], latitude: VatsimPilot['latitude'], icon: AircraftIcon, heading: number, groundspeed: number, last_updated: string][]; controllers: [VatsimController['cid'], VatsimController['callsign'], VatsimController['frequency'], VatsimController['facility']][]; atis: VatsimMandatoryData['controllers']; }; export type VatsimMandatoryConvertedData = { - pilots: Required>[]; + pilots: Required>[]; controllers: Pick[]; atis: VatsimMandatoryConvertedData['controllers']; }; diff --git a/app/utils/server/vatsim/update.ts b/app/utils/server/vatsim/update.ts index 06e4e4332..7daaef3d7 100644 --- a/app/utils/server/vatsim/update.ts +++ b/app/utils/server/vatsim/update.ts @@ -86,7 +86,7 @@ export function updateVatsimMandatoryDataStorage() { for (const pilot of data.pilots) { const coords = [pilot.longitude, pilot.latitude]; - newData.pilots.push([pilot.cid, coords[0], coords[1], getAircraftIcon(pilot).icon, pilot.heading]); + newData.pilots.push([pilot.cid, coords[0], coords[1], getAircraftIcon(pilot).icon, pilot.heading, pilot.groundspeed, pilot.last_updated]); } // Maybe no need to implement From fe17a5f8b78f7740fa09bbf35bc2cf5536671cdc Mon Sep 17 00:00:00 2001 From: max-zinn Date: Thu, 12 Mar 2026 21:40:34 +0100 Subject: [PATCH 2/2] refactor(MapAircraftList): Use rafFn to have a more precise update cycle --- app/components/map/layers/MapAircraftList.vue | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/components/map/layers/MapAircraftList.vue b/app/components/map/layers/MapAircraftList.vue index 7a64d2417..c8dbfd240 100644 --- a/app/components/map/layers/MapAircraftList.vue +++ b/app/components/map/layers/MapAircraftList.vue @@ -305,7 +305,8 @@ let init = false; const visibleSet = useThrottleFn(setVisiblePilots, 1000); -const debouncedUpdate = useThrottleFn(() => { + +useRafFn(() => { if (!canRender.value) { vectorSource.clear(); linesSource.clear(); @@ -320,15 +321,13 @@ const debouncedUpdate = useThrottleFn(() => { tracks: showTracks.value, }); } -}, 1000/20, true); +}); useUpdateCallback(['mandatory', 'short', 'extent', updateRelatedSettings], () => { if (!init) return; visibleSet(); }); -watch([getShownPilots, canRender, showTracks, renderedPilots], debouncedUpdate); - watch(map, val => { if (!val) return;