Skip to content

Commit

Permalink
Merge pull request #123 from vatsimnetwork/development
Browse files Browse the repository at this point in the history
  • Loading branch information
williammck authored Nov 26, 2023
2 parents 198bf92 + 7743dc8 commit ead0ab2
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 64 deletions.
49 changes: 17 additions & 32 deletions app/Console/Commands/PopulateTracksCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,58 +3,43 @@
namespace App\Console\Commands;

use App\Models\Track;
use App\Services\TracksService;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Http;

class PopulateTracksCommand extends Command
{
protected $signature = 'tracks:populate {--url= : Specify a custom API endpoint to pull from}';
protected $signature = 'tracks:populate';

protected $description = 'Populate active NAT tracks';

public const TRACK_API_ENDPOINT = 'https://tracks.ganderoceanic.ca/data';

public function handle()
public function handle(TracksService $tracksService): void
{
$endpoint = self::TRACK_API_ENDPOINT;
if ($this->option('url')) {
$endpoint = $this->option('url');
}

$this->info('Downloading tracks from API ('.$endpoint.')');
$trackData = Http::get($endpoint, [
'headers' => [
'Accept' => 'application/json',
],
]);
$this->line('Downloading tracks...');

if ($trackData) {
$tracks = json_decode(($trackData));
$this->line('Tracks decoded.');
} else {
$this->error('Could not decode tracks');
try {
$tracks = $tracksService->getTracks();
$this->line('Downloaded tracks.');
} catch (Exception $e) {
$this->error('Could not download tracks.');

return;
}

$this->line('Deactivating old tracks...');
foreach (Track::whereActive(true)->get() as $track) {
$track->deactivate();
}

foreach ($tracks as $track) {
$this->line("Processing track identifier {$track->id} ...");
$routeingString = '';
foreach ($track->route as $fix) {
$routeingString .= "$fix->name ";
}
Track::updateOrCreate(['identifier' => $track->id], [
'last_routeing' => trim($routeingString),
'valid_from' => Carbon::createFromTimestamp($track->validFrom),
'valid_to' => Carbon::createFromTimestamp($track->validTo),
$this->line("Processing track identifier $track[ident]...");
Track::updateOrCreate(['identifier' => $track['ident']], [
'active' => true,
'last_routeing' => $track['route'],
'valid_from' => $track['valid_from'],
'valid_to' => $track['valid_to'],
'last_active' => now(),
'flight_levels' => $track->flightLevels,
'flight_levels' => $track['flight_levels'],
]);
}

Expand Down
13 changes: 10 additions & 3 deletions app/Helpers.php
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
<?php

use App\Enums\DatalinkAuthorities;
use App\Services\TracksService;
use App\Services\VatsimDataService;
use Illuminate\Support\Facades\Auth;

function current_tmi(): string
function current_tmi(): int|string
{
$dataService = new VatsimDataService();
if ($tmi = config('services.tracks.override_tmi')) {
return (int) $tmi;
}

return cache()->remember('tmi', now()->addHours(1), function () {
$tracksService = app(TracksService::class);

return $dataService->getTmi() ?? 'N/A';
return $tracksService->getTmi() ?? 'N/A';
});
}

function current_dl_authority(): ?DatalinkAuthorities
Expand Down
154 changes: 154 additions & 0 deletions app/Services/TracksService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
<?php

namespace App\Services;

use Carbon\Carbon;
use Illuminate\Support\Facades\Http;

class TracksService
{
public const NAT_NOTAMS_URL = 'https://www.notams.faa.gov/common/nat.html';

public const MONTHS = [
'JAN' => 1, 'FEB' => 2, 'MAR' => 3, 'APR' => 4, 'MAY' => 5, 'JUN' => 6,
'JUL' => 7, 'AUG' => 8, 'SEP' => 9, 'OCT' => 10, 'NOV' => 11, 'DEC' => 12,
];

public function getTmi(): ?int
{
$messages = $this->getMessages();

return $messages[0]['tmi'] ?? null;
}

public function getTracks(): ?array
{
$tracks = [];

foreach ($this->getMessages() as $message) {
foreach ($message['tracks'] as $track) {
$tracks[] = [
'ident' => $track['ident'],
'route' => $track['route'],
'valid_from' => $message['valid_from'],
'valid_to' => $message['valid_to'],
'flight_levels' => $track['flight_levels'],
];
}
}

return $tracks;
}

private function getMessages(): array
{
$html = Http::get(self::NAT_NOTAMS_URL)->body();
$html = strip_tags($html);

// Each NOTAM is enclosed in \x02, parentheses, and \n\v\x03... thanks FAA?
preg_match_all('/\x02\((.*?)\)\n\v\x03/s', $html, $matches);
$notams = $matches[1];

$messages = [];
$message = [];
foreach ($notams as $text) {
$lines = explode("\n", $text);

// Each message is split into lines by \n
// Line 1 is the header, line 2 is the validity timespan, line 3 is the part header,
// the following lines are the part content, and the last line is the part footer.

// Parse header
preg_match('/^NAT-([1-9])\/([1-9])/', $lines[0], $matches);
$currentPart = (int) $matches[1];
$totalParts = (int) $matches[2];

// Parse validity
preg_match('/^([A-Z]{3}) ([0-9]{2})\/([0-9]{2})([0-9]{2})Z TO ([A-Z]{3}) ([0-9]{2})\/([0-9]{2})([0-9]{2})Z$/', $lines[1], $matches);
$validFrom = Carbon::create(year: null, month: self::MONTHS[$matches[1]], day: $matches[2], hour: $matches[3], minute: $matches[4]);
$validTo = Carbon::create(year: null, month: self::MONTHS[$matches[5]], day: $matches[6], hour: $matches[7], minute: $matches[8]);

// Combine parts
$message = array_merge($message, array_slice($lines, 3, -1));

// Haven't received all parts yet
if ($currentPart !== $totalParts) {
continue;
}

// Split the full message into its sections
$sections = [];
$section = [];
foreach ($message as $line) {
$section[] = rtrim($line, '-');

if (str_ends_with($line, '-')) {
$sections[] = implode("\n", $section);
$section = [];
}
}

// Parse sections
$tracks = [];
$remarks = [];
foreach ($sections as $section) {
if (preg_match('/^([A-Z]) (.*?)\nEAST LVLS (.*?)\nWEST LVLS (.*?)\n/s', $section, $matches)) {
if ($matches[3] === 'NIL') {
$direction = 'west';
$rawLevels = $matches[4];
} elseif ($matches[4] === 'NIL') {
$direction = 'east';
$rawLevels = $matches[3];
} else {
$direction = 'unknown';
$rawLevels = '';
}

$tracks[] = [
'ident' => $matches[1],
'route' => $matches[2],
'direction' => $direction,
'flight_levels' => array_map(fn ($fl) => intval($fl) * 100, explode(' ', $rawLevels)),
];
} else {
$remarkLines = array_slice(explode("\n", $section), 1);

$remark = [];
foreach ($remarkLines as $remarkLine) {
if (preg_match('/^\d+\. ?(.*)/', $remarkLine, $matches)) {
if ($remark) {
$remarks[] = implode("\n", $remark);
}

$remark = [$matches[1]];
continue;
}

$remark[] = $remarkLine;
}
$remarks[] = implode("\n", $remark);
}
}

$tmi = 0;
foreach ($remarks as $remark) {
if (preg_match('/^TMI IS ([0-9]{3})/', $remark, $matches)) {
$tmi = (int) $matches[1];
break;
}
}

// Finalize message
$messages[] = [
'tmi' => $tmi,
'valid_from' => $validFrom,
'valid_to' => $validTo,
'tracks' => $tracks,
'remarks' => $remarks,
];
$message = [];
}

return $messages;
}
}
29 changes: 0 additions & 29 deletions app/Services/VatsimDataService.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ class VatsimDataService
{
public const NETWORK_DATA_URL = 'https://data.vatsim.net/v3/vatsim-data.json';

public const TRACK_API_ENDPOINT = 'https://tracks.ganderoceanic.ca/data';

private ?object $networkData = null;

private function getNetworkData()
Expand All @@ -37,33 +35,6 @@ private function getNetworkData()
return $this->networkData;
}

public function getTmi(): ?string
{
if (config('services.tracks.override_tmi')) {
return (string) config('services.tracks.override_tmi');
}

return Cache::remember('tmi', now()->addHours(1), function () {
$trackData = Http::get(self::TRACK_API_ENDPOINT, [
'headers' => [
'Accept' => 'application/json',
],
]);

if ($trackData) {
$tracks = json_decode(($trackData));
} else {
return null;
}

if (! $tracks[0]) {
return null;
}

return $tracks[0]->tmi;
});
}

public function isActivePilot(VatsimAccount $vatsimAccount): bool
{
if (! auth()->check()) {
Expand Down

0 comments on commit ead0ab2

Please sign in to comment.