/** * Accessor so we can lazy-parse the results. * * @param string $property The property name. * * @return mixed The value of requested property * @throws Horde_Service_Weather_Exception_InvalidProperty */ public function __get($property) { switch ($property) { case 'humidity': case 'precipitation_percent': case 'wind_gust': case 'snow_total': case 'rain_total': return false; case 'conditions': return Horde_Service_Weather_Translation::t($this->_properties->weather[0]->main); case 'is_pm': return false; case 'hour': return false; case 'date': return new Horde_Date($this->_properties->date); case 'high': return round($this->_properties->temp->day); case 'low': return round($this->_properties->temp->night); case 'icon': return $this->_forecast->weather->iconMap[str_replace('.png', '', $this->_properties->weather[0]->icon)]; case 'wind_direction': return Horde_Service_Weather::degToDirection($this->_properties->deg); case 'wind_degrees': return $this->_properties->deg; case 'wind_speed': return $this->_properties->speed; default: throw new Horde_Service_Weather_Exception_InvalidProperty('This provider does not support the "' . $property . '" property'); } }
/** */ protected function _content() { global $injector, $prefs; try { $this->_weather->autocompleteLocation('test'); } catch (Horde_Db_Exception $e) { return _("No metar station data found. Contact administrator to\n run the horde-service-weather-metar-database script."); } // Set up the weather driver. $this->_weather->units = $this->_params['units']; $units = $this->_weather->getUnits(); // Set up the view object. $view = $injector->getInstance('Horde_View'); $view->units = $units; $view->timezone = $prefs->getValue('timezone'); $view->date_format = $prefs->getValue('date_format'); $view->time_format = $prefs->getValue('time_format'); if (!empty($this->_refreshParams) && !empty($this->_refreshParams->location)) { $location = $view->requested_location = $this->_refreshParams->location; $view->instance = ''; } else { $view->instance = hash('md5', mt_rand()); $injector->getInstance('Horde_Core_Factory_Imple')->create('WeatherLocationAutoCompleter_Metar', array('id' => 'location' . $view->instance, 'instance' => $view->instance)); $view->requested_location = $this->_params['location']; $location = $this->_params['location']; } // Get the data. try { $weather = $this->_weather->getCurrentConditions($location)->getRawData(); } catch (Horde_Service_Weather_Exception $e) { $view->error = true; } $view->weather = $weather; // Station information. $station = $this->_weather->getStation(); $view->location_title = sprintf('%s, %s (%s)', $station->name, $station->country_name, $station->code); // Wind. if (isset($weather['wind'])) { if ($weather['windDirection'] == 'Variable') { if (!empty($this->_params['knots'])) { $view->wind = sprintf(_("%s at %s %s"), $weather['windDirection'], round(Horde_Service_Weather::convertSpeed($weather['wind'], $units['wind'], 'kt')), 'kt'); } else { $view->wind = sprintf(_("%s at %s %s"), $weather['windDirection'], round($weather['wind']), $units['wind']); } } elseif ($weather['windDegrees'] == '000' && $weather['wind'] == '0') { $view->wind = _("Calm"); } else { $view->wind = sprintf(_("from the %s (%s) at %s %s"), $weather['windDirection'], $weather['windDegrees'], empty($this->_params['knots']) ? round($weather['wind']) : round(Horde_Service_Weather::convertSpeed($weather['wind'], $units['wind'], 'kt')), empty($this->_params['knots']) ? $units['wind'] : 'kt'); } } // Gusts if (isset($weather['windGust'])) { if ($weather['windGust']) { if (!empty($this->_params['knots'])) { $view->wind .= sprintf(_(", gusting %s %s"), round(Horde_Service_Weather::convertSpeed($weather['windGust'], $units['wind'], 'kt')), 'kt'); } else { $view->wind .= sprintf(_(", gusting %s %s"), round($weather['windGust']), $units['wind']); } } } // Variability if (isset($weather['windVariability'])) { if ($weather['windVariability']['from']) { $view->wind .= sprintf(_(", variable from %s to %s"), $weather['windVariability']['from'], $weather['windVariability']['to']); } } // Clouds. $view->clouds = isset($weather['clouds']) ? $weather['clouds'] : array(); // Remarks. if (isset($weather['remark'])) { $view->remarks = ''; $view->other = ''; foreach ($weather['remark'] as $remark => $value) { switch ($remark) { case 'seapressure': $view->remarks .= '<br />' . _("Pressure at sea level: ") . $value . ' ' . $units['pres']; break; case 'precipitation': foreach ($value as $precip) { if (is_numeric($precip['amount'])) { $view->remarks .= '<br />' . sprintf(ngettext("Precipitation for last %d hour: ", "Precipitation for last %d hours: ", $precip['hours']), $precip['hours']) . $precip['amount'] . ' ' . $units['rain']; } else { $view->remarks .= '<br />' . sprintf(ngettext("Precipitation for last %d hour: ", "Precipitation for last %d hours: ", $precip['hours']), $precip['hours']) . $precip['amount']; } } break; case 'snowdepth': $view->remarks .= '<br />' . _("Snow depth: ") . $value . ' ' . $units['rain']; break; case 'snowequiv': $view->remarks .= '<br />' . _("Snow equivalent in water: ") . $value . ' ' . $units['rain']; break; case 'sunduration': $view->remarks .= '<br />' . sprintf(_("%d minutes"), $value); break; case '1htemp': $view->remarks .= '<br />' . _("Temp for last hour: ") . round($value) . '°' . Horde_String::upper($units['temp']); break; case '1hdew': $view->remarks .= '<br />' . _("Dew Point for last hour: ") . round($value) . '°' . Horde_String::upper($units['temp']); break; case '6hmaxtemp': $view->remarks .= '<br />' . _("Max temp last 6 hours: ") . round($value) . '°' . Horde_String::upper($units['temp']); break; case '6hmintemp': $view->remarks .= '<br />' . _("Min temp last 6 hours: ") . round($value) . '°' . Horde_String::upper($units['temp']); break; case '24hmaxtemp': $view->remarks .= '<br />' . _("Max temp last 24 hours: ") . round($value) . '°' . Horde_String::upper($units['temp']); break; case '24hmintemp': $view->remarks .= '<br />' . _("Min temp last 24 hours: ") . round($value) . '°' . Horde_String::upper($units['temp']); break; case 'sensors': foreach ($value as $sensor) { $view->remarks .= '<br />' . _("Sensor: ") . $sensor; } break; case '3hpresstend': $view->remarks .= '<br />' . sprintf(_("Pressure tendency last 3 hours: %s (%s %s)"), $value['description'], $value['presschng'], $units['pres']); break; default: $view->other .= '<br />' . $value; break; } } } // TAF if (!empty($this->_params['taf'])) { $taf = $this->_weather->getForecast($location)->getRawData(); $view->item = 0; $view->periods = array(); $view->taf = $taf; unset($view->taf['time']); foreach ($taf['time'] as $time => $entry) { $time_obj = new Horde_Date($time, 'UTC'); $period = array('time' => $time_obj); // Wind if (isset($entry['wind'])) { if ($entry['windDirection'] == 'Variable') { if (!empty($this->_params['knots'])) { $period['wind'] = sprintf(_("%s at %s %s"), strtolower($entry['windDirection']), round(Horde_Service_Weather::convertSpeed($entry['wind'], $units['wind'], 'kt')), 'kt'); } else { $period['wind'] = sprintf(_("%s at %s %s"), $entry['windDirection'], round($entry['wind']), $units['wind']); } } elseif ($entry['windDegrees'] == '000' && $entry['wind'] == '0') { $period['wind'] = _("Calm"); } else { $period['wind'] = sprintf(_("from the %s (%s) at %s %s"), $entry['windDirection'], $entry['windDegrees'], empty($this->_params['knots']) ? round($entry['wind']) : round(Horde_Service_Weather::convertSpeed($entry['wind'], $units['wind'], 'kt')), empty($this->_params['knots']) ? $units['wind'] : 'kt'); } } // Temp if (isset($entry['temperatureLow']) || isset($entry['temperatureHigh'])) { if (isset($entry['temperatureLow'])) { $period['temperatureLow'] = $entry['temperatureLow']; } if (isset($entry['temperatureHigh'])) { $period['temperatureHigh'] = $entry['temperatureHigh']; } } // Wind Shear if (isset($entry['windshear'])) { $period['shear'] = sprintf(_("from the %s (%s) at %s %s"), $entry['windshearDirection'], $entry['windshearDegrees'], $entry['windshearHeight'], $units['height']); } // Visibility if (isset($entry['visibility'])) { $period['visibility'] = strtolower($entry['visQualifier']) . ' ' . $entry['visibility'] . ' ' . $units['vis']; } // Conditions if (isset($entry['condition'])) { $period['condition'] = $entry['condition']; } // Clouds $period['clouds'] = $entry['clouds']; // FMC if (isset($entry['fmc'])) { $period['fmc'] = $entry['fmc']; $period['fmc']['clouds'] = !empty($period['fmc']['clouds']) ? $period['fmc']['clouds'] : array(); } // Set the period in the view. $view->periods[] = $period; } } if (!empty($view->instance)) { return $view->render('block/metar'); } else { return $view->render('block/metar_content'); } }
/** * 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; }
/** * Parses TAF data. * * TAF KLGA 271734Z 271818 11007KT P6SM -RA SCT020 BKN200 * FM2300 14007KT P6SM SCT030 BKN150 * FM0400 VRB03KT P6SM SCT035 OVC080 PROB30 0509 P6SM -RA BKN035 * FM0900 VRB03KT 6SM -RA BR SCT015 OVC035 * TEMPO 1215 5SM -RA BR SCT009 BKN015 * BECMG 1517 16007KT P6SM NSW SCT015 BKN070 * * @param array $data The TAF encoded weather data, spilt on line endings. * * @return array An array of forecast data. Keys include: * - station: (string) The station identifier. * - dataRaw: (string) The raw TAF data. * - update: (timestamp) Timestamp of last update. * - validFrom: (Horde_Date) The valid FROM time. * - validTo: (Horde_Date) The valid TO time. * - time: (array) An array of Horde_Service_Weather_Period objects for * each available valid time provided by the TAF report. */ protected function _parse(array $data) { $tafCode = $this->_getTafCodes(); // 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; } } // Ok, we have correct data, start with parsing the first line for the last update $forecastData = array(); $forecastData['station'] = ''; $forecastData['dataRaw'] = implode(' ', $data); $forecastData['update'] = strtotime(trim($data[0]) . ' GMT'); $forecastData['updateRaw'] = trim($data[0]); // and prepare the rest for stepping through array_shift($data); $taf = explode(' ', preg_replace('/\\s{2,}/', ' ', implode(' ', $data))); // The timeperiod the data gets added to $fromTime = ''; // If we have FMCs (Forecast Meteorological Conditions), we need this $fmcCount = 0; // Pointer to the array we add the data to $pointer =& $forecastData; for ($i = 0; $i < sizeof($taf); $i++) { $taf[$i] = trim($taf[$i]); if (!strlen($taf[$i])) { continue; } // Init $result = array(); $resultVF = array(); $lresult = array(); $found = false; foreach ($tafCode as $key => $regexp) { // Check if current code matches current taf snippet if (($found = preg_match('/^' . $regexp . '$/i', $taf[$i], $result)) == true) { $insert = array(); switch ($key) { case 'station': $pointer['station'] = $result[0]; unset($tafCode['station']); break; case 'valid': $pointer['validRaw'] = $result[0]; // Generates the timeperiod the report is valid for list($year, $month, $day) = explode('-', gmdate('Y-m-d', $forecastData['update'])); // Date is in next month if ($result[1] < $day) { $month++; } $pointer['validFrom'] = new Horde_Date(array('hour' => $result[2], 'month' => $month, 'mday' => $result[1], 'year' => $year), 'GMT'); $pointer['validTo'] = new Horde_Date(array('hour' => $result[4], 'month' => $month, 'mday' => $result[3], 'year' => $year), 'GMT'); unset($tafCode['valid']); // Now the groups will start, so initialize the time groups $pointer['time'] = array(); $start_time = new Horde_Date(array('year' => $year, 'month' => $month, 'mday' => $result[1], 'hour' => $result[2]), 'UTC'); $fromTime = (string) $start_time; $pointer['time'][$fromTime] = array(); // Set pointer to the first timeperiod $pointer =& $pointer['time'][$fromTime]; break; case 'wind': if ($result[5] == 'KTS') { $result[5] = 'KT'; } $pointer['wind'] = round(Horde_Service_Weather::convertSpeed($result[2], $result[5], $this->_unitMap[self::UNIT_KEY_SPEED])); if ($result[1] == 'VAR' || $result[1] == 'VRB') { $pointer['windDegrees'] = Horde_Service_Weather_Translation::t('Variable'); $pointer['windDirection'] = Horde_Service_Weather_Translation::t('Variable'); } else { $pointer['windDegrees'] = $result[1]; $pointer['windDirection'] = Horde_Service_Weather::degToDirection($result[1]); } if (is_numeric($result[4])) { $pointer['windGust'] = round(Horde_Service_Weather::convertSpeed($result[4], $result[5], $this->_unitMap[self::UNIT_KEY_SPEED])); } if (isset($probability)) { $pointer['windProb'] = $probability; unset($probability); } unset($tafCode['wind']); break; case 'visFrac': // Possible fractional visibility here. // Check if it matches with the next TAF piece for visibility if (!isset($taf[$i + 1]) || !preg_match('/^' . $tafCode['visibility'] . '$/i', $result[1] . ' ' . $taf[$i + 1], $resultVF)) { // No next TAF piece available or not matching. $found = false; break; } // Match. Hand over result and advance TAF $key = 'visibility'; $result = $resultVF; $i++; // Fall through // Fall through case 'visibility': $pointer['visQualifier'] = Horde_Service_Weather_Translation::t('AT'); if (is_numeric($result[1]) && $result[1] == 9999) { // Upper limit of visibility range $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'); } if (isset($probability)) { $pointer['visProb'] = $probability; unset($probability); } $pointer['visibility'] = $visibility; break; case 'condition': // First some basic setups 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']); if (isset($probability)) { $pointer['condition'] .= ' (' . $probability . '% ' . Horde_Service_Weather_Translation::t('probability') . ').'; unset($probability); } 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])]); } if (isset($probability)) { $cloud['prob'] = $probability; unset($probability); } $pointer['clouds'][] = $cloud; break; case 'windshear': // Parse windshear, if available if ($result[4] == 'KTS') { $result[4] = 'KT'; } $pointer['windshear'] = round(Horde_Service_Weather::convertSpeed($result[3], $result[4], $this->_unitMap[self::UNIT_KEY_SPEED])); $pointer['windshearHeight'] = $result[1] * 100; $pointer['windshearDegrees'] = $result[2]; $pointer['windshearDirection'] = Horde_Service_Weather::degToDirection($result[2]); break; case 'tempmax': $forecastData['temperatureHigh'] = Horde_Service_Weather::convertTemperature($result[1], 'c', $this->_unitMap[self::UNIT_KEY_TEMP]); break; case 'tempmin': // Parse max/min temperature $forecastData['temperatureLow'] = Horde_Service_Weather::convertTemperature($result[1], 'c', $this->_unitMap[self::UNIT_KEY_TEMP]); break; case 'tempmaxmin': $forecastData['temperatureHigh'] = Horde_Service_Weather::convertTemperature($result[1], 'c', $this->_unitMap[self::UNIT_KEY_TEMP]); $forecastData['temperatureLow'] = Horde_Service_Weather::convertTemperature($result[4], 'c', $this->_unitMap[self::UNIT_KEY_TEMP]); break; case 'from': // Next timeperiod is coming up, prepare array and // set pointer accordingly $fromTime = clone $start_time; if (sizeof($result) > 2) { // The ICAO way $fromTime->hour = $result[2]; $fromTime->min = $result[3]; } else { // The Australian way (Hey mates!) $fromTime->hour = $result[1]; } if ($start_time->compareDateTime($fromTime) >= 1) { $fromTime->mday++; } $fromTime = (string) $fromTime; $forecastData['time'][$fromTime] = array(); $fmcCount = 0; $pointer =& $forecastData['time'][$fromTime]; break; case 'fmc': // Test, if this is a probability for the next FMC if (isset($result[2]) && preg_match('/^BECMG|TEMPO$/i', $taf[$i + 1], $lresult)) { // Set type to BECMG or TEMPO $type = $lresult[0]; // Set probability $probability = $result[2]; // Now extract time for this group if (preg_match('/^(\\d{2})(\\d{2})$/i', $taf[$i + 2], $lresult)) { $from = clone $start_time; $from->hour = $lresult[1]; if ($start_time->compareDateTime($from) >= 1) { $from->mday++; } $to = clone $from; $to->hour = $lresult[2]; if ($start_time->compareDateTime($to) >= 1) { $to->mday++; } // As we now have type, probability and time for this FMC // from our TAF, increase field-counter $i += 2; } else { // No timegroup present, so just increase field-counter by one $i += 1; } } elseif (preg_match('/^(\\d{2})(\\d{2})\\/(\\d{2})(\\d{2})$/i', $taf[$i + 1], $lresult)) { // Normal group, set type and use extracted time $type = $result[1]; // Check for PROBdd if (isset($result[2])) { $probability = $result[2]; } $from = clone $start_time; $from->hour = $lresult[2]; if ($start_time->compareDateTime($from) >= 1) { $from->mday++; } $to = clone $from; $to->hour = $lresult[4]; if ($start_time->compareDateTime($to) >= 1) { $to->mday++; } // Same as above, we have a time for this FMC from our TAF, // increase field-counter $i += 1; } elseif (isset($result[2])) { // This is either a PROBdd or a malformed TAF with missing timegroup $probability = $result[2]; } // Handle the FMC, generate neccessary array if it's the first... if (isset($type)) { if (!isset($forecastData['time'][$fromTime]['fmc'])) { $forecastData['time'][$fromTime]['fmc'] = array(); } $forecastData['time'][$fromTime]['fmc'][$fmcCount] = array(); // ...and set pointer. $pointer =& $forecastData['time'][$fromTime]['fmc'][$fmcCount]; $fmcCount++; // Insert data $pointer['type'] = $type; unset($type); if (isset($from)) { $pointer['from'] = $from; $pointer['to'] = $to; unset($from, $to); } if (isset($probability)) { $pointer['probability'] = $probability; unset($probability); } } break; default: // Do nothing break; } if ($found) { break; } } } } return $forecastData; }