├── .gitignore ├── phpunit.xml ├── meteo-alarm-weather-warnings.png ├── bootstrap.php ├── src ├── Description.php ├── Result.php ├── Warning.php ├── Config.php ├── Period.php ├── AwarenessType.php ├── Warnings.php ├── Awareness.php ├── AwarenessLevel.php ├── warnings.json └── Region.php ├── LICENSE ├── test ├── DescriptionTest.php ├── AwarenessTest.php ├── WarningTest.php ├── PeriodTest.php ├── AwarenessLevelTest.php ├── AwarenessTypeTest.php ├── RegionTest.php └── WarningsTest.php ├── AutoLoader.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /meteo-alarm-weather-warnings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waynedgrant/meteo-alarm-weather-warnings/HEAD/meteo-alarm-weather-warnings.png -------------------------------------------------------------------------------- /bootstrap.php: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /src/Description.php: -------------------------------------------------------------------------------- 1 | A spell of heavy snow is possible over parts of Scotland during Sunday. 10 | if (trim($description_cell->textContent) !== '') { 11 | $this->text = $description_cell->textContent; 12 | } 13 | } 14 | 15 | public function text() { 16 | return $this->text; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Result.php: -------------------------------------------------------------------------------- 1 | warnings = new Warnings($rss); 11 | } 12 | 13 | private function createServiceUrl() { 14 | return 'http'.(!empty($_SERVER['HTTPS'])?'s':'').'://'.$_SERVER['SERVER_NAME'].$_SERVER['REQUEST_URI']; 15 | } 16 | 17 | public function serialize() { 18 | return [ 19 | 'endpoint' => [ 20 | 'url' => $this->createServiceUrl(), 21 | 'version' => self::SERVICE_VERSION, 22 | 'github_project' => self::GITHUB_PROJECT_LINK, 23 | 'copyright' => self::COPYRIGHT_NOTICE 24 | ], 25 | 'warnings' => $this->warnings->serialize() 26 | ]; 27 | } 28 | } 29 | ?> 30 | -------------------------------------------------------------------------------- /src/Warning.php: -------------------------------------------------------------------------------- 1 | ... awareness ... ... period ... 10 | $awareness_cell = $awarness_period_row->getElementsByTagName('td')->item(0); 11 | $this->awareness = new Awareness($awareness_cell); 12 | 13 | $period_cell = $awarness_period_row->getElementsByTagName('td')->item(1); 14 | $this->period = new Period($period_cell); 15 | 16 | // e.g. ... description ... 17 | $description_cell = $description_row->getElementsByTagName('td')->item(1); 18 | $this->description = new Description($description_cell); 19 | } 20 | 21 | public function serialize() { 22 | return [ 23 | 'awareness' => $this->awareness->serialize(), 24 | 'period' => $this->period->serialize(), 25 | 'description' => $this->description->text 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Wayne D Grant 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/DescriptionTest.php: -------------------------------------------------------------------------------- 1 | ' . 11 | 'A spell of heavy snow is possible over parts of Scotland during Sunday.' . 12 | ''; 13 | 14 | $description_dom = DOMDocument::loadHTML($description_html); 15 | $description_cell = $description_dom->getElementsByTagName('td')->item(0); 16 | 17 | $testee = new Description($description_cell); 18 | 19 | $this->assertSame('A spell of heavy snow is possible over parts of Scotland during Sunday.', $testee->text()); 20 | } 21 | 22 | public function test_no_description_present() { 23 | $description_html = ''; 24 | 25 | $description_dom = DOMDocument::loadHTML($description_html); 26 | $description_cell = $description_dom->getElementsByTagName('td')->item(0); 27 | 28 | $testee = new Description($description_cell); 29 | 30 | $this->assertNull($testee->text()); 31 | } 32 | } 33 | 34 | ?> 35 | -------------------------------------------------------------------------------- /src/Config.php: -------------------------------------------------------------------------------- 1 | 47 | -------------------------------------------------------------------------------- /AutoLoader.php: -------------------------------------------------------------------------------- 1 | isDir() && !$file->isLink() && !$file->isDot()) { 16 | self::registerDirectory($file->getPathname()); 17 | } 18 | elseif (substr($file->getFilename(), -4) === '.php') { 19 | $className = substr($file->getFilename(), 0, -4); 20 | AutoLoader::registerClass($className, $file->getPathname()); 21 | } 22 | } 23 | } 24 | 25 | public static function registerClass($className, $fileName) { 26 | AutoLoader::$classNames[$className] = $fileName; 27 | } 28 | 29 | public static function loadClass($className) { 30 | if (isset(AutoLoader::$classNames[$className])) { 31 | require_once(AutoLoader::$classNames[$className]); 32 | } 33 | } 34 | 35 | } 36 | 37 | spl_autoload_register(array('AutoLoader', 'loadClass')); 38 | 39 | ?> 40 | -------------------------------------------------------------------------------- /src/Period.php: -------------------------------------------------------------------------------- 1 | From: 10.12.2017 05:00 CET Until: 11.12.2017 00:55 CET 12 | or if there is no period, i.e. when awareness level == NO_WARNINGS */ 13 | 14 | if (trim($period_cell->textContent) !== '') { 15 | $from_date_text = $period_cell->getElementsByTagName('i')->item(0)->textContent; 16 | $until_date_text = $period_cell->getElementsByTagName('i')->item(1)->textContent; 17 | 18 | $this->from = $this->parseDateTime($from_date_text); 19 | $this->until = $this->parseDateTime($until_date_text); 20 | } 21 | } 22 | 23 | private function parseDateTime($date_text) { 24 | $date = date_create_from_format(self::DATE_TIME_FORMAT, $date_text); 25 | date_timezone_set($date, new DateTimeZone(Config::getTimeZone())); 26 | return $date->format(Config::getDateTimeFormat()); 27 | } 28 | 29 | public function serialize() { 30 | return [ 31 | 'from' => $this->from, 32 | 'until' => $this->until 33 | ]; 34 | } 35 | } 36 | 37 | ?> 38 | -------------------------------------------------------------------------------- /src/AwarenessType.php: -------------------------------------------------------------------------------- 1 | = sizeof($this->descriptions)) { 42 | throw new InvalidArgumentException('Awareness type ' . $type . ' is not supported.'); 43 | } 44 | 45 | $this->type = $type; 46 | $this->description = $this->descriptions[$type]; 47 | } 48 | 49 | public function serialize() { 50 | return [ 51 | 'type' => $this->type, 52 | 'description' => $this->description 53 | ]; 54 | } 55 | } 56 | 57 | ?> 58 | -------------------------------------------------------------------------------- /src/Warnings.php: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | United Kingdom 14 | ... 15 | 16 | 17 | Dumfries and Galloway 18 | ... 19 | 20 | 21 | Lothian & Borders 22 | ... 23 | 24 | 25 | */ 26 | 27 | $rss_channel = $rss->channel; 28 | $this->link = (string)$rss_channel->link; 29 | 30 | if (count($rss_channel->item) > 1) { // Mutiple items == warnings for each region in a country 31 | for ($i=0; $i < count($rss_channel->item); $i++) { 32 | if ($i !== 0) { // Drop the first item which is actually for the entire country 33 | $this->regions[] = new Region($rss_channel->item[$i]); 34 | } 35 | } 36 | } else { // Single item == warning for a single region 37 | $this->regions[] = new Region($rss_channel->item); 38 | } 39 | } 40 | 41 | public function serialize() { 42 | foreach ($this->regions as $region) { 43 | $regions[] = $region->serialize(); 44 | } 45 | 46 | return [ 47 | 'country' => Config::getCountry(), 48 | 'regions' => $regions 49 | ]; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/AwarenessTest.php: -------------------------------------------------------------------------------- 1 | ' . 11 | 'awt:2 level:3' . 12 | ''; 13 | 14 | $awareness_dom = DOMDocument::loadHTML($awareness_html); 15 | $awareness_cell = $awareness_dom->getElementsByTagName('td')->item(0); 16 | 17 | $testee = new Awareness($awareness_cell); 18 | $serialized = $testee->serialize(); 19 | 20 | $this->assertSame('http://web.meteoalarm.eu/documents/rss/wflag-l3-t2.jpg', $serialized['icon']); 21 | $this->assertSame(AwarenessType::SNOW_ICE, $serialized['awareness_type']['type']); 22 | $this->assertSame(AwarenessLevel::AMBER, $serialized['awareness_level']['level']); 23 | } 24 | 25 | public function test_no_warning() { 26 | // Any awareness type other than 'None' makes no sense when level is 'Green' 27 | $awareness_html = 28 | '' . 29 | 'awt:3 level:1' . 30 | ''; 31 | 32 | $awareness_dom = DOMDocument::loadHTML($awareness_html); 33 | $awareness_cell = $awareness_dom->getElementsByTagName('td')->item(0); 34 | 35 | $testee = new Awareness($awareness_cell); 36 | $serialized = $testee->serialize(); 37 | 38 | $this->assertSame('http://web.meteoalarm.eu/documents/rss/wflag-l1-t3.jpg', $serialized['icon']); 39 | $this->assertSame(AwarenessType::NO_WARNINGS, $serialized['awareness_type']['type']); 40 | $this->assertSame(AwarenessLevel::GREEN, $serialized['awareness_level']['level']); 41 | } 42 | } 43 | 44 | ?> 45 | -------------------------------------------------------------------------------- /src/Awareness.php: -------------------------------------------------------------------------------- 1 | awt:4 level:3 10 | $img = $awareness_cell->getElementsByTagName('img')->item(0); 11 | 12 | $this->icon = $img->getAttribute('src'); 13 | 14 | $img_alt = $img->getAttribute('alt'); 15 | 16 | $this->parse_awareness_level($img_alt); 17 | $this->parse_awareness_type($img_alt); 18 | } 19 | 20 | private function parse_awareness_level($img_alt) { 21 | // e.g. level value from "awt:4 level:3" 22 | $start_awareness_level_index = strrpos($img_alt, ':') + 1; 23 | $awareness_level_length = strlen($img_alt) - $start_awareness_level_index; 24 | $this->awarenessLevel = new AwarenessLevel(intval(substr($img_alt, $start_awareness_level_index, $awareness_level_length))); 25 | } 26 | 27 | private function parse_awareness_type($img_alt) { 28 | if ($this->awarenessLevel->serialize()['level'] == AwarenessLevel::GREEN) { 29 | $this->awarenessType = new AwarenessType(AwarenessType::NO_WARNINGS); // Awareness types need to be corrected to be 'no warnings' when level is green 30 | } else { 31 | // e.g. awt value from "awt:4 level:3" 32 | $start_awareness_type_index = strpos($img_alt, ':') + 1; 33 | $awareness_type_length = strpos($img_alt, ' ') - $start_awareness_type_index; 34 | $this->awarenessType = new AwarenessType(intval(substr($img_alt, $start_awareness_type_index, $awareness_type_length))); 35 | } 36 | } 37 | 38 | public function serialize() { 39 | return [ 40 | 'icon' => $this->icon, 41 | 'awareness_type' => $this->awarenessType->serialize(), 42 | 'awareness_level' => $this->awarenessLevel->serialize() 43 | ]; 44 | } 45 | } 46 | 47 | ?> 48 | -------------------------------------------------------------------------------- /test/WarningTest.php: -------------------------------------------------------------------------------- 1 | ' . 16 | '' . 17 | 'awt:2 level:3' . 18 | '' . 19 | '' . 20 | 'From: 10.12.2017 05:00 CET Until: 11.12.2017 00:55 CET' . 21 | '' . 22 | ''; 23 | 24 | $awarness_period_dom = DOMDocument::loadHTML($awarness_period_html); 25 | $awarness_period_row = $awarness_period_dom->getElementsByTagName('tr')->item(0); 26 | 27 | $description_html = 28 | '' . 29 | '' . 30 | '' . 31 | '' . 32 | 'A spell of heavy snow is possible over parts of Scotland during Sunday.' . 33 | '' . 34 | ''; 35 | 36 | $description_dom = DOMDocument::loadHTML($description_html); 37 | $description_row = $description_dom->getElementsByTagName('tr')->item(0); 38 | 39 | $testee = new Warning($awarness_period_row, $description_row); 40 | $serialized = $testee->serialize(); 41 | 42 | $this->assertSame(AwarenessType::SNOW_ICE, $serialized['awareness']['awareness_type']['type']); 43 | $this->assertSame(AwarenessLevel::AMBER, $serialized['awareness']['awareness_level']['level']); 44 | $this->assertSame('Sunday 04:00 GMT', $serialized['period']['from']); 45 | $this->assertSame('Sunday 23:55 GMT', $serialized['period']['until']); 46 | $this->assertSame('A spell of heavy snow is possible over parts of Scotland during Sunday.', $serialized['description']); 47 | } 48 | } 49 | 50 | ?> 51 | -------------------------------------------------------------------------------- /src/AwarenessLevel.php: -------------------------------------------------------------------------------- 1 | = sizeof($this->colours)) { 25 | throw new InvalidArgumentException('Awareness level ' . $level . ' is not supported.'); 26 | } 27 | 28 | $this->level = $level; 29 | $this->colour = $this->colours[$level]; 30 | $this->description = $this->descriptions[$level]; 31 | } 32 | 33 | public function serialize() { 34 | return [ 35 | 'level' => $this->level, 36 | 'colour' => $this->colour, 37 | 'description' => $this->description 38 | ]; 39 | } 40 | } 41 | 42 | ?> 43 | -------------------------------------------------------------------------------- /test/PeriodTest.php: -------------------------------------------------------------------------------- 1 | ' . 16 | 'From: 10.12.2017 05:00 CET Until: 11.12.2017 00:55 CET' . 17 | ''; 18 | 19 | $period_dom = DOMDocument::loadHTML($period_html); 20 | $period_cell = $period_dom->getElementsByTagName('td')->item(0); 21 | 22 | $testee = new Period($period_cell); 23 | $serialized = $testee->serialize(); 24 | 25 | $this->assertSame('Sunday 04:00 GMT', $serialized['from']); 26 | $this->assertSame('Sunday 23:55 GMT', $serialized['until']); 27 | } 28 | 29 | public function test_period_with_alternative_date_time_format_and_timezone() { 30 | Config::setDateTimeFormat('Y-m-d H:i T'); 31 | Config::setTimeZone('America/New_York'); 32 | 33 | $period_html = 34 | '' . 35 | 'From: 10.12.2017 05:00 CET Until: 11.12.2017 00:55 CET' . 36 | ''; 37 | 38 | $period_dom = DOMDocument::loadHTML($period_html); 39 | $period_cell = $period_dom->getElementsByTagName('td')->item(0); 40 | 41 | $testee = new Period($period_cell); 42 | $serialized = $testee->serialize(); 43 | 44 | $this->assertSame('2017-12-09 23:00 EST', $serialized['from']); 45 | $this->assertSame('2017-12-10 18:55 EST', $serialized['until']); 46 | } 47 | 48 | public function test_no_period_present() { 49 | $period_html = ''; 50 | 51 | $period_dom = DOMDocument::loadHTML($period_html); 52 | $period_cell = $period_dom->getElementsByTagName('td')->item(0); 53 | 54 | $testee = new Period($period_cell); 55 | $serialized = $testee->serialize(); 56 | 57 | $this->assertNull($serialized['from']); 58 | $this->assertNull($serialized['until']); 59 | } 60 | } 61 | 62 | ?> 63 | -------------------------------------------------------------------------------- /test/AwarenessLevelTest.php: -------------------------------------------------------------------------------- 1 | serialize(); 11 | 12 | $this->assertSame(0, $serialized['level']); 13 | $this->assertSame("White", $serialized['colour']); 14 | $this->assertSame("Unknown", $serialized['description']); 15 | } 16 | 17 | public function test_green() { 18 | $testee = new AwarenessLevel(AwarenessLevel::GREEN); 19 | $serialized = $testee->serialize(); 20 | 21 | $this->assertSame(1, $serialized['level']); 22 | $this->assertSame("Green", $serialized['colour']); 23 | $this->assertSame("No particular awareness of the weather is required.", $serialized['description']); 24 | } 25 | 26 | public function test_yellow() { 27 | $testee = new AwarenessLevel(AwarenessLevel::YELLOW); 28 | $serialized = $testee->serialize(); 29 | 30 | $this->assertSame(2, $serialized['level']); 31 | $this->assertSame("Yellow", $serialized['colour']); 32 | $this->assertRegexp("/The weather is potentially dangerous./", $serialized['description']); 33 | } 34 | 35 | public function test_amber() { 36 | $testee = new AwarenessLevel(AwarenessLevel::AMBER); 37 | $serialized = $testee->serialize(); 38 | 39 | $this->assertSame(3, $serialized['level']); 40 | $this->assertSame("Amber", $serialized['colour']); 41 | $this->assertRegexp("/The weather is dangerous./", $serialized['description']); 42 | } 43 | 44 | public function test_red() { 45 | $testee = new AwarenessLevel(AwarenessLevel::RED); 46 | $serialized = $testee->serialize(); 47 | 48 | $this->assertSame(4, $serialized['level']); 49 | $this->assertSame("Red", $serialized['colour']); 50 | $this->assertRegexp("/The weather is very dangerous./", $serialized['description']); 51 | } 52 | 53 | public function test_invalid_low_level() { 54 | $this->expectException(InvalidArgumentException::class); 55 | 56 | $testee = new AwarenessLevel(-1); 57 | } 58 | 59 | public function test_invalid_high_level() { 60 | $this->expectException(InvalidArgumentException::class); 61 | 62 | $testee = new AwarenessLevel(666); 63 | } 64 | } 65 | 66 | ?> 67 | -------------------------------------------------------------------------------- /src/warnings.json: -------------------------------------------------------------------------------- 1 | serialize()); 31 | 32 | respond(200, $json); 33 | 34 | function parse_parameters() { 35 | if (!isset($_GET['country'])) { 36 | respond(400, create_error_json('GET parameter country is required')); 37 | } 38 | 39 | Config::setCountry(strtoupper($_GET['country'])); 40 | 41 | if (isset($_GET['region'])) { 42 | Config::setRegion($_GET['region']); 43 | } 44 | 45 | if (isset($_GET['date_time_format'])) { 46 | Config::setDateTimeFormat($_GET['date_time_format']); 47 | } 48 | 49 | if (isset($_GET['time_zone'])) { 50 | Config::setTimeZone($_GET['time_zone']); 51 | } 52 | } 53 | 54 | function generate_rss_link() { 55 | if (!is_null(Config::getRegion())) { 56 | // Request for a single region, e.g. http://meteoalarm.eu/documents/rss/uk/UK004.rss 57 | return sprintf('http://meteoalarm.eu/documents/rss/%s/%s%s.rss', strtolower(Config::getCountry()), Config::getCountry(), Config::getRegion()); 58 | } else { 59 | // Request for an entire country, e.g. http://www.meteoalarm.eu/documents/rss/uk.rss 60 | return sprintf('http://www.meteoalarm.eu/documents/rss/%s.rss', strtolower(Config::getCountry())); 61 | } 62 | } 63 | 64 | function parse_response_code($headers) { 65 | 66 | // If we don't get parseable headers we assume we have an invalid reponse from the meteoalarm server 67 | $response_code = 502; 68 | 69 | if (is_array($headers)) { 70 | 71 | foreach ($headers as $k=>$v) { 72 | if (preg_match( "#HTTP/[0-9\.]+\s+([0-9]+)#", $v, $out)) { 73 | $response_code = intval($out[1]); 74 | break; 75 | } 76 | } 77 | } 78 | 79 | return $response_code; 80 | } 81 | 82 | function fix_rss_ampersand_escaping($rss) { 83 | /* Some ampersands are escaped in the received RSS and some are not. 84 | Address this by escaping them all and then fixing any resultant double escapes */ 85 | $rss = str_replace('&', '&', $rss); 86 | $rss = str_replace('&amp;', '&', $rss); 87 | return $rss; 88 | } 89 | 90 | function create_error_json($error_message) { 91 | return json_encode(array( 92 | 'error' => $error_message 93 | )); 94 | } 95 | 96 | function respond($code, $json) { 97 | header('Status: ' . $code); 98 | echo isset($_GET['callback']) ? "{$_GET['callback']}($json)" : $json; 99 | die(); 100 | } 101 | 102 | ?> 103 | -------------------------------------------------------------------------------- /src/Region.php: -------------------------------------------------------------------------------- 1 | 13 | Lothian & Borders 14 | http://web.meteoalarm.eu/en_UK/0/0/UK004.html 15 | ![CDATA[...]] 16 | Sun, 19 Nov 2017 10:12:02 +0100 17 | 18 | */ 19 | 20 | $this->name = (string)$rss_item->title; 21 | $this->link = (string)$rss_item->link; 22 | $this->code = $this->parseRegionCodeFromLink(); 23 | 24 | $description_html = (string)$rss_item->description; 25 | 26 | $this->parseWarningsFromDescription($description_html); 27 | 28 | $parsed_pub_date = date_create_from_format(self::DATE_TIME_FORMAT, (string)$rss_item->pubDate); 29 | date_timezone_set($parsed_pub_date, new DateTimeZone(Config::getTimeZone())); 30 | $this->published = $parsed_pub_date->format(Config::getDateTimeFormat()); 31 | } 32 | 33 | private function parseRegionCodeFromLink() { 34 | // e.g. extract '004' from http://web.meteoalarm.eu/en_UK/0/0/UK004.html 35 | $start = strripos($this->link, Config::getCountry()) + strlen(Config::getCountry()); 36 | $length = strripos($this->link, '.html') - $start; 37 | 38 | return substr($this->link, $start, $length); 39 | } 40 | 41 | private function parseWarningsFromDescription($description_html) { 42 | /* e.g. 43 | 44 | 45 | 46 | 47 | ... 48 | 49 | 50 | 51 | 52 | 53 | 54 | ... 55 | 56 | 57 |
Today
Tomorrow
58 | */ 59 | 60 | $description_dom = DOMDocument::loadHTML($description_html); 61 | 62 | $rows = $description_dom->getElementsByTagName('tr'); 63 | $tomorrows_row_index = $this->findTomorrowsRowIndex($rows); 64 | 65 | $this->todaysWarnings = $this->parseWarningsFromRows($rows, 1, $tomorrows_row_index); 66 | $this->tomorrowsWarnings = $this->parseWarningsFromRows($rows, $tomorrows_row_index+1, $rows->length); 67 | } 68 | 69 | private function findTomorrowsRowIndex($rows) { 70 | for ($i = 0; $i < $rows->length; $i++) { // Find Tomorrow 71 | $row = $rows->item($i); 72 | 73 | if ((sizeof($row->childNodes->length) === 1) && ($row->childNodes->item(0)->nodeName === 'th')) { 74 | 75 | if (trim($row->childNodes->item(0)->textContent) === 'Tomorrow') { 76 | $tomorrows_row_index = $i; 77 | break; 78 | } 79 | } 80 | } 81 | 82 | return $tomorrows_row_index; 83 | } 84 | 85 | private function parseWarningsFromRows($rows, $start_index, $end_index) { 86 | $warnings = array(); 87 | 88 | for ($i = $start_index; ($i + 2) <= $end_index; $i+=2) { 89 | $awarness_period_row = $rows->item($i); 90 | $description_row = $rows->item($i+1); 91 | $warnings[] = new Warning($awarness_period_row, $description_row); 92 | } 93 | 94 | return $warnings; 95 | } 96 | 97 | public function serialize() { 98 | foreach ($this->todaysWarnings as $warning) { 99 | $todaysWarnings[] = $warning->serialize(); 100 | } 101 | 102 | foreach ($this->tomorrowsWarnings as $warning) { 103 | $tomorrowsWarnings[] = $warning->serialize(); 104 | } 105 | 106 | return [ 107 | 'name' => $this->name, 108 | 'code' => $this->code, 109 | 'link' => $this->link, 110 | 'today' => $todaysWarnings, 111 | 'tomorrow' => $tomorrowsWarnings, 112 | 'published' => $this->published 113 | ]; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /test/AwarenessTypeTest.php: -------------------------------------------------------------------------------- 1 | serialize(); 11 | 12 | $this->assertSame(0, $serialized['type']); 13 | $this->assertSame("No Warnings", $serialized['description']); 14 | } 15 | 16 | public function test_wind() { 17 | $testee = new AwarenessType(AwarenessType::WIND); 18 | $serialized = $testee->serialize(); 19 | 20 | $this->assertSame(1, $serialized['type']); 21 | $this->assertSame("Wind", $serialized['description']); 22 | } 23 | 24 | public function test_snow_ice() { 25 | $testee = new AwarenessType(AwarenessType::SNOW_ICE); 26 | $serialized = $testee->serialize(); 27 | 28 | $this->assertSame(2, $serialized['type']); 29 | $this->assertSame("Snow/Ice", $serialized['description']); 30 | } 31 | 32 | public function test_thunderstorms() { 33 | $testee = new AwarenessType(AwarenessType::THUNDERSTORMS); 34 | $serialized = $testee->serialize(); 35 | 36 | $this->assertSame(3, $serialized['type']); 37 | $this->assertSame("Thunderstorms", $serialized['description']); 38 | } 39 | 40 | public function test_fog() { 41 | $testee = new AwarenessType(AwarenessType::FOG); 42 | $serialized = $testee->serialize(); 43 | 44 | $this->assertSame(4, $serialized['type']); 45 | $this->assertSame("Fog", $serialized['description']); 46 | } 47 | 48 | public function test_extreme_high_temperatures() { 49 | $testee = new AwarenessType(AwarenessType::EXTREME_HIGH_TEMPERATURES); 50 | $serialized = $testee->serialize(); 51 | 52 | $this->assertSame(5, $serialized['type']); 53 | $this->assertSame("Extreme High Temperatures", $serialized['description']); 54 | } 55 | 56 | public function test_extreme_low_temperatures() { 57 | $testee = new AwarenessType(AwarenessType::EXTREME_LOW_TEMPERATURES); 58 | $serialized = $testee->serialize(); 59 | 60 | $this->assertSame(6, $serialized['type']); 61 | $this->assertSame("Extreme Low Temperatures", $serialized['description']); 62 | } 63 | 64 | public function test_coastal_event() { 65 | $testee = new AwarenessType(AwarenessType::COASTAL_EVENT); 66 | $serialized = $testee->serialize(); 67 | 68 | $this->assertSame(7, $serialized['type']); 69 | $this->assertSame("Costal Event", $serialized['description']); 70 | } 71 | 72 | public function test_forest_fire() { 73 | $testee = new AwarenessType(AwarenessType::FOREST_FIRE); 74 | $serialized = $testee->serialize(); 75 | 76 | $this->assertSame(8, $serialized['type']); 77 | $this->assertSame("Forest Fire", $serialized['description']); 78 | } 79 | 80 | public function test_avalanche() { 81 | $testee = new AwarenessType(AwarenessType::AVALANCHE); 82 | $serialized = $testee->serialize(); 83 | 84 | $this->assertSame(9, $serialized['type']); 85 | $this->assertSame("Avalanche", $serialized['description']); 86 | } 87 | 88 | public function test_rain() { 89 | $testee = new AwarenessType(AwarenessType::RAIN); 90 | $serialized = $testee->serialize(); 91 | 92 | $this->assertSame(10, $serialized['type']); 93 | $this->assertSame("Rain", $serialized['description']); 94 | } 95 | 96 | public function test_unavailable() { 97 | $testee = new AwarenessType(AwarenessType::UNAVAILABLE); 98 | $serialized = $testee->serialize(); 99 | 100 | $this->assertSame(11, $serialized['type']); 101 | $this->assertSame("Unavailable", $serialized['description']); 102 | } 103 | 104 | public function test_flooding() { 105 | $testee = new AwarenessType(AwarenessType::FLOODING); 106 | $serialized = $testee->serialize(); 107 | 108 | $this->assertSame(12, $serialized['type']); 109 | $this->assertSame("Flooding", $serialized['description']); 110 | } 111 | 112 | public function test_rain_flooding() { 113 | $testee = new AwarenessType(AwarenessType::RAIN_FLOODING); 114 | $serialized = $testee->serialize(); 115 | 116 | $this->assertSame(13, $serialized['type']); 117 | $this->assertSame("Rain/Flooding", $serialized['description']); 118 | } 119 | 120 | public function test_invalid_low_type() { 121 | $this->expectException(InvalidArgumentException::class); 122 | 123 | $testee = new AwarenessType(-1); 124 | } 125 | 126 | public function test_invalid_high_type() { 127 | $this->expectException(InvalidArgumentException::class); 128 | 129 | $testee = new AwarenessType(14); 130 | } 131 | } 132 | 133 | ?> 134 | -------------------------------------------------------------------------------- /test/RegionTest.php: -------------------------------------------------------------------------------- 1 | ' . 10 | '' . 11 | '' . 12 | 'Today' . 13 | '' . 14 | '' . 15 | '' . 16 | '' . 17 | 'awt:2 level:3' . 18 | '' . 19 | '' . 20 | 'From: 10.12.2017 05:00 CET Until: 11.12.2017 00:55 CET' . 21 | '' . 22 | '' . 23 | '' . 24 | '' . 25 | '' . 26 | '' . 27 | 'A spell of heavy snow is possible over parts of Scotland during Sunday.' . 28 | '' . 29 | '' . 30 | '' . 31 | '' . 32 | 'Tomorrow' . 33 | '' . 34 | '' . 35 | '' . 36 | '' . 37 | 'awt:2 level:3' . 38 | '' . 39 | '' . 40 | 'From: 11.12.2017 05:00 CET Until: 11.12.2017 00:55 CET' . 41 | '' . 42 | '' . 43 | '' . 44 | '' . 45 | '' . 46 | '' . 47 | 'A spell of heavy snow is possible over parts of Scotland during Monday.' . 48 | '' . 49 | '' . 50 | '' . 51 | '' . 52 | 'awt:2 level:2' . 53 | '' . 54 | '' . 55 | 'From: 11.12.2017 05:00 CET Until: 12.12.2017 12:00 CET' . 56 | '' . 57 | '' . 58 | '' . 59 | '' . 60 | '' . 61 | '' . 62 | 'Ice is expected to form across many places overnight into Tuesday morning.' . 63 | '' . 64 | '' . 65 | ''; 66 | 67 | protected function setUp() { 68 | Config::setCountry('UK'); 69 | Config::setDateTimeFormat('l H:i T'); 70 | Config::setTimezone('Europe/London'); 71 | } 72 | 73 | public function test_region() { 74 | $item_xml = 75 | '' . 76 | 'Lothian & Borders' . 77 | 'http://web.meteoalarm.eu/en_UK/0/0/UK004.html' . 78 | 'description_html . ']]>' . 79 | 'Fri, 22 Dec 2017 01:00:00 +0100' . 80 | '418f52d19af464bc09619e79a6161d6a' . 81 | ''; 82 | 83 | $rss_item = simplexml_load_string($item_xml); 84 | 85 | $testee = new Region($rss_item); 86 | $serialized = $testee->serialize(); 87 | 88 | $this->assertSame('004', $serialized['code']); 89 | $this->assertSame('Lothian & Borders', $serialized['name']); 90 | $this->assertSame('http://web.meteoalarm.eu/en_UK/0/0/UK004.html', $serialized['link']); 91 | 92 | $this->assertSame(1, sizeof($serialized['today'])); 93 | $this->assertSame('A spell of heavy snow is possible over parts of Scotland during Sunday.', $serialized['today'][0]['description']); 94 | 95 | $this->assertSame(2, sizeof($serialized['tomorrow'])); 96 | $this->assertSame('A spell of heavy snow is possible over parts of Scotland during Monday.', $serialized['tomorrow'][0]['description']); 97 | $this->assertSame('Ice is expected to form across many places overnight into Tuesday morning.', $serialized['tomorrow'][1]['description']); 98 | 99 | $this->assertSame('Friday 00:00 GMT', $serialized['published']); 100 | } 101 | 102 | public function test_region_with_alternative_date_time_format_and_timezone() { 103 | Config::setDateTimeFormat('Y-m-d H:i T'); 104 | Config::setTimeZone('America/New_York'); 105 | 106 | $item_xml = 107 | '' . 108 | 'Lothian & Borders' . 109 | 'http://web.meteoalarm.eu/en_UK/0/0/UK004.html' . 110 | 'description_html . ']]>' . 111 | 'Fri, 22 Dec 2017 01:00:00 +0100' . 112 | '418f52d19af464bc09619e79a6161d6a' . 113 | ''; 114 | 115 | $rss_item = simplexml_load_string($item_xml); 116 | 117 | $testee = new Region($rss_item); 118 | $serialized = $testee->serialize(); 119 | 120 | $this->assertSame('2017-12-21 19:00 EST', $serialized['published']); 121 | } 122 | } 123 | 124 | ?> 125 | -------------------------------------------------------------------------------- /test/WarningsTest.php: -------------------------------------------------------------------------------- 1 | ' . 15 | '' . 16 | '' . 17 | '' . 18 | 'Meteoalarm United Kingdom' . 19 | 'http://web.meteoalarm.eu/en_UK/0/0/UK.html' . 20 | 'Metoalarm actual warnings from United Kingdom' . 21 | '10' . 22 | 'eng' . 23 | '' . 24 | 'United Kingdom' . 25 | 'http://web.meteoalarm.eu/en_UK/0/0/UK.html' . 26 | 'Today  Tomorrowawt:10 level:1  awt:10 level:1]]>' . 27 | 'Fri, 22 Dec 2017 01:00:00 +0100' . 28 | '1fe7d2aa271d77e37ba7f402e32ed7a2' . 29 | '' . 30 | '' . 31 | 'Dumfries and Galloway' . 32 | 'http://web.meteoalarm.eu/en_UK/0/0/UK006.html' . 33 | 'Todayawt:5 level:1No special awareness required
Tomorrowawt:5 level:1No special awareness required]]>
' . 34 | 'Fri, 22 Dec 2017 01:00:00 +0100' . 35 | '1bda31bcc0ead6ce748cc1e39fa249a1' . 36 | '
' . 37 | '' . 38 | 'Lothian & Borders' . 39 | 'http://web.meteoalarm.eu/en_UK/0/0/UK004.html' . 40 | 'Todayawt:10 level:1No special awareness required
Tomorrowawt:10 level:1No special awareness required]]>
' . 41 | 'Fri, 22 Dec 2017 01:00:00 +0100' . 42 | '418f52d19af464bc09619e79a6161d6a' . 43 | '
' . 44 | '
' . 45 | '
'; 46 | 47 | $rss = simplexml_load_string($rss_xml); 48 | 49 | $testee = new Warnings($rss); 50 | $serialized = $testee->serialize(); 51 | 52 | $this->assertSame('UK', $serialized['country']); 53 | $this->assertSame(2, sizeof($serialized['regions'])); 54 | $this->assertSame('Dumfries and Galloway', $serialized['regions'][0]['name']); 55 | $this->assertSame('Lothian & Borders', $serialized['regions'][1]['name']); 56 | } 57 | 58 | public function test_warnings_for_single_region() { 59 | $rss_xml = 60 | '' . 61 | '' . 62 | '' . 63 | '' . 64 | 'Meteoalarm United Kingdom, Highland & Islands' . 65 | 'http://web.meteoalarm.eu/en_UK/0/0/UK002.html' . 66 | 'Metoalarm actual warnings from United Kingdom, Highland & Islands' . 67 | '10' . 68 | 'eng' . 69 | '' . 70 | 'Highlands & Islands' . 71 | 'http://web.meteoalarm.eu/en_UK/0/0/UK002.html' . 72 | 'Todayawt:5 level:1No special awareness required
Tomorrowawt:10 level:2From: 24.12.2017 01:05 CET Until: 25.12.2017 00:55 CETHeavy rain will affect western parts of Scotland at times during Saturday night and Sunday. Spray and flooding on roads will probably make journey times longer. This will probably also cause delays to bus and train services. There is a chance that a few homes and businesses could be flooded. This warning has been updated to extend it further east.]]>
' . 73 | 'Sat, 23 Dec 2017 22:00:02 +0100' . 74 | '0aefbef317686efce5152f0daeea5c1e' . 75 | '
' . 76 | '
' . 77 | '
'; 78 | 79 | $rss = simplexml_load_string($rss_xml); 80 | 81 | $testee = new Warnings($rss); 82 | $serialized = $testee->serialize(); 83 | 84 | $this->assertSame('UK', $serialized['country']); 85 | $this->assertSame(1, sizeof($serialized['regions'])); 86 | $this->assertSame('Highlands & Islands', $serialized['regions'][0]['name']); 87 | } 88 | } 89 | 90 | ?> 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # meteo-alarm-weather-warnings 2 | 3 | Copyright © 2018 Wayne D Grant 4 | 5 | Licensed under the MIT License 6 | 7 | JSON formatted Web Service API to fetch weather warnings from [Metoalarm](http://www.meteoalarm.eu/). Written in PHP. 8 | 9 | ## Overview 10 | 11 | [Metoalarm](http://www.meteoalarm.eu/) is a web site that provides up-to-date weather warnings for most of Europe. While it provides a useful website and RSS feeds it does not, at the time of writing, provide a web service interface. 12 | 13 | **meteo-alarm-weather-warnings** remedies this by providing a simple JSON formatted Web Service API that proxies through to Meteoalarm's RSS feeds. 14 | 15 | ![alt tag](meteo-alarm-weather-warnings.png) 16 | 17 | **meteo-alarm-weather-warnings** is especially useful for integrating weather warnings into a 3rd-party website (as has been done here for several UK regions: [https://waynedgrant.com/weather/warn.html](https://waynedgrant.com/weather/warn.html)). 18 | 19 | ## Requirements 20 | 21 | 1. PHP version 5 or above installed on the web server 22 | 23 | ## Installation 24 | 25 | * Download the source code for the [latest release](https://github.com/waynedgrant/meteo-alarm-weather-warnings/releases) and unzip it 26 | 27 | * Upload all **.php** and **.json** files in **meteo-alarm-weather-warnings/src** to a location on your web server 28 | 29 | * Modify your web server to process **.json** files using PHP. For example, for Apache add the following to your **.htaccess** file: 30 | 31 | ``` 32 | AddHandler application/x-httpd-php5 .json 33 | ``` 34 | 35 | * Alternatively rename the **warnings.json** file to be called **warnings.php** 36 | 37 | ## Execution 38 | 39 | Hit the URL of your deployed **warnings.json** file using a web browser or other REST client. Both JSON and JSONP are supported. 40 | 41 | The following GET parameters are supported: 42 | 43 | | GET Parameter | Description | Required? | Default Value | 44 | |------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------|------------------------| 45 | | country | Country to fetch weather warnings for. e.g. UK, DE, FR, etc. See [Metoalarm](http://www.meteoalarm.eu/) for a full list of countries. | Yes | N/A | 46 | | region | Region within country to fetch weather warnings for, e.g. 004 is Lothian & Borders in the UK. See [Metoalarm](http://www.meteoalarm.eu/) for a full list of regions. | No | All regions in country | 47 | | date_time_format | Format string to use for fetched date/time values. See [datetime.createfromformat.php](http://php.net/manual/en/datetime.createfromformat.php) for details. | No | l H:i T | 48 | | time_zone | Timezone used to express fetched date/time values. See [timezones.php](http://php.net/manual/en/timezones.php) for a full list of supported values. | No | Europe/London | 49 | 50 | For example: 51 | 52 | All weather warnings for the UK: 53 | 54 | ``` 55 | /warnings.json?country=UK 56 | ``` 57 | 58 | Weather warnings for the Gelderland region of the Netherlands: 59 | 60 | ``` 61 | /warnings.json?country=NL®ion=014 62 | ``` 63 | 64 | Weather warnings for Germany with a specialized date/time format expressed in the local timezone: 65 | 66 | ``` 67 | /warnings.json?country=DE&date_time_format=Y/M/d%20H:i%20T&time_zone=Europe/Berlin 68 | ``` 69 | 70 | ## Response Fields 71 | 72 | ### Success Response 73 | 74 | An example of a successful reply: 75 | 76 | ``` 77 | { 78 | "endpoint": { 79 | "url": "...", 80 | "version": "1.1", 81 | "github_project": "https://github.com/waynedgrant/meteo-alarm-weather-warnings", 82 | "copyright": "Copyright © 2018 Wayne D Grant (www.waynedgrant.com)" 83 | }, 84 | "warnings": { 85 | "country": "UK", 86 | "regions": [ 87 | { 88 | "name": "Dumfries and Galloway", 89 | "code": "006", 90 | "link": "http://web.meteoalarm.eu/en_UK/0/0/UK006.html", 91 | "today": [ 92 | { 93 | "awareness": { 94 | "icon": "http://web.meteoalarm.eu/documents/rss/wflag-l3-t1.jpg", 95 | "awareness_type": { 96 | "type": 1, 97 | "description": "Wind" 98 | }, 99 | "awareness_level": { 100 | "level": 3, 101 | "colour": "Amber", 102 | "description": "The weather is dangerous. Unusual meteorological phenomena have been forecast. Damage and casualties are likely to happen. Be very vigilant and keep regularly informed about the detailed expected meteorological conditions. Be aware of the risks that might be unavoidable. Follow any advice given by your authorities." 103 | } 104 | }, 105 | "period": { 106 | "from": "Sunday 03:00 GMT", 107 | "until": "Sunday 12:00 GMT" 108 | }, 109 | "description": "Some very strong winds are expected over parts of Northern Ireland and southwest Scotland on New Years Eve. There will probably be some damage to buildings such as tiles blown from roofs, with flying debris likely with the possibility of injuries or danger to life. In coastal areas large waves are likely as well as beach material being thrown onto coastal roads, sea fronts and perhaps some properties. Longer journey times and cancellations are likely as road, rail, air and ferry services may be affected. Some roads and bridges are likely to close. There is also a good chance of power cuts and the potential to affect other services such as mobile phone coverage." 110 | }, 111 | 112 | ... any other warnings for today ... 113 | ], 114 | "tomorrow": [ 115 | { 116 | ... warnings for tomorrow ... 117 | } 118 | ], 119 | "published": "Sunday 00:00 GMT" 120 | }, 121 | { 122 | ... other regions ... 123 | } 124 | ] 125 | } 126 | } 127 | ``` 128 | 129 | #### Awareness Types 130 | 131 | | Code | Type | 132 | |------|---------------------------| 133 | | 0 | No Warnings | 134 | | 1 | Wind | 135 | | 2 | Snow/Ice | 136 | | 3 | Thunderstorms | 137 | | 4 | Fog | 138 | | 5 | Extreme High Temperatures | 139 | | 6 | Extreme Low Temperatures | 140 | | 7 | Costal Event | 141 | | 8 | Forest Fire | 142 | | 9 | Avalanche | 143 | | 10 | Rain | 144 | | 11 | Unavailable | 145 | | 12 | Flooding | 146 | | 13 | Rain/Flooding | 147 | 148 | #### Awareness Levels 149 | 150 | | Code | Level | 151 | |------|-------------------| 152 | | 0 | White/Unknown | 153 | | 1 | Green/No Warnings | 154 | | 2 | Yellow | 155 | | 3 | Amber | 156 | | 4 | Red | 157 | 158 | ### Failure Response 159 | 160 | Examples of failure replies: 161 | 162 | ``` 163 | { 164 | "error": "GET parameter country is required" 165 | } 166 | ``` 167 | 168 | ``` 169 | { 170 | "error": "Response code 404 returned by http://www.meteoalarm.eu/documents/rss/NARNIA.rss" 171 | } 172 | ``` 173 | 174 | ## Unit Testing 175 | 176 | * Install [PHPUnit](https://phpunit.de/) 177 | * cd meteo-alarm-weather-warnings 178 | * phpunit --bootstrap bootstrap.php test/ 179 | --------------------------------------------------------------------------------