diff --git a/app/Contracts/Metar.php b/app/Contracts/Metar.php index 861aaae9..350a86c9 100644 --- a/app/Contracts/Metar.php +++ b/app/Contracts/Metar.php @@ -12,13 +12,24 @@ abstract class Metar { /** * Implement retrieving the METAR - return the METAR string. Needs to be protected, - * since this shouldn't be directly called. Call `get_metar($icao)` instead + * since this shouldn't be directly called. Call `metar($icao)`. If not implemented, + * return a blank string * * @param $icao * * @return mixed */ - abstract protected function metar($icao): string; + abstract protected function get_metar($icao): string; + + /** + * Implement retrieving the TAF - return the string. Call `taf($icao)`. If not implemented, + * return a blank string + * + * @param $icao + * + * @return mixed + */ + abstract protected function get_taf($icao): string; /** * Download the METAR, wrap in caching @@ -27,9 +38,9 @@ abstract class Metar * * @return string */ - public function get_metar($icao): string + public function metar($icao): string { - $cache = config('cache.keys.WEATHER_LOOKUP'); + $cache = config('cache.keys.METAR_WEATHER_LOOKUP'); $key = $cache['key'].$icao; if (Cache::has($key)) { @@ -40,7 +51,7 @@ abstract class Metar } try { - $raw_metar = $this->metar($icao); + $raw_metar = $this->get_metar($icao); } catch (\Exception $e) { Log::error('Error getting METAR: '.$e->getMessage(), $e->getTrace()); return ''; @@ -52,4 +63,37 @@ abstract class Metar return $raw_metar; } + + /** + * Download the TAF, wrap in caching + * + * @param $icao + * + * @return string + */ + public function taf($icao): string + { + $cache = config('cache.keys.TAF_WEATHER_LOOKUP'); + $key = $cache['key'].$icao; + + if (Cache::has($key)) { + $taf = Cache::get($key); + if ($taf !== '') { + return $taf; + } + } + + try { + $taf = $this->get_taf($icao); + } catch (\Exception $e) { + Log::error('Error getting TAF: '.$e->getMessage(), $e->getTrace()); + return ''; + } + + if ($taf !== '') { + Cache::put($key, $taf, $cache['time']); + } + + return $taf; + } } diff --git a/app/Services/AirportService.php b/app/Services/AirportService.php index 179fdc0e..77cd1ad3 100644 --- a/app/Services/AirportService.php +++ b/app/Services/AirportService.php @@ -46,12 +46,32 @@ class AirportService extends Service return; } - $raw_metar = $this->metarProvider->get_metar($icao); + $raw_metar = $this->metarProvider->metar($icao); if ($raw_metar && $raw_metar !== '') { return new Metar($raw_metar); } } + /** + * Return the METAR for a given airport + * + * @param $icao + * + * @return Metar|null + */ + public function getTaf($icao) + { + $icao = trim($icao); + if ($icao === '') { + return; + } + + $raw_taf = $this->metarProvider->taf($icao); + if ($raw_taf && $raw_taf !== '') { + return new Metar($raw_taf, true); + } + } + /** * Lookup an airport's information from a remote provider. This handles caching * the data internally diff --git a/app/Services/Metar/AviationWeather.php b/app/Services/Metar/AviationWeather.php index 169287eb..888f65d7 100644 --- a/app/Services/Metar/AviationWeather.php +++ b/app/Services/Metar/AviationWeather.php @@ -9,13 +9,16 @@ use Exception; use Illuminate\Support\Facades\Log; /** - * Return the raw METAR string from the NOAA Aviation Weather Service + * Return the raw METAR/TAF string from the NOAA Aviation Weather Service */ class AviationWeather extends Metar { private const METAR_URL = 'https://www.aviationweather.gov/adds/dataserver_current/httpparam?dataSource=metars&requestType=retrieve&format=xml&hoursBeforeNow=3&mostRecent=true&stationString='; + private const TAF_URL = + 'https://www.aviationweather.gov/adds/dataserver_current/httpparam?dataSource=tafs&requestType=retrieve&format=xml&hoursBeforeNow=3&mostRecent=true&stationString='; + private $httpClient; public function __construct(HttpClient $httpClient) @@ -33,7 +36,7 @@ class AviationWeather extends Metar * * @return string */ - protected function metar($icao): string + protected function get_metar($icao): string { if ($icao === '') { return ''; @@ -67,7 +70,53 @@ class AviationWeather extends Metar return $xml->data->METAR->raw_text->__toString(); } catch (Exception $e) { Log::error('Error reading METAR: '.$e->getMessage()); + return ''; + } + } + /** + * Do the actual retrieval of the TAF + * + * @param $icao + * + * @throws \GuzzleHttp\Exception\GuzzleException + * + * @return string + */ + protected function get_taf($icao): string + { + if ($icao === '') { + return ''; + } + + $tafurl = static::TAF_URL.$icao; + + try { + $tafres = $this->httpClient->get($tafurl, []); + $tafxml = simplexml_load_string($tafres); + + $tafattrs = $tafxml->data->attributes(); + if (!isset($tafattrs['num_results'])) { + return ''; + } + + $tafnum_results = $tafattrs['num_results']; + if (empty($tafnum_results)) { + return ''; + } + + $tafnum_results = (int) $tafnum_results; + if ($tafnum_results === 0) { + return ''; + } + + if (count($tafxml->data->TAF->raw_text) === 0) { + return ''; + } + + return $tafxml->data->TAF->raw_text->__toString(); + } catch (Exception $e) { + Log::error('Error reading TAF: '.$e->getMessage()); return ''; } } diff --git a/app/Widgets/Weather.php b/app/Widgets/Weather.php index 7ecfec96..f8d1cd25 100644 --- a/app/Widgets/Weather.php +++ b/app/Widgets/Weather.php @@ -23,10 +23,12 @@ class Weather extends Widget /** @var \App\Services\AirportService $airportSvc */ $airportSvc = app(AirportService::class); $metar = $airportSvc->getMetar($this->config['icao']); + $taf = $airportSvc->getTaf($this->config['icao']); return view('widgets.weather', [ 'config' => $this->config, 'metar' => $metar, + 'taf' => $taf, 'unit_alt' => setting('units.altitude'), 'unit_dist' => setting('units.distance'), 'unit_temp' => setting('units.temperature'), diff --git a/config/cache.php b/config/cache.php index 33917d03..2565c98d 100755 --- a/config/cache.php +++ b/config/cache.php @@ -9,14 +9,18 @@ return [ 'key' => 'airports.lookup:', 'time' => 60 * 30, ], - 'WEATHER_LOOKUP' => [ - 'key' => 'airports.weather.', // append icao + 'METAR_WEATHER_LOOKUP' => [ + 'key' => 'airports.weather.metar.', // append icao 'time' => 60 * 60, // Cache for 60 minutes ], 'RANKS_PILOT_LIST' => [ 'key' => 'ranks.pilot_list', 'time' => 60 * 10, ], + 'TAF_WEATHER_LOOKUP' => [ + 'key' => 'airports.weather.taf.', // append icao + 'time' => 60 * 60, // Cache for 60 minutes + ], 'USER_API_KEY' => [ 'key' => 'user.apikey', 'time' => 60 * 5, // 5 min diff --git a/resources/views/layouts/default/widgets/weather.blade.php b/resources/views/layouts/default/widgets/weather.blade.php index 3ee2abdc..c5b83403 100644 --- a/resources/views/layouts/default/widgets/weather.blade.php +++ b/resources/views/layouts/default/widgets/weather.blade.php @@ -4,10 +4,8 @@ If you want to edit this, you can reference the CheckWX API docs: https://api.checkwx.com/#metar-decoded --}} -@if(!$metar) -

@lang('widgets.weather.nometar')

-@else - +
+ @if($config['raw_only'] != true && $metar) @@ -20,35 +18,35 @@ https://api.checkwx.com/#metar-decoded @if($metar['visibility']) - - - - + + + + @endif @if($metar['runways_visual_range']) - + - + @endif @if($metar['present_weather_report'] <> 'Dry') - + - + @endif @if($metar['clouds'] || $metar['cavok']) - + - + @endif @@ -63,20 +61,20 @@ https://api.checkwx.com/#metar-decoded @if($metar['recent_weather_report']) - + - + @endif @if($metar['runways_report']) - + - + @endif @if($metar['remarks']) @@ -88,9 +86,13 @@ https://api.checkwx.com/#metar-decoded + @endif - + -
@lang('widgets.weather.conditions') {{ $metar['category'] }}
Visibility{{ $metar['visibility'][$unit_dist] }} {{$unit_dist}}
Visibility{{ $metar['visibility'][$unit_dist] }} {{$unit_dist}}
Runway Visual Range @foreach($metar['runways_visual_range'] as $rvr) RWY{{ $rvr['runway'] }}; {{ $rvr['report'] }}
@endforeach
Phenomena {{ $metar['present_weather_report'] }}
@lang('widgets.weather.clouds') @if($unit_alt === 'ft') {{ $metar['clouds_report_ft'] }} @else {{ $metar['clouds_report'] }} @endif @if($metar['cavok'] == 1) Ceiling and Visibility OK @endif
Temperature{{ number_format($metar['barometer']['hPa']) }} hPa / {{ number_format($metar['barometer']['inHg'], 2) }} inHg
Recent Phenomena {{ $metar['recent_weather_report'] }}
Runway Condition @foreach($metar['runways_report'] as $runway) RWY{{ $runway['runway'] }}; {{ $runway['report'] }}
@endforeach
@lang('widgets.weather.updated') {{$metar['observed_time']}} ({{$metar['observed_age']}})
@lang('common.metar'){{ $metar['raw'] }}@if($metar) {{ $metar['raw'] }} @else @lang('widgets.weather.nometar') @endif
-@endif + + TAF + @if($taf) {{ $taf['raw'] }} @else @lang('widgets.weather.nometar') @endif + + diff --git a/tests/MetarTest.php b/tests/MetarTest.php index 913864e4..1838ef28 100644 --- a/tests/MetarTest.php +++ b/tests/MetarTest.php @@ -168,6 +168,8 @@ class MetarTest extends TestCase public function testHttpCallSuccess() { $this->mockXmlResponse('aviationweather/kjfk.xml'); + + /** @var AirportService $airportSvc */ $airportSvc = app(AirportService::class); $this->assertInstanceOf(Metar::class, $airportSvc->getMetar('kjfk'));