/**
* Parses METAR
*
* @param array $data An array of METAR data lines.
*
* @return array An array of weather data. Possible keys include:
* - station:
* - dataRaw:
* - update:
* - updateRaw:
* - wind:
* - windDegrees:
* - windDirection:
* - windGust:
* - windVariability:
* - visibility:
* - visQualifier:
* - clouds:
* - amount
* - height
* - type
* - temperature
* - dewpoint
* - humidity
* - felttemperature
* - pressure
* - trend
* - type
* - from
* - to
* - at
* - remark
* - autostation
* - seapressure
* - presschg
* - snowdepth
* - snowequiv
* - cloudtypes
* - sunduration
* - 1hrtemp
* - 1hrdew
* - 6hmaxtemp
* - 6hmintemp
* - 24hmaxtemp
* - 24hmintemp
* - 3hpresstrend
* - nospeci
* - sensors
* - maintain
* - precipitation
* - amount
* - hours
*/
protected function _parse(array $data)
{
// Eliminate trailing information
for ($i = 0; $i < sizeof($data); $i++) {
if (strpos($data[$i], '=') !== false) {
$data[$i] = substr($data[$i], 0, strpos($data[$i], '='));
$data = array_slice($data, 0, $i + 1);
break;
}
}
// Start with parsing the first line for the last update
$weatherData = array();
$weatherData['station'] = '';
$weatherData['dataRaw'] = implode(' ', $data);
$weatherData['update'] = strtotime(trim($data[0]) . ' GMT');
$weatherData['updateRaw'] = trim($data[0]);
if (empty($weatherData['update'])) {
throw new Horde_Service_Weather_Exception('Unable to parse data.');
}
// and prepare the rest for stepping through
array_shift($data);
$metar = explode(' ', preg_replace('/\\s{2,}/', ' ', implode(' ', $data)));
// Trend handling
$trendCount = 0;
// Pointer to the array we add the data to. Needed for handling trends.
$pointer =& $weatherData;
// Load the metar codes for this go around.
$metarCode = $this->_getMetarCodes();
for ($i = 0; $i < sizeof($metar); $i++) {
$metar[$i] = trim($metar[$i]);
if (!strlen($metar[$i])) {
continue;
}
$result = array();
$resultVF = array();
$lresult = array();
$found = false;
foreach ($metarCode as $key => $regexp) {
// Check if current code matches current metar snippet
if (($found = preg_match('/^' . $regexp . '$/i', $metar[$i], $result)) == true) {
switch ($key) {
case 'station':
$pointer['station'] = $result[0];
unset($metarCode['station']);
break;
case 'wind':
$pointer['wind'] = round(Horde_Service_Weather::convertSpeed($result[2], $result[5], $this->_unitMap[self::UNIT_KEY_SPEED]));
$wind_mph = Horde_Service_Weather::convertSpeed($result[2], $result[5], 'mph', $this->_unitMap[self::UNIT_KEY_SPEED]);
if ($result[1] == 'VAR' || $result[1] == 'VRB') {
// Variable winds
$pointer['windDegrees'] = round(Horde_Service_Weather_Translation::t('Variable'));
$pointer['windDirection'] = Horde_Service_Weather_Translation::t('Variable');
} else {
// Save wind degree and calc direction
$pointer['windDegrees'] = intval($result[1]);
$pointer['windDirection'] = Horde_Service_Weather::degToDirection($result[1]);
}
if (is_numeric($result[4])) {
// Wind with gusts...
$pointer['windGust'] = round(Horde_Service_Weather::convertSpeed($result[4], $result[5], $this->_unitMap[self::UNIT_KEY_SPEED]));
}
break;
case 'windVar':
// Once more wind, now variability around the current wind-direction
$pointer['windVariability'] = array('from' => intval($result[1]), 'to' => intval($result[2]));
break;
case 'visFrac':
// Possible fractional visibility here. Check if it matches with the next METAR piece for visibility
if (!isset($metar[$i + 1]) || !preg_match('/^' . $metarCode['visibility'] . '$/i', $result[1] . ' ' . $metar[$i + 1], $resultVF)) {
// No next METAR piece available or not matching.
$found = false;
break;
} else {
// Match. Hand over result and advance METAR
$key = 'visibility';
$result = $resultVF;
$i++;
}
case 'visibility':
$pointer['visQualifier'] = Horde_Service_Weather_Translation::t('AT');
if (is_numeric($result[1]) && $result[1] == 9999) {
// Upper limit of visibility range is 10KM.
$visibility = Horde_Service_Weather::convertDistance(10, 'km', $this->_unitMap[self::UNIT_KEY_DISTANCE]);
$pointer['visQualifier'] = Horde_Service_Weather_Translation::t('BEYOND');
} elseif (is_numeric($result[1])) {
// 4-digit visibility in m
$visibility = Horde_Service_Weather::convertDistance($result[1], 'm', $this->_unitMap[self::UNIT_KEY_DISTANCE]);
} elseif (!isset($result[11]) || $result[11] != 'CAVOK') {
if ($result[3] == 'M') {
$pointer['visQualifier'] = Horde_Service_Weather_Translation::t('BELOW');
} elseif ($result[3] == 'P') {
$pointer['visQualifier'] = Horde_Service_Weather_Translation::t('BEYOND');
}
if (is_numeric($result[5])) {
// visibility as one/two-digit number
$visibility = Horde_Service_Weather::convertDistance($result[5], $result[10], $this->_unitMap[self::UNIT_KEY_DISTANCE]);
} else {
// the y/z part, add if we had a x part (see visibility1)
if (is_numeric($result[7])) {
$visibility = Horde_Service_Weather::convertDistance($result[7] + $result[8] / $result[9], $result[10], $this->_unitMap[self::UNIT_KEY_DISTANCE]);
} else {
$visibility = Horde_Service_Weather::convertDistance($result[8] / $result[9], $result[10], $this->_unitMap[self::UNIT_KEY_DISTANCE]);
}
}
} else {
$pointer['visQualifier'] = Horde_Service_Weather_Translation::t('BEYOND');
$visibility = Horde_Service_Weather::convertDistance(10, 'km', $this->_unitMap[self::UNIT_KEY_DISTANCE]);
$pointer['clouds'] = array(array('amount' => Horde_Service_Weather_Translation::t('Clear below'), 'height' => 5000));
$pointer['condition'] = Horde_Service_Weather_Translation::t('no significant weather');
}
$pointer['visibility'] = $visibility;
break;
case 'condition':
if (!isset($pointer['condition'])) {
$pointer['condition'] = '';
} elseif (strlen($pointer['condition']) > 0) {
$pointer['condition'] .= ',';
}
if (in_array(strtolower($result[0]), $this->_conditions)) {
// First try matching the complete string
$pointer['condition'] .= ' ' . $this->_conditions[strtolower($result[0])];
} else {
// No luck, match part by part
array_shift($result);
$result = array_unique($result);
foreach ($result as $condition) {
if (strlen($condition) > 0) {
$pointer['condition'] .= ' ' . $this->_conditions[strtolower($condition)];
}
}
}
$pointer['condition'] = trim($pointer['condition']);
break;
case 'clouds':
if (!isset($pointer['clouds'])) {
$pointer['clouds'] = array();
}
if (sizeof($result) == 5) {
// Only amount and height
$cloud = array('amount' => $this->_clouds[strtolower($result[3])]);
if ($result[4] == '///') {
$cloud['height'] = Horde_Service_Weather_Translation::t('station level or below');
} else {
$cloud['height'] = $result[4] * 100;
}
} elseif (sizeof($result) == 6) {
// Amount, height and type
$cloud = array('amount' => $this->_clouds[strtolower($result[3])], 'type' => $this->_clouds[strtolower($result[5])]);
if ($result[4] == '///') {
$cloud['height'] = Horde_Service_Weather_Translation::t('station level or below');
} else {
$cloud['height'] = $result[4] * 100;
}
} else {
// SKC or CLR or NSC
$cloud = array('amount' => $this->_clouds[strtolower($result[0])]);
}
$pointer['clouds'][] = $cloud;
break;
case 'temperature':
// normal temperature in first part
// negative value
if ($result[1] == 'M') {
$result[2] *= -1;
}
$pointer['temperature'] = round(Horde_Service_Weather::convertTemperature($result[2], 'c', $this->_unitMap[self::UNIT_KEY_TEMP]));
$temp_f = Horde_Service_Weather::convertTemperature($result[2], 'c', 'f');
if (sizeof($result) > 4) {
// same for dewpoint
if ($result[4] == 'M') {
$result[5] *= -1;
}
$pointer['dewPoint'] = round(Horde_Service_Weather::convertTemperature($result[5], 'c', $this->_unitMap[self::UNIT_KEY_TEMP]));
$pointer['humidity'] = round(Horde_Service_Weather::calculateHumidity($result[2], $result[5])) . '%';
}
if (isset($pointer['wind'])) {
// Now calculate windchill from temperature and windspeed
// Note these must be in F and MPH.
$pointer['feltTemperature'] = round(Horde_Service_Weather::convertTemperature(Horde_Service_Weather::calculateWindChill($temp_f, $wind_mph), 'f', $this->_unitMap[self::UNIT_KEY_TEMP]));
}
break;
case 'pressure':
if ($result[1] == 'A') {
// Pressure provided in inches
$pointer['pressure'] = round(Horde_Service_Weather::convertPressure($result[2] / 100, 'in', $this->_unitMap[self::UNIT_KEY_PRESSURE]), 2);
} elseif ($result[3] == 'Q') {
// ... in hectopascal
$pointer['pressure'] = round(Horde_Service_Weather::convertPressure($result[4], 'hpa', $this->_unitMap[self::UNIT_KEY_PRESSURE]), 2);
}
break;
case 'trend':
// We may have a trend here... extract type and set pointer on
// created new array
if (!isset($weatherData['trend'])) {
$weatherData['trend'] = array();
$weatherData['trend'][$trendCount] = array();
}
$pointer =& $weatherData['trend'][$trendCount];
$trendCount++;
$pointer['type'] = $result[0];
while (isset($metar[$i + 1]) && preg_match('/^(FM|TL|AT)(\\d{2})(\\d{2})$/i', $metar[$i + 1], $lresult)) {
if ($lresult[1] == 'FM') {
$pointer['from'] = $lresult[2] . ':' . $lresult[3];
} elseif ($lresult[1] == 'TL') {
$pointer['to'] = $lresult[2] . ':' . $lresult[3];
} else {
$pointer['at'] = $lresult[2] . ':' . $lresult[3];
}
// As we have just extracted the time for this trend
// from our METAR, increase field-counter
$i++;
}
break;
case 'remark':
// Remark part begins
$metarCode = $this->_getRemarks();
$weatherData['remark'] = array();
break;
case 'autostation':
// Which autostation do we have here?
if ($result[1] == 0) {
$weatherData['remark']['autostation'] = Horde_Service_Weather_Translation::t('Automatic weatherstation w/o precipitation discriminator');
} else {
$weatherData['remark']['autostation'] = Horde_Service_Weather_Translation::t('Automatic weatherstation w/ precipitation discriminator');
}
unset($metarCode['autostation']);
break;
case 'presschg':
// Decoding for rapid pressure changes
if (strtolower($result[1]) == 'r') {
$weatherData['remark']['presschg'] = Horde_Service_Weather_Translation::t('Pressure rising rapidly');
} else {
$weatherData['remark']['presschg'] = Horde_Service_Weather_Translation::t('Pressure falling rapidly');
}
unset($metarCode['presschg']);
break;
case 'seapressure':
// Pressure at sea level (delivered in hpa)
// Decoding is a bit obscure as 982 gets 998.2
// whereas 113 becomes 1113 -> no real rule here
if (strtolower($result[1]) != 'no') {
if ($result[1] > 500) {
$press = 900 + round($result[1] / 100, 1);
} else {
$press = 1000 + $result[1];
}
$weatherData['remark']['seapressure'] = Horde_Service_Weather::convertPressure($press, 'hpa', $this->_unitMap[self::UNIT_KEY_PRESSURE]);
}
unset($metarCode['seapressure']);
break;
case 'precip':
// Precipitation in inches
if (!isset($weatherData['precipitation'])) {
$weatherData['precipitation'] = array();
}
if (!is_numeric($result[2])) {
$precip = 'indeterminable';
} elseif ($result[2] == '0000') {
$precip = 'traceable';
} else {
$precip = $result[2] / 100;
}
$weatherData['precipitation'][] = array('amount' => $precip, 'hours' => $this->_hours[$result[1]]);
break;
case 'snowdepth':
// Snow depth in inches
// @todo convert to metric
$weatherData['remark']['snowdepth'] = $result[1];
unset($metarCode['snowdepth']);
break;
case 'snowequiv':
// Same for equivalent in Water... (inches)
// @todo convert
$weatherData['remark']['snowequiv'] = $result[1] / 10;
unset($metarCode['snowequiv']);
break;
case 'cloudtypes':
// Cloud types
$weatherData['remark']['cloudtypes'] = array('low' => $this->_cloudTypes['low'][$result[1]], 'middle' => $this->_cloudTypes['middle'][$result[2]], 'high' => $this->_cloudTypes['high'][$result[3]]);
unset($metarCode['cloudtypes']);
break;
case 'sunduration':
// Duration of sunshine (in minutes)
$weatherData['remark']['sunduration'] = sprintf(Horde_Service_Weather_Translation::t('Total minutes of sunshine: %s'), $result[1]);
unset($metarCode['sunduration']);
break;
case '1htempdew':
// Temperatures in the last hour in C
if ($result[1] == '1') {
$result[2] *= -1;
}
$weatherData['remark']['1htemp'] = Horde_Service_Weather::convertTemperature($result[2] / 10, 'c', $this->_unitMap[self::UNIT_KEY_TEMP]);
if (sizeof($result) > 3) {
// same for dewpoint
if ($result[4] == '1') {
$result[5] *= -1;
}
$weatherData['remark']['1hdew'] = Horde_Service_Weather::convertTemperature($result[5] / 10, 'c', $this->_unitMap[self::UNIT_KEY_TEMP]);
}
unset($metarCode['1htempdew']);
break;
case '6hmaxtemp':
// Max temperature in the last 6 hours in C
if ($result[1] == '1') {
$result[2] *= -1;
}
$weatherData['remark']['6hmaxtemp'] = Horde_Service_Weather::convertTemperature($result[2] / 10, 'c', $this->_unitMap[self::UNIT_KEY_TEMP]);
unset($metarCode['6hmaxtemp']);
break;
case '6hmintemp':
// Min temperature in the last 6 hours in C
if ($result[1] == '1') {
$result[2] *= -1;
}
$weatherData['remark']['6hmintemp'] = Horde_Service_Weather::convertTemperature($result[2] / 10, 'c', $this->_unitMap[self::UNIT_KEY_TEMP]);
unset($metarCode['6hmintemp']);
break;
case '24htemp':
// Max/Min temperatures in the last 24 hours in C
if ($result[1] == '1') {
$result[2] *= -1;
}
$weatherData['remark']['24hmaxtemp'] = Horde_Service_Weather::convertTemperature($result[2] / 10, 'c', $this->_unitMap[self::UNIT_KEY_TEMP]);
if ($result[3] == '1') {
$result[4] *= -1;
}
$weatherData['remark']['24hmintemp'] = Horde_Service_Weather::convertTemperature($result[4] / 10, 'c', $this->_unitMap[self::UNIT_KEY_TEMP]);
unset($metarCode['24htemp']);
break;
case '3hpresstend':
// Pressure tendency of the last 3 hours
// no special processing, just passing the data
$weatherData['remark']['3hpresstend'] = array('presscode' => $result[1], 'presschng' => Horde_Service_Weather::convertPressure($result[2] / 10, 'hpa', $this->_unitMap[self::UNIT_KEY_PRESSURE]), 'description' => $result[1] >= 0 && $result[1] <= 3 ? Horde_Service_Weather_Translation::t('Rising') : $result[1] == 4 ? Horde_Service_Weather_Translation::t('Steady') : $result[1] > 4 ? Horde_Service_Weather_Translation::t('Falling') : '');
unset($metarCode['3hpresstend']);
break;
case 'nospeci':
// No change during the last hour
$weatherData['remark']['nospeci'] = Horde_Service_Weather_Translation::t('No changes in weather conditions');
unset($metarCode['nospeci']);
break;
case 'sensors':
// We may have multiple broken sensors, so do not unset
if (!isset($weatherData['remark']['sensors'])) {
$weatherData['remark']['sensors'] = array();
}
$weatherData['remark']['sensors'][strtolower($result[0])] = $this->_sensors[strtolower($result[0])];
break;
case 'maintain':
$weatherData['remark']['maintain'] = Horde_Service_Weather_Translation::t('Maintainance needed');
unset($metarCode['maintain']);
break;
default:
// Do nothing, just prevent further matching
unset($metarCode[$key]);
break;
}
if ($found) {
break;
}
}
}
}
return $weatherData;
}