feat: Extrapolate aircraft positions based on last known location, heading and groundspeed#1466
feat: Extrapolate aircraft positions based on last known location, heading and groundspeed#1466max-zinn wants to merge 2 commits into
Conversation
- 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
There was a problem hiding this comment.
Pull request overview
This PR extends the “mandatory” VATSIM data payload to include each pilot’s groundspeed and last_updated, and uses that data client-side to extrapolate aircraft positions for smoother map motion. It also changes the aircraft layer update loop to render continuously.
Changes:
- Extend
VatsimMandatoryDatapilot tuple + converted pilot type to includegroundspeedandlast_updated. - Populate the new fields on the server and propagate them into the client’s mandatory data store.
- Extrapolate aircraft coordinates during rendering and switch map aircraft updates to a RAF-driven loop.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| app/utils/server/vatsim/update.ts | Adds groundspeed and last_updated into the server-built mandatory pilots tuple. |
| app/types/data/vatsim.ts | Updates mandatory tuple/converted types to include the new pilot fields. |
| app/composables/render/storage.ts | Destructures and stores groundspeed/last_updated when setting mandatory data. |
| app/composables/render/aircraft/index.ts | Extrapolates aircraft coordinates using heading/speed/last update time. |
| app/components/map/layers/MapAircraftList.vue | Replaces throttled/watch-driven aircraft rendering with a useRafFn continuous render loop. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| 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) |
There was a problem hiding this comment.
In the self-flight branch, the coordinate source is selfCoordinate.value.coordinate (local websocket updates), but the extrapolation timestamp comes from aircraft.last_updated (VATSIM feed). If these clocks/updates are out of sync, the delta time can be wrong (often much larger), causing the self aircraft to be extrapolated far away from its actual position. Consider either not extrapolating when selfCoordinate is present, or using selfCoordinate.value.date as the reference time for extrapolation (and only using VATSIM last_updated for non-self aircraft).
| ? extrapolateCoordinates(dataStore.vatsim.selfCoordinate.value.coordinate, aircraft.heading, aircraft.groundspeed, aircraft.last_updated) | |
| ? extrapolateCoordinates( | |
| dataStore.vatsim.selfCoordinate.value.coordinate, | |
| aircraft.heading, | |
| aircraft.groundspeed, | |
| dataStore.vatsim.selfCoordinate.value.date, | |
| ) |
| 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)); |
There was a problem hiding this comment.
extrapolateCoordinates doesn’t guard against invalid/edge inputs: new Date(last_updated).getTime() can be NaN (bad/empty timestamp), deltaTimeHours can be negative (clock skew / future timestamps) or very large (stale data), and Math.cos(degreesToRadians(lat)) can approach 0 near the poles, producing Infinity/NaN longitudes. This can propagate NaN coordinates into OpenLayers and break rendering. Suggest validating the parsed timestamp, clamping deltaTimeHours to a reasonable range (and to >= 0), and protecting against near-zero longitude scaling (or skipping extrapolation when |lat| is too high).
| 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)); | |
| // If the timestamp is invalid, avoid extrapolation. | |
| if (!Number.isFinite(lastUpdateTime)) { | |
| return coordinates; | |
| } | |
| const currentTime = Date.now(); | |
| let deltaTimeHours = (currentTime - lastUpdateTime) / (1000 * 60 * 60); | |
| // Guard against negative, zero, or non-finite time differences. | |
| if (!Number.isFinite(deltaTimeHours) || deltaTimeHours <= 0) { | |
| return coordinates; | |
| } | |
| // Clamp extrapolation horizon to avoid huge jumps for stale data. | |
| const MAX_EXTRAPOLATION_HOURS = 2; | |
| if (deltaTimeHours > MAX_EXTRAPOLATION_HOURS) { | |
| deltaTimeHours = MAX_EXTRAPOLATION_HOURS; | |
| } | |
| // Guard against non-finite groundspeed/heading. | |
| if (!Number.isFinite(groundspeed) || !Number.isFinite(heading)) { | |
| return coordinates; | |
| } | |
| const distanceNauticalMiles = groundspeed * deltaTimeHours; | |
| const distanceDegrees = distanceNauticalMiles / 60; | |
| const headingRadians = degreesToRadians(heading); | |
| const [lon, lat] = coordinates; | |
| const latRadians = degreesToRadians(lat); | |
| const cosLat = Math.cos(latRadians); | |
| // Avoid division by values close to zero near the poles. | |
| const MIN_COS_LAT = 1e-6; | |
| if (!Number.isFinite(cosLat) || Math.abs(cosLat) < MIN_COS_LAT) { | |
| return coordinates; | |
| } | |
| const deltaLat = distanceDegrees * Math.cos(headingRadians); | |
| const deltaLon = distanceDegrees * Math.sin(headingRadians) / cosLat; |
| @@ -320,15 +321,13 @@ const debouncedUpdate = useThrottleFn(() => { | |||
| tracks: showTracks.value, | |||
| }); | |||
| } | |||
| }, 1000, true); | |||
| }); | |||
There was a problem hiding this comment.
Switching from a throttled/watch-driven update to an unconditional useRafFn causes setMapAircraft to run every animation frame even when nothing changes. setMapAircraft iterates all shown pilots and mutates OpenLayers features, so this is very likely to become a major CPU/battery regression on large traffic sets. If the goal is smoother motion for extrapolation, consider updating at a capped FPS / fixed interval (e.g., 5–10 Hz), only updating geometry coordinates (not full feature props/tracks) on RAF, or keeping the throttled update and adding a lightweight RAF-only position update path.
|
lol what is this copilot ai review I'll take a look on this PR later |
@daniluk4000 Apologies if that was annoying you! 😆 I didn't know it was active for public repositories, too. I have it active automatically for my personal stuff and work stuff. Didn't realize it would automatically review my contributions. |
|
Do you still want to do it? I have fully reworked render, would appreciate if you could update your PR Also it should be limited to global aircraft limit |
🔗 Your VATSIM ID
1432278
🔗 Linked Issue
❓ Type of change
📚 Description
I like how flightradar24 and planefinder seem to extrapolate aircraft positions for a smoother movement on the map. It's very well done there, although it's not perfect (sometimes you can spot the aircraft "jumping" when new data comes in). However, I thought it's a very cool UX feature and thought this could be a cool addition to vatsim-radar 2.
My implementation is by far not perfect, but it's a start and I wanted to get early input on this, before I spend time on something where you say you don't want it at all.
Open issues in this PR
As general question: I'm not sure how to handle ground traffic. Currently (especially at intersections), traffic just taxi over grass until they snap into their updated position. That's extremely ugly and I'm thinking of restricting the fluid motion to airborne traffic. It's almost impossible to extrapolate ground movement as headings and speeds change quite rapidly and I believe we hit a technical limitation that's splitting realworld from Vatsim ^^