PHPで指定河川の洪水予報を表示する

(1/1)
これまで国土交通省「川の防災情報」を参照し、河川の水位が氾濫注意水位以上の地点をマッピングするプログラムを紹介してきたが、サイト・リニューアルに伴い利用できなくなったため、PHPを使って気象庁防災情報XMLから指定河川の洪水予報をマッピングするPHPプログラムに作り替えた。
表示マップの種類を変えたり、マップを含めてツイートすることができる。

(2024年11月2日)Bluesky投稿機能を追加
(2024年6月23日)TwitterOAuth 7.0.0 対応,Twitter(現・X)ボタンを "X" に変更

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

PHPで指定河川の洪水予報を表示する
Googleマップ

目次

サンプル・プログラム

圧縮ファイルの内容
jmaRiverFloodForecast.phpサンプル・プログラム本体。
pahooGeoCode.php住所・緯度・経度に関わるクラス pahooGeoCode。
使い方は「PHPで住所・ランドマークから最寄り駅を求める」などを参照。include_path が通ったディレクトリに配置すること。
pahooCache.phpキャッシュ処理に関わるクラス pahooCache。
キャッシュ処理に関わるクラスの使い方は「PHPで天気予報を求める」を参照。include_path が通ったディレクトリに配置すること。
pahooTwitterAPI.phpTwitter APIを利用するクラス pahooTwitterAPI。
使い方は「PHPでTwitterに投稿(ツイート)する」などを参照。include_path が通ったディレクトリに配置すること。
pahooBlueskyAPI.phpBluesky APIに関わるクラス pahooBlueskyAPI。
使い方は「PHPでPHPでBlueskyに投稿する」などを参照。include_path が通ったディレクトリに配置すること。
jmaRiverFloodForecast.php 更新履歴
バージョン 更新日 内容
1.4.0 2024/11/01 Bluesky投稿機能を追加, isButton()修正
1.3.0 2024/06/23 Twitter(現・X)ボタンを "X" に変更
1.2.3 2023/09/20 js_html2image()--Leaflet用html2image()発火プロセス見直し
1.22 2022/10/08 getRiverFloodForecast() - 予報がないときの処理追加
1.21 2022/08/04 info2points() - areasが無いデータをスキップ
pahooGeoCode.php 更新履歴
バージョン 更新日 内容
6.3.3 2024/09/14 $this->NOMINATIM_EMAIL 追加
6.3.2 2024/02/14 getStaticMap() -- bug-fix
6.3.1 2023/07/09 bug-fix
6.3.0 2023/07/02 getPointsGSI()追加
6.2.0 2023/07/02 ip2address()追加
pahooCache.php 更新履歴
バージョン 更新日 内容
1.1.1 2023/02/11 コメント追記
1.1 2021/04/08 simplexml_load()メソッド追加
1.0 2021/04/02 初版
pahooTwitterAPI.php 更新履歴
バージョン 更新日 内容
5.5.1 2024/11/23 __construct() -- PHP8.4における応急処置
5.5.0 2024/06/21 TwitterOAuth 7.0.0 対応
5.4.0 2024/05/18 twitter.com → x.com 変更対応
5.3.0 2023/08/15 tweet3() -- メディアのシャフル機能
5.2.1 2023/07/22 bug-fix
pahooBlueskyAPI.php 更新履歴
バージョン 更新日 内容
1.4.2 2024/11/24 parseRichText() -- bugfix
1.4.1 2024/11/22 PHP8.4対応
1.4.0 2024/11/21 ハッシュタグに対応
1.3.5 2024/11/10 URL_LENを23文字に訂正
1.3.4 2024/10/31 getOGPInformation() -- 文字化け対策

気象庁防災情報XMLフォーマット

気象庁防災情報XMLフォーマットは、気象や地震、火山に関する情報を随時流している。
まず、下表の6つの Atomフィードにアクセスし、必要なXML情報のURLを求める。
フィードの構造(xml) feed title タイトル subtitle サブタイトル updated 配信日時 id ID rights 著作権表記 entry title XML情報タイトル(1) id XML情報のURL(1) updated 更新日時(1) author name 作者(1) content 内容説明(1) entry title XML情報タイトル(2) id XML情報のURL(2) updated 更新日時(2) author name 作者(2) content 内容説明(2)
XML情報のURLの命名規則は以下の通り。
https://www.data.jma.go.jp/developer/xml/data/yyyymmddhhmmss_番号_電文コード_連番.xml
指定河川の洪水予報に関する情報は、長期フィードの随時の中にある電文コード VXKO に入っている。そこで、フィードから VXKO を含むURLを取り出せば、それが目指す情報XMLとなる。

VXKOの構造

指定河川の洪水予報に関する情報XML VXKO の構造は下記の通りである。
ここから必要な情報を取り出す。
VXKOの構造(xml) Report Control Title 指定河川洪水予報に関する情報 DateTime 作成日時 Status ステータス EditorialOffice 編集者 PublishingOffice 配信者 Head Title 標題 ReportDateTime 発表日時 TargetDateTime 基点日時 EventID 識別情報 InfoType 情報系大雨 Serial 情報番号 InfoKind スキーマの運用種別情報 InfoKindVersion スキーマの運用種別のバージョン情報 Headline Text 見出し Body Warning Item Kind Property Type 主文1 Text 主文1の内容 Kind Property Type 主文2 Text 主文2の内容 Areas Area Name 河川名 Code 河川コード Item Kind Property Type 浸水想定地区 Areas Area Name 観測所名称1 Code 観測所コード1 Prefecture 都道府県名1 PrefectureCode 都道府県コード1 City 市町村名1 CityCode 市町村コード1 Area Name 観測所名称2 Code 観測所コード2 Prefecture 都道府県名2 PrefectureCode 都道府県コード2 City 市町村名2 CityCode 市町村コード2 MeteorologicalInfos MeteorologicalInfo DateTime 予報・観測の基点時刻 Item Kind Property Type 雨量 Text 雨量の予報・観測文

