サンプル・プログラムの実行例
目次
- サンプル・プログラムの実行例
- サンプル・プログラム
- 準備:PHP の https対応
- 準備:pahooGeoCode クラス
- 準備:pahooCache クラス
- 準備:pahooInputData 関数群
- 気象庁防災情報XMLフォーマット
- VXKOの構造
- 準備:地図サービスの選択
- 準備:各種定数など
- 解説:気象庁防災情報XMLから指定河川洪水予報に関する情報URLを取得
- 解説:指定河川洪水予報情報の取り出し
- 解説:指定河川洪水予報情報をマッピング情報に変換
- 解説:地図描画について
- 解説:オーバーレイ表示(Leaflet選択時のみ)
- 解説:SNS投稿機能
- 解説:html2canvasライブラリ
- 準備:pahooTwitterAPI クラス
- 解説:メディア付き投稿(RAWデータ)
- 解説:Twitter(現・X)へ投稿する
- 準備:pahooBlueskyAPI クラス
- 解説:Blueskyへ投稿する
- 解説:SNSへ投稿する(メイン・プログラム)
- 活用例
- 参考サイト
サンプル・プログラム
| jmaRiverFloodForecast.php | サンプル・プログラム本体。 |
| pahooInputData.php | データ入力に関わる関数群。 使い方は「数値入力とバリデーション」「文字入力とバリデーション」などを参照。include_path が通ったディレクトリに配置すること。 |
| pahooGeoCode.php | 住所・緯度・経度に関わるクラス pahooGeoCode。 使い方は「PHPで住所・ランドマークから最寄り駅を求める」などを参照。include_path が通ったディレクトリに配置すること。 |
| pahooCache.php | キャッシュ処理に関わるクラス pahooCache。 キャッシュ処理に関わるクラスの使い方は「PHPで天気予報を求める」を参照。include_path が通ったディレクトリに配置すること。 |
| pahooTwitterAPI.php | Twitter APIを利用するクラス pahooTwitterAPI。 使い方は「PHPでTwitterに投稿(ツイート)する」などを参照。include_path が通ったディレクトリに配置すること。 |
| pahooBlueskyAPI.php | Bluesky APIに関わるクラス pahooBlueskyAPI。 使い方は「PHPでPHPでBlueskyに投稿する」などを参照。include_path が通ったディレクトリに配置すること。 |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 1.6.0 | 2025/08/22 | count オプションを追加 |
| 1.5.0 | 2025/08/11 | pahooInputData.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()発火プロセス見直し |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 2.0.1 | 2025/08/11 | getParam() bug-fix |
| 2.0.0 | 2025/08/11 | pahooLoadEnv() 追加 |
| 1.9.0 | 2025/07/26 | getParam() 引数に$trim追加 |
| 1.8.1 | 2025/03/15 | validRegexPattern() debug |
| 1.8.0 | 2024/11/12 | validRegexPattern() 追加 |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 6.8.0 | 2025/08/10 | アクセスキーなどを ".env" に分離 |
| 6.7.1 | 2025/07/26 | jsLine_Gmap() - bug-fix |
| 6.7.0 | 2025/07/20 | drawJSmap,drawGMap -- 引数 $markerLevel 追加 |
| 6.6.0 | 2025/07/19 | drawJSmap,drawGMap,drawLeaflet -- マップ中心マーカー表示引数を追加 |
| 6.5.0 | 2025/06/14 | GoogleMaps JavaScript APIの変更に対応 |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 1.1.3 | 2025/08/10 | var→public |
| 1.1.2 | 2023/07/22 | bug-fix |
| 1.1.1 | 2023/02/11 | コメント追記 |
| 1.1 | 2021/04/08 | simplexml_load()メソッド追加 |
| 1.0 | 2021/04/02 | 初版 |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 5.6.0 | 2025/08/13 | .pahooEnv導入 |
| 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() -- メディアのシャフル機能 |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 2.7.0 | 2025/08/17 | reductImage,uploadBlob仕様変更←画像に余計な空白が入らないようにするため |
| 2.6.0 | 2025/08/14 | .pahooEnv 導入 |
| 2.5.1 | 2025/08/10 | uploadBlob() -- bug-fix |
| 2.5.0 | 2025/08/02 | getOGPInformation() -- og:imageがないページに対応 |
| 2.4.0 | 2025/07/19 | atruri2postURL() 追加, uploadBlob() 修正 |
準備:PHP の https対応
Windowsでは、"php.ini" の下記の行を有効化する。
extension=php_openssl.dllLinuxでは --with-openssl=/usr オプションを付けて再ビルドする。→OpenSSLインストール手順
これで準備は完了だ。
準備:pahooGeoCode クラス
pahooGeoCode.php
40: class pahooGeoCode {
41: public $items; // 検索結果格納用
42: public $error; // エラー・フラグ
43: public $errmsg; // エラー・メッセージ
44: public $hits; // 検索ヒット件数
45: public $webapi; // 直前に呼び出したWebAPI URL
46:
47: // -- 以下のデータは .env ファイルに記述可能
48: // Google Cloud Platform APIキー
49: // https://cloud.google.com/maps-platform/
50: // ※Google Maps APIを利用しないのなら登録不要
51: public $GOOGLE_API_KEY_1 = ''; // HTTPリファラ用
52: public $GOOGLE_API_KEY_2 = ''; // IP制限用
53: public $GOOGLE_MAP_ID = ''; // GoogleMaps ID
54:
55: // Yahoo! JAPAN Webサービス アプリケーションID
56: // https://e.developer.yahoo.co.jp/register
57: // ※Yahoo! JAPAN Webサービスを利用しないのなら登録不要
58: public $YAHOO_APPLICATION_ID = '';
59:
60: // OSM Nominatim Search API利用時に知らせるメールアドレス
61: // https://wiki.openstreetmap.org/wiki/JA:Nominatim#.E6.A4.9C.E7.B4.A2
62: // ※OSM Nominatim Search APIを利用しないのなら登録不要
63: public $NOMINATIM_EMAIL = '';
64:
65: // IP2Location.io APIキー
66: // https://www.ip2location.io/
67: // ※IP2Location.ioを利用しないのなら登録不要
68: public $IP2LOCATION_API_KEY = '';
地図や住所検索として Google を利用するのであれば Google Cloud Platform APIキー とマップID が必要で、その入手方法は「Google Cloud Platform - WebAPIの登録方法」を、Yahoo!JAPAN を利用するのであれば Yahoo! JAPAN Webサービス アプリケーションIDが必要で、その入手方法は「Yahoo!JAPAN デベロッパーネットワーク - WebAPIの登録方法」を、IP2Location.ioを利用するのであれば「PHPでIPアドレスやホスト名から住所を求める」を、それぞれ参照されたい。
PHPのクラスについては「PHPでクラスを使ってテキストの読みやすさを調べる」を参照されたい。
準備:pahooCache クラス
pahooCache.php
13: class pahooCache {
14: const LIFE_CACHE = (2 * 60); // キャッシュ保持時間(デフォルト;分)
15: const DEF_DIRCACHE = './pcache/'; // キャッシュ・ディレクトリ(デフォルト)
16:
17: public $lifeCache; // キャッシュ保持時間(分)(0:キャッシュしない)
18: public $dirCache; // キャッシュ用ディレクトリ
19: public $error; // エラーフラグ
20: public $errmsg; // エラーメッセージ
21: public $debug; // デバッグ用ファイル名
22:
23: /**
24: * コンストラクタ
25: * 参考サイト https://www.pahoo.org/e-soul/webtech/php06/php06-72-01.shtm
26: * @param int $life キャッシュ保持時間(分)(省略可能)
27: * @param string $dir キャッシュ・ディレクトリ(省略可能)
28: * @return なし
29: */
30: function __construct($life=self::LIFE_CACHE, $dir=self::DEF_DIRCACHE) {
31: if ($life < 0) {
32: $life = 0;
33: }
34: if (preg_match('/\/$/ui', $dir) == 0) {
35: $dir = $dir . '/';
36: }
37: $this->error = FALSE;
38: $this->errmsg = '';
39: $this->debug = '';
40: $this->lifeCache = $life;
41: $this->dirCache = $dir;
42:
43: // PHP5以上であることを調べる.
44: if (! $this->isphp5over()) {
45: $this->error = TRUE;
46: $this->errmsg = '動作にはPHP5以上が必要です';
47: return;
48: }
49:
50: // キャッシュ・ディレクトリが無ければ作成する.
51: if (! is_dir($this->dirCache)) {
52: $res = mkdir($this->dirCache, 0744);
53: if ($res == FALSE) {
54: $this->error = TRUE;
55: $this->errmsg = 'キャッシュ・ディレクトリ "' . $this->$dirCache . '" の作成に失敗しました';
56: return;
57: }
58: }
59: }
そこで、頻繁に変更がないデータについては、一度取り込んだら、こちら側のサーバのローカルストレージにキャッシュしておく仕組みを用意した。それが pahooCacheクラス である。同梱のクラス・ファイル "pahooCache.php" は include_path が通ったディレクトリに配置してほしい。他のプログラムでも pahooCacheクラス を利用するが、常に最新のクラス・ファイルを1つ配置すればよい。
pahooCacheクラス の注意ポイントは、キャッシュ時間(単位:分)とキャッシュを保存するディレクトリをコンストラクタで指定している点だ。これらはプログラムによって変わるものである。インスタンス化するときの値は、pahooCacheクラス を利用するメイン・プログラムの方で解説する。
PHPのクラスについては「PHPでクラスを使ってテキストの読みやすさを調べる」を参照されたい。
準備:pahooInputData 関数群
また、各種クラウドサービスに登録したときに取得するアカウント情報、アプリケーションパスワードなどを登録した .pahooEnv ファイルから読み込む関数 pahooLoadEnv を備えている。こちらについては、「各種クラウド連携サービス(WebAPI)の登録方法」をご覧いただきたい。
気象庁防災情報XMLフォーマット
VXKOの構造
ここから必要な情報を取り出す。
準備:地図サービスの選択
観測所の都道府県・市町村名からマッピング位置を特定するための住所検索サービスは、Google、Yahoo!JAPAN、HeartRails Geo API、OSM Nominatim Search API から選べる。あらかじめ、定数 GEOSERVICE に値を設定すること。
準備:各種定数など
jmaRiverFloodForecast.php
60: // 各種定数(START) ===========================================================
61:
62: // Twitter(現・X)投稿ボタン TRUE:有効,FALSE:無効
63: define('TWITTER', FALSE);
64:
65: // Bluesky投稿ボタン TRUE:有効,FALSE:無効
66: define('BLUESKY', FALSE);
67:
68: // 画像化したいオブジェクト
69: define('TARGET', 'target');
70:
71: // 地図描画サービスの選択
72: // 0:Google
73: // 2:地理院地図・OSM
74: define('MAPSERVICE', 2);
75:
76: // 住所検索サービスの選択
77: // 0:Google
78: // 1:Yahoo!JAPAN
79: // 11:HeartRails Geo API
80: // 12:OSM Nominatim Search API
81: define('GEOSERVICE', 1);
82:
83: // マップの表示サイズ(単位:ピクセル)
84: define('MAP_WIDTH', 640);
85: define('MAP_HEIGHT', 480);
86: // マップID
87: define('MAPID', 'map_id');
88: // 初期値
89: define('DEF_LATITUDE', 38.82); // 緯度
90: define('DEF_LONGITUDE', 138.28); // 経度
91: define('DEF_ZOOM', 5); // ズーム
92: define('DEF_TYPE', 'ROADMAP'); // マップタイプ
93: define('DEF_OVERLAYS', 'GSIELEV,GSIFAULT'); // オーバーレイ(Leaflet使用時のみ)
94: define('INFO_WIDTH', (MAP_WIDTH * 0.75)); // 情報ウィンドウの最大幅
95: define('INFO_OFFSET_X', 0); // 情報ウィンドウのオフセット位置(X)
96: define('INFO_OFFSET_Y', -25); // 情報ウィンドウのオフセット位置(Y)
97:
98: // キャッシュ保持時間(分) 0:キャッシュしない
99: // 気象庁へのアクセス負荷軽減のため,適切な時間を設定してください.
100: define('LIFE_CACHE_FEED', 5); // 高頻度 - 随時フィードに対して
101: define('LIFE_CACHE_FEED_L', 120); // 長期 - 随時フィードに対して
102: define('LIFE_CACHE_DATA', 720); // 指定河川洪水予報情報に対して
103:
104: // キャッシュ・ディレクトリ
105: // 書き込み可能で,外部からアクセスされないディレクトリを指定してください.
106: // 天気予報系のプログラムとは別のディレクトリを指定してください.
107: define('DIR_CACHE_FEED', './pcache1/');
108: define('DIR_CACHE_FEED_L', './pcache2/');
109: define('DIR_CACHE_DATA', './pcache3/');
110:
111: // これより古い情報は無視する(単位:時間)
112: define('INTERVAL', 24);
113:
114: // 最大プロット数
115: define('MAX_MARKERS', 999);
116:
117: // 警戒レベルのアイコン
118: define('ICON_CAUSION', 'https://maps.google.com/mapfiles/ms/micons/red-dot.png');
119: // 予報アイコン
120: define('ICON_FORECAST', 'https://maps.google.com/mapfiles/ms/micons/yellow-dot.png');
121:
122: // 洪水情報がないときに表示するメッセージ
123: define('NO_FLOOD_FORECAST', '現在,指定河川の洪水予報はありません.');
124:
125: // SQLite DBファイル名:各自の環境に合わせて変更すること
126: define('DBFILE', './riverFloodArea.sqlite3');
127:
128: // SQLite テーブル名:浸水想定地区
129: define('TABLE_AREA', 'area');
130:
131: // 実行するSQL:浸水想定地区【変更不可】
132: define('PRE_SELECT', 'SELECT * FROM ' . TABLE_AREA . ' WHERE id=:id');
133: define('PRE_INSERT', 'INSERT INTO ' . TABLE_AREA . ' (id, area, latitude, longitude, premiere, latest) VALUES (:id, :area, :latitude, :longitude, :premiere, :latest)');
134: define('PRE_UPDATE', 'UPDATE ' . TABLE_AREA . ' set area=:area, latitude=:latitude, longitude=:longitude, latest=:latest WHERE id=:id');
135:
136: // 気象庁防災情報XML:高頻度フィード - 随時【変更不可】
137: define('FEED', 'https://www.data.jma.go.jp/developer/xml/feed/extra.xml');
138:
139: // 気象庁防災情報XML:長期フィード - 随時【変更不可】
140: define('FEED_L', 'https://www.data.jma.go.jp/developer/xml/feed/extra_l.xml');
141:
142: // 各種定数(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
488: /**
489: * 気象庁防災情報XMLから指定河川洪水予報に関する情報URLを取得
490: * @param array $urls URL格納配列
491: * @param string $errmsg エラーメッセージ格納用
492: * @return bool TRUE:取得成功/FALSE:取得失敗
493: */
494: function jmaGetRiverFloodForecastURLs(&$urls, &$errmsg) {
495: // URLパターン
496: $vxko = '/https?\:\/\/www\.data\.jma\.go\.jp\/developer\/xml\/data\/([0-9\_]+)VXKO[0-9]+\_[0-9]+\.xml/ui';
497:
498: // 随時フィードの解析
499: $cnt = 0;
500: $pcc = new pahooCache(LIFE_CACHE_FEED, DIR_CACHE_FEED);
501: $xml = $pcc->simplexml_load(FEED);
502: // レスポンス・チェック
503: if ($pcc->iserror() || !isset($xml->entry)) {
504: $errmsg = '気象庁防災情報XMLにアクセスできません';
505: return FALSE;
506: }
507: foreach ($xml->entry as $node) {
508: // URLを取得
509: if (preg_match($vxko, $node->id, $arr) > 0) {
510: $urls[$cnt] = $arr[0];
511: $cnt++;
512: }
513: }
514: $pcc = NULL;
515:
516: // 長期フィードの解析
517: $pcc = new pahooCache(LIFE_CACHE_FEED_L, DIR_CACHE_FEED_L);
518: $xml = $pcc->simplexml_load(FEED_L);
519: // レスポンス・チェック
520: if ($pcc->iserror() || !isset($xml->entry)) {
521: $errmsg = '気象庁防災情報XMLにアクセスできません';
522: return FALSE;
523: }
524: foreach ($xml->entry as $node) {
525: // URLを取得
526: if (preg_match($vxko, $node->id, $arr) > 0) {
527: if (array_search($arr[0], $urls) === FALSE) {
528: $urls[$cnt] = $arr[0];
529: $cnt++;
530: }
531: }
532: }
533: $pcc = NULL;
534:
535: // エラー・チェック
536: // if ($cnt == 0) {
537: // $errmsg = '指定河川洪水予報はありません';
538: // return FALSE;
539: // }
540:
541: // URLを日時の新しい順にソート
542: if (count($urls) > 0) rsort($urls);
543:
544: return TRUE;
545: }
VXKO を含むURLを配列 $urls に格納していき、最後に配列 $urls を大きい順にソートすることで、日付の新しい順にソートしたことになる。
解説:指定河川洪水予報情報の取り出し
VXKO からは洪水想定地区の緯度・経度が分からないため、pahooGeoCodeクラスのメソッド searchPoint3 を使って緯度・経度を取得する。
このメソッドは、WebAPIを呼び出して住所から緯度・経度を取得するものだが、いちいち呼び出すと時間がかかる(GoogleAPIは費用もかかる)ので、一度検索したデータはデータベース SQLite に格納しておき、二度目からはDBのデータを取り出すようにした。
jmaRiverFloodForecast.php
547: /**
548: * 指定河川洪水予報情報を取得(気象庁防災情報XMLから)
549: * @param object $pgc pahooGeoCodeオブジェクト
550: * @param array $items 指定河川洪水予報を格納する配列
551: * @param string $urls 情報XMLのURLを格納する配列
552: * @param string $errmsg エラーメッセージ格納用
553: * @return bool TRUE:取得成功/FALSE:失敗
554: */
555: function getRiverFloodForecast($pgc, &$items, &$urls, &$errmsg) {
556: // マッチングパターン
557: $pat1 = '/([0-9]+)\-([0-9]+)\-([0-9]+)T([0-9]+)\:([0-9]+)/ui'; // 年月日時分
558:
559: // オブジェクト生成
560: $pcc = new pahooCache(LIFE_CACHE_DATA, DIR_CACHE_DATA);
561: $pgc = new pahooGeoCode();
562:
563: // DBオープン
564: $pdo = new PDO('sqlite:' . DBFILE);
565:
566: // 指定河川洪水予報取に関する情報URLを取得
567: $urls = array('');
568: jmaGetRiverFloodForecastURLs($urls, $errmsg);
569: if ($errmsg != '') return FALSE;
570: if (count($urls) == 0) return TRUE;
571:
572: $count = 1;
573: foreach ($urls as $key=>$vxko) {
574: // 指定河川洪水予報の取得
575: $xml = $pcc->simplexml_load($vxko);
576: if ($xml == FALSE) continue; // ver.1.22
577:
578: // レスポンス・チェック
579: if ($pcc->iserror() || !isset($xml->Body->Warning->Item)) {
580: $errmsg = '気象庁防災情報XMLから指定河川洪水予報を取得できません';
581: return FALSE;
582: }
583: // 発表日時
584: $dt = (string)$xml->Head->ReportDateTime;
585: // 古い情報かどうか
586: if (time() - strtotime($dt) > INTERVAL * 60 * 60) {
587: continue;
588: }
589: $items[$count]['dt'] = $dt;
590: preg_match($pat1, $dt, $arr);
591: $items[$count]['year'] = (int)$arr[1];
592: $items[$count]['month'] = (int)$arr[2];
593: $items[$count]['day'] = (int)$arr[3];
594: $items[$count]['hour'] = (int)$arr[4];
595: $items[$count]['minuite'] = (int)$arr[5];
596: // 指定河川洪水予報
597: $data = array();
598: foreach ($xml->Body->Warning->Item as $item) {
599: // 主文
600: if (isset($item->Kind->Property->Type) && ((string)$item->Kind->Property->Type == '主文')) {
601: if (isset($items[$count]['main'])) {
602: $items[$count]['main'] .= "\n" . (string)$item->Kind->Property->Text;
603: } else {
604: $items[$count]['main'] = (string)$item->Kind->Property->Text;
605: }
606: // 浸水想定地区
607: } else if (isset($item->Kind->Property->Type) && ((string)$item->Kind->Property->Type == '浸水想定地区')) {
608: $i = 1;
609: foreach ($item->Areas->Area as $area) {
610: $query = (string)$area->Prefecture . (string)$area->City;
611: $items[$count]['areas'][$i]['area'] = $query;
612: $id = (string)$area->CityCode;
613: // 緯度・経度をDBから取得
614: if (getAreaFromDB($pdo, $id, $data)) {
615: $latitude = $data['latitude'];
616: $longitude = $data['longitude'];
617: } else {
618: list($n, $url) = $pgc->searchPoint3($query, GEOSERVICE, 'address');
619: // エラーがなければ最初の住所を対象にする
620: if (! $pgc->iserror()) {
621: list($latitude, $longitude, $address) = $pgc->getPoint(1);
622: // DB登録
623: $data['area'] = $query;
624: $data['latitude'] = $latitude;
625: $data['longitude'] = $longitude;
626: $res = storeAreaToDB($pdo, $id, $data);
627: if (! $res) {
628: $errmsg = 'DB書き込みに失敗しました';
629: return FALSE;
630: }
631: } else {
632: $errmsg = $pgc->geterror();
633: return FALSE;
634: }
635: }
636: $items[$count]['areas'][$i]['latitude'] = $latitude;
637: $items[$count]['areas'][$i]['longitude'] = $longitude;
638: $i++;
639: }
640: }
641: }
642: // 雨量情報
643: if (isset($xml->Body->MeteorologicalInfos->MeteorologicalInfo->Item)) {
644: foreach ($xml->Body->MeteorologicalInfos->MeteorologicalInfo->Item as $item) {
645: if (isset($item->Kind->Property->Type) && ((string)$item->Kind->Property->Type == '雨量')) {
646: if (isset($items[$count]['rainfall'])) {
647: $items[$count]['rainfall'] .= "\n" . (string)$item->Kind->Property->Text;
648: } else {
649: $items[$count]['rainfall'] = (string)$item->Kind->Property->Text;
650: }
651: }
652: }
653: }
654: $count++;
655: }
656:
657: // DBクローズ
658: $pdo = NULL;
659: // オブジェクト解放
660: $pgc = $pcc = NULL;
661:
662: return TRUE;
663: }
jmaRiverFloodForecast.php
278: /**
279: * DBの初期化
280: * @param なし
281: * @return bool TRUE/FALSE
282: */
283: function initDB() {
284: try {
285: $pdo = new PDO('sqlite:' . DBFILE);
286: $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
287:
288: // テーブル作成:浸水想定地区
289: // id:観測所のCityCode, area:都道府県+市町村名
290: // latitude:緯度, longitude:経度
291: // premiere:登録日時, latest:更新日時
292: $pdo->exec('CREATE TABLE IF NOT EXISTS ' . TABLE_AREA . '(
293: id INTEGER PRIMARY KEY AUTOINCREMENT,
294: area TEXT,
295: latitude REAL,
296: longitude REAL,
297: premiere TEXT,
298: latest TEXT
299: )');
300: $res = TRUE;
301: } catch (PDOException $e) {
302: $res = FALSE;
303: }
304:
305: return $res;
306: }
解説:指定河川洪水予報情報をマッピング情報に変換
jmaRiverFloodForecast.php
665: /**
666: * 指定河川洪水予報情報をマッピング情報に変換
667: * @param array $items 指定河川洪水予報情報を格納した配列
668: * @param array $points マッピング情報を格納する配列
669: * @return int 変換したマッピング情報の件数
670: */
671: function info2points(&$items, &$points) {
672: // データがない
673: if (count($items) == 0) return 0;
674:
675: // 変換処理
676: $i = $j = 0;
677: $cnt = 1;
678: foreach ($items as $i=>$item) {
679: if ($cnt > MAX_MARKERS) break; // 最大プロット数
680: if (!isset($item['areas'])) continue; // Ver.1.21修正
681: foreach ($item['areas'] as $j=>$val) {
682: // 同じマッピング位置があるかどうか
683: $flag = FALSE;
684: for ($k = 1; $k < $cnt; $k++) {
685: if ($val['area'] == $points[$k]['title']) {
686: $flag = TRUE;
687: break;
688: }
689: }
690: // 新規のマッピング情報
691: if (! $flag) {
692: // アイコン
693: if (preg_match('/警戒レベル/ui', $items[$i]['main']) > 0) {
694: $points[$cnt]['icon'] = ICON_CAUSION;
695: } else {
696: $points[$cnt]['icon'] = ICON_FORECAST;
697: }
698: // タイトル
699: $points[$cnt]['title'] = $val['area'];
700: // 緯度・経度
701: $points[$cnt]['latitude'] = $items[$i]['areas'][$j]['latitude'];
702: $points[$cnt]['longitude'] = $items[$i]['areas'][$j]['longitude'];
703: // 情報ウィンドウの内容
704: $points[$cnt]['description'] =
705: sprintf('%04d年%02d月%02d日 %02d時%02d分 発表<br /><span style="font-weight:bold;">%s</span><br />%s<br />%s', $items[$i]['year'],
706: $items[$i]['month'],
707: $items[$i]['day'],
708: $items[$i]['hour'],
709: $items[$i]['minuite'],
710: $val['area'],
711: preg_replace('/\n/', '<br />', $items[$i]['main']),
712: preg_replace('/\n/', '<br />', $items[$i]['rainfall']));
713: $cnt++;
714: }
715: }
716: }
717:
718: return $cnt;
719: }
解説:地図描画について
解説:オーバーレイ表示(Leaflet選択時のみ)
解説:SNS投稿機能
コンテンツを描画しているのはクライアントにあるブラウザ(レンダリングエンジン)であることから、クライアント側で画像を作成し、サーバ側で SNS の API をコールするという方針とした。
- ブラウザはサーバにコンテンツ描画をリクエストする。
- サーバはデータサイトからデータ取得する。(サーバキャッシュにデータがあればそれを利用する)
- サーバはコンテンツ描画スクリプトを生成する。
- サーバはブラウザへレスポンス(HTML文)を返す。
- ブラウザはコンテンツをレンダリングする。
- ブラウザはレンダリングしたコンテンツを画像データとしてサーバへアップロードする。
- サーバは SNS の API を使ってツイートする。
- サーバは SNS へメッセージと画像を送る。
- サーバはブラウザへレスポンス(HTML文)を返す。
解説:html2canvasライブラリ
jmaRiverFloodForecast.php
183: <script src="https://html2canvas.hertzen.com/dist/html2canvas.js"></script>
184: <script>
185: function mytweet() {
186: $('#tweet').val('1');
187: document.myform.submit();
188: }
画像化を実行するJavaScript関数は html2canvas である。
jmaRiverFloodForecast.php
857: <div id="{$target}" name="{$target}" style="width:{$width2}px;">
858: <p>
859: 🌊指定河川洪水予報情報 {$dt}現在
860: {$tweet}{$bluesky}
861: </p>
862: {$html}
863: </div>
レンダリングエンジンによって違うのかもしれないが、html2canvas ライブラリによって画像化される範囲が実際よりやや小さいため、あえてマップ領域より、幅を20ピクセル大きくした範囲を画像化範囲としている。
jmaRiverFloodForecast.php
422: /**
423: * HTMLオブジェクトの画像化
424: * @param なし
425: * @return string JavaScriptコード
426: */
427: function js_html2image() {
428: $target = TARGET;
429: $js = '';
430:
431: // Googleマップの場合
432: if (MAPSERVICE == 0) {
433: $js .=<<< EOT
434: google.maps.event.addListener(map, 'tilesloaded', function() {
435: var capture = document.querySelector('#{$target}');
436: html2canvas(capture, {useCORS: true, allowTaint:true}).then(canvas => {
437: var base64 = canvas.toDataURL('image/png'); // 画像化
438: $('#base64').val(base64);
439: });
440: });
441:
442: EOT;
443:
444: // Leafletの場合(ブラウザによってはうまく動作しない)
445: } else {
446: $js .=<<< EOT
447: HTMLCanvasElement.prototype.getContext = function(origFn) {
448: return function(type, attribs) {
449: attribs = attribs || {};
450: attribs.preserveDrawingBuffer = true;
451: return origFn.call(this, type, attribs);
452: };
453: } (HTMLCanvasElement.prototype.getContext);
454:
455: // HTML画像化イベント登録
456: function html2image() {
457: var capture = document.querySelector('#{$target}');
458: html2canvas(capture, {useCORS: true, allowTaint:true}).then(canvas => {
459: var base64 = canvas.toDataURL('image/png'); // 画像化
460: $('#base64').val(base64);
461: });
462: };
463:
464: // ズーム変更イベント
465: map.on('zoomend', function() {
466: html2image();
467: });
468:
469: // マップ移動イベント
470: map.on('moveend', function() {
471: html2image();
472: });
473:
474: // html2image()を発火させるために,ズームアウトして500msec後に元に戻す.
475: var zoom = map.getZoom();
476: map.setZoom(zoom - 1);
477: setTimeout(function() {
478: map.setZoom(zoom);
479: }, 500);
480:
481: EOT;
482: }
483:
484: return $js;
485: }
Leaflet の場合、これに相当するイベントがないため、ズーム変更完了イベントにフックし、強制的にズーム変更イベントを発生させるタイミングで画像化する。しかし、Leaflet では、画像を描画するレイヤ構造の関係で、html2canvas ライブラリでは位置ずれが起きることがわかっている。その場合は、Googleマップを選択してほしい。また、対策方法をご存じの方がいたらお知らせいただきたい。
これらをJavaScriptとして生成するユーザー関数が js_html2image である。
準備:pahooTwitterAPI クラス
pahooTwitterAPI.php
11: // TwitterOAuth クラスをロードする.
12: $version = explode('.', phpversion());
13: if ($version[0] >= 8) {
14: require __DIR__ . '/vendor/autoload.php';
15: }
16: use Abraham\TwitterOAuth\TwitterOAuth;
17:
「Twitter API」を利用するために、API key、API key secret、Access Token、Access Token secret が必要で、入手方法は「Twitter API - WebAPIの登録方法」を参照されたい。
PHPのクラスについては「PHPでクラスを使ってテキストの読みやすさを調べる」を参照されたい。
OAuth認証 および Twitter API へのアクセスには、TwitterOAuth を利用する。導入方法は公式サイトにあるように composer を使うことを推奨している。
⚠ TwitterOAuth 7.0.0 は PHP 8.4(2024年11月21日リリース)でいくつかのエラーが発生する。これは、引数に暗黙の null を指定することが非推奨になったためで、プログラムの改修が行われるまでは PHP 8.3 で利用することをおすすめする。
TwitterOAuth クラスをロードする。composer でインストールされたフォルダ "\vendor" を "pahooTwitterAPI.php" と同じフォルダへコピーすること。
pahooTwitterAPI.php
37: /**
38: * コンストラクタ
39: * @param なし
40: * @return なし
41: */
42: function __construct() {
43: if (isset($_ENV['PAHOO_TWTR_CONSUMER_KEY'])) {
44: $this->TWTR_CONSUMER_KEY = $_ENV['PAHOO_TWTR_CONSUMER_KEY'];
45: }
46: if (isset($_ENV['PAHOO_TWTR_CONSUMER_SECRET'])) {
47: $this->TWTR_CONSUMER_SECRET = $_ENV['PAHOO_TWTR_CONSUMER_SECRET'];
48: }
49: if (isset($_ENV['PAHOO_TWTR_ACCESS_KEY'])) {
50: $this->TWTR_ACCESS_KEY = $_ENV['PAHOO_TWTR_ACCESS_KEY'];
51: }
52: if (isset($_ENV['PAHOO_TWTR_ACCESS_SECRET'])) {
53: $this->TWTR_ACCESS_SECRET = $_ENV['PAHOO_TWTR_ACCESS_SECRET'];
54: }
55:
56: // プロパティを初期化する.
57: $this->responses = array();
58: $this->webapi = '';
59: $this->error = FALSE;
60: $this->errmsg = '';
61: $this->errcode = 0;
62:
63: $this->connection = new TwitterOAuth($this->TWTR_CONSUMER_KEY, $this->TWTR_CONSUMER_SECRET, $this->TWTR_ACCESS_KEY, $this->TWTR_ACCESS_SECRET);
64: // v2使用を宣言
65: $this->connection->setApiVersion('2');
66:
67: // PHP8.4で暗黙的にnullableを指定することが非推奨になったため
68: // TwitterOAuthが対応するまでの応急処置
69: error_reporting(E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED);
70: }
pahooTwitterAPI.php
30: // OAuth用パラメータ
31: // https://apps.twitter.com/
32: public $TWTR_CONSUMER_KEY = ''; // Cunsumer key
33: public $TWTR_CONSUMER_SECRET = ''; // Consumer secret
34: public $TWTR_ACCESS_KEY = ''; // Access Token (oauth_token)
35: public $TWTR_ACCESS_SECRET = ''; // Access Token Secret (oauth_token_secret)
36:
解説:メディア付き投稿(RAWデータ)
pahooTwitterAPI.php
587: /**
588: * バイナリデータを使ったメディア付きメッセージをツイートする.
589: * Tweetet API v2 を使用する.
590: * @param string $message 投稿メッセージ(UTF-8限定)
591: * @param array $items メディアデータ(バイナリデータ配列)
592: * @return bool TRUE:リクエスト成功/FALSE:失敗
593: */
594: function tweet_media_raw($message, $items) {
595: //メディアのアップロード
596: $media_ids = array();
597: $cnt = 0;
598: //Tweetet API v1.1 を使用する(v2にメディアアップロードが未実装のため)
599: $this->connection->setApiVersion('1.1');
600: foreach ($items as $data) {
601: $tmpname = $this->saveTempFile($data);
602: // $media = $this->connection->upload('media/upload', ['media' => $tmpname]);
603: $media = $this->connection->upload('media/upload', ['media' => $tmpname], ['chunkedUpload' => true]); //twitteroauth 7.0.0 対応
604: unlink($tmpname);
605: if (! isset($media->media_id_string)) break; //処理失敗
606: $media_ids[] = (string)$media->media_id_string;
607: $cnt++;
608: if ($cnt > 3) break; //最大4つまで
609: }
610:
611: //メディア付きツイート(Tweetet API v2 を使用する)
612: $this->connection->setApiVersion('2');
613: $option = [
614: 'text' => $message,
615: 'media' => [
616: 'media_ids' => $media_ids
617: ]
618: ];
619: // $status = $this->connection->post('tweets', $option, TRUE);
620: $status = $this->connection->post('tweets', $option, ['jsonPayload' => true]); //twitteroauth 7.0.0 対応
621: $this->webapi = 'https://api.twitter.com/2/tweets';
622:
623: //処理に成功した.
624: if ($this->isSuccess()) {
625: $this->responses = $status->data;
626: $this->errcode = NULL;
627: $this->errmsg = '';
628: $this->error = FALSE;
629: $res = TRUE;
630: //処理に失敗した.
631: } else {
632: if ($this->isAuthError() == FALSE) {
633: $this->errmsg = $status->detail;
634: $this->error = TRUE;
635: }
636: $res = FALSE;
637: }
638: return $res;
639: }
解説:Twitter(現・X)へ投稿する
jmaRiverFloodForecast.php
370: /**
371: * Twitter(現・X)投稿
372: * @param string $message 投稿文
373: * @param string $res 応答メッセージ格納用
374: * @return bool TRUE:成功/FALSE:失敗または未処理
375: */
376: function mediaTweet($message, &$res) {
377: if (! TWITTER) return FALSE;
378:
379: $ret = TRUE;
380: if (isset($_POST['base64']) && ($_POST['base64'] != '')) {
381: $base64 = preg_replace('/data\:image\/png\;base64\,/ui', '', $_POST['base64']);
382: $raws = array(base64_decode($base64));
383: $ptw = new pahooTwitterAPI();
384: $ptw->tweet_media_raw($message, $raws);
385: $errmsg = $ptw->errmsg;
386: $ret = ! $ptw->error;
387: $ptw = NULL;
388: if ($ret) {
389: $res = 'ツイートしました';
390: }
391: }
392: return $ret;
393: }
ブラウザからPOSTで受け取った画像データ $_POST['base64'] はBASE64でデコードされており、冒頭に余計なヘッダ情報が付いているので、このヘッダ情報を除き( preg_replace )、バイナリデータにデコードする( base64_decode )。
続いて pahooTwitterAPI クラスを呼び出し、tweet_media_raw メソッドを使って、メッセージと画像を一気に投稿する。
準備:pahooBlueskyAPI クラス
pahooBlueskyAPI.php
「Bluesky API」を利用するために、ハンドル名、アプリケーション・パスワード が必要で、入手方法は「Bluesky API - WebAPIの登録方法」を参照されたい。
PHPのクラスについては「PHPでクラスを使ってテキストの読みやすさを調べる」を参照されたい。
解説:Blueskyへ投稿する
jmaRiverFloodForecast.php
395: /**
396: * Bluesky投稿
397: * @param string $message 投稿文
398: * @param string $res 応答メッセージ格納用
399: * @return bool TRUE:成功/FALSE:失敗または未処理
400: */
401: function mediaBluesky($message, &$res) {
402: if (! BLUESKY) return FALSE;
403:
404: $ret = TRUE;
405: if (isset($_POST['base64']) && ($_POST['base64'] != '')) {
406: $base64 = preg_replace('/data\:image\/png\;base64\,/ui', '', $_POST['base64']);
407: $raws = array(base64_decode($base64));
408: $pbs = new pahooBlueskyAPI(BLUESKY_DOMAIN);
409: $res = $pbs->createSession();
410: $pbs->post($message, FALSE, NULL, NULL, $raws);
411: $errmsg = $pbs->geterror();
412: $ret = ! $pbs->iserror();
413: $res = $pbs->deleteSession();
414: $pbs = NULL;
415: if ($ret) {
416: $res = 'Blueskyに投稿しました';
417: }
418: }
419: return $ret;
420: }
ブラウザから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: // 表示コンテンツを作成する.
活用例
参考サイト
- WebAPIの登録方法:ぱふぅ家のホームページ
- PHPで最近の地震情報を表示する:ぱふぅ家のホームページ
- PHPで住所・ランドマークから最寄り駅を求める:ぱふぅ家のホームページ
- PHPで緯度・経度から住所を求める:ぱふぅ家のホームページ
- JavaScriptでHTML表示を画像保存する:ぱふぅ家のホームページ
- HTMLとCSSでさまざまなアイコンを表示する:ぱふぅ家のホームページ
- Twitter API - WebAPIの登録方法:ぱふぅ家のホームページ
- PHPでTwitterに画像付きメッセージ投稿:ぱふぅ家のホームページ
- Bluesky API - WebAPIの登録方法:ぱふぅ家のホームページ
- PHPでBlueskyに投稿する:ぱふぅ家のホームページ
- 指定河川洪水予報を地図上に表示:みんなの知識 ちょっと便利帳

表示マップの種類を変えたり、マップを含めてツイートすることができる。
(2025年8月22日)count オプションを追加, .pahooEnv導入
(2025年6月14日)GoogleMaps JavaScript APIの変更に対応した.
(2024年11月2日)Bluesky投稿機能を追加
(2024年6月23日)TwitterOAuth 7.0.0 対応,Twitter(現・X)ボタンを "X" に変更