PHPで台風情報を取得する

(1/1)
今年(2013年)は台風の被害が多い年となった。
そこで、気象庁の台風情報のサイトから、現在日本に接近している台風の情報を取得するPHPスクリプトを作成してみることにする。

2021年(令和3年)2月24日の気象庁サイト・リニューアルにより、スクレイピングによる取り出しが難しくなったため、気象庁防災情報XMLからの情報取得に変更した。あわせてキャッシュ・システムを導入した。

(2025年8月13日).pahooEnv導入
(2025年7月6日)count, infoオプションを追加.GoogleMaps JavaScript APIの変更に対応した.
(2024年11月1日)Bluesky投稿機能を追加

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

PHPで台風情報を取得する

目次

サンプル・プログラム

圧縮ファイルの内容
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.phpTwitter APIを利用するクラス pahooTwitterAPI。
使い方は「PHPでTwitterに投稿(ツイート)する」などを参照。include_path が通ったディレクトリに配置すること。
pahooBlueskyAPI.phpBluesky APIに関わるクラス pahooBlueskyAPI。
使い方は「PHPでPHPでBlueskyに投稿する」などを参照。include_path が通ったディレクトリに配置すること。
typhoon.php 更新履歴
バージョン 更新日 内容
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()発火プロセス見直し
pahooInputData.php 関数/メソッド一覧
関数/メソッド 機能 詳細
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をサプレスする.
pahooGeoCode.php 更新履歴
バージョン 更新日 内容
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の変更に対応
pahooCache.php 更新履歴
バージョン 更新日 内容
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 初版
pahooTwitterAPI.php 更新履歴
バージョン 更新日 内容
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() -- メディアのシャフル機能
pahooBlueskyAPI.php 更新履歴
バージョン 更新日 内容
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対応

クラウド連携や相手先サイトのデータを読み込むのに https通信を使うため、PHPに OpenSSLモジュールが組み込まれている必要がある。関数  phpinfo  を使って、下図のように表示されればOKだ。
OpenSSL - PHP
そうでない場合は、次の手順に従ってOpenSSLを有効化し、PHPを再起動させる必要がある。

Windowsでは、"php.ini" の下記の行を有効化する。
extension=php_openssl.dll
Linuxでは --with-openssl=/usr オプションを付けて再ビルドする。→OpenSSLインストール手順

これで準備は完了だ。

準備:pahooInputData 関数群

PHPのバージョンや入力データのバリデーションなど、汎用的に使う関数群を収めたファイル "pahooInputData.php" が同梱されているが、include_path が通ったディレクトリに配置してほしい。他のプログラムでも "pahooInputData.php" を利用するが、常に最新のファイルを1つ配置すればよい。

また、各種クラウドサービスに登録したときに取得するアカウント情報、アプリケーションパスワードなどを登録した .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 = '';

GoogleマップやLeafletなどによる地図描画や住所検索を行うためのクラスが pahooGeoCode である。同梱のクラス・ファイル "pahooGeoCode.php" は include_path が通ったディレクトリに配置してほしい。他のプログラムでも pahooGeoCodeクラス を利用するが、常に最新のクラス・ファイルを1つ配置すればよい。
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);

表示する地図は、Googleマップ地理院地図・オープンストリートマップ(OSM)から選べる。あらかじめ、定数 MAPSERVICE に値を設定すること。
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: }

クラウド連携や相手先サイトの公開データを利用する際、こちらのアプリを起動する都度、APIを呼び出したりデータをダウンロードするのでは、相手サーバに負荷をかけてしまう。
そこで、頻繁に変更がないデータについては、一度取り込んだら、こちら側のサーバのローカルストレージにキャッシュしておく仕組みを用意した。それが 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フォーマット

気象庁防災情報XMLフォーマットについては、「PHPで天気予報を求める - 気象庁防災情報XMLフォーマット」をご覧いただきたい。
今回は、高頻度 - 随時フィード および 長期 - 随時フィード にアクセスし、電文コード VPTW60 の台風解析・予報情報(5日予報)を取得する。

VPTW60の構造