準備:pahooGeoCode クラス

pahooGeoCode.php

  37: class pahooGeoCode {
  38:     var $items;     //検索結果格納用
  39:     var $error;     //エラー・フラグ
  40:     var $errmsg;    //エラー・メッセージ
  41:     var $hits;      //検索ヒット件数
  42:     var $webapi;    //直前に呼び出したWebAPI URL
  43: 
  44:     //Google Cloud Platform APIキー
  45:     //https://cloud.google.com/maps-platform/
  46:     //※Google Maps APIを利用しないのなら登録不要
  47:     var $GOOGLE_API_KEY_1 = '**************************';   //HTTPリファラ用
  48:     var $GOOGLE_API_KEY_2 = '**************************';   //IP制限用
  49: 
  50:     //Yahoo! JAPAN Webサービス アプリケーションID
  51:     //https://e.developer.yahoo.co.jp/register
  52:     //※Yahoo! JAPAN Webサービスを利用しないのなら登録不要
  53:     var $YAHOO_APPLICATION_ID = '*****************************';

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

地図としてGoogleマップを利用するのであれば、Google Cloud Platform APIキー が必要で、その入手方法は「Google Cloud Platform - WebAPIの登録方法」を参照されたい。

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

jmaRiverFloodForecast.php

  44: // 地図描画サービスの選択
  45: //    0:Google
  46: //    2:地理院地図・OSM
  47: define('MAPSERVICE', 2);
  48: 
  49: // 住所検索サービスの選択
  50: //    0:Google
  51: //    1:Yahoo!JAPAN
  52: //   11:HeartRails Geo API
  53: //   12:OSM Nominatim Search API
  54: define('GEOSERVICE', 1);

表示する地図は、Googleマップ地理院地図・オープンストリートマップ(OSM)から選べる。あらかじめ、定数 MAPSERVICE に値を設定すること。
観測所の都道府県・市町村名からマッピング位置を特定するための住所検索サービスは、GoogleYahoo!JAPANHeartRails Geo APIOSM Nominatim Search API から選べる。あらかじめ、定数 GEOSERVICE に値を設定すること。
PHPで直近の地震情報を表示する
オープンストリートマップ+色別標高図+活断層図

準備:キャッシュ・システム

jmaRiverFloodForecast.php

  71: // キャッシュ保持時間(分) 0:キャッシュしない
  72: // 気象庁へのアクセス負荷軽減のため,適切な時間を設定してください.
  73: define('LIFE_CACHE_FEED',     5);       // 高頻度 - 随時フィードに対して
  74: define('LIFE_CACHE_FEED_L', 120);       // 長期 - 随時フィードに対して
  75: define('LIFE_CACHE_DATA',   720);       // 指定河川洪水予報情報に対して
  76: 
  77: // キャッシュ・ディレクトリ
  78: // 書き込み可能で,外部からアクセスされないディレクトリを指定してください.
  79: // 天気予報系のプログラムとは別のディレクトリを指定してください.
  80: define('DIR_CACHE_FEED',   './pcache1/');
  81: define('DIR_CACHE_FEED_L', './pcache2/');
  82: define('DIR_CACHE_DATA',   './pcache3/');

気象庁サイトへ負荷をかけないよう、キャッシュ・クラス pahooCache を導入した。使用方法については、「PHPで天気予報を求める - キャッシュ・システム」を参照いただきたい。
キャッシュ保持時間、キャッシュ・ディレクトリともに、自サイトの環境に応じて変更してほしい。天気予報系プログラムと別のキャッシュ・ディレクトリにした方が、お互いのキャッシュ保持時間の干渉を受けなくなる。
配布ファイルは、新しい地震情報が入ってくることを考え、フィードの方を短く、指定河川洪水予報情報の方は長くキャッシュ保持時間を設定してある。

準備:各種定数など

jmaRiverFloodForecast.php

  34: // 各種定数(START) ===========================================================
  35: // Twitter(現・X)投稿ボタン  TRUE:有効,FALSE:無効
  36: define('TWITTER', FALSE);
  37: 
  38: // Bluesky投稿ボタン  TRUE:有効,FALSE:無効
  39: define('BLUESKY', FALSE);
  40: 
  41: // 画像化したいオブジェクト
  42: define('TARGET', 'target');
  43: 
  44: // 地図描画サービスの選択
  45: //    0:Google
  46: //    2:地理院地図・OSM
  47: define('MAPSERVICE', 2);
  48: 
  49: // 住所検索サービスの選択
  50: //    0:Google
  51: //    1:Yahoo!JAPAN
  52: //   11:HeartRails Geo API
  53: //   12:OSM Nominatim Search API
  54: define('GEOSERVICE', 1);
  55: 
  56: // マップの表示サイズ(単位:ピクセル)
  57: define('MAP_WIDTH',  640);
  58: define('MAP_HEIGHT', 480);
  59: // マップID
  60: define('MAPID', 'map_id');
  61: // 初期値
  62: define('DEF_LATITUDE',  38.82);             // 緯度
  63: define('DEF_LONGITUDE', 138.28);            // 経度
  64: define('DEF_ZOOM',      5);                 // ズーム
  65: define('DEF_TYPE',     'ROADMAP');          // マップタイプ
  66: define('DEF_OVERLAYS', 'GSIELEV,GSIFAULT'); // オーバーレイ(Leaflet使用時のみ)
  67: define('INFO_WIDTH', (MAP_WIDTH * 0.75));   // 情報ウィンドウの最大幅
  68: define('INFO_OFFSET_X',   0);           // 情報ウィンドウのオフセット位置(X)
  69: define('INFO_OFFSET_Y', -25);           // 情報ウィンドウのオフセット位置(Y)
  70: 
  71: // キャッシュ保持時間(分) 0:キャッシュしない
  72: // 気象庁へのアクセス負荷軽減のため,適切な時間を設定してください.
  73: define('LIFE_CACHE_FEED',     5);       // 高頻度 - 随時フィードに対して
  74: define('LIFE_CACHE_FEED_L', 120);       // 長期 - 随時フィードに対して
  75: define('LIFE_CACHE_DATA',   720);       // 指定河川洪水予報情報に対して
  76: 
  77: // キャッシュ・ディレクトリ
  78: // 書き込み可能で,外部からアクセスされないディレクトリを指定してください.
  79: // 天気予報系のプログラムとは別のディレクトリを指定してください.
  80: define('DIR_CACHE_FEED',   './pcache1/');
  81: define('DIR_CACHE_FEED_L', './pcache2/');
  82: define('DIR_CACHE_DATA',   './pcache3/');
  83: 
  84: // これより古い情報は無視する(単位:時間)
  85: define('INTERVAL', 24);
  86: 
  87: // 最大プロット数
  88: define('MAX_MARKERS', 999);
  89: 
  90: // 警戒レベルのアイコン
  91: define('ICON_CAUSION', 'https://maps.google.com/mapfiles/ms/micons/red-dot.png');
  92: // 予報アイコン
  93: define('ICON_FORECAST', 'https://maps.google.com/mapfiles/ms/micons/yellow-dot.png');
  94: 
  95: // SQLite DBファイル名:各自の環境に合わせて変更すること
  96: define('DBFILE', './riverFloodArea.sqlite3');
  97: 
  98: // SQLite テーブル名:浸水想定地区
  99: define('TABLE_AREA', 'area');
 100: 
 101: // 実行するSQL:浸水想定地区【変更不可】
 102: define('PRE_SELECT', 'SELECT * FROM ' . TABLE_AREA . ' WHERE id=:id');
 103: define('PRE_INSERT', 'INSERT INTO ' . TABLE_AREA . ' (id, area, latitude, longitude, premiere, latest) VALUES (:id, :area, :latitude, :longitude, :premiere, :latest)');
 104: define('PRE_UPDATE', 'UPDATE ' . TABLE_AREA . ' set area=:area, latitude=:latitude, longitude=:longitude, latest=:latest WHERE id=:id');
 105: 
 106: // 気象庁防災情報XML:高頻度フィード - 随時【変更不可】
 107: define('FEED', 'https://www.data.jma.go.jp/developer/xml/feed/extra.xml');
 108: 
 109: // 気象庁防災情報XML:長期フィード - 随時【変更不可】
 110: define('FEED_L', 'https://www.data.jma.go.jp/developer/xml/feed/extra_l.xml');
 111: 
 112: // 住所・緯度・経度に関わるクラス:include_pathが通ったディレクトリに配置
 113: require_once('pahooGeoCode.php');
 114: 
 115: // キャッシュ処理に関わるクラス:include_pathが通ったディレクトリに配置
 116: require_once('pahooCache.php');
 117: 
 118: // TwitterAPIクラス:include_pathが通ったディレクトリに配置
 119: if (TWITTER) {
 120:     require_once('pahooTwitterAPI.php');
 121: }
 122: 
 123: // BlueskyAPIクラス:include_pathが通ったディレクトリに配置
 124: if (BLUESKY) {
 125:     require_once('pahooBlueskyAPI.php');
 126:     define('BLUESKY_DOMAIN', 'bsky.social');    // あなたのドメインを記入
 127: }
 128: // 各種定数(END) ============================================================

各種定数などの内容は、プログラムのコメントに記載した通りである。「変更不可」以外の定数は自由に変更できる。

出力結果を Twitter(現・X) に投稿することができる。投稿機能を有効化するときは、定数 TWITTER を TRUE にする。ユーザー定義クラス pahooTwitterAPI を利用するので、"pahooTwitterAPI.php" をinclude_pathが通ったディレクトリに配置すること。また、事前にアプリケーションの登録が必要になる。詳しくは「PHPでTwitter(現・X)に投稿(ツイート)する」を参照してほしい。

出力結果を Bluesky に投稿することができる。投稿機能を有効化するときは、定数 BLUESKY を TRUE にする。ユーザー定義クラス pahooBlueskyAPI を利用するので、"pahooBlueskyAPI.php" をinclude_pathが通ったディレクトリに配置すること。また、事前にアプリケーションの登録が必要になる。詳しくは「PHPでPHPでBlueskyに投稿する」を参照してほしい。

Twitter(現・X) や Bluesky のボタン・アイコンについては、「HTMLとCSSでさまざまなアイコンを表示する」を参照して欲しい。

解説:気象庁防災情報XMLから指定河川洪水予報に関する情報URLを取得

jmaRiverFloodForecast.php

 499: /**
 500:  * 気象庁防災情報XMLから指定河川洪水予報に関する情報URLを取得
 501:  * @param   array  $urls    URL格納配列
 502:  * @param   string $errmsg  エラーメッセージ格納用
 503:  * @return  bool TRUE:取得成功/FALSE:取得失敗
 504: */
 505: function jmaGetRiverFloodForecastURLs(&$urls, &$errmsg) {
 506:     // URLパターン
 507:     $vxko = '/https?\:\/\/www\.data\.jma\.go\.jp\/developer\/xml\/data\/([0-9\_]+)VXKO[0-9]+\_[0-9]+\.xml/ui';
 508: 
 509:     // 随時フィードの解析
 510:     $cnt = 0;
 511:     $pcc = new pahooCache(LIFE_CACHE_FEED, DIR_CACHE_FEED);
 512:     $xml = $pcc->simplexml_load(FEED);
 513:     // レスポンス・チェック
 514:     if ($pcc->iserror() || !isset($xml->entry)) {
 515:         $errmsg = '気象庁防災情報XMLにアクセスできません';
 516:         return FALSE;
 517:     }
 518:     foreach ($xml->entry as $node) {
 519:         // URLを取得
 520:         if (preg_match($vxko, $node->id, $arr> 0) {
 521:             $urls[$cnt] = $arr[0];
 522:             $cnt++;
 523:         }
 524:     }
 525:     $pcc = NULL;
 526: 
 527:     // 長期フィードの解析
 528:     $pcc = new pahooCache(LIFE_CACHE_FEED_L, DIR_CACHE_FEED_L);
 529:     $xml = $pcc->simplexml_load(FEED_L);
 530:     // レスポンス・チェック
 531:     if ($pcc->iserror() || !isset($xml->entry)) {
 532:         $errmsg = '気象庁防災情報XMLにアクセスできません';
 533:         return FALSE;
 534:     }
 535:     foreach ($xml->entry as $node) {
 536:         // URLを取得
 537:         if (preg_match($vxko, $node->id, $arr> 0) {
 538:             if (array_search($arr[0], $urls) === FALSE) {
 539:                 $urls[$cnt] = $arr[0];
 540:                 $cnt++;
 541:             }
 542:         }
 543:     }
 544:     $pcc = NULL;
 545: 
 546:     // エラー・チェック
 547: //  if ($cnt == 0) {
 548: //      $errmsg = '指定河川洪水予報はありません';
 549: //      return FALSE;
 550: //  }
 551: 
 552:     // URLを日時の新しい順にソート
 553:     if (count($urls> 0)   rsort($urls);
 554: 
 555:     return TRUE;
 556: }

前述のフィードから、指定河川洪水予報に関する情報URLを取得するユーザー関数が jmaGetRiverFloodForecastURLs である。
VXKO を含むURLを配列 $urls に格納していき、最後に配列 $urls を大きい順にソートすることで、日付の新しい順にソートしたことになる。

解説:指定河川洪水予報情報の取り出し

指定河川洪水予報情報の取り出し
指定河川洪水予報情報に関する情報XML VXKO から、発表日時、主文、洪水想定地区、雨量を取り出し、配列 $items に格納する。

VXKO からは洪水想定地区の緯度・経度が分からないため、pahooGeoCodeクラスのメソッド searchPoint3 を使って緯度・経度を取得する。
このメソッドは、WebAPIを呼び出して住所から緯度・経度を取得するものだが、いちいち呼び出すと時間がかかる(GoogleAPIは費用もかかる)ので、一度検索したデータはデータベース SQLite に格納しておき、二度目からはDBのデータを取り出すようにした。

jmaRiverFloodForecast.php

 558: /**
 559:  * 指定河川洪水予報情報を取得(気象庁防災情報XMLから)
 560:  * @param   object $pgc     pahooGeoCodeオブジェクト
 561:  * @param   array  $items   指定河川洪水予報を格納する配列
 562:  * @param   string $urls    情報XMLのURLを格納する配列
 563:  * @param   string $errmsg  エラーメッセージ格納用
 564:  * @return  bool TRUE:取得成功/FALSE:失敗
 565: */
 566: function getRiverFloodForecast($pgc, &$items, &$urls, &$errmsg) {
 567:     // マッチングパターン
 568:     $pat1 = '/([0-9]+)\-([0-9]+)\-([0-9]+)T([0-9]+)\:([0-9]+)/ui';      // 年月日時分
 569: 
 570:     // オブジェクト生成
 571:     $pcc = new pahooCache(LIFE_CACHE_DATA, DIR_CACHE_DATA);
 572:     $pgc = new pahooGeoCode();
 573: 
 574:     // DBオープン
 575:     $pdo = new PDO('sqlite:' . DBFILE);
 576: 
 577:     // 指定河川洪水予報取に関する情報URLを取得
 578:     $urls = array('');
 579:     jmaGetRiverFloodForecastURLs($urls, $errmsg);
 580:     if ($errmsg !'')      return FALSE;
 581:     if (count($urls) == 0)  return TRUE;
 582: 
 583:     $count = 1;
 584:     foreach ($urls as $key=>$vxko) {
 585:         // 指定河川洪水予報の取得
 586:         $xml = $pcc->simplexml_load($vxko);
 587:         if ($xml == FALSE)  continue;           // ver.1.22
 588: 
 589:         // レスポンス・チェック
 590:         if ($pcc->iserror() || !isset($xml->Body->Warning->Item)) {
 591:             $errmsg = '気象庁防災情報XMLから指定河川洪水予報を取得できません';
 592:             return FALSE;
 593:         }
 594:         // 発表日時
 595:         $dt = (string)$xml->Head->ReportDateTime;
 596:         // 古い情報かどうか
 597:         if (time() - strtotime($dt> INTERVAL * 60 * 60) {
 598:             continue;
 599:         }
 600:         $items[$count]['dt'] = $dt;
 601:         preg_match($pat1, $dt, $arr);
 602:         $items[$count]['year']    = (int)$arr[1];
 603:         $items[$count]['month']   = (int)$arr[2];
 604:         $items[$count]['day']     = (int)$arr[3];
 605:         $items[$count]['hour']    = (int)$arr[4];
 606:         $items[$count]['minuite'] = (int)$arr[5];
 607:         // 指定河川洪水予報
 608:         $data = array();
 609:         foreach ($xml->Body->Warning->Item as $item) {
 610:             // 主文
 611:             if (isset($item->Kind->Property->Type&& ((string)$item->Kind->Property->Type == '主文')) {
 612:                 if (isset($items[$count]['main'])) {
 613:                     $items[$count]['main'."\n" . (string)$item->Kind->Property->Text;
 614:                 } else {
 615:                     $items[$count]['main'] = (string)$item->Kind->Property->Text;
 616:                 }
 617:             // 浸水想定地区
 618:             } else if (isset($item->Kind->Property->Type&& ((string)$item->Kind->Property->Type == '浸水想定地区')) {
 619:                 $i = 1;
 620:                 foreach ($item->Areas->Area as $area) {
 621:                     $query = (string)$area->Prefecture . (string)$area->City;
 622:                     $items[$count]['areas'][$i]['area'] = $query;
 623:                     $id = (string)$area->CityCode;
 624:                     // 緯度・経度をDBから取得
 625:                     if (getAreaFromDB($pdo, $id, $data)) {
 626:                         $latitude  = $data['latitude'];
 627:                         $longitude = $data['longitude'];
 628:                     } else {
 629:                         list($n, $url) = $pgc->searchPoint3($query, GEOSERVICE, 'address');
 630:                         // エラーがなければ最初の住所を対象にする
 631:                         if (! $pgc->iserror()) {
 632:                             list($latitude, $longitude, $address) = $pgc->getPoint(1);
 633:                             // DB登録
 634:                             $data['area']      = $query;
 635:                             $data['latitude']  = $latitude;
 636:                             $data['longitude'] = $longitude;
 637:                             $res = storeAreaToDB($pdo, $id, $data);
 638:                             if (! $res) {
 639:                                 $errmsg = 'DB書き込みに失敗しました';
 640:                                 return FALSE;
 641:                             }
 642:                         } else {
 643:                             $errmsg = $pgc->geterror();
 644:                             return FALSE;
 645:                         }
 646:                     }
 647:                     $items[$count]['areas'][$i]['latitude']  = $latitude;
 648:                     $items[$count]['areas'][$i]['longitude'] = $longitude;
 649:                     $i++;
 650:                 }
 651:             }
 652:         }
 653:         // 雨量情報
 654:         if (isset($xml->Body->MeteorologicalInfos->MeteorologicalInfo->Item)) {
 655:             foreach ($xml->Body->MeteorologicalInfos->MeteorologicalInfo->Item as $item) {
 656:                 if (isset($item->Kind->Property->Type&& ((string)$item->Kind->Property->Type == '雨量')) {
 657:                     if (isset($items[$count]['rainfall'])) {
 658:                         $items[$count]['rainfall'."\n" . (string)$item->Kind->Property->Text;
 659:                     } else {
 660:                         $items[$count]['rainfall'] = (string)$item->Kind->Property->Text;
 661:                     }
 662:                 }
 663:             }
 664:         }
 665:         $count++;
 666:     }
 667: 
 668:     // DBクローズ
 669:     $pdo = NULL;
 670:     // オブジェクト解放
 671:     $pgc = $pcc = NULL;
 672: 
 673:     return TRUE;
 674: }

SQLite のデータベースは、メインプログラムの冒頭でユーザー関数 initDB を呼び出し、データベースファイル DBFILE が存在しなければ新規作成するようにしている。

jmaRiverFloodForecast.php

 289: /**
 290:  * DBの初期化
 291:  * @param   なし
 292:  * @return  bool TRUE/FALSE
 293: */
 294: function initDB() {
 295:     try {
 296:         $pdo = new PDO('sqlite:' . DBFILE);
 297:         $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
 298: 
 299:         // テーブル作成:浸水想定地区
 300:         // id:観測所のCityCode, area:都道府県+市町村名
 301:         // latitude:緯度, longitude:経度
 302:         // premiere:登録日時, latest:更新日時
 303:         $pdo->exec('CREATE TABLE IF NOT EXISTS ' . TABLE_AREA . '(
 304:          id        INTEGER PRIMARY KEY AUTOINCREMENT,
 305:             area      TEXT,
 306:             latitude  REAL,
 307:             longitude REAL,
 308:             premiere  TEXT,
 309:             latest    TEXT
 310:         )');
 311:      $res = TRUE;
 312:     } catch (PDOException $e) {
 313:         $res = FALSE;
 314:     }
 315: 
 316:     return $res;
 317: }

解説:指定河川洪水予報情報をマッピング情報に変換

jmaRiverFloodForecast.php

 676: /**
 677:  * 指定河川洪水予報情報をマッピング情報に変換
 678:  * @param   array  $items   指定河川洪水予報情報を格納した配列
 679:  * @param   array  $points  マッピング情報を格納する配列
 680:  * @return  int 変換したマッピング情報の件数
 681: */
 682: function info2points(&$items, &$points) {
 683:     // データがない
 684:     if (count($items) == 0)     return 0;
 685: 
 686:     // 変換処理
 687:     $i = $j = 0;
 688:     $cnt = 1;
 689:     foreach ($items as $i=>$item) {
 690:         if ($cnt > MAX_MARKERS)     break;          // 最大プロット数
 691:         if (!isset($item['areas']))     continue;   // Ver.1.21修正
 692:         foreach ($item['areas'as $j=>$val) {
 693:             // 同じマッピング位置があるかどうか
 694:             $flag = FALSE;
 695:             for ($k = 1$k < $cnt$k++) {
 696:                 if ($val['area'] == $points[$k]['title']) {
 697:                     $flag = TRUE;
 698:                     break;
 699:                 }
 700:             }
 701:             // 新規のマッピング情報
 702:             if (! $flag) {
 703:                 // アイコン
 704:                 if (preg_match('/警戒レベル/ui', $items[$i]['main']) > 0) {
 705:                     $points[$cnt]['icon'] = ICON_CAUSION;
 706:                 } else {
 707:                     $points[$cnt]['icon'] = ICON_FORECAST;
 708:                 }
 709:                 // タイトル
 710:                 $points[$cnt]['title'] = $val['area'];
 711:                 // 緯度・経度
 712:                 $points[$cnt]['latitude']  = $items[$i]['areas'][$j]['latitude'];
 713:                 $points[$cnt]['longitude'] = $items[$i]['areas'][$j]['longitude'];
 714:                 // 情報ウィンドウの内容
 715:                 $points[$cnt]['description'] = 
 716:                     sprintf('%04d年%02d月%02d日 %02d時%02d分 発表<br /><span style="font-weight:bold;">%s</span><br />%s<br />%s',  $items[$i]['year'],
 717:                         $items[$i]['month'],
 718:                         $items[$i]['day'],
 719:                         $items[$i]['hour'],
 720:                         $items[$i]['minuite'],
 721:                         $val['area'],
 722:                         preg_replace('/\n/', '<br />', $items[$i]['main']),
 723:                         preg_replace('/\n/', '<br />', $items[$i]['rainfall']));
 724:                     $cnt++;
 725:             }
 726:         }
 727:     }
 728: 
 729:     return $cnt;
 730: }

getRiverFloodForecast で得た指定河川洪水予報情報を、マッピングしやすいように、洪水想定地区をキーに配列 $points に展開するユーザー関数が info2points である。

解説:地図描画について

マップの描画については、「PHPで住所・ランドマークから最寄り駅を求める」をご覧いただきたい。

解説:オーバーレイ表示(Leaflet選択時のみ)

マップで Leaflet を選択したとき、オーバーレイ地図を表示できるようにした。
詳しくは「PHPで最近の地震情報を表示する」をご覧いただきたい。

解説:SNS投稿機能

PHPによるSNS投稿機能の概念図
PHPによるSNS投稿機能の概念図
表示したコンテンツを画像として、メッセージと一緒にボタン1つでSNSに投稿する機能を追加した。流れは次の通りである。
コンテンツを描画しているのはクライアントにあるブラウザ(レンダリングエンジン)であることから、クライアント側で画像を作成し、サーバ側で SNS の API をコールするという方針とした。
  1. ブラウザはサーバにコンテンツ描画をリクエストする。
  2. サーバはデータサイトからデータ取得する。(サーバキャッシュにデータがあればそれを利用する)
  3. サーバはコンテンツ描画スクリプトを生成する。
  4. サーバはブラウザへレスポンス(HTML文)を返す。
  5. ブラウザはコンテンツをレンダリングする。
  6. ブラウザはレンダリングしたコンテンツを画像データとしてサーバへアップロードする。
  7. サーバは SNS の API を使ってツイートする。
  8. サーバは SNS へメッセージと画像を送る。
  9. サーバはブラウザへレスポンス(HTML文)を返す。
ブラウザでレンダリングしたグラフを画像に変換する処理は、JavaScriptの html2canvas ライブラリを利用した。このライブラリの使い方は後述する。投稿するSNSのうち、Twitter(現・X) については、「PHPでTwitterに投稿(ツイート)する」で作成した pahooTwitterAPI クラスを利用する。Bluesky については、「PHPでBlueskyに投稿する」で作成した pahooBlueskyAPI クラスを利用する。

解説:html2canvasライブラリ

jmaRiverFloodForecast.php

 152: <script src="https://html2canvas.hertzen.com/dist/html2canvas.js"></script>
 153: <script>
 154: function mytweet() {
 155:     $('#tweet').val('1');
 156:     document.myform.submit();
 157: }

HTML表示を画像化するために、html2canvas ライブラリを利用する。このライブラリは Niklas von Hertzen氏によって開発されたもので、ライセンスはMITとなっている。
画像化を実行するJavaScript関数は html2canvas である。

jmaRiverFloodForecast.php

 866: <div id="{$target}" name="{$target}" style="width:{$width2}px;">
 867: <p>
 868: 🌊指定河川洪水予報情報 {$dt}現在
 869: &nbsp;{$tweet}{$bluesky}
 870: </p>
 871: {$html}
 872: </div>

画像化するオブジェクトは、<div id="{$target}"> で指定する範囲である。
レンダリングエンジンによって違うのかもしれないが、html2canvas ライブラリによって画像化される範囲が実際よりやや小さいため、あえてマップ領域より、幅を20ピクセル大きくした範囲を画像化範囲としている。

jmaRiverFloodForecast.php

 433: /**
 434:  * HTMLオブジェクトの画像化
 435:  * @param   なし
 436:  * @return  string JavaScriptコード
 437: */
 438: function js_html2image() {
 439:     $target = TARGET;
 440:     $js = '';
 441: 
 442:     // Googleマップの場合
 443:     if (MAPSERVICE == 0) {
 444:         $js .=<<< EOT
 445: google.maps.event.addListener(map, 'tilesloaded', function() {
 446:     var capture = document.querySelector('#{$target}');
 447:     html2canvas(capture, {useCORS: true, allowTaint:true}).then(canvas => {
 448:         var base64 = canvas.toDataURL('image/png');     // 画像化
 449:         $('#base64').val(base64);
 450:     });
 451: });
 452: 
 453: EOT;
 454: 
 455:     // Leafletの場合(ブラウザによってはうまく動作しない)
 456:     } else {
 457:         $js .=<<< EOT
 458: HTMLCanvasElement.prototype.getContext = function(origFn) {
 459:     return function(type, attribs) {
 460:         attribs = attribs || {};
 461:         attribs.preserveDrawingBuffer = true;
 462:         return origFn.call(this, type, attribs);
 463:     };
 464: } (HTMLCanvasElement.prototype.getContext);
 465: 
 466: // HTML画像化イベント登録
 467: function html2image() {
 468:     var capture = document.querySelector('#{$target}');
 469:     html2canvas(capture, {useCORS: true, allowTaint:true}).then(canvas => {
 470:         var base64 = canvas.toDataURL('image/png');     // 画像化
 471:         $('#base64').val(base64);
 472:     });
 473: };
 474: 
 475: // ズーム変更イベント
 476: map.on('zoomend', function() {
 477:     html2image();
 478: });
 479: 
 480: // マップ移動イベント
 481: map.on('moveend', function() {
 482:     html2image();
 483: });
 484: 
 485: // html2image()を発火させるために,ズームアウトして500msec後に元に戻す.
 486: var zoom = map.getZoom();
 487: map.setZoom(zoom - 1);
 488: setTimeout(function() {
 489:     map.setZoom(zoom);
 490: }, 500);
 491: 
 492: EOT;
 493:     }
 494: 
 495:     return $js;
 496: }

html2canvas を呼び出すタイミングだが、Googleマップの場合は tilesloadedイベントにフックする。
Leaflet の場合、これに相当するイベントがないため、ズーム変更完了イベントにフックし、強制的にズーム変更イベントを発生させるタイミングで画像化する。しかし、Leaflet では、画像を描画するレイヤ構造の関係で、html2canvas ライブラリでは位置ずれが起きることがわかっている。その場合は、Googleマップを選択してほしい。また、対策方法をご存じの方がいたらお知らせいただきたい
これらをJavaScriptとして生成するユーザー関数が js_html2image である。

準備:pahooTwitterAPI クラス

pahooTwitterAPI.php

  19: class pahooTwitterAPI {
  20:     var $responses;     //応答データ
  21:     var $webapi;        //直前に呼び出したWebAPI URL
  22:     var $error;         //エラーフラグ
  23:     var $errmsg;        //エラーメッセージ
  24:     var $errcode;       //エラーコード
  25:     var $connection;
  26: 
  27:     //OAuth用パラメータ
  28:     // https://apps.twitter.com/
  29:     var $TWTR_CONSUMER_KEY    = '***************';  //Cunsumer key
  30:     var $TWTR_CONSUMER_SECRET = '***************';  //Consumer secret
  31:     var $TWTR_ACCESS_KEY      = '***************';  //Access Token (oauth_token)
  32:     var $TWTR_ACCESS_SECRET   = '***************';  //Access Token Secret (oauth_token_secret)

Twitter API」を利用するために、API keyAPI secret keyAccess tokenAccess token secret が必要で、入手方法は「Twitter API - WebAPIの登録方法」を参照されたい。

解説:メディア付き投稿(RAWデータ)

pahooTwitterAPI.php

 590: /**
 591:  * バイナリデータを使ったメディア付きメッセージをツイートする.
 592:  * Tweetet API v2 を使用する.
 593:  * @param   string $message 投稿メッセージ(UTF-8限定)
 594:  * @param   array  $items   メディアデータ(バイナリデータ配列)
 595:  * @return  bool TRUE:リクエスト成功/FALSE:失敗
 596: */
 597: function tweet_media_raw($message, $items) {
 598:     //メディアのアップロード
 599:     $media_ids = array();
 600:     $cnt = 0;
 601:     //Tweetet API v1.1 を使用する(v2にメディアアップロードが未実装のため)
 602:     $this->connection->setApiVersion('1.1'); 
 603:     foreach ($items as $data) {
 604:         $tmpname = $this->saveTempFile($data);
 605: //      $media = $this->connection->upload('media/upload', ['media' => $tmpname]);
 606:         $media = $this->connection->upload('media/upload', ['media' => $tmpname], ['chunkedUpload' => true]);   //twitteroauth 7.0.0 対応
 607:         unlink($tmpname);
 608:         if (! isset($media->media_id_string))   break;      //処理失敗
 609:         $media_ids[] = (string)$media->media_id_string;
 610:         $cnt++;
 611:         if ($cnt > 3)   break;      //最大4つまで
 612:     }
 613: 
 614:     //メディア付きツイート(Tweetet API v2 を使用する)
 615:     $this->connection->setApiVersion('2'); 
 616:     $option = [
 617:         'text' => $message,
 618:         'media' => [
 619:             'media_ids' => $media_ids
 620:         ]
 621:     ];
 622: //  $status = $this->connection->post('tweets', $option, TRUE);
 623:     $status = $this->connection->post('tweets', $option, ['jsonPayload' => true]);  //twitteroauth 7.0.0 対応
 624:     $this->webapi = 'https://api.twitter.com/2/tweets';
 625: 
 626:     //処理に成功した.
 627:     if ($this->isSuccess()) {
 628:         $this->responses = $status->data;
 629:         $this->errcode   = NULL;
 630:         $this->errmsg    = '';
 631:         $this->error     = FALSE;
 632:         $res = TRUE;
 633:     //処理に失敗した.
 634:     } else {
 635:         if ($this->isAuthError() == FALSE) {
 636:             $this->errmsg = $status->detail;
 637:             $this->error  = TRUE;
 638:         }
 639:         $res = FALSE;
 640:     }
 641:     return $res;
 642: }

メッセージと画像を同時にツイートするのに、「PHPでTwitterに画像付きメッセージ投稿」で作ったメソッド tweet_media を改良し、引数に画像バイナリデータ(RAWデータ)を渡してTwitterAPIを呼び出す tweet_media_raw メソッドを用意した。

解説:Twitter(現・X)へ投稿する

jmaRiverFloodForecast.php

 381: /**
 382:  * Twitter(現・X)投稿
 383:  * @param   string $message 投稿文
 384:  * @param   string $res     応答メッセージ格納用
 385:  * @return  bool TRUE:成功/FALSE:失敗または未処理
 386: */
 387: function mediaTweet($message, &$res) {
 388:     if (! TWITTER)  return FALSE;
 389: 
 390:     $ret = TRUE;
 391:     if (isset($_POST['base64']) && ($_POST['base64'!'')) {
 392:         $base64 = preg_replace('/data\:image\/png\;base64\,/ui', '', $_POST['base64']);
 393:         $raws = array(base64_decode($base64));
 394:         $ptw = new pahooTwitterAPI();
 395:         $ptw->tweet_media_raw($message, $raws);
 396:         $errmsg = $ptw->errmsg;
 397:         $ret = ! $ptw->error;
 398:         $ptw = NULL;
 399:         if ($ret) {
 400:             $res = 'ツイートしました';
 401:         }
 402:     }
 403:     return $ret;
 404: }

メッセージと画像を Twitter(現・X)に投稿するのはサーバ側のPHPユーザー関数 mediaTweet である。
ブラウザからPOSTで受け取った画像データ $_POST['base64'] はBASE64でデコードされており、冒頭に余計なヘッダ情報が付いているので、このヘッダ情報を除き( preg_replace )、バイナリデータにデコードする( base64_decode )。
続いて pahooTwitterAPI クラスを呼び出し、tweet_media_raw メソッドを使って、メッセージと画像を一気に投稿する。

準備:pahooBlueskyAPI クラス

pahooBlueskyAPI.php

  15: class pahooBlueskyAPI {
  16:     var $pds;           // PDSドメイン
  17:     var $webapi;        // 直前に呼び出したWebAPI URL
  18:     var $errmsg;        // エラーメッセージ
  19:     var $accessJwt;     // accessJwt
  20: 
  21:     const INTERNAL_ENCODING = 'UTF-8';  //内部エンコーディング
  22:     const MAX_MESSAGE_LEN = 300;        // 投稿可能なメッセージ文字数
  23:     const URL_LEN = 23;                 // メッセージ中のURL文字数(相当)
  24:     const MAX_IMAGE_WIDTH  = 1200;      // 投稿可能な最大画像幅(ピクセル)
  25:     const MAX_IMAGE_HEIGHT = 900;       // 投稿可能な最大画像高さ(ピクセル)
  26:                                         // これより大きいときは自動縮小する
  27: 
  28:     // Bluesky API アプリパスワード
  29:     // https://bsky.app/
  30:     var $BLUESKY_HANDLE   = '***************';      // ハンドル名
  31:     var $BLUESKY_PASSWORD = '***************';      // アプリケーション・パスワード

Bluesky API」を利用するために、ハンドル名アプリケーション・パスワード が必要で、入手方法は「Bluesky API - WebAPIの登録方法」を参照されたい。

解説:Blueskyへ投稿する

jmaRiverFloodForecast.php

 406: /**
 407:  * Bluesky投稿
 408:  * @param   string $message 投稿文
 409:  * @param   string $res     応答メッセージ格納用
 410:  * @return  bool TRUE:成功/FALSE:失敗または未処理
 411: */
 412: function mediaBluesky($message, &$res) {
 413:     if (! BLUESKY)  return FALSE;
 414: 
 415:     $ret = TRUE;
 416:     if (isset($_POST['base64']) && ($_POST['base64'!'')) {
 417:         $base64 = preg_replace('/data\:image\/png\;base64\,/ui', '', $_POST['base64']);
 418:         $raws = array(base64_decode($base64));
 419:         $pbs = new pahooBlueskyAPI(BLUESKY_DOMAIN);
 420:         $res = $pbs->createSession();
 421:         $pbs->post($message, FALSE, NULL, NULL, $raws);
 422:         $errmsg = $pbs->geterror();
 423:         $ret = ! $pbs->iserror();
 424:         $res = $pbs->deleteSession();
 425:         $pbs = NULL;
 426:         if ($ret) {
 427:             $res = 'Blueskyに投稿しました';
 428:         }
 429:     }
 430:     return $ret;
 431: }

メッセージと画像を Blueskyに投稿するのはサーバ側のPHPユーザー関数 mediaBluesky である。
ブラウザからPOSTで受け取った画像データ $_POST['base64'] はBASE64でデコードされており、冒頭に余計なヘッダ情報が付いているので、このヘッダ情報を除き( preg_replace )、バイナリデータにデコードする( base64_decode )。
続いて pahooBlueskyAPI クラスを呼び出し、post メソッドを使って、メッセージと画像を一気に投稿する。

解説:SNSへ投稿する(メイン・プログラム)

jmaRiverFloodForecast.php

 916: // Twitter(現・X)、Blueskyへ投稿する.
 917: $dt = nowDT();
 918: $message =<<< EOT
 919: 🌊指定河川洪水予報情報 {$dt}現在
 920: 
 921: (ご参考)PHPで指定河川洪水予報情報を表示する https://www.pahoo.org/e-soul/webtech/php05/php05-19-01.shtm #洪水
 922: 
 923: EOT;
 924: if (TWITTER && isButton('tweet')) {
 925:     mediaTweet($message, $res);
 926: }
 927: if (BLUESKY && isButton('bluesky')) {
 928:     mediaBluesky($message, $res);
 929: }
 930: 
 931: // 表示コンテンツを作成する.

メイン・プログラムでは、mediaTweetmediaBluesky を呼び出し、メッセージと画像をSNSに投稿する。メッセージの内容は自由に変更していただいて構わない。

活用例

みんなの知識 ちょっと便利帳」では、「指定河川洪水予報を地図上に表示」で本プログラムを利用し、見やすいページを提供している。ありがとうございます。

参考サイト

(この項おわり)
header