Version: current

      1 <?php
      2
      3 /**
      4  * Модуль получения погоды с сервиса eurometeo
      5  * Получает погоду через xml сервис с сайта eurometeo.ru, кеширует данные в течении указаного времени (10 часов)
      6  * @todo в настоящий момент на сайте может быть информер только по одному городу
      7  *
      8  * @author dvp
      9  */
     10 class PageInformerWeather extends Extension {
     11
     12     /**
     13      * базовый адрес для запроса данных
     14      */
     15     protected static $api_base_url = "http://www.eurometeo.ru/";
     16
     17     /**
     18      * ID города в eurometeo
     19      */
     20     protected static $city_id = 'russia/kareliya/petrozavodsk';
     21
     22     /**
     23      * Время кеширования в секундах
     24      */
     25     protected static $expire = 7200; // 2 * 3600
     26
     27     /**
     28      * Каталог с картинками
     29      */
     30     protected static $ImageSrc = 'informer_weather/images';
     31
     32     /**
     33      * Расширение картинок
     34      */
     35     protected static $ImageExt = '.png';
     36
     37     /**
     38      * Какие точки времени показывать
     39      */
     40     protected static $hours_to_show = array(4, 16);
     41
     42     /**
     43      * Трансляция id rp5 в id eurometeo
     44      * Нужна для старых сайтов
     45      */
     46     protected static $rp5_eurometeo = array(
     47         6601 => 'russia/kareliya/petrozavodsk',
     48         1461 => 'russia/kareliya/belomorskiy-rayon/belomorsk',
     49         75485 => 'russia/leningradskaya-oblast/podporozhskiy-rayon/podporozhe',
     50         4967 => 'russia/leningradskaya-oblast/lodeynopolskiy-rayon/lodeynoe-pole',
     51         6942 => 'russia/kareliya/pudozhskiy-rayon/pudozh',
     52     );
     53
     54     /**
     55      * Кешированные данные погоды
     56      */
     57     protected $weather = false;
     58
     59     /**
     60      * Кешированные имена города
     61      */
     62     protected $cityName = array();
     63
     64     protected $cityId = false;
     65
     66     /**
     67      * Устанавливает ID города
     68      * вызов в конфиге: PageInformerWeather::SetCityId('russia/region/city');
     69      *
     70      * @param $id   int ID города
     71      */
     72     public static function SetCityId($id) {
     73         if (preg_match('/^\d+$/', $id)) {
     74             if (!isset(self::$rp5_eurometeo[$id])) die("Invalid old rp5 city id: '$id'");
     75             $id = self::$rp5_eurometeo[$id];
     76         }
     77         self::$city_id = $id;
     78     }
     79
     80     /**
     81      * Устанавливает время жизни кеша
     82      * вызов в конфиге: PageInformerWeather::SetExpire(2);
     83      *
     84      * @param $hours    int время жизни кеша в часах
     85      */
     86     public static function SetExpire($hours) {
     87         self::$expire = $hours * 3600;
     88     }
     89
     90     /**
     91      * Устанавливает базовый каталог для картинок погоды
     92      * вызов в конфиге: PageInformerWeather::SetImagePath('site/images/weather');
     93      *
     94      * @param   $path   string  Путь к картинкам относительно корня сайта (без ведущего /)
     95      */
     96     public static function SetImagePath($path) {
     97         self::$ImageSrc = $path;
     98     }
     99
    100     /**
    101      * Устанавливает расширение для картинок
    102      * вызов в конфиге: PageInformerWeather::SetImageExt('.gif');
    103      *
    104      * @param $ext  string  строка с расширением файлов (с .)
    105      */
    106     public static function SetImageExt($ext) {
    107         if (strpos($ext, '.') === false)
    108             $ext = '.' . $ext;
    109         self::$ImageExt = $ext;
    110     }
    111
    112     /**
    113      * Использовать 4 показания в день (ночь, утро, день, вечер), а не 2 (ночь, день)
    114      *
    115      * @param boolean $val флаг показывать 4 или 2 [true]
    116      */
    117     public static function ShowFullDay($val=true) {
    118         self::$hours_to_show = ($val) ? array(4, 10, 16, 22) : array(4, 16);
    119     }
    120
    121     /**
    122      * Возвращает данные погодного информера сгруппированные по дням
    123      * Формат каждого дня:
    124      *  $Date - дата этого дня (объект)
    125      *  $Items - список показаний за этот день
    126      * Формат каждого показания:
    127      *  $Date - дата/время показания
    128      *  $Hour - час показания (сейчас это 4 и 16)
    129      *  $DayTimeName - локализованное название времени суток
    130      *  $CloudCover - облачность в %
    131      *  $CloudLevel - уровень облачности для визаулизации (наш)
    132      *  $CloudName - текстовое описание облачности
    133      *  $CloudImageUrl - url картинки с облачностью, типа informer_weather/images/c<d|n><level>.png, где: <d> - дневная, <n> - ночная, <level> - $CloudLevel
    134      *  $Pressure - давление, мм.р.ст.
    135      *  $Temperature - температура, С
    136      *  $Humidity - влажность, %
    137      *  $Precipitation - кол-во осадков, мм за период между показаниями (3 часа)
    138      *  $Drops - кол-во капель для визуализации 0, 0.5, 1, 2, 3, 4, 5, 6, 7, 8, 9
    139      *  $FallsType - тип осадков 0 - без осадков, 1 - дождь, 2 - дождь + снег, 3 - снег
    140      *  $FallsName - локализованное название осадков
    141      *  $FallsLevel - уровень осадков для визуализации (наш)
    142      *  $FallsImageUrl - url картинки осадков, типа informer_weather/images/o<type>d<level>.png, где: <type> - $FallsType, <level> - $FallsLevel
    143      *  $WindDirection - направление ветра (локализовано)
    144      *  $WindVelocity - скорость ветра, м/с
    145      *  $WindGust - скорость порывов ветра, м/с
    146      *
    147      * @param $days int Кол-во дней в прогнозе [2] максимум 3
    148      * @param $cityId   string  город (если не указан - берется из конфига)
    149      * @return DataObjectSet - данные погоды или false если их не получить
    150      */
    151     public function WeatherInfo($days=2, $cityId = false) {
    152         if ($days > 3)
    153             $days = 3;
    154
    155         if (!$cityId)
    156             $cityId = self::$city_id;
    157
    158         if ($cityId != $this->cityId) {
    159             $this->cityId = $cityId;
    160             $this->weather = false;
    161         }
    162
    163         if (!$this->_getWeather())
    164             return false;
    165
    166         $res = new DataObjectSet();
    167         $i = 0;
    168         foreach ($this->weather->GroupBy('Day') as $day => $data) {
    169             $dt = new Date();
    170             $dt->setValue($day);
    171             $res->push(new ArrayData(array(
    172                 'CityName0' => $this->cityName[0],
    173                 'CityName1' => $this->cityName[1],
    174                 'Date' => $dt,
    175                 'Items' => $data
    176             )));
    177             $i++;
    178             if ($i >= $days) break;
    179         }
    180         return $res;
    181     }
    182
    183     /**
    184      * Возвращает id города
    185      *
    186      * @return string - id города на сайте eurometeo.ru
    187      */
    188     public function CityID() {
    189         return self::$city_id;
    190     }
    191
    192     /**
    193      * Возвращает ссылку на прогноз погоды на сайте источнике
    194      *
    195      * @return string
    196      */
    197     public function CityLink() {
    198         return self::$api_base_url . self::$city_id . '/';
    199     }
    200
    201     /**
    202      * Возвращает название города
    203      * Работает только после вызова WeatherInfo()
    204      *
    205      * @param $index    int номер названия 0 - именительный падеж, 1 - родительный
    206      * @return string   строка с названием
    207      */
    208     public function CityName($index = 0) {
    209         if (isset($this->cityName[$index]))
    210             return $this->cityName[$index];
    211
    212         return false;
    213     }
    214
    215     /**
    216      * Получение данных с eurometeo с кешированием
    217      * Производит запрос данных с eurometeo с помощью RestfulService, преобразует данные в наш формат и кеширует данные на стороне сайта
    218      * Даже при новом запросе к eurometeo подклеивает старые данные (т.к. данные за сегодняшную ночь уже могут отсутсвовать на eurometeo)
    219      * @todo локализация имени города
    220      *
    221      * @return boolean  флаг успешности получения данных
    222      */
    223     private function _getWeather() {
    224         // данные уже считаны в ОП
    225         if ($this->weather)
    226             return true;
    227
    228         // вытаскиваем данные из кеша всегда
    229         $cache_path = TEMP_FOLDER . '/cache_weather_' . md5($this->cityId . join(':', self::$hours_to_show)) . '_' . i18n::get_locale();
    230         if (@file_exists($cache_path)) {
    231             $buffer = unserialize(file_get_contents($cache_path));
    232             $this->weather = $buffer['weather'];
    233             $this->cityName = $buffer['cityName'];
    234             unset($buffer);
    235         }
    236         else {
    237             $this->weather = new DataObjectSet();
    238         }
    239
    240         // использовать кешированные данные?
    241         if($this->weather->Count() > 0 && !isset($_GET['flush']) && @file_exists($cache_path) && @filemtime($cache_path) + self::$expire > time()) {
    242             return true;
    243         }
    244
    245         // выкинуть данные до сегодня
    246         $now = date('Y-m-d');
    247         while ($this->weather->Count()) {
    248             $row = $this->weather->First();
    249             if ($row->Date->Format('Y-m-d') == $now)
    250                 break;
    251             $this->weather->remove($row);
    252         }
    253
    254         // получить данные от поставщика
    255         $server = new RestfulService(self::$api_base_url, self::$expire);
    256         try {
    257             $conn = $server->request($this->cityId . '/export/xml/data/');
    258             if (!$conn)
    259                 throw new Exception("Can't connect to: ". $this->cityId . '/export/xml/data/');
    260
    261             $data = $server->getValues($conn->getBody(), 'city', 'step');
    262             $this->cityName[0] = $server->getValue($conn->getBody(), 'city', 'cityname');
    263             $this->cityName[1] = $server->getValue($conn->getBody(), 'city', 'cityname2');
    264         } catch (Exception $e) {
    265             //user_error("Error occurred in processing Weather response");
    266             $this->weather = false;
    267             return false;
    268         }
    269
    270         if (!$data) {
    271             $this->weather = false;
    272             return false;
    273         }
    274
    275         // вытаскиваем данные за нужное время
    276         foreach ($data as $row) {
    277             $dt = new SS_Datetime();
    278             $dt->setValue($row->datetime);
    279             if (array_search($dt->Format('G'), self::$hours_to_show) === false) continue;
    280
    281             $item = array(
    282                 'ID' => strtotime($row->datetime),
    283                 'Date' => $dt,
    284                 'CloudCover' => round($row->cloudcover),
    285                 'CloudLevel' => $this->_cloudCover2Level($row->cloudcover),
    286                 'Pressure' => round($row->pressure),
    287                 'Temperature' => round($row->temperature),
    288                 'Humidity' => round($row->humidity),
    289                 'Precipitation' => $row->precipitation,
    290                 'FallsType' => $this->_fallsType($row->temperature, $row->precipitation),
    291                 'Drops' => $this->_dropsLevel($row->precipitation),
    292                 'FallsLevel' => $this->_fallsLevel($row->precipitation),
    293                 'WindDirection' => $this->_windDirection($row->winddir, $row->windspeed),
    294                 'WindVelocity' => round($row->windspeed),
    295                 'WindGust' => round($row->windgust),
    296             );
    297
    298             $item['FallsName'] = $this->_fallsName($item['FallsType']);
    299
    300             $item['Hour'] = date('G', $item['ID']);
    301             $item['DayTimeName'] = _t('WEATHER.DAYTIME_'.$item['Hour'], $item['Hour']);
    302             $item['Day'] = date('Y-m-d', $item['ID']);
    303             $item['CloudName'] = $this->_cloudName($item['CloudLevel'], $item['CloudCover']);
    304             $item['CloudImageUrl'] = $this->_getCloudImage($item['CloudLevel'], $item['Hour']);
    305             $item['FallsImageUrl'] = $this->_getFallsImage($item['FallsType'], $item['FallsLevel']);
    306
    307             $this->weather->push(new ArrayData($item), $item['ID']);
    308         }
    309
    310         if (!$this->weather->Count()) {
    311             $this->weather = false;
    312             return false;
    313         }
    314
    315         file_put_contents($cache_path, serialize(array(
    316             'weather' => $this->weather,
    317             'cityName' => $this->cityName,
    318         )));
    319         return true;
    320     }
    321
    322     /**
    323      * Переводит процент облачности в наши баллы
    324      *
    325      * @param $percent  int процент облачности
    326      * @return int  уровень облачности (0-7)
    327      */
    328     private function _cloudCover2Level($percent) {
    329         if ($percent > 84)
    330             return 7;
    331         return floor($percent / 12);
    332     }
    333
    334     /**
    335      * Переводит уровень осадков в drops rp5
    336      *
    337      * @param <type> $falls
    338      *
    339      * @return <type>
    340      */
    341     protected function _dropsLevel($falls) {
    342         if ($falls == 0) return '0';
    343         if ($falls <= 0.5) return 0.5;
    344         if ($falls <= 1) return 1;
    345         if ($falls <= 1.5) return 2;
    346         if ($falls <= 2) return 3;
    347         if ($falls <= 2.5) return 4;
    348         if ($falls <= 3) return 5;
    349         if ($falls <= 3.5) return 6;
    350         if ($falls <= 4) return 7;
    351         return 8;
    352     }
    353
    354     /**
    355      * Переводит уровень осдаков в наш уровень визуализации
    356      * @todo огрубить кол-во уровней
    357      *
    358      * @param $falls    float   уровень осдаков
    359      * @return int  наш уровень осдаков (0-9)
    360      */
    361     protected function _fallsLevel($falls) {
    362         if ($falls == 0) return '0';
    363         if ($falls <= 1) return 1;
    364         if ($falls <= 1.5) return 2;
    365         if ($falls <= 2) return 3;
    366         if ($falls <= 2.5) return 4;
    367         if ($falls <= 3) return 5;
    368         if ($falls <= 3.5) return 6;
    369         if ($falls <= 4) return 7;
    370         return 8;
    371     }
    372
    373     /**
    374      * Локализует направление ветра !!!
    375      *
    376      * @param $dir  int строка с направлением ветра в градусах
    377      * @param $speed    float   скорость ветра м/с
    378      * @return string   локализованное направление ветра
    379      */
    380     protected function _windDirection($dir, $speed) {
    381         $code = 'NONE';
    382         if ($speed > 0.2) {
    383             if ($dir < 22.5) $code = 'N';
    384             elseif ($dir < 67.5) $code = 'NE';
    385             elseif ($dir < 112.5) $code = 'E';
    386             elseif ($dir < 157.5) $code = 'SE';
    387             elseif ($dir < 202.5) $code = 'S';
    388             elseif ($dir < 247.5) $code = 'SW';
    389             elseif ($dir < 292.5) $code = 'W';
    390             elseif ($dir < 337.5) $code = 'NW';
    391             else $code = 'N';
    392         }
    393
    394         return _t('WEATHER.WIND_DIRECTION_'.$code, $code);
    395     }
    396
    397     /**
    398      * Фантазирует на тему типа осадков (на eurometeo его нет)
    399      *
    400      * @param float $temp - температура
    401      * @param float $falls - кол-во осадков
    402      *
    403      * @return int
    404      */
    405     protected function _fallsType($temp, $falls) {
    406         if ($falls == 0 ) return 0;
    407         if ($temp > 1) return 1;
    408         if ($temp < -1) return 3;
    409         return 2;
    410     }
    411
    412     /**
    413      * Локализованное название типа осадков
    414      *
    415      * @param $type int тип осадков rp5
    416      * @return string   название типа осадков
    417      */
    418     protected function _fallsName($type) {
    419         return _t('WEATHER.FALLS_TYPE_'.$type, $type);
    420     }
    421
    422     /**
    423      * Локализованное название облачности
    424      *
    425      * @param $level    int уровень облачности в наших баллах
    426      * @param $percent  int порцент покрытия облачности
    427      * @return string   название облачности
    428      */
    429     protected function _cloudName($level, $percent) {
    430         return _t('WEATHER.CLOUD_LEVEL_'.$level, sprintf('%d %%', $percent));
    431     }
    432
    433     /**
    434      * Формирует url картинки облачности
    435      *
    436      * @param $level    int уровень облачности
    437      * @param $hour     int час показаний (для времени суток)
    438      * @return string   url картинки относительно корня сайта (без ведущего /)
    439      */
    440     protected function _getCloudImage($level, $hour) {
    441         return self::$ImageSrc . '/c' . (($hour > 8 && $hour < 20) ? 'd' : 'n') . $level . self::$ImageExt;
    442     }
    443
    444     /**
    445      * Формирует url картинки осадков
    446      *
    447      * @param $type     int тип осадков
    448      * @param $level    int уровень осадков
    449      * @return string   url картинки относительно корня сайта (без ведущего /)
    450      */
    451     protected function _getFallsImage($type, $level) {
    452         return self::$ImageSrc . '/o' . $type . 'd' . $level . self::$ImageExt;
    453     }
    454 }
    455
    456

[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -