PHPで河川氾濫注意地点をマッピング

(1/1)
2018 年(平成 30 年)夏、大雨や台風で河川が氾濫し、水害が相次いだ。
そこで今回は、国土交通省「川の防災情報」をリアルタイム参照し、河川の水位が氾濫注意水位以上の地点を Google マップにマッピングするプログラムを作る。応答速度をあげるために、Google Geocoding API を使って取得した地点情報はデータベース(SQLite)に格納してゆく。
高潮被害については、「PHP で潮位を計算する」をあわせて参考にしていただきたい。

(2020 年 11 月 3 日)RIVER_URL 変更,地理院地図・ OSM に対応

目次

サンプル・プログラムの実行例

PHPで河川氾濫注意地点をマッピング

サンプル・プログラム

圧縮ファイルの内容
floodMap.phpサンプル・プログラム本体。
pahooGeoCode.php住所・緯度・経度に関わるクラス pahooGeoCode。
使い方は「PHPで住所・ランドマークから最寄り駅を求める」などを参照のこと。include_pathが通ったディレクトリに配置する。

プログラムの方針

国土交通省「川の防災情報」の配下にある「基準値を超えた水位観測所」に、河川の水位が基準を超えた観測地点の一覧がリアルタイム表示されている。
この一覧表の実体ページから、各々の観測所について
  1. 観測所 ID
  2. 観測所名
  3. 観測所 URL
  4. 河川名
  5. 水位
  6. 観測時刻
  7. 基準水位(4 段階)
の 7種類10 件の情報を取り出す。

一覧表にある観測所の所在地は市町村名までのため、このままでは正確な場所をマッピングできない。そこで、観測所 URL を開き、そこに表示される所在地を使って、Google Geocoding API を呼び出し、位置情報(緯度・経度)を取得する。

台風などで基準を超えた観測地点情報が多くなると、いちいち Google Geocoding API を呼び出して位置情報を取得するのに時間がかかる。そこで、一度取得した位置情報は、サーバのデータベースに格納し、次からはデータベース上の位置情報を参照するようにする。DBMS は、PHP からインストール不要で使える SQLite を使うことにする。
PHPで河川氾濫注意地点をマッピング

解説:準備

0035: //河川の水位が氾濫注意水位以上の地点URL
0036: //define('RIVER_URL', 'http://www.river.go.jp/kawabou/overOut/stageOverP1.html');
0037: define('RIVER_URL', 'https://www.river.go.jp/kawabou/html/list/4/ipSuiiChouka_ac80_fw0.html');     //v.1.02
0038: 
0039: //SQLite DBファイル名:各自の環境に合わせて変更すること
0040: define('DBFILE', './flodMapBD.sqlite3');
0041: 
0042: //SQLite テーブル名:観測地点情報
0043: define('TABLE_OBPOINT', 'obpoint');
0044: 
0045: //実行するSQL:観測地点情報
0046: define('PRE_SELECT', 'SELECT * FROM ' . TABLE_OBPOINT . ' WHERE id=:id');
0047: define('PRE_INSERT', 'INSERT INTO ' . TABLE_OBPOINT . ' (id, title, address, latitude, longitude, premiere, latest) VALUES (:id, :title, :address, :latitude, :longitude, :premiere, :latest)');
0048: define('PRE_UPDATE', 'UPDATE ' . TABLE_OBPOINT . ' set title=:title, address=:address, latitude=:latitude, longitude=:longitude, latest=:latest WHERE id=:id');
0049: 
0050: //マップ・アイコン
0051: define('GICON_LEVEL1', 'https://maps.google.com/mapfiles/ms/micons/green-dot.png');  //水防団待機
0052: define('GICON_LEVEL2', 'https://maps.google.com/mapfiles/ms/micons/yellow-dot.png');  //氾濫注意
0053: define('GICON_LEVEL3', 'https://maps.google.com/mapfiles/ms/micons/orange-dot.png');  //避難判断
0054: define('GICON_LEVEL4', 'https://maps.google.com/mapfiles/ms/micons/red-dot.png');  //氾濫危険

各種定数を用意する。

SQLite のデータベース実体ファイルは定数 DBFILE に定義する。PHP から書き込み可能なファイルであれば、フォルダやファイル名は自由に設定してかまわない。
本プログラム中のユーザー関数 initDB によってデータベースを自動生成するので、事前準備は不用である。

GoogleMaps の表示サイズ、マップ ID、マップ・アイコンは自由に変更することができる。

準備:pahooGeoCode クラス

