From b4d5c0fbcde8480b8c66d2e408e01fbee3e261a6 Mon Sep 17 00:00:00 2001 From: "B.Fatih KOZ" <74361521+FatihKoz@users.noreply.github.com> Date: Fri, 19 Feb 2021 20:45:39 +0300 Subject: [PATCH] Weather METAR/TAF enhancements (#964) * Update AviationWeather.php Added the ability to fetch latest TAF report of given icao from ADDS/NOAA * Update Weather.php Used updated Metar\AviationWeather service to improve the widget ability and provide raw TAF data for the view * Style Fix 1 * Update weather.blade.php Updated blade to match updated Metar\AviationWeather service and the Weather widget controller. Also fixed the order of temp - dewpoint - humidity - visibility display according to aviation usage. Widget now displays raw TAF data just below raw METAR data. * Update Metar inferface and wrap TAF retrieval in cache * Styles fix * Add call to getTaf. Don't call AviationWeather directly * Fix cache lookup strings * Fix recursion error * Update weather.blade.php Used latest weather.blade , added $taf['raw'] after raw metar. * Compatibility Update Updated the widget controller to match latest dev (added raw_only to config) * Update Weather Widget Blade Made the widget blade compatible with the TAF reports, widget will display raw metar and taf after the decoder, if no metar or taf recieved it will be displayed in its own row. Also added the new option of raw_only to blade, if it is true, metar decoding will be skipped and only raw values will be displayed. ( Useful when displaying multiple wx widgets at the same page for departure, destination and alternate etc ) Co-authored-by: Nabeel Shahzad Co-authored-by: Nabeel S --- app/Contracts/Metar.php | 54 +++++++++++++++++-- app/Services/AirportService.php | 22 +++++++- app/Services/Metar/AviationWeather.php | 53 +++++++++++++++++- app/Widgets/Weather.php | 2 + config/cache.php | 8 ++- .../layouts/default/widgets/weather.blade.php | 44 +++++++-------- tests/MetarTest.php | 2 + 7 files changed, 154 insertions(+), 31 deletions(-) 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'));