paulhollmann / vatsim-data
VATSIM data parser
Requires
- php: ^8.3
- ext-curl: *
- illuminate/cache: ^v11|^v12|^v13
- illuminate/collections: ^v11|^v12|^v13
- illuminate/config: ^v11|^v12|^v13
- illuminate/console: ^v11|^v12|^v13
- illuminate/support: ^v11|^v12|^v13
Requires (Dev)
- illuminate/container: ^v11|^v12|^v13
- laravel/pint: ^1.18
- phpoption/phpoption: ^1.9
- psy/psysh: ^0.12.18
README
vatsim-data is a Laravel package for VATSIM's live network data. It provides typed pilots, controllers, ATIS, transceivers, METARs, aerodrome summaries, and airport stand occupancy in one package.
All live data access is cached internally. Application code queries the API and receives typed objects; it does not need to download, parse, or cache VATSIM payloads itself.
Requirements
- PHP 8.3 or later
- Laravel 11, 12, or 13
Installation
composer require paulhollmann/vatsim-data php artisan vendor:publish --tag=vatsimdata-config
The published config/vatsimdata.php controls the VATSIM endpoint, cache-key prefix, and optional local-airspace helpers. The global methods described below do not depend on any VATSIM Germany-specific configuration.
Cache lifetime
All cache entries are internal and use Laravel's configured cache store. The defaults balance live-data freshness against repeated slow requests:
| Environment variable | Default | Cached data |
|---|---|---|
VATSIM_DATAFEED_CACHE_TTL |
15 seconds | Main VATSIM datafeed and derived station lookups |
VATSIM_DATAFEED_STALE_CACHE_TTL |
86400 seconds | How long the last known-good datafeed remains available during an invalid upstream response |
VATSIM_DATAFEED_HISTORY_COUNT |
5 | Movement points retained per pilot |
VATSIM_DATAFEED_HISTORY_TTL |
86400 seconds | Lifetime of pilot movement history |
VATSIM_METAR_CACHE_TTL |
300 seconds | METAR responses per ICAO code |
VATSIM_TRANSCEIVER_CACHE_TTL |
120 seconds | Transceiver data and controller transceiver lookups |
VATSIM_AERODROME_SUMMARY_CACHE_TTL |
60 seconds | Aerodrome summaries |
OpenStreetMap stand data is cached for three months. Change a TTL only when your application needs a different freshness/performance trade-off; no cache calls are required in application code.
To protect consumers from an invalid or incomplete VATSIM response, a datafeed with fewer than 50 pilots does not replace the last known-good feed. The prior accepted data remains available for up to one day by default; its retention is configurable with the variable above.
Freshness timestamps
Every external source exposes the time at which this package last fetched a successful response. These methods never make an additional request and return null until the corresponding source has been fetched at least once. Timestamps are returned as DateTimeImmutable instances in UTC.
Datafeed::FetchedAt(); // Package fetch time for the VATSIM datafeed Datafeed::UpdatedAt(); // VATSIM's own timestamp within the datafeed Statusfile::FetchedAt(); // VATSIM status file TransceiverData::FetchedAt(); // Transceiver feed Metar::FetchedAt('EDDF'); // METAR for one ICAO code StandStatus::OSMFetchedAt('EDDF'); // OSM stand data for one ICAO code
Live datafeed
use VatsimData\Datafeed; $feed = Datafeed::get(); // ?RootObject $pilots = Datafeed::Pilots(); // Pilot[] $controllers = Datafeed::Controllers(); // Controller[] $atis = Datafeed::Atis(); // Atis[] $history = Datafeed::PilotHistory(); // array<int, PilotPosition[]> $tracks = Datafeed::PilotTracks(); // array<int, PilotTrack>
Within one PHP request, Datafeed::get() reuses the same hydrated feed. Airport
views should prefer scoped queries so the complete typed feed is not hydrated
just to match a small set of aircraft:
$pilots = Datafeed::PilotsNearAerodrome('EDDF', 50.033333, 8.570556, 2.0); $tracks = Datafeed::PilotTracksForCids(array_map(fn ($pilot) => $pilot->cid, $pilots)); $controllers = Datafeed::ControllersForAerodrome('EDDF');
PilotTracksForCids() hydrates only the requested track points. Track cache
entries are stored as arrays under a versioned key, so old serialized object
entries are naturally ignored after package updates.
Refresh worker and movement history
The package registers vatsimdata:refresh, which fetches the feed immediately, refreshes the main datafeed cache, and appends the current position of every pilot to a bounded history keyed by VATSIM CID. The default history contains the latest five actual points per pilot. PilotTracks() returns a PilotTrack for each CID. Each track contains the actual points and five predicted points at exactly 5, 10, 15, 20, and 25 seconds after the latest point. The predicted path follows a local quadratic fitted through the latest three actual positions, so recent turns are carried into the short projection. Backtracking caused by a sharp slowdown is clamped so the predicted path never reverses behind the latest valid point. Predicted points have predicted === true; actual points have predicted === false. Every point contains latitude, longitude, altitude, groundspeed, heading, and recorded_at.
Run it from Laravel's scheduler, for example in routes/console.php:
use Illuminate\Support\Facades\Schedule; Schedule::command('vatsimdata:refresh')->everyFifteenSeconds()->withoutOverlapping();
Then run php artisan schedule:work (or configure your normal scheduler worker). The refresh command also precomputes and caches PilotTrack objects, so flightpath predictions are ready without recalculating them during an airport-view request. StandStatus::parseData() similarly precomputes its default airport flight-status map after stand assignment. The history and derived data are stored in the configured Laravel cache store, so use a shared store when multiple application instances collect or read it.
RootObject, Pilot, Controller, Atis, FlightPlan, and related classes are typed DTOs in the VatsimData\DatafeedClasses namespace. For example:
foreach (Datafeed::Pilots() as $pilot) { echo $pilot->callsign; echo $pilot->latitude; echo $pilot->flight_plan?->departure; }
Pilot queries
Datafeed::PilotsArrivingAt('EDDF'); Datafeed::PilotsDepartingFrom('EDDF'); // Existing compatibility method; it delegates to PilotsArrivingAt(). Datafeed::PilotsArrivingAerodrome('EDDF');
ICAO input is normalized, and pilots without a flight plan are ignored by arrival/departure queries.
For geographic filtering, pass the included polygon helper:
use VatsimData\Helpers\Polygon; $polygon = new Polygon('POLYGON((...))'); $pilots = Datafeed::PilotsWithinPolygon($polygon);
PilotsLocal() remains available as a convenience wrapper around the polygon configured in vatsimdata.local_airspace_polygon.
Controllers, callsigns, and stations
use VatsimData\Datafeed; use VatsimData\Helpers\Callsign; $active = Datafeed::ControllersActive(); $eddfControllers = Datafeed::ControllersForAerodrome('EDDF'); $withTransceivers = Datafeed::ControllersWithTransceiversForAerodrome('EDDF'); $callsign = Callsign::parse('EDDF_N_TWR'); $callsign->airport(); // 'EDDF' $callsign->role; // 'TWR' $callsign->observer; // false
ControllersActive() is global and excludes observers. ControllersLocal() is retained for applications using the configured regional callsign pattern.
To resolve a controller from station data, supply the station ident and frequency:
$match = Datafeed::ControllerForStation('EDDF_TWR', 118.7); $controller = $match?->controller; // ?Controller $ident = $match?->stationIdent; $frequency = $match?->stationFrequency;
The matcher normalizes frequencies and accepts sectorised controller callsigns, so EDDF_N_TWR can match an EDDF_TWR station at the same frequency.
Aerodrome summaries
AerodromeSummary combines a single airport's live controllers, ATIS, active controller roles, arrivals, and departures.
$summary = Datafeed::AerodromeSummary('EDDF'); $summary->departures; // int $summary->arrivals; // int $summary->controllers; // Controller[] $summary->atis; // Atis[] $summary->roles; // array<string, bool> $summary->hasRole('TWR'); // bool
For airport lists:
$summaries = Datafeed::AerodromeSummaries(['EDDF', 'EDDM', 'LOWW']); $eddf = $summaries['EDDF'];
ATIS, METAR, and transceivers
use VatsimData\Metar; use VatsimData\TransceiverData; $atis = Datafeed::AtisAerodrome('EDDF'); $metar = Metar::get('EDDF'); $owner = TransceiverData::Owner('EDDF_N_TWR'); $transceivers = $owner?->transceivers ?? [];
Stand status
VatsimData\StandStatus replaces the separate vatsim-stand-status package. It associates eligible VATSIM pilots with the nearest airport parking stand.
Quick start
use VatsimData\StandStatus; $stands = new StandStatus(51.148056, -0.190278); $stands->loadStandDataFromArray([ ['43N', 51.15712, -0.17373], ['43W', 51.15712, -0.17373], ])->parseData(); foreach ($stands->occupiedStands() as $stand) { echo $stand->getName().' '.$stand->occupier->callsign; }
Aerodrome elevation
VATSIM reports altitude in feet MSL. Supply the aerodrome elevation (also feet MSL) to make stand eligibility and the ground/taxi/takeoff/arrival phase thresholds relative to the airport. The optional final constructor argument preserves the existing constructor contract; omitting it retains the previous sea-level behaviour.
$stands = new StandStatus( 50.033333, 8.570556, StandStatus::COORD_FORMAT_DECIMAL, 'EDDF', 364, // aerodrome elevation in feet MSL ); // Or configure it after construction: $stands->setAerodromeElevation(364);
When an ICAO is supplied to the constructor, parseData() uses
Datafeed::PilotsNearAerodrome() by default. For tests or application-owned
sources, pass an iterable of typed Pilot objects or the legacy pilot-array shape:
$stands->parseData([ [ 'callsign' => 'TEST1', 'latitude' => 51.15712, 'longitude' => -0.17373, 'altitude' => 0, 'groundspeed' => 0, ], ]);
Stand input
The legacy input contract is retained. Every stand row has exactly three values: [identifier, latitude, longitude].
$stands->loadStandDataFromArray([ ['1', 51.154819, -0.164813], ['10', 51.155090, -0.164660], ]); $stands->loadStandDataFromCSV(storage_path('stands.csv'));
CSV files use the same three columns; an optional header row is accepted.
For CAA/Aerospace coordinate input, use the legacy-compatible format constant:
$stands = new StandStatus( 51.148056, -0.190278, StandStatus::COORD_FORMAT_CAA, );
OpenStreetMap stand data
$stands = new StandStatus(51.4775, -0.461389); // EGLL $stands->fetchAndLoadStandDataFromOSM('EGLL')->parseData();
This downloads OSM aeroway=parking_position data around the airport and caches it internally for three months. OSM data may be incomplete; consumers displaying or redistributing it must provide OpenStreetMap attribution as required by the ODbL.
Matching settings
All setters are fluent. Run parseData() again after changing one.
| Setter | Default | Meaning |
|---|---|---|
setMaxStandDistance(float $km) |
0.07 km |
Maximum aircraft-to-stand distance. |
setMaxDistanceFromAirport(float $km) |
2 km |
Aircraft outside this airport-centre radius are ignored. |
setMaxAircraftAltitude(int $feet) |
3000 ft |
Aircraft above this altitude are ignored. |
setMaxAircraftGroundspeed(int $knots) |
10 kt |
Aircraft faster than this are ignored. |
setHideStandSidesWhenOccupied(bool $hide) |
true |
Hides related stands such as 42L and 42R. |
setStandExtensions(array $extensions) |
['L', 'C', 'R', 'A', 'B', 'N', 'E', 'S', 'W'] |
Defines side-stand suffixes. |
setStandExtensionPattern(string $pattern) |
'<standroot><extensions>' |
Defines how stand groups are detected. |
Results
$all = $stands->allStands(); $visible = $stands->stands(); $occupied = $stands->occupiedStands(); $unoccupied = $stands->unoccupiedStands(); $aircraft = $stands->allAircraft(); // Pass true for arrays keyed by stand identifier. $standsByName = $stands->allStands(true);
Each returned Stand exposes id, latitude, longitude, occupier, isOccupied(), getName(), getRoot(), and getExtension(). Aircraft exposes the VATSIM pilot fields through properties such as callsign, latitude, longitude, altitude, and groundspeed, plus onStand() and getStandIndex().
Flight phases
Set the airport ICAO as the optional fourth constructor argument (or with setAirportIcao()) to calculate a typed FlightStatus for pilots around an airport:
use VatsimData\StandData\FlightStatus; $stands = new StandStatus(50.033333, 8.570556, StandStatus::COORD_FORMAT_DECIMAL, 'EDDF'); $status = $stands->calculateFlightStatus($pilot); if ($status === FlightStatus::TAXI_FOR_DEPARTURE) { // … }
The possible values are AT_GATE, TAXI_FOR_DEPARTURE, TAKING_OFF, DEPARTING, ARRIVING, TAXI_TO_GATE, ARRIVED_AT_GATE, and UNKNOWN.
flightStatuses() calculates statuses for every current VATSIM pilot; pass an iterable of Pilot objects or legacy arrays to classify a supplied data set. The classifier is snapshot-based: it uses stand occupancy, altitude, groundspeed, and flight-plan departure/arrival ICAOs. An aircraft not on a stand and at airport-surface altitude is classified as TAXI_FOR_DEPARTURE or TAXI_TO_GATE when its groundspeed is below 30 knots. It therefore returns UNKNOWN where a current snapshot cannot establish a reliable phase, rather than inferring a route-specific status.
flightStatuses() uses the first-class pilot history exposed by Datafeed::PilotTracks(). This improves transitions such as climb, descent, and gate departure movement when a flight plan is missing, without adding a second tracking store or per-aircraft cache writes.
Record history by scheduling the built-in refresh command at the interval your application needs (one minute is a sensible default):
use Illuminate\Support\Facades\Schedule; Schedule::command('vatsimdata:refresh')->everyFifteenSeconds();
The command refreshes the datafeed cache and stores a compact position history per VATSIM CID. History length and retention are controlled by VATSIM_DATAFEED_HISTORY_COUNT (default 5) and VATSIM_DATAFEED_HISTORY_TTL (default 86400 seconds).
License
GPL-3.0-only. See LICENSE.