台風解析・予報情報(5日予報)XML VPTW60 には、その時点の台風情報と、5日先の予報円(予報進路)が収められており、構造は下記の通りである。ここから必要な情報を取り出す。
注意すべきは、1つのXMLファイルの中に1つまたは複数の台風情報が含まれていること。
また、過去の中心位置は分からないので、キャッシュ・ファイルとして保存した VZSA50 から中心位置を拾い出し、それを台風の過去進路にする方針である。
また、VPTW60 の発表時刻によっては予報円の情報が無い。その場合、予報円情報が存在する最も新しい VPTW60 から予報円情報を取り出すことにする。
VPTW60の構造(xml) Report Control Title 台風解析・予報情報(5日予報)(H30) DateTime 配信日時 Status ステータス EditorialOffice 編集者 PublishingOffice 配信者 Head Title タイトル ReportDateTime 配信日時 TargetDateTime 予報対象日時 TargetDuration 予報間隔 InfoType 発表 InfoKind 情報の種類 Serial 通し番号 InfoKindVersion 情報バージョン Body MeteorologicalInfos MeteorologicalInfo DateTime 予報日時 Item Kind Property Type 呼称 TyphoonNamePart Name 名称(英語) NameKana 名称(日本語) Number 番号(西暦下2桁+連番2桁) Remark Kind Property Type 階級 ClassPart Kind Property Type 中心 CenterPart Location 位置(テキスト) Kind Property Type WindPart WarningAreaPart WarningAreaPart MeteorologicalInfo DateTime 24時間後予報日時 Item Kind Property Type 階級 ClassPart Kind Property Type 中心 CenterPart ProbabilityCircle Location 位置(テキスト) Kind Property Type WindPart WarningAreaPart WarningAreaPart MeteorologicalInfo DateTime 48時間後予報日時 MeteorologicalInfo DateTime 72時間後予報日時 MeteorologicalInfo DateTime 96時間後予報日時 MeteorologicalInfo DateTime 120時間後予報日時

解説:台風に関する情報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: }

フィードから台風に関する情報URLを取得するユーザー関数は jmaGetTyphoonURLs である。
正規表現で 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: }

VPTW60 から台風情報をを配列 $items へ格納するユーザー関数が getTyphoon である。

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: }

ユーザー関数が jsTyphoonMap は、暴風域・強風域、予報円を含め、マップ上に台風情報を描くためのスクリプトを生成する。
前述の通り、円の中心がズレている場合があり、そのためのユーザー関数 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: }

ユーザー関数が getTyphoonInfo は、台風情報一覧や、マップ上のアイコンをクリックするとあらわれる情報ウィンドウの内容(テキスト)を生成するユーザー関数である。

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

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;

メイン・プログラムでは、まず、getTyphoon 関数で台風情報を取得し、エラーがなかったら、jsTyphoonMap 関数で描画スクリプトを生成する。
ここまででエラーがなければ、マップを生成する。
エラーがあれば、台風情報を描かずにマップのみ生成する。

解説:SNS投稿機能

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

解説:html2canvasライブラリ

typhoon.php

 161: <script src="https://html2canvas.hertzen.com/dist/html2canvas.js"></script>
 162: 
 163: <style>
 164: p.werror {
 165:     color: red;
 166: }

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

typhoon.php

1084: <div id="{$target}" name="{$target}" style="width:{$width2}px;">
1085: <p>
1086: 🌀台風情報 {$dt}現在{$res2}
1087: &nbsp;{$tweet}{$bluesky}
1088: </p>
1089: <div id="{$mapid}" style="width:{$width}px; height:{$height}px;"></div>
1090: {$table}
1091: </div>

画像化するオブジェクトは、<div id="{$target}"> で指定する範囲である。
レンダリングエンジンによって違うのかもしれないが、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: }

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

準備:pahooTwitterAPI クラス

pahooTwitterAPI.php

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

解説:メディア付き投稿(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: }

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

解説: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: }

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

準備:pahooBlueskyAPI クラス

pahooBlueskyAPI.php

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

解説: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: }

メッセージと画像を Blueskyに投稿するのはサーバ側のPHPユーザー関数 mediaBluesky である。
ブラウザから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: // 表示コンテンツを作成する.

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

解説:count, info オプション

typhoon.php

1123: $countOnly = isButton('count');
1124: $flagInfo  = isButton('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: }

現在の台風の個数だけを取得するためのAPIとして、countオプションを付けた。 "typhoon.php?count" のようにして呼び出すことで、数字を返す。0のときは台風がない。
また、infoオプションを指定すると、マップ上のinfoウィンドウに表示するタグ付きテキストをJSON形式で取り出すことができる。台風がないときは空JSONを返す。
ぱふぅ家のホームページでは、台風があるときに自動的にSNSに投稿するbotプログラムで利用している。

活用例

みんなの知識 ちょっと便利帳では、「日本付近の台風情報 - 気象庁発表」のコーナーで、このサンプル・プログラムを活用している。ありがとうございます。

参考サイト

(この項おわり)
header