12848091a2
Update to Laravel 9 and PHP 8+ Co-authored-by: B.Fatih KOZ <fatih.koz@gmail.com>
1914 lines
60 KiB
PHP
1914 lines
60 KiB
PHP
<?php
|
|
|
|
namespace App\Support;
|
|
|
|
use App\Support\Units\Altitude;
|
|
use App\Support\Units\Distance;
|
|
use App\Support\Units\Pressure;
|
|
use App\Support\Units\Temperature;
|
|
use App\Support\Units\Velocity;
|
|
use function count;
|
|
use PhpUnitsOfMeasure\Exception\NonNumericValue;
|
|
use PhpUnitsOfMeasure\Exception\NonStringUnitName;
|
|
|
|
/**
|
|
* Class Metar
|
|
*/
|
|
|
|
/*
|
|
===========================
|
|
HSDN METAR/TAF Parser Class
|
|
===========================
|
|
Version: 0.55.4b
|
|
Based on GetWx script by Mark Woodward.
|
|
(c) 2013-2015, Information Networks, Ltd. (http://www.hsdn.org/)
|
|
(c) 2001-2006, Mark Woodward (http://woody.cowpi.com/phpscripts/)
|
|
This script is a PHP library which allows to parse the METAR and TAF code,
|
|
and convert it to an array of data parameters. These METAR or TAF can be given
|
|
in the form of the ICAO code string (in this case, the script will receive data
|
|
from the NOAA website) or in raw format (just METAR/TAF code string). METAR or
|
|
TAF code parsed using the syntactic analysis and regular expressions. It solves
|
|
the problem of parsing the data in the presence of any error in the code METAR
|
|
or TAF. In addition to the return METAR parameters, the script also displays the
|
|
interpreted (easy to understand) information of these parameters.
|
|
*/
|
|
class Metar implements \ArrayAccess
|
|
{
|
|
/*
|
|
* Array of decoded result, by default all parameters is null.
|
|
*/
|
|
public $result = [
|
|
'category' => null,
|
|
'raw' => null,
|
|
'taf' => null,
|
|
'taf_flag' => null,
|
|
'station' => null,
|
|
'observed_date' => null,
|
|
'observed_day' => null,
|
|
'observed_time' => null,
|
|
'observed_age' => null,
|
|
'wind_speed' => null,
|
|
'wind_gust_speed' => null,
|
|
'wind_direction' => null,
|
|
'wind_direction_label' => null,
|
|
'wind_direction_varies' => null,
|
|
'varies_wind_min' => null,
|
|
'varies_wind_min_label' => null,
|
|
'varies_wind_max' => null,
|
|
'varies_wind_max_label' => null,
|
|
'visibility' => null,
|
|
'visibility_report' => null,
|
|
'visibility_min' => null,
|
|
'visibility_min_direction' => null,
|
|
'runways_visual_range' => null,
|
|
'present_weather' => null,
|
|
'present_weather_report' => null,
|
|
'clouds' => null,
|
|
'clouds_report' => null,
|
|
'clouds_report_ft' => null,
|
|
'cloud_height' => null,
|
|
'cavok' => null,
|
|
'temperature' => null,
|
|
'dew_point' => null,
|
|
'humidity' => null,
|
|
'heat_index' => null,
|
|
'wind_chill' => null,
|
|
'barometer' => null,
|
|
'barometer_in' => null,
|
|
'barometer_mb' => null,
|
|
'recent_weather' => null,
|
|
'recent_weather_report' => null,
|
|
'runways_report' => null,
|
|
'runways_snoclo' => null,
|
|
'wind_shear_all_runways' => null,
|
|
'wind_shear_runways' => null,
|
|
'forecast_temperature_min' => null,
|
|
'forecast_temperature_max' => null,
|
|
'trends' => null,
|
|
'remarks' => null,
|
|
];
|
|
|
|
/*
|
|
* Methods used for parsing in the order of data
|
|
*/
|
|
private static $method_names = [
|
|
'taf',
|
|
'station',
|
|
'time',
|
|
'station_type',
|
|
'wind',
|
|
'varies_wind',
|
|
'visibility',
|
|
'visibility_min',
|
|
'runway_vr',
|
|
'present_weather',
|
|
'clouds',
|
|
'temperature',
|
|
'pressure',
|
|
'recent_weather',
|
|
'runways_report',
|
|
'wind_shear',
|
|
'forecast_temperature',
|
|
'trends',
|
|
'remarks',
|
|
];
|
|
|
|
/*
|
|
* Interpretation of weather conditions intensity codes.
|
|
*/
|
|
private static $weather_intensity_codes = [
|
|
'' => 'moderate',
|
|
'-' => 'light',
|
|
'+' => 'strong',
|
|
'VC' => 'in the vicinity',
|
|
];
|
|
|
|
/*
|
|
* Interpretation of weather conditions characteristics codes.
|
|
*/
|
|
private static $weather_char_codes = [
|
|
'MI' => 'shallow',
|
|
'PR' => 'partial',
|
|
'BC' => 'patches of',
|
|
'DR' => 'low drifting',
|
|
'BL' => 'blowing',
|
|
'SH' => 'showers of',
|
|
'TS' => 'thunderstorms',
|
|
'FZ' => 'freezing',
|
|
];
|
|
|
|
/*
|
|
* Interpretation of weather conditions type codes.
|
|
*/
|
|
private static $weather_type_codes = [
|
|
'DZ' => 'drizzle',
|
|
'RA' => 'rain',
|
|
'SN' => 'snow',
|
|
'SG' => 'snow grains',
|
|
'IC' => 'ice crystals',
|
|
'PE' => 'ice pellets',
|
|
'GR' => 'hail',
|
|
'GS' => 'small hail', // and/or snow pellets
|
|
'UP' => 'unknown',
|
|
'BR' => 'mist',
|
|
'FG' => 'fog',
|
|
'FU' => 'smoke',
|
|
'VA' => 'volcanic ash',
|
|
'DU' => 'widespread dust',
|
|
'SA' => 'sand',
|
|
'HZ' => 'haze',
|
|
'PY' => 'spray',
|
|
'PO' => 'well-developed dust/sand whirls',
|
|
'SQ' => 'squalls',
|
|
'FC' => 'funnel cloud, tornado, or waterspout',
|
|
'SS' => 'sandstorm/duststorm',
|
|
];
|
|
|
|
/*
|
|
* Interpretation of cloud cover codes.
|
|
*/
|
|
private static $cloud_codes = [
|
|
'NSW' => 'no significant weather are observed',
|
|
'NSC' => 'no significant clouds are observed',
|
|
'NCD' => 'nil cloud detected',
|
|
'SKC' => 'sky is clear',
|
|
'CLR' => 'clear skies',
|
|
'NOBS' => 'no observation',
|
|
//
|
|
'FEW' => 'few',
|
|
'SCT' => 'scattered',
|
|
'BKN' => 'broken sky',
|
|
'OVC' => 'overcast sky',
|
|
//
|
|
'VV' => 'vertical visibility',
|
|
];
|
|
|
|
/*
|
|
* Interpretation of cloud cover type codes.
|
|
*/
|
|
private static $cloud_type_codes = [
|
|
'CB' => 'cumulonimbus',
|
|
'TCU' => 'towering cumulus',
|
|
];
|
|
|
|
/*
|
|
* Interpretation of runway visual range tendency codes.
|
|
*/
|
|
private static $rvr_tendency_codes = [
|
|
'D' => 'decreasing',
|
|
'U' => 'increasing',
|
|
'N' => 'no tendency',
|
|
];
|
|
|
|
/*
|
|
* Interpretation of runway visual range prefix codes.
|
|
*/
|
|
private static $rvr_prefix_codes = [
|
|
'P' => 'more',
|
|
'M' => 'less',
|
|
];
|
|
|
|
/*
|
|
* Interpretation of runway runway deposits codes.
|
|
*/
|
|
private static $runway_deposits_codes = [
|
|
'0' => 'clear and dry',
|
|
'1' => 'damp',
|
|
'2' => 'wet or water patches',
|
|
'3' => 'rime or frost covered',
|
|
'4' => 'dry snow',
|
|
'5' => 'wet snow',
|
|
'6' => 'slush',
|
|
'7' => 'ice',
|
|
'8' => 'compacted or rolled snow',
|
|
'9' => 'frozen ruts or ridges',
|
|
'/' => 'not reported',
|
|
];
|
|
|
|
/*
|
|
* Interpretation of runway runway deposits extent codes.
|
|
*/
|
|
private static $runway_deposits_extent_codes = [
|
|
'1' => 'from 10% or less',
|
|
'2' => 'from 11% to 25%',
|
|
'5' => 'from 26% to 50%',
|
|
'9' => 'from 51% to 100%',
|
|
'/' => null,
|
|
];
|
|
|
|
/*
|
|
* Interpretation of runway runway deposits depth codes.
|
|
*/
|
|
private static $runway_deposits_depth_codes = [
|
|
'00' => 'less than 1 mm',
|
|
'92' => '10 cm',
|
|
'93' => '15 cm',
|
|
'94' => '20 cm',
|
|
'95' => '25 cm',
|
|
'96' => '30 cm',
|
|
'97' => '35 cm',
|
|
'98' => '40 cm or more',
|
|
'99' => 'closed',
|
|
'//' => null,
|
|
];
|
|
|
|
/*
|
|
* Interpretation of runway runway friction codes.
|
|
*/
|
|
private static $runway_friction_codes = [
|
|
'91' => 'poor',
|
|
'92' => 'medium/poor',
|
|
'93' => 'medium',
|
|
'94' => 'medium/good',
|
|
'95' => 'good',
|
|
'99' => 'figures unreliable',
|
|
'//' => null,
|
|
];
|
|
|
|
/*
|
|
* Trends time codes.
|
|
*/
|
|
private static $trends_flag_codes = [
|
|
'BECMG' => 'expected to arise soon',
|
|
'TEMPO' => 'expected to arise temporarily',
|
|
'INTER' => 'expected to arise intermittent',
|
|
'PROV' => 'provisional forecast',
|
|
'CNL' => 'cancelled forecast',
|
|
'NIL' => 'nil forecast',
|
|
];
|
|
|
|
/*
|
|
* Trends time codes.
|
|
*/
|
|
private static $trends_time_codes = [
|
|
'AT' => 'at',
|
|
'FM' => 'from',
|
|
'TL' => 'until',
|
|
];
|
|
|
|
/*
|
|
* Interpretation of compass degrees codes.
|
|
*/
|
|
private static $direction_codes = [
|
|
'N', 'NNE', 'NE', 'ENE',
|
|
'E', 'ESE', 'SE', 'SSE',
|
|
'S', 'SSW', 'SW', 'WSW',
|
|
'W', 'WNW', 'NW', 'NNW',
|
|
];
|
|
|
|
/*
|
|
* Debug and parse errors information.
|
|
*/
|
|
|
|
private $debug = [];
|
|
|
|
private $debug_enabled;
|
|
|
|
public $errors = [];
|
|
|
|
/*
|
|
* Other variables.
|
|
*/
|
|
public $raw;
|
|
|
|
private $raw_parts = [];
|
|
|
|
private $method = 0;
|
|
|
|
private $part = 0;
|
|
|
|
/**
|
|
* This method provides METAR and TAF information, you want to parse.
|
|
*
|
|
* Examples of raw METAR for test:
|
|
* UMMS 231530Z 21002MPS 2100 BR OVC002 07/07 Q1008 R13/290062 NOSIG RMK QBB070
|
|
* UWSS 231500Z 14007MPS 9999 -SHRA BR BKN033CB OVC066 03/M02 Q1019 R12/220395 NOSIG RMK QFE752
|
|
* UWSS 241200Z 12003MPS 0300 R12/1000 DZ FG VV003CB 05/05 Q1015 R12/220395 NOSIG RMK QFE749
|
|
* UATT 231530Z 18004MPS 130V200 CAVOK M03/M08 Q1033 R13/0///60 NOSIG RMK QFE755/1006
|
|
* KEYW 231553Z 04008G16KT 10SM FEW060 28/22 A3002 RMK AO2 SLP166 T02780222
|
|
* EFVR 231620Z AUTO 19002KT 5000 BR FEW003 BKN005 OVC007 09/08 Q0998
|
|
* KTTN 051853Z 04011KT M1/2SM VCTS SN FZFG BKN003 OVC010 M02/M02 A3006 RMK AO2 TSB40 SLP176 P0002 T10171017=
|
|
* UEEE 072000Z 00000MPS 0150 R23L/0500 R10/1000VP1800D FG VV003 M50/M53 Q1028 RETSRA R12/290395 R31/CLRD// R/SNOCLO WS RWY10L WS RWY11L TEMPO 4000 RADZ BKN010 RMK QBB080 OFE745
|
|
* UKDR 251830Z 00000MPS CAVOK 08/07 Q1019 3619//60 NOSIG
|
|
* UBBB 251900Z 34015KT 9999 FEW013 BKN030 16/14 Q1016 88CLRD70 NOSIG
|
|
* UMMS 251936Z 19002MPS 9999 SCT006 OVC026 06/05 Q1015 R31/D NOSIG RMK QBB080 OFE745
|
|
*
|
|
* @param $raw
|
|
* @param bool $taf
|
|
* @param bool $debug
|
|
* @param bool $icao
|
|
*/
|
|
public function __construct($raw, $taf = false, $debug = false, $icao = true)
|
|
{
|
|
$this->debug_enabled = $debug;
|
|
// Log::info('Parsing metar="'.$raw.'"');
|
|
|
|
$raw_lines = explode("\n", $raw, 2);
|
|
if (isset($raw_lines[1])) {
|
|
$raw = trim($raw_lines[1]);
|
|
// Get observed time from a file data
|
|
$observed_time = strtotime(trim($raw_lines[0]));
|
|
if ($observed_time !== 0) {
|
|
$this->set_observed_date($observed_time);
|
|
}
|
|
} else {
|
|
$raw = trim($raw_lines[0]);
|
|
}
|
|
|
|
$this->raw = rtrim(trim(preg_replace('/[\s\t]+/s', ' ', $raw)), '=');
|
|
/*if ($taf) {
|
|
$this->set_debug('Information presented as TAF or trend.');
|
|
} else {
|
|
$this->set_debug('Information presented as METAR.');
|
|
}*/
|
|
|
|
$this->set_result_value('taf', $taf);
|
|
$this->set_result_value('raw', $this->raw);
|
|
|
|
$this->parse_all();
|
|
}
|
|
|
|
/**
|
|
* Shortcut to call
|
|
*
|
|
* @param $metar
|
|
* @param string $taf
|
|
*
|
|
* @return mixed
|
|
*/
|
|
public static function parse($metar, $taf = '')
|
|
{
|
|
$mtr = new static($metar, $taf);
|
|
return $mtr->parse_all();
|
|
}
|
|
|
|
/**
|
|
* Gets the value from result array as class property.
|
|
*
|
|
* @param $parameter
|
|
*
|
|
* @return mixed|null
|
|
*/
|
|
public function __get($parameter)
|
|
{
|
|
if (isset($this->result[$parameter])) {
|
|
return $this->result[$parameter];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return an Altitude value or object
|
|
*
|
|
* @param int|float $value
|
|
* @param string $unit "feet" or "meters"
|
|
*
|
|
* @return Altitude
|
|
*/
|
|
protected function createAltitude($value, $unit)
|
|
{
|
|
return Altitude::make((float) $value, $unit);
|
|
}
|
|
|
|
/**
|
|
* Return a Distance value or object
|
|
*
|
|
* @param int|float $value
|
|
* @param string $unit "m" (meters) or "mi" (miles)
|
|
*
|
|
* @return Distance
|
|
*/
|
|
protected function createDistance($value, $unit)
|
|
{
|
|
return Distance::make((float) $value, $unit);
|
|
}
|
|
|
|
/**
|
|
* Return a Pressure value or object
|
|
*
|
|
* @param int|float $value
|
|
* @param string $unit "F" or "C"
|
|
*
|
|
* @throws NonNumericValue
|
|
* @throws NonStringUnitName
|
|
*
|
|
* @return Pressure
|
|
*/
|
|
protected function createPressure($value, $unit)
|
|
{
|
|
return Pressure::make((float) $value, $unit);
|
|
}
|
|
|
|
/**
|
|
* Return a Temperature value or object
|
|
*
|
|
* @param int|float $value
|
|
* @param string $unit "F" or "C"
|
|
*
|
|
* @throws NonNumericValue
|
|
* @throws NonStringUnitName
|
|
*
|
|
* @return Temperature
|
|
*/
|
|
protected function createTemperature($value, $unit)
|
|
{
|
|
return Temperature::make((float) $value, $unit);
|
|
}
|
|
|
|
/**
|
|
* Create a new velocity unit
|
|
*
|
|
* @param int|float $value
|
|
* @param string $unit "knots", "km/hour", "m/s"
|
|
*
|
|
* @throws NonStringUnitName
|
|
* @throws NonNumericValue
|
|
*
|
|
* @return Velocity
|
|
*/
|
|
protected function createVelocity($value, $unit)
|
|
{
|
|
return Velocity::make((float) $value, $unit);
|
|
}
|
|
|
|
/**
|
|
* Parses the METAR or TAF information and returns result array.
|
|
*/
|
|
public function parse_all(): array
|
|
{
|
|
$this->raw_parts = explode(' ', $this->raw);
|
|
$current_method = 0;
|
|
|
|
$raw_part_count = count($this->raw_parts);
|
|
$method_name_count = count(static::$method_names);
|
|
|
|
while ($this->part < $raw_part_count) {
|
|
$this->method = $current_method;
|
|
while ($this->method < $method_name_count) {
|
|
$method = 'get_'.static::$method_names[$this->method];
|
|
$token = $this->raw_parts[$this->part];
|
|
if ($this->$method($token) === true) {
|
|
/*$this->set_debug('Token "'.$token.'" is parsed by method: '.$method.', '.
|
|
($this->method - $current_method).' previous methods skipped.');*/
|
|
$current_method = $this->method;
|
|
$this->method++;
|
|
break;
|
|
}
|
|
$this->method++;
|
|
}
|
|
|
|
if ($current_method !== $this->method - 1) {
|
|
/*$this->set_error('Unknown token: '.$this->raw_parts[$this->part]);
|
|
$this->set_debug('Token "'.$this->raw_parts[$this->part].'" is NOT PARSED, '.
|
|
($this->method - $current_method).' methods attempted.');*/
|
|
}
|
|
|
|
$this->part++;
|
|
}
|
|
|
|
// Delete null values from the TAF report
|
|
if ($this->result['taf'] === true) {
|
|
foreach ($this->result as $parameter => $value) {
|
|
if ($value === null) {
|
|
unset($this->result[$parameter]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Finally determine if it's VFR or IFR conditions
|
|
// https://www.aviationweather.gov/cva/help
|
|
// https://www.skybrary.aero/index.php/Visual_Meteorological_Conditions_(VMC)
|
|
// This may be changed to ICAO standards as VMC and IMC
|
|
$this->result['category'] = 'VFR';
|
|
|
|
if (array_key_exists('cavok', $this->result) && $this->result['cavok']) {
|
|
$this->result['category'] = 'VFR';
|
|
} else {
|
|
/* @noinspection NestedPositiveIfStatementsInspection */
|
|
if (array_key_exists('cloud_height', $this->result) && $this->result['cloud_height'] !== null) {
|
|
if ($this->result['cloud_height']['ft'] > 3000
|
|
&& (empty($this->result['visibility']) || $this->result['visibility']['km'] > 5)) {
|
|
$this->result['category'] = 'VFR';
|
|
} else {
|
|
$this->result['category'] = 'IFR';
|
|
}
|
|
}
|
|
}
|
|
|
|
return $this->result;
|
|
}
|
|
|
|
/**
|
|
* Returns array with debug information.
|
|
*/
|
|
public function debug()
|
|
{
|
|
return $this->debug;
|
|
}
|
|
|
|
/**
|
|
* Returns array with parse errors.
|
|
*/
|
|
public function errors()
|
|
{
|
|
return $this->errors;
|
|
}
|
|
|
|
/**
|
|
* This method formats observation date and time in the local time zone of server,
|
|
* the current local time on server, and time difference since observation. $time_utc is a
|
|
* UNIX timestamp for Universal Coordinated Time (Greenwich Mean Time or Zulu Time).
|
|
*
|
|
* @param mixed $time_utc
|
|
*/
|
|
private function set_observed_date($time_utc)
|
|
{
|
|
$now = time();
|
|
$local = $time_utc; // + date('Z');
|
|
|
|
$this->set_result_value('observed_date', date('r', $local)); // or "D M j, H:i T"
|
|
$time_diff = floor(($now - $local) / 60);
|
|
|
|
if ($time_diff < 91) {
|
|
$this->set_result_value('observed_age', $time_diff.' '.trans_choice('widgets.weather.minago', $time_diff));
|
|
} else {
|
|
$this->set_result_value('observed_age', floor($time_diff / 60).':'.sprintf('%02d', $time_diff % 60).' '.trans_choice('widgets.weather.hrago', floor($time_diff / 60)));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the new value to parameter in result array.
|
|
*
|
|
* @param $parameter
|
|
* @param $value
|
|
* @param bool $only_if_null
|
|
*/
|
|
private function set_result_value($parameter, $value, $only_if_null = false)
|
|
{
|
|
if ($only_if_null) {
|
|
if ($this->result[$parameter] === null) {
|
|
$this->result[$parameter] = $value;
|
|
}
|
|
} else {
|
|
$this->result[$parameter] = $value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the data group to parameter in result array.
|
|
*
|
|
* @param mixed $parameter
|
|
* @param mixed $group
|
|
*/
|
|
private function set_result_group($parameter, $group)
|
|
{
|
|
if ($this->result[$parameter] === null) {
|
|
$this->result[$parameter] = [];
|
|
}
|
|
|
|
$this->result[$parameter][] = $group;
|
|
}
|
|
|
|
/**
|
|
* Sets the report text to parameter in result array.
|
|
*
|
|
* @param $parameter
|
|
* @param $report
|
|
* @param string $separator
|
|
*/
|
|
private function set_result_report($parameter, $report, $separator = ';')
|
|
{
|
|
$this->result[$parameter] .= $separator.' '.$report;
|
|
if ($this->result[$parameter] !== null) {
|
|
$this->result[$parameter] = ucfirst(ltrim($this->result[$parameter], ' '.$separator));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds the debug text to debug information array.
|
|
*
|
|
* @param mixed $text
|
|
*/
|
|
private function set_debug($text)
|
|
{
|
|
if ($this->debug_enabled) {
|
|
$this->debug[] = $text;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds the error text to parse errors array.
|
|
*
|
|
* @param mixed $text
|
|
*/
|
|
private function set_error($text)
|
|
{
|
|
$this->errors[] = $text;
|
|
}
|
|
|
|
// --------------------------------------------------------------------
|
|
// Methods for parsing raw parts
|
|
// --------------------------------------------------------------------
|
|
|
|
/**
|
|
* Decodes TAF code if present.
|
|
*
|
|
* @param mixed $part
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function get_taf($part)
|
|
{
|
|
if ($part !== 'TAF') {
|
|
return false;
|
|
}
|
|
|
|
if ($this->raw_parts[$this->part + 1] === 'COR'
|
|
|| $this->raw_parts[$this->part + 1] === 'AMD') {
|
|
$this->set_result_value('taf_flag', $this->raw_parts[$this->part + 1], true);
|
|
$this->part++;
|
|
}
|
|
|
|
//$this->set_debug('TAF information detected.');
|
|
$this->set_result_value('taf', true);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Decodes station code.
|
|
*
|
|
* @param mixed $part
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function get_station($part)
|
|
{
|
|
$r = '@^([A-Z]{1}'.'[A-Z0-9]{3})$@'; // 1
|
|
if (!preg_match($r, $part, $found)) {
|
|
return false;
|
|
}
|
|
|
|
$this->set_result_value('station', $found[1]);
|
|
$this->method++;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Decodes observation time.
|
|
* Format is ddhhmmZ where dd = day, hh = hours, mm = minutes in UTC time.
|
|
*
|
|
* @param mixed $part
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function get_time($part)
|
|
{
|
|
$r = '@^([\d]{2})' // 1
|
|
.'([\d]{2})' // 2
|
|
.'([\d]{2})Z$@'; // 3
|
|
|
|
if (!preg_match($r, $part, $found)) {
|
|
return false;
|
|
}
|
|
|
|
$day = (int) $found[1];
|
|
$hour = (int) $found[2];
|
|
$minute = (int) $found[3];
|
|
|
|
if ($this->result['observed_date'] === null) {
|
|
// Take one month, if the observed day is greater than the current day
|
|
if ($day > date('j')) {
|
|
$month = date('n') - 1;
|
|
} else {
|
|
$month = date('n');
|
|
}
|
|
|
|
// Get observed time from a METAR/TAF part
|
|
$observed_time = mktime($hour, $minute, 0, $month, $day, date('Y'));
|
|
|
|
$this->set_observed_date($observed_time);
|
|
}
|
|
|
|
$this->set_result_value('observed_day', $day);
|
|
$this->set_result_value('observed_time', $found[2].':'.$found[3].' UTC');
|
|
$this->method++;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Ignore station type if present.
|
|
*
|
|
* @param mixed $part
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function get_station_type($part)
|
|
{
|
|
if ($part !== 'AUTO' && $part !== 'COR') {
|
|
return false;
|
|
}
|
|
|
|
$this->method++;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Decodes wind direction and speed information.
|
|
* Format is dddssKT where ddd = degrees from North, ss = speed, KT for knots,
|
|
* or dddssGggKT where G stands for gust and gg = gust speed. (ss or gg can be a 3-digit number.)
|
|
* KT can be replaced with MPH for meters per second or KMH for kilometers per hour.
|
|
*
|
|
* @param $part
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function get_wind($part)
|
|
{
|
|
$r = '@^([\d]{3}|VRB|///)P?' // 1
|
|
.'([/0-9]{2,3}|//)' // 2
|
|
.'(GP?' // 3
|
|
.'([\d]{2,3}))?' // 4
|
|
.'(KT|MPS|KPH)@'; // 5
|
|
|
|
if (!preg_match($r, $part, $found)) {
|
|
return false;
|
|
}
|
|
|
|
$this->set_result_value('wind_direction_varies', false, true);
|
|
|
|
if ($found[1] === '///' && $found[2] === '//') {
|
|
} // handle the case where nothing is observed
|
|
else {
|
|
$unit = $found[5];
|
|
|
|
// Speed
|
|
$this->set_result_value('wind_speed', $this->convert_speed($found[2], $unit));
|
|
|
|
// Direction
|
|
if ($found[1] === 'VRB') {
|
|
$this->set_result_value('wind_direction_varies', true);
|
|
} else {
|
|
$direction = (int) $found[1];
|
|
if ($direction >= 0 && $direction <= 360) {
|
|
$this->set_result_value('wind_direction', $direction);
|
|
$this->set_result_value('wind_direction_label', $this->convert_direction_label($direction));
|
|
}
|
|
}
|
|
|
|
// Speed variations (gust speed)
|
|
if (isset($found[4]) && !empty($found[4])) {
|
|
$this->set_result_value('wind_gust_speed', $this->convert_speed($found[4], $unit));
|
|
}
|
|
}
|
|
|
|
$this->method++;
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* Decodes varies wind direction information if present.
|
|
* Format is fffVttt where V stands for varies from fff degrees to ttt degrees.
|
|
*/
|
|
private function get_varies_wind($part)
|
|
{
|
|
$r = '@^([\d]{3})' // 1
|
|
.'V([\d]{3})$@'; // 2
|
|
|
|
if (!preg_match($r, $part, $found)) {
|
|
return false;
|
|
}
|
|
|
|
$min_direction = (int) $found[1];
|
|
$max_direction = (int) $found[2];
|
|
|
|
if ($min_direction >= 0 && $min_direction <= 360) {
|
|
$this->set_result_value('varies_wind_min', $min_direction);
|
|
$this->set_result_value('varies_wind_min_label', $this->convert_direction_label($min_direction));
|
|
}
|
|
|
|
if ($max_direction >= 0 && $max_direction <= 360) {
|
|
$this->set_result_value('varies_wind_max', $max_direction);
|
|
$this->set_result_value('varies_wind_max_label', $this->convert_direction_label($max_direction));
|
|
}
|
|
|
|
$this->method++;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Decodes visibility information. This function will be called a second time
|
|
* if visibility is limited to an integer mile plus a fraction part.
|
|
* Format is mmSM for mm = statute miles, or m n/dSM for m = mile and n/d = fraction of a mile,
|
|
* or just a 4-digit number nnnn (with leading zeros) for nnnn = meters.
|
|
* Unit can also be in KM
|
|
*
|
|
* @param mixed $part
|
|
*
|
|
* @throws NonStringUnitName
|
|
* @throws NonNumericValue
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function get_visibility($part)
|
|
{
|
|
$r = '@^(CAVOK|([\d]{4})' // 1
|
|
.'|(M)?' // 2
|
|
.'([\d]{0,2})?' // 3
|
|
.'(([1357])' // 4
|
|
.'/(2|4|8|16))?' // 5
|
|
.'(SM|KM|M|MI)|////)$@'; // 6
|
|
|
|
if (!preg_match($r, $part, $found)) {
|
|
return false;
|
|
}
|
|
|
|
$this->set_result_value('cavok', false, true);
|
|
|
|
// Cloud and visibilty OK or ICAO visibilty greater than 10 km
|
|
if (strtoupper($found[1]) === 'CAVOK' || $found[1] === '9999') {
|
|
$this->set_result_value('visibility', $this->createDistance(10000, 'm'));
|
|
$this->set_result_value('visibility_report', 'Greater than 10 km');
|
|
/* @noinspection NotOptimalIfConditionsInspection */
|
|
if (strtoupper($found[1]) === 'CAVOK') {
|
|
$this->set_result_value('cavok', true);
|
|
$this->method += 4; // can skip the next 4 methods: visibility_min, runway_vr, present_weather, clouds
|
|
}
|
|
} elseif ($found[1] === '////') {
|
|
} // information not available
|
|
|
|
else {
|
|
$prefix = '';
|
|
|
|
// ICAO visibility (in meters)
|
|
if (isset($found[2]) && !empty($found[2])) {
|
|
$visibility = $this->createDistance((int) $found[2], 'm');
|
|
} else {
|
|
if (isset($found[3]) && !empty($found[3])) {
|
|
$prefix = 'Less than ';
|
|
}
|
|
|
|
if (isset($found[7]) && !empty($found[7])) {
|
|
$visibility = (int) $found[4] + (int) $found[6] / (int) $found[7];
|
|
} else {
|
|
$visibility = (int) $found[4];
|
|
}
|
|
|
|
$units = strtoupper($found[8]);
|
|
if ($units == 'MI' || $units == 'SM') {
|
|
$unit = 'mi';
|
|
} elseif ($units == 'M') {
|
|
$unit = 'm';
|
|
} elseif ($units == 'KM') {
|
|
$unit = 'km';
|
|
} else {
|
|
$unit = $units;
|
|
}
|
|
|
|
$visibility = $this->createDistance($visibility, $unit);
|
|
}
|
|
|
|
if ($visibility['m'] > 1000) {
|
|
$unit = ' km';
|
|
$report = $prefix.$visibility['km'].$unit;
|
|
} else {
|
|
$unit = ' meters';
|
|
if ($visibility['m'] <= 1) {
|
|
$unit = ' meter';
|
|
}
|
|
|
|
$report = $prefix.$visibility['m'].$unit;
|
|
}
|
|
|
|
$this->set_result_value('visibility', $visibility);
|
|
$this->set_result_value('visibility_report', $report);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Decodes visibility minimum value and direction if present.
|
|
* Format is vvvvDD for vvvv = the minimum horizontal visibility in meters
|
|
* (if the visibility is better than 10 km, 9999 is used. 9999 means a minimum
|
|
* visibility of 50 m or less), and for DD = the approximate direction of minimum and
|
|
* maximum visibility is given as one of eight compass points (N, SW, ...).
|
|
*
|
|
* @param $part
|
|
*
|
|
* @throws NonStringUnitName
|
|
* @throws NonNumericValue
|
|
* @throws NonStringUnitName
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function get_visibility_min($part)
|
|
{
|
|
if (!preg_match('@^([\d]{4})(NE|NW|SE|SW|N|E|S|W|)?$@', $part, $found)) {
|
|
return false;
|
|
}
|
|
|
|
$meters = $this->createDistance((int) $found[1], 'm');
|
|
$this->set_result_value('visibility_min', $meters);
|
|
|
|
if (isset($found[2]) && !empty($found[2])) {
|
|
$this->set_result_value('visibility_min_direction', $found[2]);
|
|
}
|
|
|
|
$this->method++;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Decodes runway visual range information if present.
|
|
* Format is Rrrr/vvvvFT where rrr = runway number, vvvv = visibility,
|
|
* and FT = the visibility in feet.
|
|
*
|
|
* @param $part
|
|
*
|
|
* @throws NonStringUnitName
|
|
* @throws NonNumericValue
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function get_runway_vr($part)
|
|
{
|
|
$r = '@^R([\d]{2}[LCR]?)/' // 1
|
|
.'(([PM])?' // 2
|
|
.'([\d]{4})V)?' // 3
|
|
.'([PM])?([\d]{4})' // 4
|
|
.'(FT)?/?' // 6
|
|
.'([UDN]?)$@'; // 7
|
|
|
|
if (!preg_match($r, $part, $found)) {
|
|
return false;
|
|
}
|
|
|
|
if ((int) $found[1] > 36 || (int) $found[1] < 1) {
|
|
return false;
|
|
}
|
|
|
|
$unit = 'meter';
|
|
$report_unit = 'm';
|
|
if (isset($found[6]) && $found[6] === 'FT') {
|
|
$unit = 'feet';
|
|
$report_unit = 'nmi';
|
|
}
|
|
|
|
$observed = [
|
|
'runway' => $found[1],
|
|
'variable' => null,
|
|
'variable_prefix' => null,
|
|
'interval_min' => null,
|
|
'interval_max' => null,
|
|
'tendency' => null,
|
|
'report' => null,
|
|
];
|
|
|
|
// Runway past tendency
|
|
if (isset($found[8], static::$rvr_tendency_codes[$found[8]])) {
|
|
$observed['tendency'] = $found[8];
|
|
}
|
|
|
|
// Runway visual range
|
|
if (isset($found[6])) {
|
|
if (!empty($found[4])) {
|
|
$observed['interval_min'] = $this->createDistance($found[4], $unit);
|
|
$observed['interval_max'] = $this->createDistance($found[6], $unit);
|
|
if (!empty($found[5])) {
|
|
$observed['variable_prefix'] = $found[5];
|
|
}
|
|
} else {
|
|
$observed['variable'] = $this->createDistance($found[6], $unit);
|
|
}
|
|
}
|
|
|
|
// Runway visual range report
|
|
if (!empty($observed['runway'])) {
|
|
$report = [];
|
|
if ($observed['variable'] !== null) {
|
|
$report[] = $observed['variable'][$report_unit].$report_unit;
|
|
} elseif ($observed['interval_min'] !== null && $observed['interval_max'] !== null) {
|
|
if (isset(static::$rvr_prefix_codes[$observed['variable_prefix']])) {
|
|
$report[] = 'varying from a min. of '.$observed['interval_min'][$report_unit].$report_unit.' until a max. of '.
|
|
static::$rvr_prefix_codes[$observed['variable_prefix']].' that '.
|
|
$observed['interval_max'][$report_unit].' '.$report_unit;
|
|
} else {
|
|
$report[] = 'varying from a min. of '.$observed['interval_min'][$report_unit].$report_unit.' until a max. of '.
|
|
$observed['interval_max'][$report_unit].$report_unit;
|
|
}
|
|
}
|
|
|
|
if (null !== $observed['tendency'] && isset(static::$rvr_tendency_codes[$observed['tendency']])) {
|
|
$report[] = 'and '.static::$rvr_tendency_codes[$observed['tendency']];
|
|
}
|
|
|
|
$observed['report'] = ucfirst(implode(' ', $report));
|
|
}
|
|
|
|
$this->set_result_group('runways_visual_range', $observed);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Decodes present weather conditions if present. This function maybe called several times
|
|
* to decode all conditions. To learn more about weather condition codes, visit section
|
|
* 12.6.8 - Present Weather Group of the Federal Meteorological Handbook No. 1 at
|
|
* www.nws.noaa.gov/oso/oso1/oso12/fmh1/fmh1ch12.htm
|
|
*
|
|
* @param $part
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function get_present_weather($part)
|
|
{
|
|
return $this->decode_weather($part, 'present');
|
|
}
|
|
|
|
/**
|
|
* Decodes cloud cover information if present. This function maybe called several times
|
|
* to decode all cloud layer observations. Only the last layer is saved.
|
|
* Format is SKC or CLR for clear skies, or cccnnn where ccc = 3-letter code and
|
|
* nnn = height of cloud layer in hundreds of feet. 'VV' seems to be used for
|
|
* very low cloud layers.
|
|
*
|
|
* @param $part
|
|
*
|
|
* @throws NonStringUnitName
|
|
* @throws NonNumericValue
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function get_clouds($part)
|
|
{
|
|
$r = '@^((NSW|NSC|NCD|CLR|SKC|NOBS)|' // 1
|
|
.'((VV|FEW|SCT|BKN|OVC|///)' // 2
|
|
.'([\d]{3}|///)' // 3
|
|
.'(CB|TCU|///)?))$@'; // 4
|
|
|
|
if (!preg_match($r, $part, $found)) {
|
|
return false;
|
|
}
|
|
|
|
$observed = [
|
|
'amount' => null,
|
|
'height' => null,
|
|
'type' => null,
|
|
'report' => null,
|
|
];
|
|
|
|
// Clear skies or no observation
|
|
if (isset($found[2]) && !empty($found[2])) {
|
|
if (isset(static::$cloud_codes[$found[2]])) {
|
|
$observed['amount'] = $found[2];
|
|
}
|
|
} // Cloud cover observed
|
|
elseif (isset($found[5]) && !empty($found[5]) && is_numeric($found[5])) {
|
|
$observed['height'] = $this->createAltitude($found[5] * 100, 'feet');
|
|
|
|
// Cloud height
|
|
if (null === $this->result['cloud_height'] || $observed['height']['m'] < $this->result['cloud_height']['m']) {
|
|
$this->set_result_value('cloud_height', $observed['height']);
|
|
}
|
|
|
|
if (isset(static::$cloud_codes[$found[4]])) {
|
|
$observed['amount'] = $found[4];
|
|
}
|
|
}
|
|
// Type
|
|
if (isset($found[6], static::$cloud_type_codes[$found[6]]) && !empty($found[6]) && $found[4] !== 'VV') {
|
|
$observed['type'] = $found[6];
|
|
}
|
|
|
|
// Build clouds report
|
|
if (null !== $observed['amount']) {
|
|
$report = [];
|
|
$report_ft = [];
|
|
|
|
$report[] = static::$cloud_codes[$observed['amount']];
|
|
$report_ft[] = static::$cloud_codes[$observed['amount']];
|
|
|
|
if ($observed['height']) {
|
|
if (null !== $observed['type']) {
|
|
$report[] = 'at '.round($observed['height']['m'], 0).' meters, '.static::$cloud_type_codes[$observed['type']];
|
|
$report_ft[] = 'at '.round($observed['height']['ft'], 0).' feet, '.static::$cloud_type_codes[$observed['type']];
|
|
} else {
|
|
$report[] = 'at '.round($observed['height']['m'], 0).' meters';
|
|
$report_ft[] = 'at '.round($observed['height']['ft'], 0).' feet';
|
|
}
|
|
}
|
|
|
|
$report = implode(' ', $report);
|
|
$report_ft = implode(' ', $report_ft);
|
|
|
|
$observed['report'] = $this->createAltitude($report_ft, 'ft');
|
|
$observed['report'] = ucfirst($report);
|
|
$observed['report_ft'] = ucfirst($report_ft);
|
|
|
|
$this->set_result_report('clouds_report', $report);
|
|
$this->set_result_report('clouds_report_ft', $report_ft);
|
|
}
|
|
|
|
$this->set_result_group('clouds', $observed);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Decodes temperature and dew point information. Relative humidity is calculated. Also,
|
|
* depending on the temperature, Heat Index or Wind Chill Temperature is calculated.
|
|
* Format is tt/dd where tt = temperature and dd = dew point temperature. All units are
|
|
* in Celsius. A 'M' preceeding the tt or dd indicates a negative temperature. Some
|
|
* stations do not report dew point, so the format is tt/ or tt/XX.
|
|
*
|
|
* @param mixed $part
|
|
*
|
|
* @throws NonNumericValue
|
|
* @throws NonStringUnitName
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function get_temperature($part)
|
|
{
|
|
$r = '@^(M?[\d]{2})' // 1
|
|
.'/(M?[\d]{2}' // 2
|
|
.'|[X]{2})?@'; // 3
|
|
if (!preg_match($r, $part, $found)) {
|
|
return false;
|
|
}
|
|
|
|
// Set clouds and weather reports if its not observed (e.g. clear and dry)
|
|
$this->set_result_value('clouds_report', 'Clear skies', true);
|
|
$this->set_result_value('present_weather_report', 'Dry', true);
|
|
|
|
// Temperature
|
|
$temperature_c = (int) str_replace('M', '-', $found[1]);
|
|
$temperature = $this->createTemperature($temperature_c, 'C');
|
|
|
|
$this->set_result_value('temperature', $temperature);
|
|
$this->calculate_wind_chill($temperature['F']);
|
|
|
|
// Dew point
|
|
if (isset($found[2]) && '' !== $found[2] && $found[2] !== 'XX') {
|
|
$dew_point_c = (int) str_replace('M', '-', $found[2]);
|
|
$dew_point = $this->createTemperature($dew_point_c, 'C');
|
|
$rh = round(100 * (((112 - (0.1 * $temperature_c) + $dew_point_c) / (112 + (0.9 * $temperature_c))) ** 8));
|
|
|
|
$this->set_result_value('dew_point', $dew_point);
|
|
$this->set_result_value('humidity', $rh);
|
|
$this->calculate_heat_index($temperature['F'], $rh);
|
|
}
|
|
|
|
$this->method++;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Decodes altimeter or barometer information.
|
|
* Format is Annnn where nnnn represents a real number as nn.nn in inches of Hg,
|
|
* or Qpppp where pppp = hectoPascals.
|
|
* Some other common conversion factors:
|
|
* 1 millibar = 1 hPa
|
|
* 1 in Hg = 0.02953 hPa
|
|
* 1 mm Hg = 25.4 in Hg = 0.750062 hPa
|
|
* 1 lb/sq in = 0.491154 in Hg = 0.014504 hPa
|
|
* 1 atm = 0.33421 in Hg = 0.0009869 hPa
|
|
*
|
|
*
|
|
* @param mixed $part
|
|
*
|
|
* @throws NonNumericValue
|
|
* @throws NonStringUnitName
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function get_pressure($part)
|
|
{
|
|
if (!preg_match('@^(Q|A)(////|[\d]{4})@', $part, $found)) {
|
|
return false;
|
|
}
|
|
|
|
$pressure = (int) $found[2];
|
|
if ($found[1] === 'A') {
|
|
$pressure /= 100;
|
|
$this->set_result_value('barometer', $this->createPressure($pressure, 'inHg'));
|
|
} else {
|
|
$this->set_result_value('barometer', $this->createPressure($pressure, 'hPa'));
|
|
}
|
|
|
|
$this->method++;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Decodes recent weather conditions if present.
|
|
* Format is REww where ww = Weather phenomenon code (see get_present_weather above).
|
|
*
|
|
* @param mixed $part
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function get_recent_weather($part)
|
|
{
|
|
return $this->decode_weather($part, 'recent', 'RE');
|
|
}
|
|
|
|
/**
|
|
* Decodes runways report information if present.
|
|
* Format rrrECeeBB or Rrrr/ECeeBB where rr = runway number, E = deposits,
|
|
* C = extent of deposit, ee = depth of deposit, BB = friction coefficient.
|
|
*
|
|
* @param mixed $part
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function get_runways_report($part)
|
|
{
|
|
$r = '@^R?'
|
|
.'(/?(SNOCLO)' // 1
|
|
.'|([\d]{2}[LCR]?)/?' // 2
|
|
.'(CLRD|([\d]{1}|/)' // 3
|
|
.'([\d]{1}|/)' // 4
|
|
.'([\d]{2}|//))' // 5
|
|
.'([\d]{2}|//))$@'; // 6
|
|
|
|
if (!preg_match($r, $part, $found)) {
|
|
return false;
|
|
}
|
|
|
|
$this->set_result_value('runways_snoclo', false, true);
|
|
|
|
// Airport closed due to snow
|
|
if (isset($found[2]) && $found[2] === 'SNOCLO') {
|
|
$this->set_result_value('runways_snoclo', true);
|
|
} else {
|
|
$observed = [
|
|
'runway' => $found[3], // just runway number
|
|
'deposits' => null,
|
|
'deposits_extent' => null,
|
|
'deposits_depth' => null,
|
|
'friction' => null,
|
|
'report' => null,
|
|
];
|
|
// Contamination has disappeared (runway has been cleared)
|
|
if (isset($found[4]) && $found[4] === 'CLRD') {
|
|
$observed['deposits'] = 0; // cleared
|
|
} // Deposits observed
|
|
else {
|
|
// Type
|
|
$deposits = $found[5];
|
|
if (isset(static::$runway_deposits_codes[$deposits])) {
|
|
$observed['deposits'] = $deposits;
|
|
}
|
|
|
|
// Extent
|
|
$deposits_extent = $found[6];
|
|
if (isset(static::$runway_deposits_extent_codes[$deposits_extent])) {
|
|
$observed['deposits_extent'] = $deposits_extent;
|
|
}
|
|
|
|
// Depth
|
|
$deposits_depth = $found[7];
|
|
|
|
// Uses in mm
|
|
if ((int) $deposits_depth >= 1 && (int) $deposits_depth <= 90) {
|
|
$observed['deposits_depth'] = (int) $deposits_depth;
|
|
} // Uses codes
|
|
elseif (isset(static::$runway_deposits_depth_codes[$deposits_depth])) {
|
|
$observed['deposits_depth'] = $deposits_depth;
|
|
}
|
|
}
|
|
|
|
// Friction observed
|
|
$friction = $found[8];
|
|
|
|
// Uses coefficient
|
|
if ((int) $friction > 0 && (int) $friction <= 90) {
|
|
$observed['friction'] = round($friction / 100, 2);
|
|
} // Uses codes
|
|
elseif (isset(static::$runway_friction_codes[$friction])) {
|
|
$observed['friction'] = $friction;
|
|
}
|
|
|
|
// Build runways report
|
|
$report = [];
|
|
if ($observed['deposits'] !== null) {
|
|
$report[] = static::$runway_deposits_codes[$observed['deposits']];
|
|
if (null !== $observed['deposits_extent']) {
|
|
$report[] = 'contamination '.static::$runway_deposits_extent_codes[$observed['deposits_extent']];
|
|
}
|
|
|
|
if ($observed['deposits_depth'] !== null) {
|
|
if ($observed['deposits_depth'] === '99') {
|
|
$report[] = 'runway closed';
|
|
} elseif (isset(static::$runway_deposits_depth_codes[$observed['deposits_depth']])) {
|
|
$report[] = 'deposit is '.static::$runway_deposits_depth_codes[$observed['deposits_depth']].' deep';
|
|
} else {
|
|
$report[] = 'deposit is '.$observed['deposits_depth'].' mm deep';
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($observed['friction'] !== null) {
|
|
if (isset(static::$runway_friction_codes[$observed['friction']])) {
|
|
$report[] = 'a braking action is '.static::$runway_friction_codes[$observed['friction']];
|
|
} else {
|
|
$report[] = 'a friction coefficient is '.$observed['friction'];
|
|
}
|
|
}
|
|
|
|
$observed['report'] = ucfirst(implode(', ', $report));
|
|
$this->set_result_group('runways_report', $observed);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Decodes wind shear information if present.
|
|
* Format is 'WS ALL RWY' or 'WS RWYdd' where dd = Runway designator (see get_runway_vr above).
|
|
*
|
|
* @param mixed $part
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function get_wind_shear($part)
|
|
{
|
|
if ($part !== 'WS') {
|
|
return false;
|
|
}
|
|
|
|
$this->set_result_value('wind_shear_all_runways', false, true);
|
|
$this->part++; // skip this part with WS
|
|
|
|
// See two next parts for 'ALL RWY' records
|
|
if (implode(' ', \array_slice($this->raw_parts, $this->part, 2)) === 'ALL RWY') {
|
|
$this->set_result_value('wind_shear_all_runways', true);
|
|
$this->part += 2; // can skip neext parts with ALL and RWY records
|
|
} // See one next part for RWYdd record
|
|
elseif (isset($this->raw_parts[$this->part])) {
|
|
$r = '@^R(WY)?' // 1
|
|
.'([\d]{2}[LCR]?)$@'; // 2
|
|
|
|
$part = $this->raw_parts[$this->part];
|
|
if (!preg_match($r, $part, $found)) {
|
|
return false;
|
|
}
|
|
|
|
if ((int) $found[2] > 36 || (int) $found[2] < 1) {
|
|
return false;
|
|
}
|
|
|
|
$this->set_result_group('wind_shear_runways', $found[2]);
|
|
} else {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Decodes max and min temperature forecast information if present.
|
|
*
|
|
* @param string $part
|
|
* Format TXTtTt/ddHHZ or TNTtTt/ddHHZ, where:
|
|
* TX - Indicator for Maximum temperature
|
|
* TN - Indicator for Minimum temperature
|
|
* TtTt - Temperature value in Celsius
|
|
* dd - Forecast day of month
|
|
* HH - Forecast hour, i.e. the time(hour) when the temperature is expected
|
|
* Z - Time Zone indicator, Z=GMT.
|
|
*
|
|
* @throws NonStringUnitName
|
|
* @throws NonNumericValue
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function get_forecast_temperature($part): bool
|
|
{
|
|
$r = '@^(TX|TN)' // 1
|
|
.'(M?[\d]{2})' // 2
|
|
.'/([\d]{2})?' // 3
|
|
.'([\d]{2})Z$@'; // 4
|
|
|
|
if (!preg_match($r, $this->raw_parts[$this->part], $found)) {
|
|
return false;
|
|
}
|
|
|
|
// Temperature
|
|
$temperature_c = (int) str_replace('M', '-', $found[2]);
|
|
$temperture = $this->createTemperature($temperature_c, 'C');
|
|
|
|
$forecast = [
|
|
'value' => $temperture,
|
|
'day' => null,
|
|
'time' => null,
|
|
];
|
|
|
|
if (!empty($found[3])) {
|
|
$forecast['day'] = (int) $found[3];
|
|
}
|
|
|
|
$forecast['time'] = $found[4].':00 UTC';
|
|
|
|
$parameter = 'forecast_temperature_max';
|
|
if ($found[1] === 'TN') {
|
|
$parameter = 'forecast_temperature_min';
|
|
}
|
|
|
|
$this->set_result_group($parameter, $forecast);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Decodes trends information if present.
|
|
* All METAR trend and TAF records is beginning at: NOSIG, BECMG, TEMP, ATDDhhmm, FMDDhhmm,
|
|
* LTDDhhmm or DDhh/DDhh, where hh = hours, mm = minutes, DD = day of month.
|
|
*
|
|
* @param mixed $part
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function get_trends($part)
|
|
{
|
|
$r = '@^((NOSIG|BECMG|TEMPO|INTER|CNL|NIL|PROV|(PROB)' // 1
|
|
.'([\d]{2})|' // 2
|
|
.'(AT|FM|TL)' // 3
|
|
.'([\d]{2})?' // 4
|
|
.'([\d]{2})' // 5
|
|
.'([\d]{2}))|' // 6
|
|
.'(([\d]{2})' // 7
|
|
.'([\d]{2}))/' // 8
|
|
.'(([\d]{2})' // 9
|
|
.'([\d]{2})))$@'; // 10
|
|
|
|
if (!preg_match($r, $part, $found)) {
|
|
return false;
|
|
}
|
|
|
|
// Ignore trends
|
|
return true;
|
|
// Detects TAF on report
|
|
if ($this->part <= 4) {
|
|
$this->set_result_value('taf', true);
|
|
}
|
|
|
|
// Nil significant changes, skip trend
|
|
if ($found[2] === 'NOSIG') {
|
|
return true;
|
|
}
|
|
|
|
$trend = [
|
|
'flag' => null,
|
|
'probability' => null,
|
|
'period_report' => null,
|
|
'period' => [
|
|
'flag' => null,
|
|
'day' => null,
|
|
'time' => null,
|
|
'from_day' => null,
|
|
'from_time' => null,
|
|
'to_day' => null,
|
|
'to_time' => null,
|
|
],
|
|
];
|
|
|
|
$raw_parts = [];
|
|
|
|
// Get all parts after trend part
|
|
while ($this->part < count($this->raw_parts)) {
|
|
if (preg_match($r, $this->raw_parts[$this->part], $found)) {
|
|
// Get trend flag
|
|
if (isset($found[2], static::$trends_flag_codes[$found[2]])) {
|
|
$trend['flag'] = $found[2];
|
|
} // Get PROBpp formatted period
|
|
|
|
elseif (isset($found[3]) && $found[3] === 'PROB') {
|
|
$trend['probability'] = $found[4];
|
|
} // Get AT, FM, TL formatted period
|
|
|
|
elseif (isset($found[8], static::$trends_time_codes[$found[5]])) {
|
|
$trend['period']['flag'] = $found[5];
|
|
if (!empty($found[6])) {
|
|
$trend['period']['day'] = (int) $found[6];
|
|
}
|
|
$trend['period']['time'] = $found[7].':'.$found[8].' UTC';
|
|
} // Get DDhh/DDhh formatted period
|
|
|
|
elseif (isset($found[14])) {
|
|
$trend['period']['from_day'] = $found[10];
|
|
$trend['period']['from_time'] = $found[11].':00 UTC';
|
|
$trend['period']['to_day'] = $found[13];
|
|
$trend['period']['to_time'] = $found[14].':00 UTC';
|
|
}
|
|
} // If RMK observed -- the trend is ended
|
|
|
|
elseif ($this->raw_parts[$this->part] === 'RMK') {
|
|
if (!empty($raw_parts)) {
|
|
$this->part--; // return pointer to RMK part
|
|
}
|
|
|
|
break;
|
|
} // Other data addrs to METAR raw
|
|
|
|
else {
|
|
$raw_parts[] = $this->raw_parts[$this->part];
|
|
}
|
|
|
|
$this->part++; // go to next part
|
|
|
|
// Detect ends of this trend, if the METAR raw data observed
|
|
if (!empty($raw_parts) && (!isset($this->raw_parts[$this->part]) || preg_match($r, $this->raw_parts[$this->part]))) {
|
|
$this->part--; // return pointer to finded part
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Empty trend is a bad trend, except for flags CNL and NIL
|
|
if (empty($raw_parts)) {
|
|
if ($trend['flag'] !== 'CNL' && $trend['flag'] !== 'NIL') {
|
|
$this->part--; // return pointer to previous part
|
|
return false;
|
|
}
|
|
} // Parse raw data from trend
|
|
|
|
else {
|
|
$parser = new static(implode(' ', $raw_parts), true, $this->debug_enabled, false);
|
|
if ($parsed = $parser->parse_all()) {
|
|
unset($parsed['taf']);
|
|
// Add parsed data to trend
|
|
if (!empty($parsed)) {
|
|
$trend = array_merge($trend, $parsed);
|
|
}
|
|
}
|
|
|
|
// Process debug messages
|
|
/*if ($debug = $parser->debug()) {
|
|
foreach ($debug as $message) {
|
|
$this->set_debug('Recursion: '.$message);
|
|
}
|
|
}*/
|
|
|
|
// Process parse errors
|
|
if ($errors = $parser->errors()) {
|
|
foreach ($errors as $message) {
|
|
$this->set_error('Recursion: '.$message);
|
|
}
|
|
}
|
|
}
|
|
// Build the report
|
|
$report = [];
|
|
if (null !== $trend['flag']) {
|
|
$report[] = static::$trends_flag_codes[$trend['flag']];
|
|
}
|
|
|
|
if (null !== $trend['period']['flag']) {
|
|
if (null !== $trend['period']['day']) {
|
|
$report[] = static::$trends_time_codes[$trend['period']['flag']].
|
|
' a '.$trend['period']['day'].' day of the month on '.$trend['period']['time'];
|
|
} else {
|
|
$report[] = static::$trends_time_codes[$trend['period']['flag']].' '.$trend['period']['time'];
|
|
}
|
|
}
|
|
|
|
if (null !== $trend['period']['from_day'] && null !== $trend['period']['to_day']) {
|
|
$report[] = 'from a '.$trend['period']['from_day'].' day of the month on '.$trend['period']['from_time'];
|
|
$report[] = 'to a '.$trend['period']['to_day'].' day of the month on '.$trend['period']['to_time'];
|
|
}
|
|
|
|
if (null !== $trend['probability']) {
|
|
$report[] = 'probability '.$trend['probability'].'% of the conditions existing';
|
|
}
|
|
|
|
if (!empty($report)) {
|
|
$trend['period_report'] = ucfirst(implode(', ', $report));
|
|
}
|
|
|
|
$this->set_result_group('trends', $trend);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get remarks information if present.
|
|
* The information is everything that comes after RMK.
|
|
*
|
|
* @param string $part
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function get_remarks($part): bool
|
|
{
|
|
if ($part !== 'RMK') {
|
|
return false;
|
|
}
|
|
|
|
$this->part++; // skip this part with RMK
|
|
|
|
$remarks = [];
|
|
// Get all parts after
|
|
while ($this->part < count($this->raw_parts)) {
|
|
if (isset($this->raw_parts[$this->part])) {
|
|
$remarks[] = $this->raw_parts[$this->part];
|
|
}
|
|
$this->part++; // go to next part
|
|
}
|
|
|
|
if (!empty($remarks)) {
|
|
$this->set_result_value('remarks', implode(' ', $remarks));
|
|
}
|
|
|
|
$this->method++;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Decodes present or recent weather conditions.
|
|
*
|
|
* @param $part
|
|
* @param $method
|
|
* @param string $regexp_prefix
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function decode_weather($part, $method, $regexp_prefix = '')
|
|
{
|
|
$wx_codes = implode('|', array_keys(array_merge(static::$weather_char_codes, static::$weather_type_codes)));
|
|
if (!preg_match('@^'.$regexp_prefix.'([-+]|VC)?('.$wx_codes.')?('.$wx_codes.')?('.$wx_codes.')?('.$wx_codes.')@', $part, $found)) {
|
|
return false;
|
|
}
|
|
|
|
$observed = [
|
|
'intensity' => null,
|
|
'types' => null,
|
|
'characteristics' => null,
|
|
'report' => null,
|
|
];
|
|
|
|
// Intensity
|
|
if ($found[1] !== null) {
|
|
$observed['intensity'] = $found[1];
|
|
}
|
|
|
|
foreach (\array_slice($found, 1) as $code) {
|
|
// Types
|
|
if (isset(static::$weather_type_codes[$code])) {
|
|
if (null === $observed['types']) {
|
|
$observed['types'] = [];
|
|
}
|
|
|
|
$observed['types'][] = $code;
|
|
}
|
|
|
|
// Characteristics (uses last)
|
|
if (isset(static::$weather_char_codes[$code])) {
|
|
$observed['characteristics'] = $code;
|
|
}
|
|
}
|
|
|
|
// Build recent weather report
|
|
if (null !== $observed['characteristics'] || null !== $observed['types']) {
|
|
$report = [];
|
|
if (null !== $observed['intensity']) {
|
|
if ($observed['intensity'] === 'VC') {
|
|
$report[] = static::$weather_intensity_codes[$observed['intensity']].',';
|
|
} else {
|
|
$report[] = static::$weather_intensity_codes[$observed['intensity']];
|
|
}
|
|
}
|
|
|
|
if ($observed['characteristics'] !== null) {
|
|
$report[] = static::$weather_char_codes[$observed['characteristics']];
|
|
}
|
|
|
|
if ($observed['types'] !== null) {
|
|
foreach ($observed['types'] as $code) {
|
|
$report[] = static::$weather_type_codes[$code];
|
|
}
|
|
}
|
|
|
|
$report = implode(' ', $report);
|
|
$observed['report'] = ucfirst($report);
|
|
|
|
$this->set_result_report($method.'_weather_report', $report);
|
|
}
|
|
|
|
$this->set_result_group($method.'_weather', $observed);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Calculate Heat Index based on temperature in F and relative humidity (65 = 65%)
|
|
*
|
|
* @param $temperature_f
|
|
* @param $rh
|
|
*
|
|
* @throws NonNumericValue
|
|
* @throws NonStringUnitName
|
|
*/
|
|
private function calculate_heat_index($temperature_f, $rh): void
|
|
{
|
|
if ($temperature_f > 79 && $rh > 39) {
|
|
$hi_f = -42.379 + 2.04901523 * $temperature_f + 10.14333127 * $rh - 0.22475541 * $temperature_f * $rh;
|
|
$hi_f += -0.00683783 * ($temperature_f ** 2) - 0.05481717 * ($rh ** 2);
|
|
$hi_f += 0.00122874 * ($temperature_f ** 2) * $rh + 0.00085282 * $temperature_f * ($rh ** 2);
|
|
$hi_f += -0.00000199 * ($temperature_f ** 2) * ($rh ** 2);
|
|
$hi_f = round($hi_f);
|
|
$hi_c = round(($hi_f - 32) / 1.8);
|
|
|
|
$this->set_result_value('heat_index', $this->createTemperature($hi_c, 'C'));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculate Wind Chill Temperature based on temperature in F
|
|
* and wind speed in miles per hour.
|
|
*
|
|
* @param $temperature_f
|
|
*
|
|
* @throws NonNumericValue
|
|
* @throws NonStringUnitName
|
|
*/
|
|
private function calculate_wind_chill($temperature_f): void
|
|
{
|
|
if ($temperature_f < 51 && $this->result['wind_speed'] && $this->result['wind_speed'] !== 0) {
|
|
$windspeed = $this->result['wind_speed']->toUnit('mph');
|
|
if ($windspeed > 3) {
|
|
$chill_f = 35.74 + 0.6215 * $temperature_f - 35.75 * ($windspeed ** 0.16);
|
|
$chill_f += 0.4275 * $temperature_f * ($windspeed ** 0.16);
|
|
$chill_f = round($chill_f);
|
|
$chill_c = round(($chill_f - 32) / 1.8);
|
|
|
|
$this->set_result_value('wind_chill', $this->createTemperature($chill_c, 'C'));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert wind speed into meters per second.
|
|
* Some other common conversion factors:
|
|
* 1 mi/hr = 0.868976 knots = 0.000447 km/hr = 0.44704 m/s = 1.466667 ft/s
|
|
* 1 ft/s = 0.592483 knots = 1.097279 km/hr = 0.304799 m/s = 0.681818 mi/hr
|
|
* 1 knot = 1.852 km/hr = 0.514444 m/s = 1.687809 ft/s = 1.150779 mi/hr
|
|
* 1 km/hr = 0.539957 knots = 0.277778 m/s = 0.911344 ft/s = 0.621371 mi/hr
|
|
* 1 m/s = 1.943844 knots = 3.6 km/h = 3.28084 ft/s = 2.236936 mi/hr
|
|
*
|
|
* @param $speed
|
|
* @param $unit
|
|
*
|
|
* @throws NonStringUnitName
|
|
* @throws NonNumericValue
|
|
*
|
|
* @return Velocity
|
|
*/
|
|
private function convert_speed($speed, $unit)
|
|
{
|
|
// TODO: return dict w/ multiple units - NS
|
|
|
|
switch ($unit) {
|
|
case 'KT':
|
|
return $this->createVelocity($speed, 'knots');
|
|
case 'KPH':
|
|
return $this->createVelocity($speed, 'km/hour');
|
|
default:
|
|
return $this->createVelocity($speed, 'm/s');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert direction degrees to compass label.
|
|
*
|
|
* @param mixed $direction
|
|
*
|
|
* @return string Direction string
|
|
*/
|
|
private function convert_direction_label($direction): string
|
|
{
|
|
if ($direction >= 0 && $direction <= 360) {
|
|
return static::$direction_codes[round($direction / 22.5) % 16];
|
|
}
|
|
|
|
return 'N';
|
|
}
|
|
|
|
/**
|
|
* These methods below the implementation of the stubs for ArrayAccess
|
|
*
|
|
* @param mixed $offset
|
|
*/
|
|
|
|
/**
|
|
* Whether a offset exists
|
|
*
|
|
* @link http://php.net/manual/en/arrayaccess.offsetexists.php
|
|
*
|
|
* @param mixed $offset <p>
|
|
* An offset to check for.
|
|
* </p>
|
|
*
|
|
* @return bool true on success or false on failure.
|
|
* </p>
|
|
* <p>
|
|
* The return value will be casted to boolean if non-boolean was returned.
|
|
*
|
|
* @since 5.0.0
|
|
*/
|
|
public function offsetExists($offset)
|
|
{
|
|
return array_key_exists($offset, $this->result);
|
|
}
|
|
|
|
/**
|
|
* Offset to retrieve
|
|
*
|
|
* @link http://php.net/manual/en/arrayaccess.offsetget.php
|
|
*
|
|
* @param mixed $offset <p>
|
|
* The offset to retrieve.
|
|
* </p>
|
|
*
|
|
* @return mixed Can return all value types.
|
|
*
|
|
* @since 5.0.0
|
|
*/
|
|
public function offsetGet($offset)
|
|
{
|
|
return $this->result[$offset];
|
|
}
|
|
|
|
/**
|
|
* Offset to set
|
|
*
|
|
* @link http://php.net/manual/en/arrayaccess.offsetset.php
|
|
*
|
|
* @param mixed $offset <p>
|
|
* The offset to assign the value to.
|
|
* </p>
|
|
* @param mixed $value <p>
|
|
* The value to set.
|
|
* </p>
|
|
*
|
|
* @return void
|
|
*
|
|
* @since 5.0.0
|
|
*/
|
|
public function offsetSet($offset, $value)
|
|
{
|
|
$this->result[$offset] = $value;
|
|
}
|
|
|
|
/**
|
|
* Offset to unset
|
|
*
|
|
* @link http://php.net/manual/en/arrayaccess.offsetunset.php
|
|
*
|
|
* @param mixed $offset <p>
|
|
* The offset to unset.
|
|
* </p>
|
|
*
|
|
* @return void
|
|
*
|
|
* @since 5.0.0
|
|
*/
|
|
public function offsetUnset($offset)
|
|
{
|
|
$this->result[$offset] = null;
|
|
}
|
|
}
|