サンプル・プログラムの実行例
目次
- サンプル・プログラムの実行例
- サンプル・プログラム
- 準備:PHP の https対応
- 準備:pahooInputData 関数群
- 準備:pahooGeoCode クラス
- 準備:地図サービスの選択
- 準備:pahooCache クラス
- 準備:各種定数など
- 気象庁防災情報XMLフォーマット
- VPTW60の構造
- 解説:台風に関する情報URLを取得
- 解説:台風情報を読み込む
- 解説:台風情報を描くスクリプトを生成
- 解説:マップ描画用情報を生成
- 解説:メイン・プログラム
- 解説:SNS投稿機能
- 解説:html2canvasライブラリ
- 準備:pahooTwitterAPI クラス
- 解説:メディア付き投稿(RAWデータ)
- 解説:Twitter(現・X)へ投稿する
- 準備:pahooBlueskyAPI クラス
- 解説:Blueskyへ投稿する
- 解説:SNSへ投稿する(メイン・プログラム)
- 解説:count, info オプション
- 活用例
- 参考サイト
サンプル・プログラム
| typhoon.php | サンプル・プログラム本体 |
| .pahooEnv | クラウドサービスを利用するためのアカウント情報などを記入する .env ファイル。 使い方は「各種クラウド連携サービス(WebAPI)の登録方法」を参照。include_path が通ったディレクトリに配置すること。 |
| pahooInputData.php | データ入力に関わる関数群。 使い方は「数値入力とバリデーション」「文字入力とバリデーション」などを参照。include_path が通ったディレクトリに配置すること。 |
| pahooGeoCode.php | 住所・緯度・経度に関わるクラス pahooGeoCode。 使い方は「PHPで住所・ランドマークから最寄り駅を求める」「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 が通ったディレクトリに配置すること。 |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 2.8.0 | 2025/08/13 | pahooInputData.php 導入 |
| 2.7.0 | 2025/06/24 | count, infoオプションを追加 |
| 2.6.0 | 2024/11/01 | Bluesky投稿機能を追加, isButton()修正 |
| 2.5.0 | 2024/06/21 | Twitter(現・X)ボタンを "X" に変更 |
| 2.4.1 | 2023/09/20 | js_html2image()--Leaflet用html2image()発火プロセス見直し |
| 関数/メソッド | 機能 | 詳細 |
|---|---|---|
| pahooLoadEnv | .env ファイルからデータを読み込む | .env ファイルは include_path が通っているディレクトリに配置すること |
| execFunc | 指定した関数を実行し,その戻り値を返す. | phpversion関数が実装されていなければ強制終了する. |
| exitIfLessVersion | PHP処理系が指定したバージョン未満ならメッセージを表示して強制終了する. | |
| exitIfExceedVersion | PHP処理系が指定したバージョンを超えたらメッセージを表示して強制終了する. | |
| isCommandLine | コマンドラインから起動されたかどうかを求める. | |
| isButton | HTML FORMで指定したボタンが押されたかどうかを求める. | filter_input()関数および $argv を参照する. |
| getParam | HTML FORMで指定したINPUTの内容を取り出す. | filter_input()関数および $argv を参照する. |
| validNumber | 数値バリデーションを行う. | 数値が整数か小数かを指定し,最小値と最大値を指定する. |
| getValidNumber | HTML FORMで指定したINPUTの内容を数値として取り出す(バリデーション付き) | filter_input()関数および $argv を参照する. |
| validString | 文字列バリデーションを行う. | filter_input()関数および $argv を参照する. 文字列の最小長,最大長,排除または受容するパターンを指定する. |
| validURL | URLバリデーションを行う. | RFC 1738 Uniform Resource Locators にマッチするかどうかを照合する。 |
| validEmail | メールアドレス・URLバリデーションを行う. | RFC 2282 Internet Message Format にマッチするかどうかを照合する。 |
| getValidString | HTML FORMで指定したINPUTの内容を文字列として取り出す(バリデーション付き) | filter_input()関数および $argv を参照する. |
| validRegexPattern | 正規表現のバリデーションを行う. | |
| getPasswordHash | パスワード・ハッシュをつくる. | |
| verifyPassword | パスワードがハッシュに合致するかどうかを求める. | |
| roundFloat | 小数を指定した桁数で丸めて文字列として返す。 | 与えた小数の有効桁数より指定桁数が多いときには,末尾に0をサプレスする. |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 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.6.0 | 2025/08/11 | アプリケーション・パスワードなどを .env へ移行 |
| 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() 修正 |
| 2.3.0 | 2025/07/17 | uploadBlob() リダイレクト先からも読み込めるように |
準備:PHP の https対応
Windowsでは、"php.ini" の下記の行を有効化する。
extension=php_openssl.dllLinuxでは --with-openssl=/usr オプションを付けて再ビルドする。→OpenSSLインストール手順
これで準備は完了だ。
準備:pahooInputData 関数群
また、各種クラウドサービスに登録したときに取得するアカウント情報、アプリケーションパスワードなどを登録した .pahooEnv ファイルから読み込む関数 pahooLoadEnv を備えている。こちらについては、「各種クラウド連携サービス(WebAPI)の登録方法」をご覧いただきたい。
準備: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 = '';
PHPのクラスについては「PHPでクラスを使ってテキストの読みやすさを調べる」を参照されたい。
地図や住所検索として 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でクラスを使ってテキストの読みやすさを調べる」を参照されたい。
準備:地図サービスの選択
typhoon.php
85: // 地図描画サービスの選択
86: // 0:Google
87: // 2:地理院地図・OSM
88: define('MAPSERVICE', 2);
準備: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でクラスを使ってテキストの読みやすさを調べる」を参照されたい。
準備:各種定数など
typhoon.php
58: // 各種定数(START) ===========================================================
59: // Twitter(現・X)投稿ボタン TRUE:有効,FALSE:無効
60: define('TWITTER', FALSE);
61:
62: // Bluesky投稿ボタン TRUE:有効,FALSE:無効
63: define('BLUESKY', FALSE);
64:
65: // 住所・緯度・経度に関わるクラス:include_pathが通ったディレクトリに配置
66: require_once('pahooGeoCode.php');
67:
68: // キャッシュ処理に関わるクラス:include_pathが通ったディレクトリに配置
69: require_once('pahooCache.php');
70:
71: // Twitterクラス:include_pathが通ったディレクトリに配置
72: if (TWITTER) {
73: require_once('pahooTwitterAPI.php');
74: }
75:
76: // BlueskyAPIクラス:include_pathが通ったディレクトリに配置
77: if (BLUESKY) {
78: require_once('pahooBlueskyAPI.php');
79: define('BLUESKY_DOMAIN', 'bsky.social'); // あなたのドメインを記入
80: }
81:
82: // 画像化したいオブジェクト
83: define('TARGET', 'target');
84:
85: // 地図描画サービスの選択
86: // 0:Google
87: // 2:地理院地図・OSM
88: define('MAPSERVICE', 2);
89:
90: // 古い台風情報を捨てる条件:現在日時との差(秒数)
91: define('SCRAP_TIME', (60 * 60 * 12));
92:
93: // 予報円を間引く条件(km):次の予報円がこれより近ければ描画しない
94: define('THIN_OUT', 50);
95:
96: // マップの表示サイズ(単位:ピクセル)
97: define('MAP_WIDTH', 600);
98: define('MAP_HEIGHT', 480);
99: // マップID
100: define('MAPID', 'map_id');
101: // マップ座標
102: define('DEF_LATITUDE', 35.0); // 緯度
103: define('DEF_LONGITUDE', 137.0); // 経度
104: define('DEF_TYPE', 'GSISTD'); // マップタイプ
105: define('DEF_ZOOM', 6); // ズーム
106: // マップ描画色
107: define('COLOR_NAME1', '#FF8800'); // 台風名称の描画色
108: define('COLOR_NAME2', '#0000FF'); // 熱低名称の描画色
109: define('COLOR_LINE', '#0000FF'); // 過去経路の描画色
110: define('COLOR_WIND1', '#FFFF00'); // 強風域の描画色
111: define('COLOR_WIND2', '#FF0000'); // 暴風域の描画色
112: define('COLOR_FORECAST', '#FFFFFF'); // 予報円の描画色
113:
114: // 基準座標;台風との距離計算に使う(これは東京駅の値;変更可能)
115: define('LATITUDE00', 35.681111,); // 緯度
116: define('LONGITUDE00', 139.766667); // 経度
117:
118: // キャッシュ保持時間(分) 0:キャッシュしない
119: // 気象庁へのアクセス負荷軽減+台風の進路プロット
120: define('LIFE_CACHE_FEED', 15); // 高頻度 - 随時フィードに対して
121: define('LIFE_CACHE_FEED_L', 120); // 長期 - 随時フィードに対して
122: define('LIFE_CACHE_DATA', (60 * 24 * 14)); // 台風情報の保持時間(進路プロット)
123:
124: // キャッシュ・ディレクトリ
125: // 書き込み可能で,外部からアクセスされないディレクトリを指定してください.
126: define('DIR_CACHE_FEED', './pcache_typhoon1/');
127: define('DIR_CACHE_FEED_L', './pcache_typhoon2/');
128: define('DIR_CACHE_DATA', './pcache_typhoon3/');
129:
130: // 気象庁防災情報XML:高頻度フィード - 随時【変更不可】
131: define('FEED', 'https://www.data.jma.go.jp/developer/xml/feed/extra.xml');
132:
133: // 気象庁防災情報XML:長期フィード - 随時【変更不可】
134: define('FEED_L', 'https://www.data.jma.go.jp/developer/xml/feed/extra_l.xml');
135:
136: // 各種定数(END) ============================================================
予報円を間引く条件 THIN_OUT は、台風が停滞していたり、移動速度が極めてゆっくりの場合、予報円の密度が高すぎて地図が見にくくなる。そこで、前回予報円がこの距離以下であれば次の予報円を描かない(間引く)ことができる。0を代入すれば、すべての予報円を描くようになる。
出力結果を 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フォーマット
今回は、高頻度 - 随時フィード および 長期 - 随時フィード にアクセスし、電文コード VPTW60 の台風解析・予報情報(5日予報)を取得する。
VPTW60の構造
注意すべきは、1つのXMLファイルの中に1つまたは複数の台風情報が含まれていること。
また、過去の中心位置は分からないので、キャッシュ・ファイルとして保存した VZSA50 から中心位置を拾い出し、それを台風の過去進路にする方針である。
また、VPTW60 の発表時刻によっては予報円の情報が無い。その場合、予報円情報が存在する最も新しい VPTW60 から予報円情報を取り出すことにする。
解説:台風に関する情報URLを取得
typhoon.php
370: /**
371: * 気象庁防災情報XMLから台風に関する情報URLを取得
372: * @param array $urls URL格納配列
373: * @param string $errmsg エラーメッセージ格納用
374: * @return bool TRUE:取得成功/FALSE:取得失敗
375: */
376: function jmaGetTyphoonURLs(&$urls, &$errmsg) {
377: // URLパターン
378: $vptw = '/https?\:\/\/www\.data\.jma\.go\.jp\/developer\/xml\/data\/([0-9\_]+)VPTW6[0-9]+\_[0-9]+\.xml/ui';
379:
380: // 随時フィードの解析
381: $cnt = 0;
382: $pcc = new pahooCache(LIFE_CACHE_FEED, DIR_CACHE_FEED);
383: $xml = $pcc->simplexml_load(FEED);
384: // レスポンス・チェック
385: if ($pcc->iserror() || !isset($xml->entry)) {
386: $errmsg = '気象庁防災情報XMLにアクセスできません';
387: return FALSE;
388: }
389: foreach ($xml->entry as $node) {
390: // URLを取得
391: if (preg_match($vptw, $node->id, $arr) > 0) {
392: $urls[$cnt] = $arr[0];
393: $cnt++;
394: }
395: }
396: $pcc = NULL;
397:
398: // 長期フィードの解析
399: $pcc = new pahooCache(LIFE_CACHE_FEED_L, DIR_CACHE_FEED_L);
400: $xml = $pcc->simplexml_load(FEED_L);
401: // レスポンス・チェック
402: if ($pcc->iserror() || !isset($xml->entry)) {
403: $errmsg = '気象庁防災情報XMLにアクセスできません';
404: return FALSE;
405: }
406: foreach ($xml->entry as $node) {
407: // URLを取得
408: if (preg_match($vptw, $node->id, $arr) > 0) {
409: if (array_search($arr[0], $urls) === FALSE) {
410: $urls[$cnt] = $arr[0];
411: $cnt++;
412: }
413: }
414: }
415: $pcc = NULL;
416:
417: // エラー・チェック
418: if ($cnt == 0) {
419: $errmsg = '直近の台風情報はありません';
420: return FALSE;
421: }
422:
423: // URLを日時の新しい順にソート
424: rsort($urls);
425:
426: return TRUE;
427: }
正規表現で VPTW60 を含むURLを取り出し配列に格納し、日時の新しい順に並べ替えておく。
解説:台風情報を読み込む
typhoon.php
429: /**
430: * 新しい台風報かどうか
431: * @param object $xml 気象庁防災情報XML
432: * @param array $items 台風情報を格納する配列
433: * @return bool TRUE:新しい情報
434: */
435: function isNewTyphoon($xml, $items) {
436: $res = FALSE;
437:
438: if (isset($xml->Body->MeteorologicalInfos->MeteorologicalInfo->Item->Kind->Property->TyphoonNamePart->Number)) {
439: $num = (string)$xml->Body->MeteorologicalInfos->MeteorologicalInfo->Item->Kind->Property->TyphoonNamePart->Number;
440: if ($num != '') {
441: if (! isset($items[$num])) {
442: $res = TRUE;
443: }
444: }
445: }
446:
447: return $res;
448: }
typhoon.php
450: /**
451: * 台風報取得(気象庁防災情報XMLから)
452: * @param object $pgc pahooGeoCodeオブジェクト
453: * @param array $items 台風情報を格納する配列
454: * @param string $urls 情報XMLのURLを格納する配列
455: * @param string $errmsg エラーメッセージ格納用
456: * @return bool TRUE:取得成功/FALSE:失敗
457: */
458: function getTyphoon($pgc, &$items, &$urls, &$errmsg) {
459: // 名前空間
460: define('JMX_EB', 'http://xml.kishou.go.jp/jmaxml1/elementBasis1/');
461: // マッチングパターン
462: $pat2 = '/([\+\-][0-9]{1,2}\.[0-9]+)([\+\-][0-9]{1,3}\.[0-9]+)/ui'; // 緯度・経度
463: $pat3 = '/(予報)[ ]*([01234567890-9]+)時間後/ui'; // 推定|予報
464: // 予報円は取得済みか否か
465: $flag_forecast = array();
466:
467: // オブジェクト生成
468: $pcc = new pahooCache(LIFE_CACHE_DATA, DIR_CACHE_DATA);
469:
470: // 最新の台風に関する情報URLを取得
471: jmaGetTyphoonURLs($urls, $errmsg);
472: if ($errmsg != '') return FALSE;
473:
474: foreach ($urls as $key=>$vptw) {
475: // 台風情報の取得
476: $xml = $pcc->simplexml_load($vptw);
477: // レスポンス・チェック
478: if ($pcc->iserror() || !isset($xml->Body->MeteorologicalInfos)) {
479: $errmsg = '気象庁防災情報XMLから台風情報を取得できません';
480: return FALSE;
481: }
482: $flag_f = FALSE; // 予報円取得済みか否か
483:
484: // 最新の台風情報
485: if (isNewTyphoon($xml, $items)) {
486: foreach ($xml->Body->MeteorologicalInfos as $infos) {
487: $cnt = 0;
488: foreach ($infos->MeteorologicalInfo as $info) {
489: // 実況
490: if ($info->DateTime['type'] == '実況') {
491: foreach ($info->Item->Kind as $kind) {
492: if (isset($kind->Property->Type)) {
493: // 呼称
494: if ($kind->Property->Type == '呼称') {
495: $num = (string)$kind->Property->TyphoonNamePart->Number;
496: if ($num == '') break;
497: $items[$num]['Name'] = (string)$kind->Property->TyphoonNamePart->Name;
498: $items[$num]['NameKana'] = (string)$kind->Property->TyphoonNamePart->NameKana;
499: $items[$num][$cnt]['kind'] = '実況';
500: $items[$num][$cnt]['DateTime'] = (string)$info->DateTime;
501: // その台風の予報円は未取得
502: $flag_forecast[$num] = FALSE;
503: // 古い台風情報かどうか
504: $tt = time() - strtotime($info->DateTime);
505: $items[$num]['Valid'] = ($tt < SCRAP_TIME) ? TRUE : FALSE;
506: // 階級
507: } else if ($kind->Property->Type == '階級') {
508: $node = $kind->Property->ClassPart->children(JMX_EB);
509: $items[$num][$cnt]['TyphoonClass'] = (string)$node->TyphoonClass;
510: $items[$num][$cnt]['AreaClass'] = (string)$node->AreaClass;
511: $items[$num][$cnt]['IntensityClass'] = (string)$node->IntensityClass;
512: // 中心
513: } else if ($kind->Property->Type == '中心') {
514: $node = $kind->Property->CenterPart->children(JMX_EB);
515: // 中心位置
516: $items[$num][$cnt]['Location'] = (string)$kind->Property->CenterPart->Location;
517: foreach ($node->Coordinate as $val) {
518: if (preg_match($pat2, $val, $arr) > 0) {
519: $items[$num][$cnt]['latitude'] = (float)$arr[1];
520: $items[$num][$cnt]['longitude'] = (float)$arr[2];
521: }
522: }
523: // 移動速度
524: $items[$num][$cnt]['Direction'] = (string)$node->Direction;
525: foreach ($node->Speed as $val) {
526: if (isset($val->attributes()['condition'])) {
527: $items[$num][$cnt]['Speed'] = (string)$val->attributes()['condition'];
528: } else if ($val->attributes()['unit'] == 'km/h') {
529: $items[$num][$cnt]['Speed'] = (string)$val;
530: }
531: }
532: // 中心気圧
533: $items[$num][$cnt]['Pressure'] = (int)$node->Pressure;
534: // 風
535: } else if ($kind->Property->Type == '風') {
536: $node = $kind->Property->WindPart->children(JMX_EB);
537: foreach ($node->WindSpeed as $val) {
538: if (($val->attributes()['condition'] == '中心付近') && ($val->attributes()['unit'] == 'm/s')) {
539: $items[$num][$cnt]['centerWindSpeed'] = (int)$val;
540: } else if (($val->attributes()['type'] == '最大瞬間風速') && ($val->attributes()['unit'] == 'm/s')) {
541: $items[$num][$cnt]['maxWindSpeed'] = (int)$val;
542: }
543: }
544: // 暴風域・強風域
545: foreach ($kind->Property->WarningAreaPart as $val) {
546: $key = (string)$val['type'];
547: // 半径
548: $node = $val->children(JMX_EB);
549: $n = 0;
550: foreach ($node->Circle->Axes->Axis as $axis) {
551: if (isset($axis->Direction) && ($axis->Direction != '')) {
552: $items[$num][$cnt][$key][$n]['direction'] = (string)$axis->Direction;
553: } else {
554: $items[$num][$cnt][$key][$n]['direction'] = (string)$axis->Direction->attributes()['condition'];
555: }
556: foreach ($axis->Radius as $val) {
557: if ($val->attributes()['unit'] == 'km') {
558: $items[$num][$cnt][$key][$n]['radius'] = (int)$val;
559: }
560: }
561: $n++;
562: }
563: }
564: }
565: }
566: }
567: // 予報
568: } else if (preg_match($pat3, (string)$info->DateTime['type'], $arr) > 0) {
569: if ($num == '') break; // ver.2.04
570: $cnt++;
571: $items[$num][$cnt]['kind'] = '予報';
572: $items[$num][$cnt]['DateTime'] = (string)$info->DateTime;
573: $flag_f = TRUE; // 予報円取得済み
574: foreach ($info->Item->Kind as $kind) {
575: // 階級
576: if ($kind->Property->Type == '階級') {
577: $node = $kind->Property->ClassPart->children(JMX_EB);
578: $items[$num][$cnt]['TyphoonClass'] = (string)$node->TyphoonClass;
579: $items[$num][$cnt]['AreaClass'] = isset($node->AreaClass) ? (string)$node->AreaClass : '';
580: $items[$num][$cnt]['IntensityClass'] = isset($node->IntensityClass) ? (string)$node->IntensityClass : '';
581: // 中心位置(予報円)
582: } else if ($kind->Property->Type == '中心') {
583: $node = $kind->Property->CenterPart->ProbabilityCircle->children(JMX_EB);
584: foreach ($node->BasePoint as $val) {
585: if (preg_match($pat2, $val, $arr) > 0) {
586: $items[$num][$cnt]['latitude'] = (float)$arr[1];
587: $items[$num][$cnt]['longitude'] = (float)$arr[2];
588: }
589: }
590: // 半径
591: $n = 0;
592: foreach ($node->Axes->Axis as $axis) {
593: if (isset($axis->Direction) && ($axis->Direction != '')) {
594: $items[$num][$cnt]['予報円'][$n]['direction'] = (string)$axis->Direction;
595: } else {
596: $items[$num][$cnt]['予報円'][$n]['direction'] = (string)$axis->Direction->attributes()['condition'];
597: }
598: foreach ($axis->Radius as $val) {
599: if ($val->attributes()['unit'] == 'km') {
600: $items[$num][$cnt]['予報円'][$n]['radius'] = (int)$val;
601: }
602: }
603: $n++;
604: }
605: // 風
606: } else if ($kind->Property->Type == '風') {
607: $node = $kind->Property->WindPart->children(JMX_EB);
608: foreach ($node->WindSpeed as $val) {
609: if (($val->attributes()['condition'] == '中心付近') && ($val->attributes()['unit'] == 'm/s')) {
610: $items[$num][$cnt]['centerWindSpeed'] = (int)$val;
611: } else if (($val->attributes()['type'] == '最大瞬間風速') && ($val->attributes()['unit'] == 'm/s')) {
612: $items[$num][$cnt]['maxWindSpeed'] = (int)$val;
613: }
614: }
615: }
616: }
617: }
618: }
619: }
620: // 過去の台風情報
621: } else {
622: foreach ($xml->Body->MeteorologicalInfos as $infos) {
623: foreach ($infos->MeteorologicalInfo as $info) {
624: // 過去の位置
625: if ($info->DateTime['type'] == '実況') {
626: foreach ($info->Item->Kind as $kind) {
627: if (isset($kind->Property->Type)) {
628: // 呼称
629: if ($kind->Property->Type == '呼称') {
630: $num = (string)$kind->Property->TyphoonNamePart->Number;
631: if ($num == '') break;
632: $cnt = 0;
633: while (isset($items[$num][$cnt])) $cnt++;
634: $items[$num][$cnt]['kind'] = '過去';
635: $items[$num][$cnt]['DateTime'] = (string)$info->DateTime;
636: // 中心
637: } else if ($kind->Property->Type == '中心') {
638: $node = $kind->Property->CenterPart->children(JMX_EB);
639: // 中心位置
640: foreach ($node->Coordinate as $val) {
641: if (preg_match($pat2, $val, $arr) > 0) {
642: $items[$num][$cnt]['latitude'] = (float)$arr[1];
643: $items[$num][$cnt]['longitude'] = (float)$arr[2];
644: }
645: }
646: }
647: }
648: }
649: // 予報
650: } else if (isset($flag_forecast[$num]) && ! $flag_forecast[$num] && (preg_match($pat3, (string)$info->DateTime['type'], $arr) > 0)) {
651: if ($num == '') break; // ver.2.04
652: $cnt++;
653: $items[$num][$cnt]['kind'] = '予報';
654: $items[$num][$cnt]['DateTime'] = (string)$info->DateTime;
655: $flag_f = TRUE; // 予報円取得済み
656: foreach ($info->Item->Kind as $kind) {
657: // 階級
658: if ($kind->Property->Type == '階級') {
659: $node = $kind->Property->ClassPart->children(JMX_EB);
660: $items[$num][$cnt]['TyphoonClass'] = (string)$node->TyphoonClass;
661: $items[$num][$cnt]['AreaClass'] = isset($node->AreaClass) ? (string)$node->AreaClass : '';
662: $items[$num][$cnt]['IntensityClass'] = isset($node->IntensityClass) ? (string)$node->IntensityClass : '';
663: // 中心位置(予報円)
664: } else if ($kind->Property->Type == '中心') {
665: $node = $kind->Property->CenterPart->ProbabilityCircle->children(JMX_EB);
666: foreach ($node->BasePoint as $val) {
667: if (preg_match($pat2, $val, $arr) > 0) {
668: $items[$num][$cnt]['latitude'] = (float)$arr[1];
669: $items[$num][$cnt]['longitude'] = (float)$arr[2];
670: }
671: }
672: // 半径
673: $n = 0;
674: foreach ($node->Axes->Axis as $axis) {
675: if (isset($axis->Direction) && ($axis->Direction != '')) {
676: $items[$num][$cnt]['予報円'][$n]['direction'] = (string)$axis->Direction;
677: } else {
678: $items[$num][$cnt]['予報円'][$n]['direction'] = (string)$axis->Direction->attributes()['condition'];
679: }
680: foreach ($axis->Radius as $val) {
681: if ($val->attributes()['unit'] == 'km') {
682: $items[$num][$cnt]['予報円'][$n]['radius'] = (int)$val;
683: }
684: }
685: $n++;
686: }
687: // 風
688: } else if ($kind->Property->Type == '風') {
689: $node = $kind->Property->WindPart->children(JMX_EB);
690: foreach ($node->WindSpeed as $val) {
691: if (($val->attributes()['condition'] == '中心付近') && ($val->attributes()['unit'] == 'm/s')) {
692: $items[$num][$cnt]['centerWindSpeed'] = (int)$val;
693: } else if (($val->attributes()['type'] == '最大瞬間風速') && ($val->attributes()['unit'] == 'm/s')) {
694: $items[$num][$cnt]['maxWindSpeed'] = (int)$val;
695: }
696: }
697: }
698: }
699: }
700: }
701: }
702: }
703: // その台風の予報円は取得済み
704: if ($flag_f) {
705: $flag_forecast[$num] = TRUE;
706: }
707: }
708: // オブジェクト解放
709: $pcc = NULL;
710:
711: return TRUE;
712: }
XMLファイルを読み込んだら、まず、実況情報か予報情報かを識別する。
予報情報の場合、ユーザー関数 isNewTyphoon により、台風の番号を参照し、まだ登録されていない情報であれば、最新の実況情報として配列に代入する。登録済みの情報であれば、過去の台風情報として配列に代入する。過去の情報は、台風の過去の進路としてマッピングするときに参照する。
なお、現在日時から実況日時を減じ、定数 SCRAP_TIME を超えていたら、古い台風情報として、配列には記録するものの、要素 ValidにFALSEを代入し、古い台風情報であることを明示する。
暴風域・強風域、予報円については、半径の値が複数存在する。たとえば、強風域が北東240km、南西200kmとなっていたら、台風の中心から北東へズレたところに強風域の中心がある。
前述の通り、VPTW60 の発表日時によっては予報円情報を含まないことがある。
そこで、台風毎に配列 $flag_forecast に予報円を取得したかどうかのフラグを持たせる。もし isNewTyphoon で取得した最新の VPTW60 に予報円情報が無ければ、過去の台風情報から予報円情報を取得する。
このため、ほぼ同じ予報取得プロセスが2箇所に書く格好になっており、美しさに欠けてしまった。
解説:台風情報を描くスクリプトを生成
typhoon.php
759: /**
760: * 暴風域、強風域、予報円の描画スクリプトを生成する
761: * @param object $pgc pahooGeoCodeオブジェクト
762: * @param array $infos 台風情報
763: * @return string スクリプト/FALSE:生成失敗
764: */
765: function jsTyphoonMap($pgc, $infos) {
766: $js = '';
767: foreach ($infos as $info) {
768: $key = 0;
769: // 台風以外ならスキップ
770: if (! isset($info[$key]['TyphoonClass'])) continue; // v.2.11
771: if (preg_match('/台風/ui', $info[$key]['TyphoonClass']) == 0) continue;
772: // 古い台風情報ならスキップ
773: if ($info['Valid'] == FALSE) continue;
774:
775: // 暴風域
776: list($latitude, $longitude, $radius) = shiftCircle($pgc, $info[$key]['latitude'], $info[$key]['longitude'], $info[$key]['暴風域']);
777: $radius *= 1000;
778: $js .= $pgc->jsCircle($longitude, $latitude, $radius, COLOR_WIND2, '1.0', 2, MAPSERVICE);
779: // 強風域
780: list($latitude, $longitude, $radius) = shiftCircle($pgc, $info[$key]['latitude'], $info[$key]['longitude'], $info[$key]['強風域']);
781: $radius *= 1000;
782: $js .= $pgc->jsCircle($longitude, $latitude, $radius, COLOR_WIND1, '1.0', 2, MAPSERVICE);
783:
784: $key = 1;
785: $cnt = 1;
786: $lng_0 = $points[0]['longitude'] = $info[0]['longitude'];
787: $lat_0 = $points[0]['latitude'] = $info[0]['latitude'];
788: while (isset($info[$key])) {
789: // 予報円
790: if ($info[$key]['kind'] == '予報') {
791: // 予報円を間引くかどうか
792: $dd = $pgc->greatCircleDistance($lng_0, $lat_0, $info[$key]['longitude'], $info[$key]['latitude']);
793: if ($dd > THIN_OUT) {
794: list($latitude, $longitude, $radius) = shiftCircle($pgc, $info[$key]['latitude'], $info[$key]['longitude'], $info[$key]['予報円']);
795: $radius *= 1000;
796: $js .= $pgc->jsCircle($longitude, $latitude, $radius, COLOR_FORECAST, '1.0', 2, MAPSERVICE);
797: preg_match('/[0-9]{4}\-[0-9]{2}\-([0-9]{2})T([0-9]{2})/ui', $info[$key]['DateTime'], $arr);
798: $dt = sprintf('%d日%d時', $arr[1], $arr[2]);
799: list($lat, $lng) = $pgc->getPointDistance($longitude, $latitude, 0 - $radius - 30000, 9);
800: $js .= $pgc->jsLabel($lat, $lng, $dt, 12, COLOR_FORECAST, 'normal', MAPSERVICE);
801: $lat_0 = $info[$key]['latitude'];
802: $lng_0 = $info[$key]['longitude'];
803: }
804:
805: // 過去の位置
806: } else {
807: $points[$cnt]['longitude'] = $info[$key]['longitude'];
808: $points[$cnt]['latitude'] = $info[$key]['latitude'];
809: $cnt++;
810: }
811: $key++;
812: }
813: // 過去の移動経路
814: if ($js != '') {
815: $js .= $pgc->jsLine($points, COLOR_LINE, '1.0', 3, MAPSERVICE);
816: $points = array();
817: $cnt = 0;
818: if (isset($info[$key]['longitude'])) {
819: $points[0]['longitude'] = $info[$key]['longitude'];
820: $points[0]['latitude'] = $info[$key]['latitude'];
821: }
822: }
823: }
824:
825: // HTMLの画像化
826: $js .= js_html2image();
827:
828: return $js;
829: }
前述の通り、円の中心がズレている場合があり、そのためのユーザー関数 shiftCircle を呼び出して使う。
前回の予報円の中心座標 ($lat_0, $lng_0) (初回は現在の台風の中心座標)からの大圏航路距離をメソッド greatCircleDistance によって計算し、予報円の間引き条件 THIN_OUT 以下であれば予報円を描かない。
解説:マップ描画用情報を生成
typhoon.php
831: /**
832: * 台風情報からマップ描画用情報を生成する
833: * @param array $infos 台風情報
834: * @param array $items マップ描画用情報を格納
835: * @param string $table HTML文(表形式)を格納
836: * @param int $count 有効な台風情報の数
837: * @param objct $pgc pahooGeoCodeオブジェクト
838: * @return array(日時,緯度,経度) 発表日時,予報円の最後の中心座標,地図ズーム
839: */
840: function getTyphoonInfo($infos, &$items, &$table, &$count, $pgc) {
841: // 地図ズーム値=距離換算表
842: $zooms = array(10000, 5000, 3000, 1000, 500, 300);
843:
844: // 台風情報一覧
845: $table =<<< EOT
846: <table class="plists">
847: <th>名称</th>
848: <th>位置</th>
849: <th>中心気圧</th>
850: <th>最大瞬間風速</th>
851: <th>進路</th>
852: </tr>
853:
854: EOT;
855:
856: $dt0 = $lat0 = $lng0 = $zoom = FALSE;
857: $distance = 99999;
858: $cnt = 1;
859: foreach ($infos as $nn=>$info) { // v.2.11
860: $key = 0; // v.2.11
861: if (! isset($info[$key]['TyphoonClass'])) continue; // v.2.11
862: if (preg_match('/台風/ui', $info[0]['TyphoonClass']) > 0) {
863: $num = sprintf('台%d', (int)substr($nn, 2, 2)); // v.2.11
864: $name = sprintf('台風%d号(%s)', (int)substr($nn, 2, 2), $info['NameKana']);
865: } else {
866: continue;
867: }
868: // 古い台風情報ならスキップ
869: if ($info['Valid'] == FALSE) continue;
870:
871: $items[$cnt]['longitude'] = $info[0]['longitude'];
872: $items[$cnt]['latitude'] = $info[0]['latitude'];
873: if ($cnt == 1) {
874: preg_match('/([0-9]{4})\-([0-9]{2})\-([0-9]{2})T([0-9]{2})\:/ui', $info[0]['DateTime'], $arr);
875: $dt0 = $info[0]['DateTime'] = sprintf('%d年%d月%d日%d時', $arr[1], $arr[2], $arr[3], $arr[4]);
876: }
877: $items[$cnt]['label'] = $num;
878: $items[$cnt]['label_color'] = COLOR_NAME1;
879: $items[$cnt]['label_size'] = 16;
880: $items[$cnt]['label_weight'] = 'bold';
881: // 情報ウィンドウ
882: if ($info[0]['AreaClass'] != '') {
883: $AreaClass = $info[0]['AreaClass'];
884: } else {
885: $AreaClass = '-';
886: }
887: if ($info[0]['IntensityClass'] != '') {
888: $IntensityClass = $info[0]['IntensityClass'];
889: } else {
890: $IntensityClass = '-';
891: }
892: if ($items[$cnt]['longitude'] >= 0) {
893: $lng = sprintf('東経%.1f度', $items[$cnt]['longitude']);
894: } else {
895: $lng = sprintf('西経%.1f度', 0 - $items[$cnt]['longitude']);
896: }
897: if ($items[$cnt]['latitude'] >= 0) {
898: $lat = sprintf('北緯%.1f度', $items[$cnt]['latitude']);
899: } else {
900: $lat = sprintf('南緯%.1f度', 0 - $items[$cnt]['latitude']);
901: }
902: if ($info[0]['Direction'] != '') {
903: $directioin = $info[0]['Direction'] . 'へ';
904: } else {
905: $directioin = '';
906: }
907: if (is_numeric($info[0]['Speed'])) {
908: $speed = sprintf('時速%dkm', $info[0]['Speed']);
909: } else {
910: $speed = $info[0]['Speed'];
911: }
912: if (isset($info[0]['centerWindSpeed']) && is_numeric($info[0]['centerWindSpeed'])) {
913: $centerWindSpeed = $info[0]['centerWindSpeed'] . 'メートル';
914: } else {
915: $centerWindSpeed = '-';
916: }
917: if (isset($info[0]['maxWindSpeed']) && is_numeric($info[0]['maxWindSpeed'])) {
918: $maxWindSpeed = $info[0]['maxWindSpeed'] . 'メートル';
919: } else {
920: $maxWindSpeed = '-';
921: }
922: $items[$cnt]['title'] = '';
923: $items[$cnt]['description'] =<<< EOT
924: <span style="font-size:120%; font-weight:bold;">{$name}</span><br />大きさ:{$AreaClass}<br />強さ:{$IntensityClass}<br />中心位置:{$lat},{$lng}<br />({$info[0]['Location']})<br />進路:{$directioin}{$speed}<br />中心気圧:{$info[0]['Pressure']}hPa<br />中心付近の最大風速:{$centerWindSpeed}<br />最大瞬間風速:{$maxWindSpeed}<br />
925: EOT;
926: // 台風情報一覧
927: $table .=<<< EOT
928: <tr>
929: <td>{$name}</td>
930: <td>{$info[0]['Location']}</td>
931: <td>{$info[0]['Pressure']}hPa</td>
932: <td>{$maxWindSpeed}</td>
933: <td>{$directioin}{$speed}</td>
934: </tr>
935:
936: EOT;
937: // 3日以内の予報円の中心座標を求める v.2.01
938: $m = 1;
939: while (isset($info[$m])) {
940: if ($info[$m]['kind'] == '予報') {
941: $lat1 = $info[$m]['latitude'];
942: $lng1 = $info[$m]['longitude'];
943: if ($m >= 3) break;
944: }
945: $m++;
946: }
947: // 基準座標に近い方をマップ中心にする
948: $dist = $pgc->greatCircleDistance($lng1, $lat1, LONGITUDE00, LATITUDE00);
949: if ($dist < $distance) {
950: $lat0 = $lat1;
951: $lng0 = $lng1;
952: $distance = $dist;
953: // 台風中心と3日後予報円の距離から地図ズーム値を決める
954: $dist = $pgc->greatCircleDistance($lng0, $lat0, $info[0]['longitude'], $info[0]['latitude']);
955: $zoom = count($zooms) + 1;
956: foreach ($zooms as $key=>$val) {
957: if ($dist >= $val) {
958: $zoom = $key + 1;
959: break;
960: }
961: }
962: }
963:
964: $cnt++;
965: }
966:
967: $table .=<<< EOT
968: </table>
969:
970: EOT;
971: // 有効な台風情報の数
972: $count = $cnt - 1;
973:
974: return array($dt0, $lat0, $lng0, $zoom);
975: }
解説:メイン・プログラム
typhoon.php
1132: // 気象庁防災情報XMLから台風情報を取得
1133: $infos = array();
1134: $urls = array();
1135: $items = array();
1136: $dt = $date->format('Y年m月d日H時');
1137: $table = $errmsg = '';
1138: $count = 0;
1139: $js = FALSE;
1140: $ret = getTyphoon($pgc, $infos, $urls, $errmsg);
1141: if ($ret == FALSE) {
1142: $errmsg = $pgc->getError();
1143: } else {
1144: $js = jsTyphoonMap($pgc, $infos, $errmsg);
1145: list($dt, $latitude, $longitude, $zoom) = getTyphoonInfo($infos, $items, $table, $count, $pgc);
1146: if ($count == 0) {
1147: $longitude = DEF_LONGITUDE;
1148: $latitude = DEF_LATITUDE;
1149: $zoom = DEF_ZOOM;
1150: $type = DEF_TYPE;
1151: $dt = $date->format('Y年m月d日H時');
1152: }
1153: }
1154:
1155: // 現在の台風の数だけ表示
1156: if ($countOnly) {
1157: echo $count;
1158: exit(0);
1159: } else if ($flagInfo) {
1160: echo json_encode($items);
1161: exit(0);
1162: }
1163:
1164: // マップ作成
1165: if (($errmsg == '') && ($js != FALSE)) {
1166: $jsmap = $pgc->drawJSMap(MAPID, $latitude, $longitude, $type, $zoom, NULL, $items, MAPSERVICE, $js, (MAP_WIDTH / 2));
1167: } else {
1168: $jsmap = $pgc->drawJSMap(MAPID, $latitude, $longitude, $type, $zoom, NULL, $items, MAPSERVICE);
1169: }
1170:
1171: // Twitter(現・X)、Blueskyへ投稿する.
1172: $message =<<< EOT
1173: 🌀台風情報 {$dt}現在
1174:
1175: (ご参考)PHPで台風情報を取得する https://www.pahoo.org/e-soul/webtech/php05/php05-15-01.shtm #台風 #台風情報 #nhk #ntv #tbs #fujitv #tvasahi
1176:
1177: EOT;
1178: if (TWITTER && isButton('tweet')) {
1179: mediaTweet($message, $res);
1180: }
1181: if (BLUESKY && isButton('bluesky')) {
1182: mediaBluesky($message, $res);
1183: }
1184:
1185: // 表示コンテンツを作成する.
1186: $HtmlBody = makeCommonBody($dt, $jsmap, $table, $urls, $res, $errmsg, $count);
1187:
1188: // ブラウザに表示する.
1189: echo $HtmlHeader;
1190: echo $HtmlBody;
1191: echo $HtmlFooter;
ここまででエラーがなければ、マップを生成する。
エラーがあれば、台風情報を描かずにマップのみ生成する。
解説:SNS投稿機能
コンテンツを描画しているのはクライアントにあるブラウザ(レンダリングエンジン)であることから、クライアント側で画像を作成し、サーバ側で SNS の API をコールするという方針とした。
- ブラウザはサーバにコンテンツ描画をリクエストする。
- サーバはデータサイトからデータ取得する。(サーバキャッシュにデータがあればそれを利用する)
- サーバはコンテンツ描画スクリプトを生成する。
- サーバはブラウザへレスポンス(HTML文)を返す。
- ブラウザはコンテンツをレンダリングする。
- ブラウザはレンダリングしたコンテンツを画像データとしてサーバへアップロードする。
- サーバは SNS の API を使ってツイートする。
- サーバは SNS へメッセージと画像を送る。
- サーバはブラウザへレスポンス(HTML文)を返す。
解説:html2canvasライブラリ
typhoon.php
161: <script src="https://html2canvas.hertzen.com/dist/html2canvas.js"></script>
162:
163: <style>
164: p.werror {
165: color: red;
166: }
画像化を実行するJavaScript関数は html2canvas である。
typhoon.php
1084: <div id="{$target}" name="{$target}" style="width:{$width2}px;">
1085: <p>
1086: 🌀台風情報 {$dt}現在{$res2}
1087: {$tweet}{$bluesky}
1088: </p>
1089: <div id="{$mapid}" style="width:{$width}px; height:{$height}px;"></div>
1090: {$table}
1091: </div>
レンダリングエンジンによって違うのかもしれないが、html2canvas ライブラリによって画像化される範囲が実際よりやや小さいため、あえてマップ領域より、幅を20ピクセル大きくした範囲を画像化範囲としている。
typhoon.php
305: /**
306: * HTMLオブジェクトの画像化
307: * @param なし
308: * @return string JavaScriptコード
309: */
310: function js_html2image() {
311: $target = TARGET;
312: $js = '';
313:
314: // Googleマップの場合
315: if (MAPSERVICE == 0) {
316: $js .=<<< EOT
317: google.maps.event.addListener(map, 'tilesloaded', function() {
318: var capture = document.querySelector('#{$target}');
319: html2canvas(capture, {useCORS: true, allowTaint:true}).then(canvas => {
320: var base64 = canvas.toDataURL('image/png'); // 画像化
321: $('#base64').val(base64);
322: });
323: });
324:
325: EOT;
326:
327: // Leafletの場合(ブラウザによってはうまく動作しない)
328: } else {
329: $js .=<<< EOT
330: HTMLCanvasElement.prototype.getContext = function(origFn) {
331: return function(type, attribs) {
332: attribs = attribs || {};
333: attribs.preserveDrawingBuffer = true;
334: return origFn.call(this, type, attribs);
335: };
336: } (HTMLCanvasElement.prototype.getContext);
337:
338: // HTML画像化イベント登録
339: function html2image() {
340: var capture = document.querySelector('#{$target}');
341: html2canvas(capture, {useCORS: true, allowTaint:true}).then(canvas => {
342: var base64 = canvas.toDataURL('image/png'); // 画像化
343: $('#base64').val(base64);
344: });
345: };
346:
347: // ズーム変更イベント
348: map.on('zoomend', function() {
349: html2image();
350: });
351:
352: // マップ移動イベント
353: map.on('moveend', function() {
354: html2image();
355: });
356:
357: // html2image()を発火させるために,ズームアウトして500msec後に元に戻す.
358: var zoom = map.getZoom();
359: map.setZoom(zoom - 1);
360: setTimeout(function() {
361: map.setZoom(zoom);
362: }, 500);
363:
364: EOT;
365: }
366:
367: return $js;
368: }
Leaflet の場合、これに相当するイベントがないため、ズーム変更完了イベントにフックし、強制的にズーム変更イベントを発生させるタイミングで画像化する。しかし、Leaflet では、画像を描画するレイヤ構造の関係で、html2canvas ライブラリでは位置ずれが起きることがわかっている。その場合は、Googleマップを選択してほしい。また、対策方法をご存じの方がいたらお知らせいただきたい。
これらをJavaScriptとして生成するユーザー関数が js_html2image である。
準備:pahooTwitterAPI クラス
pahooTwitterAPI.php
解説:メディア付き投稿(RAWデータ)
pahooTwitterAPI.php
607: /**
608: * バイナリデータを使ったメディア付きメッセージをツイートする.
609: * Tweetet API v2 を使用する.
610: * @param string $message 投稿メッセージ(UTF-8限定)
611: * @param array $items メディアデータ(バイナリデータ配列)
612: * @return bool TRUE:リクエスト成功/FALSE:失敗
613: */
614: function tweet_media_raw($message, $items) {
615: // メディアのアップロード
616: $media_ids = array();
617: $cnt = 0;
618: // Tweetet API v1.1 を使用する(v2にメディアアップロードが未実装のため)
619: $this->connection->setApiVersion('1.1');
620: foreach ($items as $data) {
621: $tmpname = $this->saveTempFile($data);
622: // $media = $this->connection->upload('media/upload', ['media' => $tmpname]);
623: $media = $this->connection->upload('media/upload', ['media' => $tmpname], ['chunkedUpload' => true]); // twitteroauth 7.0.0 対応
624: unlink($tmpname);
625: if (! isset($media->media_id_string)) break; // 処理失敗
626: $media_ids[] = (string)$media->media_id_string;
627: $cnt++;
628: if ($cnt > 3) break; // 最大4つまで
629: }
630:
631: // メディア付きツイート(Tweetet API v2 を使用する)
632: $this->connection->setApiVersion('2');
633: $option = [
634: 'text' => $message,
635: 'media' => [
636: 'media_ids' => $media_ids
637: ]
638: ];
639: // $status = $this->connection->post('tweets', $option, TRUE);
640: $status = $this->connection->post('tweets', $option, ['jsonPayload' => true]); // twitteroauth 7.0.0 対応
641: $this->webapi = 'https://api.twitter.com/2/tweets';
642:
643: // 処理に成功した.
644: if ($this->isSuccess()) {
645: $this->responses = $status->data;
646: $this->errcode = NULL;
647: $this->errmsg = '';
648: $this->error = FALSE;
649: $res = TRUE;
650: // 処理に失敗した.
651: } else {
652: if ($this->isAuthError() == FALSE) {
653: $this->errmsg = $status->detail;
654: $this->error = TRUE;
655: }
656: $res = FALSE;
657: }
658: return $res;
659: }
解説:Twitter(現・X)へ投稿する
typhoon.php
280: /**
281: * Twitter(現・X)投稿
282: * @param string $message 投稿文
283: * @param string $res 応答メッセージ格納用
284: * @return bool TRUE:成功/FALSE:失敗または未処理
285: */
286: function mediaTweet($message, &$res) {
287: if (! TWITTER) return FALSE;
288:
289: $ret = TRUE;
290: if (isset($_POST['base64']) && ($_POST['base64'] != '')) {
291: $base64 = preg_replace('/data\:image\/png\;base64\,/ui', '', $_POST['base64']);
292: $raws = array(base64_decode($base64));
293: $ptw = new pahooTwitterAPI();
294: $ptw->tweet_media_raw($message, $raws);
295: $errmsg = $ptw->errmsg;
296: $ret = ! $ptw->error;
297: $ptw = NULL;
298: if ($ret) {
299: $res = 'ツイートしました';
300: }
301: }
302: return $ret;
303: }
ブラウザからPOSTで受け取った画像データ $_POST['base64'] はBASE64でデコードされており、冒頭に余計なヘッダ情報が付いているので、このヘッダ情報を除き( preg_replace )、バイナリデータにデコードする( base64_decode )。
続いて pahooTwitterAPI クラスを呼び出し、tweet_media_raw メソッドを使って、メッセージと画像を一気に投稿する。
準備:pahooBlueskyAPI クラス
pahooBlueskyAPI.php
解説:Blueskyへ投稿する
typhoon.php
253: /**
254: * Bluesky投稿
255: * @param string $message 投稿文
256: * @param string $res 応答メッセージ格納用
257: * @return bool TRUE:成功/FALSE:失敗または未処理
258: */
259: function mediaBluesky($message, &$res) {
260: if (! BLUESKY) return FALSE;
261:
262: $ret = TRUE;
263: if (isset($_POST['base64']) && ($_POST['base64'] != '')) {
264: $base64 = preg_replace('/data\:image\/png\;base64\,/ui', '', $_POST['base64']);
265: $raws = array(base64_decode($base64));
266: $pbs = new pahooBlueskyAPI(BLUESKY_DOMAIN);
267: $res = $pbs->createSession();
268: $pbs->post($message, FALSE, NULL, NULL, $raws);
269: $errmsg = $pbs->geterror();
270: $ret = ! $pbs->iserror();
271: $res = $pbs->deleteSession();
272: $pbs = NULL;
273: if ($ret) {
274: $res = 'Blueskyに投稿しました';
275: }
276: }
277: return $ret;
278: }
ブラウザからPOSTで受け取った画像データ $_POST['base64'] はBASE64でデコードされており、冒頭に余計なヘッダ情報が付いているので、このヘッダ情報を除き( preg_replace )、バイナリデータにデコードする( base64_decode )。
続いて pahooBlueskyAPI クラスを呼び出し、post メソッドを使って、メッセージと画像を一気に投稿する。
解説:SNSへ投稿する(メイン・プログラム)
typhoon.php
1171: // Twitter(現・X)、Blueskyへ投稿する.
1172: $message =<<< EOT
1173: 🌀台風情報 {$dt}現在
1174:
1175: (ご参考)PHPで台風情報を取得する https://www.pahoo.org/e-soul/webtech/php05/php05-15-01.shtm #台風 #台風情報 #nhk #ntv #tbs #fujitv #tvasahi
1176:
1177: EOT;
1178: if (TWITTER && isButton('tweet')) {
1179: mediaTweet($message, $res);
1180: }
1181: if (BLUESKY && isButton('bluesky')) {
1182: mediaBluesky($message, $res);
1183: }
1184:
1185: // 表示コンテンツを作成する.
解説:count, info オプション
typhoon.php
1155: // 現在の台風の数だけ表示
1156: if ($countOnly) {
1157: echo $count;
1158: exit(0);
1159: } else if ($flagInfo) {
1160: echo json_encode($items);
1161: exit(0);
1162: }
また、infoオプションを指定すると、マップ上のinfoウィンドウに表示するタグ付きテキストをJSON形式で取り出すことができる。台風がないときは空JSONを返す。
ぱふぅ家のホームページでは、台風があるときに自動的にSNSに投稿するbotプログラムで利用している。
活用例
参考サイト
- 台風情報:気象庁
- JavaScriptでHTML表示を画像保存する:ぱふぅ家のホームページ
- HTMLとCSSでさまざまなアイコンを表示する:ぱふぅ家のホームページ
- Twitter API - WebAPIの登録方法:ぱふぅ家のホームページ
- Bluesky API - WebAPIの登録方法:ぱふぅ家のホームページ
- PHPでTwitterに画像付きメッセージ投稿:ぱふぅ家のホームページ
- PHPでBlueskyに投稿する:ぱふぅ家のホームページ
- 日本付近の台風情報 - 気象庁発表:みんなの知識 ちょっと便利帳

そこで、気象庁の台風情報のサイトから、現在日本に接近している台風の情報を取得するPHPスクリプトを作成してみることにする。
2021年(令和3年)2月24日の気象庁サイト・リニューアルにより、スクレイピングによる取り出しが難しくなったため、気象庁防災情報XMLからの情報取得に変更した。あわせてキャッシュ・システムを導入した。
(2025年8月13日).pahooEnv導入
(2025年7月6日)count, infoオプションを追加.GoogleMaps JavaScript APIの変更に対応した.
(2024年11月1日)Bluesky投稿機能を追加