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:
parent
6b7eab05e2
commit
b4d5c0fbcd
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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 '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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'),
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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'));
|
||||||
|
Loading…
Reference in New Issue
Block a user