サンプル・プログラムの実行例
目次
サンプル・プログラム
jmaRiverFloodForecast.php | サンプル・プログラム本体。 |
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.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が無いデータをスキップ |
バージョン | 更新日 | 内容 |
---|---|---|
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()追加 |
バージョン | 更新日 | 内容 |
---|---|---|
1.1.1 | 2023/02/11 | コメント追記 |
1.1 | 2021/04/08 | simplexml_load()メソッド追加 |
1.0 | 2021/04/02 | 初版 |
バージョン | 更新日 | 内容 |
---|---|---|
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 |
バージョン | 更新日 | 内容 |
---|---|---|
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フォーマット
https://www.data.jma.go.jp/developer/xml/data/yyyymmddhhmmss_番号_電文コード_連番.xml指定河川の洪水予報に関する情報は、長期フィードの随時の中にある電文コード VXKO に入っている。そこで、フィードから VXKO を含むURLを取り出せば、それが目指す情報XMLとなる。
VXKOの構造
ここから必要な情報を取り出す。
準備: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 = '*****************************';
クラスについては「PHPでクラスを使ってテキストの読みやすさを調べる」を参照されたい。
地図としてGoogleマップを利用するのであれば、Google Cloud Platform APIキー が必要で、その入手方法は「Google Cloud Platform - WebAPIの登録方法」を参照されたい。
準備:地図サービスの選択
観測所の都道府県・市町村名からマッピング位置を特定するための住所検索サービスは、Google、Yahoo!JAPAN、HeartRails Geo API、OSM Nominatim Search API から選べる。あらかじめ、定数 GEOSERVICE に値を設定すること。
準備:キャッシュ・システム
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/');
キャッシュ保持時間、キャッシュ・ディレクトリともに、自サイトの環境に応じて変更してほしい。天気予報系プログラムと別のキャッシュ・ディレクトリにした方が、お互いのキャッシュ保持時間の干渉を受けなくなる。
配布ファイルは、新しい地震情報が入ってくることを考え、フィードの方を短く、指定河川洪水予報情報の方は長くキャッシュ保持時間を設定してある。
準備:各種定数など
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: }
VXKO を含むURLを配列 $urls に格納していき、最後に配列 $urls を大きい順にソートすることで、日付の新しい順にソートしたことになる。
解説:指定河川洪水予報情報の取り出し
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: }
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: }
解説:地図描画について
解説:オーバーレイ表示(Leaflet選択時のみ)
解説:SNS投稿機能
コンテンツを描画しているのはクライアントにあるブラウザ(レンダリングエンジン)であることから、クライアント側で画像を作成し、サーバ側で SNS の API をコールするという方針とした。
- ブラウザはサーバにコンテンツ描画をリクエストする。
- サーバはデータサイトからデータ取得する。(サーバキャッシュにデータがあればそれを利用する)
- サーバはコンテンツ描画スクリプトを生成する。
- サーバはブラウザへレスポンス(HTML文)を返す。
- ブラウザはコンテンツをレンダリングする。
- ブラウザはレンダリングしたコンテンツを画像データとしてサーバへアップロードする。
- サーバは SNS の API を使ってツイートする。
- サーバは SNS へメッセージと画像を送る。
- サーバはブラウザへレスポンス(HTML文)を返す。
解説: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: }
画像化を実行するJavaScript関数は html2canvas である。
jmaRiverFloodForecast.php
866: <div id="{$target}" name="{$target}" style="width:{$width2}px;">
867: <p>
868: 🌊指定河川洪水予報情報 {$dt}現在
869: {$tweet}{$bluesky}
870: </p>
871: {$html}
872: </div>
レンダリングエンジンによって違うのかもしれないが、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: }
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)
解説:メディア付き投稿(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: }
解説: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: }
ブラウザから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へ投稿する
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: }
ブラウザから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に画像付きメッセージ投稿:ぱふぅ家のホームページ
- 指定河川洪水予報を地図上に表示:みんなの知識 ちょっと便利帳
表示マップの種類を変えたり、マップを含めてツイートすることができる。
(2024年11月2日)Bluesky投稿機能を追加
(2024年6月23日)TwitterOAuth 7.0.0 対応,Twitter(現・X)ボタンを "X" に変更