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 <nshahzad@live.com>
Co-authored-by: Nabeel S <nabeelio@users.noreply.github.com>
This commit is contained in:
B.Fatih KOZ 2021-02-19 20:45:39 +03:00 committed by GitHub
parent 6b7eab05e2
commit b4d5c0fbcd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 154 additions and 31 deletions

View File

@ -12,13 +12,24 @@ abstract class Metar
{ {
/** /**
* Implement retrieving the METAR - return the METAR string. Needs to be protected, * 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 * @param $icao
* *
* @return mixed * @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 * Download the METAR, wrap in caching
@ -27,9 +38,9 @@ abstract class Metar
* *
* @return string * @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; $key = $cache['key'].$icao;
if (Cache::has($key)) { if (Cache::has($key)) {
@ -40,7 +51,7 @@ abstract class Metar
} }
try { try {
$raw_metar = $this->metar($icao); $raw_metar = $this->get_metar($icao);
} catch (\Exception $e) { } catch (\Exception $e) {
Log::error('Error getting METAR: '.$e->getMessage(), $e->getTrace()); Log::error('Error getting METAR: '.$e->getMessage(), $e->getTrace());
return ''; return '';
@ -52,4 +63,37 @@ abstract class Metar
return $raw_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;
}
} }

View File

@ -46,12 +46,32 @@ class AirportService extends Service
return; return;
} }
$raw_metar = $this->metarProvider->get_metar($icao); $raw_metar = $this->metarProvider->metar($icao);
if ($raw_metar && $raw_metar !== '') { if ($raw_metar && $raw_metar !== '') {
return new 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 * Lookup an airport's information from a remote provider. This handles caching
* the data internally * the data internally

View File

@ -9,13 +9,16 @@ use Exception;
use Illuminate\Support\Facades\Log; 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 class AviationWeather extends Metar
{ {
private const METAR_URL = private const METAR_URL =
'https://www.aviationweather.gov/adds/dataserver_current/httpparam?dataSource=metars&requestType=retrieve&format=xml&hoursBeforeNow=3&mostRecent=true&stationString='; '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; private $httpClient;
public function __construct(HttpClient $httpClient) public function __construct(HttpClient $httpClient)
@ -33,7 +36,7 @@ class AviationWeather extends Metar
* *
* @return string * @return string
*/ */
protected function metar($icao): string protected function get_metar($icao): string
{ {
if ($icao === '') { if ($icao === '') {
return ''; return '';
@ -67,7 +70,53 @@ class AviationWeather extends Metar
return $xml->data->METAR->raw_text->__toString(); return $xml->data->METAR->raw_text->__toString();
} catch (Exception $e) { } catch (Exception $e) {
Log::error('Error reading METAR: '.$e->getMessage()); 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 ''; return '';
} }
} }

View File

@ -23,10 +23,12 @@ class Weather extends Widget
/** @var \App\Services\AirportService $airportSvc */ /** @var \App\Services\AirportService $airportSvc */
$airportSvc = app(AirportService::class); $airportSvc = app(AirportService::class);
$metar = $airportSvc->getMetar($this->config['icao']); $metar = $airportSvc->getMetar($this->config['icao']);
$taf = $airportSvc->getTaf($this->config['icao']);
return view('widgets.weather', [ return view('widgets.weather', [
'config' => $this->config, 'config' => $this->config,
'metar' => $metar, 'metar' => $metar,
'taf' => $taf,
'unit_alt' => setting('units.altitude'), 'unit_alt' => setting('units.altitude'),
'unit_dist' => setting('units.distance'), 'unit_dist' => setting('units.distance'),
'unit_temp' => setting('units.temperature'), 'unit_temp' => setting('units.temperature'),

View File

@ -9,14 +9,18 @@ return [
'key' => 'airports.lookup:', 'key' => 'airports.lookup:',
'time' => 60 * 30, 'time' => 60 * 30,
], ],
'WEATHER_LOOKUP' => [ 'METAR_WEATHER_LOOKUP' => [
'key' => 'airports.weather.', // append icao 'key' => 'airports.weather.metar.', // append icao
'time' => 60 * 60, // Cache for 60 minutes 'time' => 60 * 60, // Cache for 60 minutes
], ],
'RANKS_PILOT_LIST' => [ 'RANKS_PILOT_LIST' => [
'key' => 'ranks.pilot_list', 'key' => 'ranks.pilot_list',
'time' => 60 * 10, 'time' => 60 * 10,
], ],
'TAF_WEATHER_LOOKUP' => [
'key' => 'airports.weather.taf.', // append icao
'time' => 60 * 60, // Cache for 60 minutes
],
'USER_API_KEY' => [ 'USER_API_KEY' => [
'key' => 'user.apikey', 'key' => 'user.apikey',
'time' => 60 * 5, // 5 min 'time' => 60 * 5, // 5 min

View File

@ -4,10 +4,8 @@ If you want to edit this, you can reference the CheckWX API docs:
https://api.checkwx.com/#metar-decoded https://api.checkwx.com/#metar-decoded
--}} --}}
@if(!$metar) <table class="table table-striped">
<p>@lang('widgets.weather.nometar')</p> @if($config['raw_only'] != true && $metar)
@else
<table class="table table-striped">
<tr> <tr>
<td>@lang('widgets.weather.conditions')</td> <td>@lang('widgets.weather.conditions')</td>
<td>{{ $metar['category'] }}</td> <td>{{ $metar['category'] }}</td>
@ -20,35 +18,35 @@ https://api.checkwx.com/#metar-decoded
</td> </td>
</tr> </tr>
@if($metar['visibility']) @if($metar['visibility'])
<tr> <tr>
<td>Visibility</td> <td>Visibility</td>
<td>{{ $metar['visibility'][$unit_dist] }} {{$unit_dist}}</td> <td>{{ $metar['visibility'][$unit_dist] }} {{$unit_dist}}</td>
</tr> </tr>
@endif @endif
@if($metar['runways_visual_range']) @if($metar['runways_visual_range'])
<tr> <tr>
<td>Runway Visual Range</td> <td>Runway Visual Range</td>
<td> <td>
@foreach($metar['runways_visual_range'] as $rvr) @foreach($metar['runways_visual_range'] as $rvr)
<b>RWY{{ $rvr['runway'] }}</b>; {{ $rvr['report'] }}<br> <b>RWY{{ $rvr['runway'] }}</b>; {{ $rvr['report'] }}<br>
@endforeach @endforeach
</td> </td>
</tr> </tr>
@endif @endif
@if($metar['present_weather_report'] <> 'Dry') @if($metar['present_weather_report'] <> 'Dry')
<tr> <tr>
<td>Phenomena</td> <td>Phenomena</td>
<td>{{ $metar['present_weather_report'] }}</td> <td>{{ $metar['present_weather_report'] }}</td>
</tr> </tr>
@endif @endif
@if($metar['clouds'] || $metar['cavok']) @if($metar['clouds'] || $metar['cavok'])
<tr> <tr>
<td>@lang('widgets.weather.clouds')</td> <td>@lang('widgets.weather.clouds')</td>
<td> <td>
@if($unit_alt === 'ft') {{ $metar['clouds_report_ft'] }} @else {{ $metar['clouds_report'] }} @endif @if($unit_alt === 'ft') {{ $metar['clouds_report_ft'] }} @else {{ $metar['clouds_report'] }} @endif
@if($metar['cavok'] == 1) Ceiling and Visibility OK @endif @if($metar['cavok'] == 1) Ceiling and Visibility OK @endif
</td> </td>
</tr> </tr>
@endif @endif
<tr> <tr>
<td>Temperature</td> <td>Temperature</td>
@ -63,20 +61,20 @@ https://api.checkwx.com/#metar-decoded
<td>{{ number_format($metar['barometer']['hPa']) }} hPa / {{ number_format($metar['barometer']['inHg'], 2) }} inHg</td> <td>{{ number_format($metar['barometer']['hPa']) }} hPa / {{ number_format($metar['barometer']['inHg'], 2) }} inHg</td>
</tr> </tr>
@if($metar['recent_weather_report']) @if($metar['recent_weather_report'])
<tr> <tr>
<td>Recent Phenomena</td> <td>Recent Phenomena</td>
<td>{{ $metar['recent_weather_report'] }}</td> <td>{{ $metar['recent_weather_report'] }}</td>
</tr> </tr>
@endif @endif
@if($metar['runways_report']) @if($metar['runways_report'])
<tr> <tr>
<td>Runway Condition</td> <td>Runway Condition</td>
<td> <td>
@foreach($metar['runways_report'] as $runway) @foreach($metar['runways_report'] as $runway)
<b>RWY{{ $runway['runway'] }}</b>; {{ $runway['report'] }}<br> <b>RWY{{ $runway['runway'] }}</b>; {{ $runway['report'] }}<br>
@endforeach @endforeach
</td> </td>
</tr> </tr>
@endif @endif
@if($metar['remarks']) @if($metar['remarks'])
<tr> <tr>
@ -88,9 +86,13 @@ https://api.checkwx.com/#metar-decoded
<td>@lang('widgets.weather.updated')</td> <td>@lang('widgets.weather.updated')</td>
<td>{{$metar['observed_time']}} ({{$metar['observed_age']}})</td> <td>{{$metar['observed_time']}} ({{$metar['observed_age']}})</td>
</tr> </tr>
@endif
<tr> <tr>
<td>@lang('common.metar')</td> <td>@lang('common.metar')</td>
<td>{{ $metar['raw'] }}</td> <td>@if($metar) {{ $metar['raw'] }} @else @lang('widgets.weather.nometar') @endif</td>
</tr> </tr>
</table> <tr>
@endif <td>TAF</td>
<td>@if($taf) {{ $taf['raw'] }} @else @lang('widgets.weather.nometar') @endif</td>
</tr>
</table>

View File

@ -168,6 +168,8 @@ class MetarTest extends TestCase
public function testHttpCallSuccess() public function testHttpCallSuccess()
{ {
$this->mockXmlResponse('aviationweather/kjfk.xml'); $this->mockXmlResponse('aviationweather/kjfk.xml');
/** @var AirportService $airportSvc */
$airportSvc = app(AirportService::class); $airportSvc = app(AirportService::class);
$this->assertInstanceOf(Metar::class, $airportSvc->getMetar('kjfk')); $this->assertInstanceOf(Metar::class, $airportSvc->getMetar('kjfk'));