0036: class pahooGeoCode {
0037:     var $items;      //検索結果格納用
0038:     var $error;      //エラーフラグ
0039:     var $hits;       //検索ヒット件数
0040:     var $webapi; //直前に呼び出したWebAPI URL
0041: 
0042:     //Google Cloud Platform APIキー
0043:     //https://cloud.google.com/maps-platform/
0044:     //※Google Maps APIを利用しないのなら登録不要
0045:     var $GOOGLE_API_KEY_1 = '**************************';   //HTTPリファラ用
0046:     var $GOOGLE_API_KEY_2 = '**************************';   //IP制限用
0047: 
0048:     //Yahoo! JAPAN Webサービス アプリケーションID
0049:     //https://e.developer.yahoo.co.jp/register
0050:     //※Yahoo! JAPAN Webサービスを利用しないのなら登録不要
0051:     var $YAHOO_APPLICATION_ID = '*****************************';

地図サービスを利用するために、クラスファイル "pahooGeoCode.php" を使用する。組み込み関数  require_once  を使って読めるディレクトリに配置する。ディレクトリは、設定ファイル php.ini に記述されているオプション設定 include_path に設定しておく。
クラスについては「PHP でクラスを使ってテキストの読みやすさを調べる」を参照されたい。

地図として Google マップを利用するのであれば、Google Cloud Platform API キー が必要で、その入手方法は「Google Cloud Platform - WebAPI の登録方法」を、YOLP マップを利用するのであれば、Yahoo! JAPAN Web サービス アプリケーション IDが必要で、その入手方法は「Google Cloud Platform - WebAPI の登録方法」を、それぞれ参照されたい。

準備:地図サービス(WebAPI)の選択

0056: //地図描画サービスの選択
0057: //    0:Google
0058: //    2:地理院地図・OSM
0059: define('MAPSERVICE', 2);
0060: 
0061: //マップの表示サイズ(単位:ピクセル)
0062: define('MAP_WIDTH',  600);
0063: define('MAP_HEIGHT', 600);
0064: //マップID
0065: define('MAPID', 'map_id');
0066: //初期値
0067: define('DEF_TYPE', 'roadmap');        //マップタイプ
0068: define('DEF_LATITUDE',   38.671);    //緯度
0069: define('DEF_LONGITUDE', 139.779); //経度
0070: define('DEF_ZOOM',        5);       //ズーム

表示する地図は、Google マップ地理院地図・オープンストリートマップ(OSM)から選べる。あらかじめ、定数 MAPSERVIC に値を設定すること。

解説:河川情報の取り出し

0275: /**
0276:  * 国交省「川の防災情報」をスクレイピングし
0277:  *  観測地点情報と氾濫注意情報を配列に格納
0278:  * @param string $url スクレイピングするURL
0279:  * @param array  $items 観測地点情報を格納する配列
0280:  * @return int 取得情報件数/FALSE:失敗
0281: */
0282: function getRiverInfo($url, &$items) {
0283:     static $pat1 = "/<td\snowrap\swidth\=\"80\"\sclass\=\"tb1td1\"/iu";
0284:     static $pat2 = "/'([0-9]+)','([0-9|\-]+)'/iu";
0285:     static $pat3 = "/<td\snowrap\swidth\=\"79\"\sclass\=\"tb1td2\"/iu";
0286: 
0287:     $infp =fopen($url, 'r');
0288:     if ($infp == FALSEreturn FALSE;
0289: 
0290:     $cnt = 1;
0291:     $flag = FALSE;
0292:     while (! feof($infp)) {
0293:         $str = fgets($infp);
0294:         //観測地点
0295:         if (preg_match($pat1$str) > 0) {
0296:             $str = fgets($infp);
0297:             if (preg_match($pat2$str$arr) > 0) {
0298:                 $id = $arr[1];
0299:                 $items[$cnt]['id']    = $id;
0300:                 $items[$cnt]['title'] = trim(fgets($infp));
0301:                 //DB検索
0302:                 if (getOBpointFromDB($id$items[$cnt]) == FALSE) {
0303:                     //GoogleジオコーディングAPI検索
0304:                     $items[$cnt]['url'] = "https://www.river.go.jp/kawabou/ipSuiiKobetu.do?init=init&obsrvId={$arr[1]}&gamenId={$arr[2]}&timeType=60&requestType=1&fldCtlParty=no";
0305:                     if (($res = getAddress($items[$cnt]['url'])) != FALSE) {
0306:                         $items[$cnt]['address'] = $res;
0307:                         if (getLatLong($items[$cnt]) != FALSE) {
0308:                             storeOBpointToDB($id$items[$cnt]);
0309:                         }
0310:                     }
0311:                 }
0312:                 $flag = TRUE;
0313:             } 
0314:         } else if ($flag && preg_match($pat3$str) > 0) {
0315:             //河川名
0316:             $str = fgets($infp);
0317:             $items[$cnt]['river'] = trim(strip_tags($str));
0318:             //水位
0319:             $str = fgets($infp);
0320:             $items[$cnt]['level'] = trim(strip_tags($str));
0321:             //観測時刻
0322:             $str = fgets($infp);
0323:             $str = fgets($infp);
0324:             $items[$cnt]['time'] = trim(strip_tags($str));
0325:             //警戒水位
0326:             $str = fgets($infp);
0327:             $items[$cnt]['causion1'] = trim(strip_tags($str));
0328:             $str = fgets($infp);
0329:             $items[$cnt]['causion2'] = trim(strip_tags($str));
0330:             $str = fgets($infp);
0331:             $items[$cnt]['causion3'] = trim(strip_tags($str));
0332:             $str = fgets($infp);
0333:             $items[$cnt]['causion4'] = trim(strip_tags($str));
0334:             $cnt++;
0335:             $flag = FALSE;
0336:         }
0337:     }
0338:     fclose($infp);
0339: 
0340:     return ($cnt - 1);
0341: }

基準値を超えた水位観測所」をスクレイピングし、各々の観測所の情報を配列 $items に格納する関数が getRiverInfo である。
観測所のリンク先を開く JavaScript関数 openKobetu の第一引数がユニーク数であるようなので、これを観測所 ID とみなす。

観測所 ID をキーにユーザー関数 getOBpointFromDB で DB 検索を行い、ヒットしたら取得した位置情報を $items に格納し、ヒットしなかったら観測所のリンク先を開いて所在地を Google Geocoding API に渡し、位置情報を取得するとともにユーザー関数 storeOBpointToDB で DB登録する。

このスクレイピングについては、国交省の発表書式が変わる可能性もあり、その場合は各自でパターンを改良するなり対応をお願いしたい。

解説:観測地点情報の取り出し

0343: /**
0344:  * 国交省「川の防災情報」をスクレイピングし、住所を返す
0345:  * @param string $url スクレイピングするURL
0346:  * @return string 住所/FALSE:失敗
0347: */
0348: function getAddress($url) {
0349:     static $pat1 = "/<td\sclass\=\"tb1td2Left\">/iu";
0350: 
0351:     $infp =fopen($url, 'r');
0352:     if ($infp == FALSEreturn FALSE;
0353:     $res = FALSE;
0354:     while (! feof($infp)) {
0355:         $str = fgets($infp);
0356:         //住所
0357:         if (preg_match($pat1$str) > 0) {
0358:             $res = trim(fgets($infp));
0359:             break;
0360:         }
0361:     }
0362:     fclose($infp);
0363: 
0364:     return $res;
0365: }

観測所のリンク先を開いて、スクレイピングによって所在地(文字列)を取り出すのがユーザー関数 getAddress である。
そして、この所在地を使って、位置情報(緯度・経度)を取り出すのがユーザー関数 getLatLong である。

解説:DBの初期化

0176: /**
0177:  * DBの初期化
0178:  * @param なし
0179:  * @return bool TRUE/FALSE
0180: */
0181: function initDB() {
0182:     try {
0183:         $pdo = new PDO('sqlite:' . DBFILE);
0184:         $pdo->setAttribute(PDO::ATTR_ERRMODEPDO::ERRMODE_EXCEPTION);
0185: 
0186:         //テーブル作成:出演者情報
0187:         //id:観測地点ID, title:地点名,address:住所
0188:         //latitude:緯度, longitude:経度
0189:         //premiere:登録日時, latest:更新日時
0190:         $pdo->exec('CREATE TABLE IF NOT EXISTS ' . TABLE_OBPOINT . '(
0191:             id        INTEGER PRIMARY KEY AUTOINCREMENT,
0192:             title     TEXT,
0193:             address   TEXT,
0194:             latitude  REAL,
0195:             longitude REAL,
0196:             premiere  TEXT,
0197:             latest    TEXT
0198:         )
');
0199:         $res = TRUE;
0200:     } catch (PDOException $e) {
0201:         $res = FALSE;
0202:     }
0203: 
0204:     return $res;
0205: }

本プログラムの実行直後、ユーザー関数 initDB を実行し、DB が存在しなかったら自動生成する。

解説:DBから地点情報を取得する

0207: /**
0208:  * DBから地点情報を取得する
0209:  * @param string $id   地点ID
0210:  * @param array  $item 地点情報を格納する配列
0211:  * @return bool TRUE:取得成功/FALSE:失敗(DB登録がない)
0212: */
0213: function getOBpointFromDB($id, &$item) {
0214:     //DB検索
0215:     try {
0216:         $pdo = new PDO('sqlite:' . DBFILE);
0217:         $pdo->setAttribute(PDO::ATTR_ERRMODEPDO::ERRMODE_EXCEPTION);
0218:         $stmt = $pdo->prepare(PRE_SELECT);
0219:         $stmt->bindValue(':id', $idPDO::PARAM_STR);
0220:         $res = $stmt->execute();
0221:         $row = $stmt->fetch();
0222:         if ($row == FALSE) {
0223:             $pdo = NULL;
0224:             return FALSE;
0225:         }
0226:         $item['title']     = $row['title'];
0227:         $item['address']   = $row['address'];
0228:         $item['latitude']  = $row['latitude'];
0229:         $item['longitude'] = $row['longitude'];
0230:         $pdo = NULL;
0231:     } catch (PDOException $e) {
0232:         $pdo = NULL;
0233:         return FALSE;
0234:     }
0235: 
0236:     return TRUE;
0237: }

ユーザー関数 getOBpointFromDB は、観測地点 ID をキーに、位置情報(緯度・経度)を含む地点情報を配列 $item に格納する。
観測地点 ID がヒットしなければ、FALSE を返す。

解説:DBに地点情報を追加する

0239: /**
0240:  * DBに地点情報を追加する
0241:  * @param string $id   地点ID
0242:  * @param array  $item 地点情報を格納する配列
0243:  * @return bool TRUE:登録成功/FALSE:登録失敗
0244: */
0245: function storeOBpointToDB($id$item) {
0246:     if (!isset($item['latitude']) || !$item['longitude'])  return FALSE;
0247:     //DB追加
0248:     try {
0249:         $dt = date(DATE_W3Ctime());
0250:         $pdo = new PDO('sqlite:' . DBFILE);
0251:         $pdo->setAttribute(PDO::ATTR_ERRMODEPDO::ERRMODE_EXCEPTION);
0252:         $stmt = $pdo->prepare(PRE_INSERT);
0253:         $stmt->bindValue(':id',        $id,                PDO::PARAM_STR);
0254:         $stmt->bindValue(':title',     $item['title'],     PDO::PARAM_STR);
0255:         $stmt->bindValue(':address',   $item['address'],   PDO::PARAM_STR);
0256:         $stmt->bindValue(':latitude',  $item['latitude'],    PDO::PARAM_STR);
0257:         $stmt->bindValue(':longitude', $item['longitude'], PDO::PARAM_STR);
0258:         $stmt->bindValue(':premiere',  $dt,              PDO::PARAM_STR);
0259:         $stmt->bindValue(':latest',    $dt,                PDO::PARAM_STR);
0260:         $row  = $stmt->execute();
0261:         if ($row == FALSE) {
0262:             $pdo = NULL;
0263:             return FALSE;
0264:         }
0265:         $pdo = NULL;
0266:     } catch (PDOException $e) {
0267:         $pdo = NULL;
0268:         return FALSE;
0269:     }
0270: 
0271:     return TRUE;
0272: }

ユーザー関数 storeOBpointToDB は、getAddress などによって取得した、位置情報(緯度・経度)を含む地点情報を DB に追加する。

解説:メイン・プログラム

0489: //オブジェクト生成
0490: $pgc = new pahooGeoCode();
0491: 
0492: $items = array();
0493: $message = $errmsg = '';
0494: initDB();
0495: 
0496: $n = getRiverInfo(RIVER_URL$items);
0497: if ($n > 0) {
0498:     setDescription($items);
0499: else {
0500:     $errmsg = '注意情報はない';
0501: }
0502: 
0503: //緯度・経度情報のない観測地点を排除;version 1.01
0504: foreach ($items as $key=>$item) {
0505:     if (!isset($item['latitude']) || !$item['longitude']) {
0506:         unset($items[$key]);
0507:     }
0508: }
0509: 
0510: //マップ描画スクリプト
0511: $jsmap = $pgc->drawJSMap(MAPIDDEF_LATITUDEDEF_LONGITUDEDEF_TYPEDEF_ZOOMNULL$itemsMAPSERVICE);
0512: 
0513: $HtmlBody = makeCommonBody($items$jsmap$errmsg);
0514: 
0515: // 表示処理
0516: echo $HtmlHeader;
0517: echo $HtmlBody;
0518: echo $HtmlFooter;

冒頭で initDB を実行し、必要に応じて DB生成を行う。
次に、getRiverInfo を実行し、氾濫注意情報があればマップ上に表示する情報を setDescription を使って生成する。
最後に、メソッド pahooGeoCode::drawGMap を使って Google マップを作成する。

参考サイト

(この項おわり)
header