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

目次
サンプル・プログラム
typhoon.php | サンプル・プログラム本体 |
pahooGeoCode.php | 住所・緯度・経度に関わるクラス pahooGeoCode。 使い方は「PHPで住所・ランドマークから最寄り駅を求める」「PHPで住所・ランドマークから緯度・経度を求める」などを参照。include_path が通ったディレクトリに配置すること。 |
pahooCache.php | キャッシュ処理に関わるクラス pahooCache。 キャッシュ処理に関わるクラスの使い方は「PHPで天気予報を求める」を参照。include_path が通ったディレクトリに配置すること。 |
pahooTwitterAPI.php | Twitter APIを利用するクラス pahooTwitterAPI。 使い方は「PHPでTwitterに投稿(ツイート)する」などを参照。include_path が通ったディレクトリに配置すること。 |
準備:pahooGeoCode クラス
0037: class pahooGeoCode {
0038: var $items; //検索結果格納用
0039: var $error; //エラーフラグ
0040: var $hits; //検索ヒット件数
0041: var $webapi; //直前に呼び出したWebAPI URL
0042:
0043: //Google Cloud Platform APIキー
0044: //https://cloud.google.com/maps-platform/
0045: //※Google Maps APIを利用しないのなら登録不要
0046: var $GOOGLE_API_KEY_1 = '**************************'; //HTTPリファラ用
0047: var $GOOGLE_API_KEY_2 = '**************************'; //IP制限用
0048:
0049: //Yahoo! JAPAN Webサービス アプリケーションID
0050: //https://e.developer.yahoo.co.jp/register
0051: //※Yahoo! JAPAN Webサービスを利用しないのなら登録不要
0052: var $YAHOO_APPLICATION_ID = '*****************************';
クラスについては「PHPでクラスを使ってテキストの読みやすさを調べる」を参照されたい。

地図や住所検索として Google を利用するのであれば、Google Cloud Platform APIキー が必要で、その入手方法は「Google Cloud Platform - WebAPIの登録方法」を参照されたい。
準備:地図サービスの選択
0040: //地図描画サービスの選択
0041: // 0:Google
0042: // 2:地理院地図・OSM
0043: define('MAPSERVICE', 2);


準備:キャッシュ・システム
0070: //キャッシュ保持時間(分) 0:キャッシュしない
0071: //気象庁へのアクセス負荷軽減+台風の進路プロット
0072: define('LIFE_CACHE_FEED', 5); //高頻度 - 随時フィードに対して
0073: define('LIFE_CACHE_FEED_L', 120); //長期 - 随時フィードに対して
0074: define('LIFE_CACHE_DATA', (60 * 24 * 14)); //台風情報の保持時間(進路プロット)
0075:
0076: //キャッシュ・ディレクトリ
0077: //書き込み可能で,外部からアクセスされないディレクトリを指定してください.
0078: define('DIR_CACHE_FEED', './pcache_typhoon1/');
0079: define('DIR_CACHE_FEED_L', './pcache_typhoon2/');
0080: define('DIR_CACHE_DATA', './pcache_typhoon3/');
キャッシュ保持時間、キャッシュ・ディレクトリともに、自サイトの環境に応じて変更してほしい。
LIFE_CACHE_DATA は、過去の台風の進路をプロットできるように、初期値では14日間を指定してある。
気象庁防災情報XMLフォーマット
今回は、高頻度 - 随時フィード および 長期 - 随時フィード にアクセスし、電文コード VPTW60 の台風解析・予報情報(5日予報)を取得する。
VPTW60の構造
注意すべきは、1つのXMLファイルの中に1つまたは複数の台風情報が含まれていること。
また、過去の中心位置は分からないので、キャッシュ・ファイルとして保存した VZSA50 から中心位置を拾い出し、それを台風の過去進路にする方針である。
また、VPTW60 の発表時刻によっては予報円の情報が無い。その場合、予報円情報が存在する最も新しい VPTW60 から予報円情報を取り出すことにする。
準備:マップの表示サイズなど
0048: //予報円を間引く条件(km):次の予報円がこれより近ければ描画しない
0049: define('THIN_OUT', 50);
0050:
0051: //マップの表示サイズ(単位:ピクセル)
0052: define('MAP_WIDTH', 600);
0053: define('MAP_HEIGHT', 480);
0054: //マップID
0055: define('MAPID', 'map_id');
0056: //マップ座標
0057: define('DEF_LATITUDE', 35.0); //緯度
0058: define('DEF_LONGITUDE', 137.0); //経度
0059: //define('DEF_TYPE', 'GSISTD'); //マップタイプ
0060: define('DEF_TYPE', 'HYBRID'); //マップタイプ
0061: define('DEF_ZOOM', 5); //ズーム
0062: //マップ描画色
0063: define('COLOR_NAME1', '#FF8800'); //台風名称の描画色
0064: define('COLOR_NAME2', '#0000FF'); //熱低名称の描画色
0065: define('COLOR_LINE', '#0000FF'); //過去経路の描画色
0066: define('COLOR_WIND1', '#FFFF00'); //強風域の描画色
0067: define('COLOR_WIND2', '#FF0000'); //暴風域の描画色
0068: define('COLOR_FORECAST', '#FFFFFF'); //予報円の描画色

予報円を間引く条件 THIN_OUT は、台風が停滞していたり、移動速度が極めてゆっくりの場合、予報円の密度が高すぎて地図が見にくくなる。そこで、前回予報円がこの距離以下であれば次の予報円を描かない(間引く)ことができる。0を代入すれば、すべての予報円を描くようになる。
解説:台風に関する情報URLを取得
0331: /**
0332: * 気象庁防災情報XMLから台風に関する情報URLを取得
0333: * @param array $urls URL格納配列
0334: * @param string $errmsg エラーメッセージ格納用
0335: * @return bool TRUE:取得成功/FALSE:取得失敗
0336: */
0337: function jmaGetTyphoonURLs(&$urls, &$errmsg) {
0338: //URLパターン
0339: $vptw = '/https?\:\/\/www\.data\.jma\.go\.jp\/developer\/xml\/data\/([0-9\_]+)VPTW6[0-9]+\_[0-9]+\.xml/ui';
0340:
0341: //随時フィードの解析
0342: $cnt = 0;
0343: $pcc = new pahooCache(LIFE_CACHE_FEED, DIR_CACHE_FEED);
0344: $xml = $pcc->simplexml_load(FEED);
0345: //レスポンス・チェック
0346: if ($pcc->iserror() || !isset($xml->entry)) {
0347: $errmsg = '気象庁防災情報XMLにアクセスできません';
0348: return FALSE;
0349: }
0350: foreach ($xml->entry as $node) {
0351: //URLを取得
0352: if (preg_match($vptw, $node->id, $arr) > 0) {
0353: $urls[$cnt] = $arr[0];
0354: $cnt++;
0355: }
0356: }
0357: $pcc = NULL;
0358:
0359: //長期フィードの解析
0360: $pcc = new pahooCache(LIFE_CACHE_FEED_L, DIR_CACHE_FEED_L);
0361: $xml = $pcc->simplexml_load(FEED_L);
0362: //レスポンス・チェック
0363: if ($pcc->iserror() || !isset($xml->entry)) {
0364: $errmsg = '気象庁防災情報XMLにアクセスできません';
0365: return FALSE;
0366: }
0367: foreach ($xml->entry as $node) {
0368: //URLを取得
0369: if (preg_match($vptw, $node->id, $arr) > 0) {
0370: if (array_search($arr[0], $urls) === FALSE) {
0371: $urls[$cnt] = $arr[0];
0372: $cnt++;
0373: }
0374: }
0375: }
0376: $pcc = NULL;
0377:
0378: //エラー・チェック
0379: if ($cnt == 0) {
0380: $errmsg = '直近の台風情報はありません';
0381: return FALSE;
0382: }
0383:
0384: //URLを日時の新しい順にソート
0385: rsort($urls);
0386:
0387: return TRUE;
0388: }
正規表現で VPTW60 を含むURLを取り出し配列に格納し、日時の新しい順に並べ替えておく。
解説:台風情報を読み込む
0390: /**
0391: * 新しい台風報かどうか
0392: * @param object $xml 気象庁防災情報XML
0393: * @param array $items 台風情報を格納する配列
0394: * @return bool TRUE:新しい情報
0395: */
0396: function isNewTyphoon($xml, $items) {
0397: $res = FALSE;
0398:
0399: if (isset($xml->Body->MeteorologicalInfos->MeteorologicalInfo->Item->Kind->Property->TyphoonNamePart->Number)) {
0400: $num = (string)$xml->Body->MeteorologicalInfos->MeteorologicalInfo->Item->Kind->Property->TyphoonNamePart->Number;
0401: if ($num != '') {
0402: if (! isset($items[$num])) {
0403: $res = TRUE;
0404: }
0405: }
0406: }
0407:
0408: return $res;
0409: }
0411: /**
0412: * 台風報取得(気象庁防災情報XMLから)
0413: * @param object $pgc pahooGeoCodeオブジェクト
0414: * @param array $items 台風情報を格納する配列
0415: * @param string $urls 情報XMLのURLを格納する配列
0416: * @param string $errmsg エラーメッセージ格納用
0417: * @return bool TRUE:取得成功/FALSE:失敗
0418: */
0419: function getTyphoon($pgc, &$items, &$urls, &$errmsg) {
0420: //名前空間
0421: define('JMX_EB', 'http://xml.kishou.go.jp/jmaxml1/elementBasis1/');
0422: //マッチングパターン
0423: $pat2 = '/([\+\-][0-9]{1,2}\.[0-9]+)([\+\-][0-9]{1,3}\.[0-9]+)/ui'; //緯度・経度
0424: $pat3 = '/(予報)[ ]*([01234567890-9]+)時間後/ui'; //推定|予報
0425: //予報円は取得済みか否か
0426: $flag_forecast = array();
0427:
0428: //オブジェクト生成
0429: $pcc = new pahooCache(LIFE_CACHE_DATA, DIR_CACHE_DATA);
0430:
0431: //最新の台風に関する情報URLを取得
0432: jmaGetTyphoonURLs($urls, $errmsg);
0433: if ($errmsg != '') return FALSE;
0434:
0435: foreach ($urls as $key=>$vptw) {
0436: //台風情報の取得
0437: $xml = $pcc->simplexml_load($vptw);
0438: //レスポンス・チェック
0439: if ($pcc->iserror() || !isset($xml->Body->MeteorologicalInfos)) {
0440: $errmsg = '気象庁防災情報XMLから台風情報を取得できません';
0441: return FALSE;
0442: }
0443: $flag_f = FALSE; //予報円取得済みか否か
0444:
0445: //最新の台風情報
0446: if (isNewTyphoon($xml, $items)) {
0447: foreach ($xml->Body->MeteorologicalInfos as $infos) {
0448: $cnt = 0;
0449: foreach ($infos->MeteorologicalInfo as $info) {
0450: //実況
0451: if ($info->DateTime['type'] == '実況') {
0452: foreach ($info->Item->Kind as $kind) {
0453: if (isset($kind->Property->Type)) {
0454: //呼称
0455: if ($kind->Property->Type == '呼称') {
0456: $num = (string)$kind->Property->TyphoonNamePart->Number;
0457: if ($num == '') break;
0458: $items[$num]['Name'] = (string)$kind->Property->TyphoonNamePart->Name;
0459: $items[$num]['NameKana'] = (string)$kind->Property->TyphoonNamePart->NameKana;
0460: $items[$num][$cnt]['kind'] = '実況';
0461: $items[$num][$cnt]['DateTime'] = (string)$info->DateTime;
0462: //その台風の予報円は未取得
0463: $flag_forecast[$num] = FALSE;
0464: //古い台風情報かどうか
0465: $tt = time() - strtotime($info->DateTime);
0466: $items[$num]['Valid'] = ($tt < SCRAP_TIME) ? TRUE : FALSE;
0467: //階級
0468: } else if ($kind->Property->Type == '階級') {
0469: $node = $kind->Property->ClassPart->children(JMX_EB);
0470: $items[$num][$cnt]['TyphoonClass'] = (string)$node->TyphoonClass;
0471: $items[$num][$cnt]['AreaClass'] = (string)$node->AreaClass;
0472: $items[$num][$cnt]['IntensityClass'] = (string)$node->IntensityClass;
0473: //中心
0474: } else if ($kind->Property->Type == '中心') {
0475: $node = $kind->Property->CenterPart->children(JMX_EB);
0476: //中心位置
0477: $items[$num][$cnt]['Location'] = (string)$kind->Property->CenterPart->Location;
0478: foreach ($node->Coordinate as $val) {
0479: if (preg_match($pat2, $val, $arr) > 0) {
0480: $items[$num][$cnt]['latitude'] = (float)$arr[1];
0481: $items[$num][$cnt]['longitude'] = (float)$arr[2];
0482: }
0483: }
0484: //移動速度
0485: $items[$num][$cnt]['Direction'] = (string)$node->Direction;
0486: foreach ($node->Speed as $val) {
0487: if (isset($val->attributes()['condition'])) {
0488: $items[$num][$cnt]['Speed'] = (string)$val->attributes()['condition'];
0489: } else if ($val->attributes()['unit'] == 'km/h') {
0490: $items[$num][$cnt]['Speed'] = (string)$val;
0491: }
0492: }
0493: //中心気圧
0494: $items[$num][$cnt]['Pressure'] = (int)$node->Pressure;
0495: //風
0496: } else if ($kind->Property->Type == '風') {
0497: $node = $kind->Property->WindPart->children(JMX_EB);
0498: foreach ($node->WindSpeed as $val) {
0499: if (($val->attributes()['condition'] == '中心付近') && ($val->attributes()['unit'] == 'm/s')) {
0500: $items[$num][$cnt]['centerWindSpeed'] = (int)$val;
0501: } else if (($val->attributes()['type'] == '最大瞬間風速') && ($val->attributes()['unit'] == 'm/s')) {
0502: $items[$num][$cnt]['maxWindSpeed'] = (int)$val;
0503: }
0504: }
0505: //暴風域・強風域
0506: foreach ($kind->Property->WarningAreaPart as $val) {
0507: $key = (string)$val['type'];
0508: //半径
0509: $node = $val->children(JMX_EB);
0510: $n = 0;
0511: foreach ($node->Circle->Axes->Axis as $axis) {
0512: if (isset($axis->Direction) && ($axis->Direction != '')) {
0513: $items[$num][$cnt][$key][$n]['direction'] = (string)$axis->Direction;
0514: } else {
0515: $items[$num][$cnt][$key][$n]['direction'] = (string)$axis->Direction->attributes()['condition'];
0516: }
0517: foreach ($axis->Radius as $val) {
0518: if ($val->attributes()['unit'] == 'km') {
0519: $items[$num][$cnt][$key][$n]['radius'] = (int)$val;
0520: }
0521: }
0522: $n++;
0523: }
0524: }
0525: }
0526: }
0527: }
0528: //予報
0529: } else if (preg_match($pat3, (string)$info->DateTime['type'], $arr) > 0) {
0530: if ($num == '') break; //ver.2.04
0531: $cnt++;
0532: $items[$num][$cnt]['kind'] = '予報';
0533: $items[$num][$cnt]['DateTime'] = (string)$info->DateTime;
0534: $flag_f = TRUE; //予報円取得済み
0535: foreach ($info->Item->Kind as $kind) {
0536: //階級
0537: if ($kind->Property->Type == '階級') {
0538: $node = $kind->Property->ClassPart->children(JMX_EB);
0539: $items[$num][$cnt]['TyphoonClass'] = (string)$node->TyphoonClass;
0540: $items[$num][$cnt]['AreaClass'] = isset($node->AreaClass) ? (string)$node->AreaClass : '';
0541: $items[$num][$cnt]['IntensityClass'] = isset($node->IntensityClass) ? (string)$node->IntensityClass : '';
0542: //中心位置(予報円)
0543: } else if ($kind->Property->Type == '中心') {
0544: $node = $kind->Property->CenterPart->ProbabilityCircle->children(JMX_EB);
0545: foreach ($node->BasePoint as $val) {
0546: if (preg_match($pat2, $val, $arr) > 0) {
0547: $items[$num][$cnt]['latitude'] = (float)$arr[1];
0548: $items[$num][$cnt]['longitude'] = (float)$arr[2];
0549: }
0550: }
0551: //半径
0552: $n = 0;
0553: foreach ($node->Axes->Axis as $axis) {
0554: if (isset($axis->Direction) && ($axis->Direction != '')) {
0555: $items[$num][$cnt]['予報円'][$n]['direction'] = (string)$axis->Direction;
0556: } else {
0557: $items[$num][$cnt]['予報円'][$n]['direction'] = (string)$axis->Direction->attributes()['condition'];
0558: }
0559: foreach ($axis->Radius as $val) {
0560: if ($val->attributes()['unit'] == 'km') {
0561: $items[$num][$cnt]['予報円'][$n]['radius'] = (int)$val;
0562: }
0563: }
0564: $n++;
0565: }
0566: //風
0567: } else if ($kind->Property->Type == '風') {
0568: $node = $kind->Property->WindPart->children(JMX_EB);
0569: foreach ($node->WindSpeed as $val) {
0570: if (($val->attributes()['condition'] == '中心付近') && ($val->attributes()['unit'] == 'm/s')) {
0571: $items[$num][$cnt]['centerWindSpeed'] = (int)$val;
0572: } else if (($val->attributes()['type'] == '最大瞬間風速') && ($val->attributes()['unit'] == 'm/s')) {
0573: $items[$num][$cnt]['maxWindSpeed'] = (int)$val;
0574: }
0575: }
0576: }
0577: }
0578: }
0579: }
0580: }
0581: //過去の台風情報
0582: } else {
0583: foreach ($xml->Body->MeteorologicalInfos as $infos) {
0584: foreach ($infos->MeteorologicalInfo as $info) {
0585: //過去の位置
0586: if ($info->DateTime['type'] == '実況') {
0587: foreach ($info->Item->Kind as $kind) {
0588: if (isset($kind->Property->Type)) {
0589: //呼称
0590: if ($kind->Property->Type == '呼称') {
0591: $num = (string)$kind->Property->TyphoonNamePart->Number;
0592: if ($num == '') break;
0593: $cnt = 0;
0594: while (isset($items[$num][$cnt])) $cnt++;
0595: $items[$num][$cnt]['kind'] = '過去';
0596: $items[$num][$cnt]['DateTime'] = (string)$info->DateTime;
0597: //中心
0598: } else if ($kind->Property->Type == '中心') {
0599: $node = $kind->Property->CenterPart->children(JMX_EB);
0600: //中心位置
0601: foreach ($node->Coordinate as $val) {
0602: if (preg_match($pat2, $val, $arr) > 0) {
0603: $items[$num][$cnt]['latitude'] = (float)$arr[1];
0604: $items[$num][$cnt]['longitude'] = (float)$arr[2];
0605: }
0606: }
0607: }
0608: }
0609: }
0610: //予報
0611: } else if (isset($flag_forecast[$num]) && ! $flag_forecast[$num] && (preg_match($pat3, (string)$info->DateTime['type'], $arr) > 0)) {
0612: if ($num == '') break; //ver.2.04
0613: $cnt++;
0614: $items[$num][$cnt]['kind'] = '予報';
0615: $items[$num][$cnt]['DateTime'] = (string)$info->DateTime;
0616: $flag_f = TRUE; //予報円取得済み
0617: foreach ($info->Item->Kind as $kind) {
0618: //階級
0619: if ($kind->Property->Type == '階級') {
0620: $node = $kind->Property->ClassPart->children(JMX_EB);
0621: $items[$num][$cnt]['TyphoonClass'] = (string)$node->TyphoonClass;
0622: $items[$num][$cnt]['AreaClass'] = isset($node->AreaClass) ? (string)$node->AreaClass : '';
0623: $items[$num][$cnt]['IntensityClass'] = isset($node->IntensityClass) ? (string)$node->IntensityClass : '';
0624: //中心位置(予報円)
0625: } else if ($kind->Property->Type == '中心') {
0626: $node = $kind->Property->CenterPart->ProbabilityCircle->children(JMX_EB);
0627: foreach ($node->BasePoint as $val) {
0628: if (preg_match($pat2, $val, $arr) > 0) {
0629: $items[$num][$cnt]['latitude'] = (float)$arr[1];
0630: $items[$num][$cnt]['longitude'] = (float)$arr[2];
0631: }
0632: }
0633: //半径
0634: $n = 0;
0635: foreach ($node->Axes->Axis as $axis) {
0636: if (isset($axis->Direction) && ($axis->Direction != '')) {
0637: $items[$num][$cnt]['予報円'][$n]['direction'] = (string)$axis->Direction;
0638: } else {
0639: $items[$num][$cnt]['予報円'][$n]['direction'] = (string)$axis->Direction->attributes()['condition'];
0640: }
0641: foreach ($axis->Radius as $val) {
0642: if ($val->attributes()['unit'] == 'km') {
0643: $items[$num][$cnt]['予報円'][$n]['radius'] = (int)$val;
0644: }
0645: }
0646: $n++;
0647: }
0648: //風
0649: } else if ($kind->Property->Type == '風') {
0650: $node = $kind->Property->WindPart->children(JMX_EB);
0651: foreach ($node->WindSpeed as $val) {
0652: if (($val->attributes()['condition'] == '中心付近') && ($val->attributes()['unit'] == 'm/s')) {
0653: $items[$num][$cnt]['centerWindSpeed'] = (int)$val;
0654: } else if (($val->attributes()['type'] == '最大瞬間風速') && ($val->attributes()['unit'] == 'm/s')) {
0655: $items[$num][$cnt]['maxWindSpeed'] = (int)$val;
0656: }
0657: }
0658: }
0659: }
0660: }
0661: }
0662: }
0663: }
0664: //その台風の予報円は取得済み
0665: if ($flag_f) {
0666: $flag_forecast[$num] = TRUE;
0667: }
0668: }
0669: //オブジェクト解放
0670: $pcc = NULL;
0671:
0672: return TRUE;
0673: }

XMLファイルを読み込んだら、まず、実況情報か予報情報かを識別する。
予報情報の場合、ユーザー関数 isNewTyphoon により、台風の番号を参照し、まだ登録されていない情報であれば、最新の実況情報として配列に代入する。登録済みの情報であれば、過去の台風情報として配列に代入する。過去の情報は、台風の過去の進路としてマッピングするときに参照する。
なお、現在日時から実況日時を減じ、定数 SCRAP_TIME を超えていたら、古い台風情報として、配列には記録するものの、要素 ValidにFALSEを代入し、古い台風情報であることを明示する。

暴風域・強風域、予報円については、半径の値が複数存在する。たとえば、強風域が北東240km、南西200kmとなっていたら、台風の中心から北東へズレたところに強風域の中心がある。

前述の通り、VPTW60 の発表日時によっては予報円情報を含まないことがある。
そこで、台風毎に配列 $flag_forecast に予報円を取得したかどうかのフラグを持たせる。もし isNewTyphoon で取得した最新の VPTW60 に予報円情報が無ければ、過去の台風情報から予報円情報を取得する。
このため、ほぼ同じ予報取得プロセスが2箇所に書く格好になっており、美しさに欠けてしまった。
解説:台風情報を描くスクリプトを生成
0720: /**
0721: * 暴風域、強風域、予報円の描画スクリプトを生成する
0722: * @param object $pgc pahooGeoCodeオブジェクト
0723: * @param array $infos 台風情報
0724: * @return string スクリプト/FALSE:生成失敗
0725: */
0726: function jsTyphoonMap($pgc, $infos) {
0727: $js = '';
0728: foreach ($infos as $info) {
0729: $key = 0;
0730: //台風以外ならスキップ
0731: if (! isset($info[$key]['TyphoonClass'])) continue; //v.2.11
0732: if (preg_match('/台風/ui', $info[$key]['TyphoonClass']) == 0) continue;
0733: //古い台風情報ならスキップ
0734: if ($info['Valid'] == FALSE) continue;
0735:
0736: //暴風域
0737: list($latitude, $longitude, $radius) = shiftCircle($pgc, $info[$key]['latitude'], $info[$key]['longitude'], $info[$key]['暴風域']);
0738: $radius *= 1000;
0739: $js .= $pgc->jsCircle($longitude, $latitude, $radius, COLOR_WIND2, '1.0', 2, MAPSERVICE);
0740: //強風域
0741: list($latitude, $longitude, $radius) = shiftCircle($pgc, $info[$key]['latitude'], $info[$key]['longitude'], $info[$key]['強風域']);
0742: $radius *= 1000;
0743: $js .= $pgc->jsCircle($longitude, $latitude, $radius, COLOR_WIND1, '1.0', 2, MAPSERVICE);
0744:
0745: $key = 1;
0746: $cnt = 1;
0747: $lng_0 = $points[0]['longitude'] = $info[0]['longitude'];
0748: $lat_0 = $points[0]['latitude'] = $info[0]['latitude'];
0749: while (isset($info[$key])) {
0750: //予報円
0751: if ($info[$key]['kind'] == '予報') {
0752: //予報円を間引くかどうか
0753: $dd = $pgc->greatCircleDistance($lng_0, $lat_0, $info[$key]['longitude'], $info[$key]['latitude']);
0754: if ($dd > THIN_OUT) {
0755: list($latitude, $longitude, $radius) = shiftCircle($pgc, $info[$key]['latitude'], $info[$key]['longitude'], $info[$key]['予報円']);
0756: $radius *= 1000;
0757: $js .= $pgc->jsCircle($longitude, $latitude, $radius, COLOR_FORECAST, '1.0', 2, MAPSERVICE);
0758: preg_match('/[0-9]{4}\-[0-9]{2}\-([0-9]{2})T([0-9]{2})/ui', $info[$key]['DateTime'], $arr);
0759: $dt = sprintf('%d日%d時', $arr[1], $arr[2]);
0760: list($lat, $lng) = $pgc->getPointDistance($longitude, $latitude, 0 - $radius - 30000, 9);
0761: $js .= $pgc->jsLabel($lat, $lng, $dt, 12, COLOR_FORECAST, 'normal', MAPSERVICE);
0762: $lat_0 = $info[$key]['latitude'];
0763: $lng_0 = $info[$key]['longitude'];
0764: }
0765:
0766: //過去の位置
0767: } else {
0768: $points[$cnt]['longitude'] = $info[$key]['longitude'];
0769: $points[$cnt]['latitude'] = $info[$key]['latitude'];
0770: $cnt++;
0771: }
0772: $key++;
0773: }
0774: //過去の移動経路
0775: if ($js != '') {
0776: $js .= $pgc->jsLine($points, COLOR_LINE, '1.0', 3, MAPSERVICE);
0777: $points = array();
0778: $cnt = 0;
0779: if (isset($info[$key]['longitude'])) {
0780: $points[0]['longitude'] = $info[$key]['longitude'];
0781: $points[0]['latitude'] = $info[$key]['latitude'];
0782: }
0783: }
0784: }
0785:
0786: //HTMLの画像化
0787: $js .= js_html2image();
0788:
0789: return $js;
0790: }
前述の通り、円の中心がズレている場合があり、そのためのユーザー関数 shiftCircle を呼び出して使う。

前回の予報円の中心座標 ($lat_0, $lng_0) (初回は現在の台風の中心座標)からの大圏航路距離をメソッド greatCircleDistance によって計算し、予報円の間引き条件 THIN_OUT 以下であれば予報円を描かない。
解説:マップ描画用情報を生成
0792: /**
0793: * 台風情報からマップ描画用情報を生成する
0794: * @param array $infos 台風情報
0795: * @param array $items マップ描画用情報を格納
0796: * @param string $table HTML文(表形式)を格納
0797: * @param int $count 有効な台風情報の数
0798: * @return array(日時,緯度,経度) 発表日時,予報円の最後の中心座標
0799: */
0800: function getTyphoonInfo($infos, &$items, &$table, &$count) {
0801: //台風情報一覧
0802: $table =<<< EOD
0803: <table class="plists">
0804: <th>名称</th>
0805: <th>位置</th>
0806: <th>中心気圧</th>
0807: <th>最大瞬間風速</th>
0808: <th>進路</th>
0809: </tr>
0810:
0811: EOD;
0812:
0813: $dt0 = $lat0 = $lng0 = FALSE;
0814: $cnt = 1;
0815: foreach ($infos as $nn=>$info) { //v.2.11
0816: $key = 0; //v.2.11
0817: if (! isset($info[$key]['TyphoonClass'])) continue; //v.2.11
0818: if (preg_match('/台風/ui', $info[0]['TyphoonClass']) > 0) {
0819: $num = sprintf('台%d', (int)substr($nn, 2, 2)); //v.2.11
0820: $name = sprintf('台風%d号(%s)', (int)substr($nn, 2, 2), $info['NameKana']);
0821: } else {
0822: continue;
0823: }
0824: //古い台風情報ならスキップ
0825: if ($info['Valid'] == FALSE) continue;
0826:
0827: $items[$cnt]['longitude'] = $info[0]['longitude'];
0828: $items[$cnt]['latitude'] = $info[0]['latitude'];
0829: if ($cnt == 1) {
0830: preg_match('/([0-9]{4})\-([0-9]{2})\-([0-9]{2})T([0-9]{2})\:/ui', $info[0]['DateTime'], $arr);
0831: $dt0 = $info[0]['DateTime'] = sprintf('%d年%d月%d日%d時', $arr[1], $arr[2], $arr[3], $arr[4]);
0832: }
0833: $items[$cnt]['label'] = $num;
0834: $items[$cnt]['label_color'] = COLOR_NAME1;
0835: $items[$cnt]['label_size'] = 16;
0836: $items[$cnt]['label_weight'] = 'bold';
0837: //情報ウィンドウ
0838: if ($info[0]['AreaClass'] != '') {
0839: $AreaClass = $info[0]['AreaClass'];
0840: } else {
0841: $AreaClass = '-';
0842: }
0843: if ($info[0]['IntensityClass'] != '') {
0844: $IntensityClass = $info[0]['IntensityClass'];
0845: } else {
0846: $IntensityClass = '-';
0847: }
0848: if ($items[$cnt]['longitude'] >=0) {
0849: $lng = sprintf('東経%.1f度', $items[$cnt]['longitude']);
0850: } else {
0851: $lng = sprintf('西経%.1f度', 0 - $items[$cnt]['longitude']);
0852: }
0853: if ($items[$cnt]['latitude'] >=0) {
0854: $lat = sprintf('北緯%.1f度', $items[$cnt]['latitude']);
0855: } else {
0856: $lat = sprintf('南緯%.1f度', 0 - $items[$cnt]['latitude']);
0857: }
0858: if ($info[0]['Direction'] != '') {
0859: $directioin = $info[0]['Direction'] . 'へ';
0860: } else {
0861: $directioin = '';
0862: }
0863: if (is_numeric($info[0]['Speed'])) {
0864: $speed = sprintf('時速%dkm', $info[0]['Speed']);
0865: } else {
0866: $speed = $info[0]['Speed'];
0867: }
0868: if (isset($info[0]['centerWindSpeed']) && is_numeric($info[0]['centerWindSpeed'])) {
0869: $centerWindSpeed = $info[0]['centerWindSpeed'] . 'メートル';
0870: } else {
0871: $centerWindSpeed = '-';
0872: }
0873: if (isset($info[0]['maxWindSpeed']) && is_numeric($info[0]['maxWindSpeed'])) {
0874: $maxWindSpeed = $info[0]['maxWindSpeed'] . 'メートル';
0875: } else {
0876: $maxWindSpeed = '-';
0877: }
0878: $items[$cnt]['title'] = '';
0879: $items[$cnt]['description'] =<<< EOD
0880: <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 />
0881: EOD;
0882: //台風情報一覧
0883: $table .=<<< EOD
0884: <tr>
0885: <td>{$name}</td>
0886: <td>{$info[0]['Location']}</td>
0887: <td>{$info[0]['Pressure']}hPa</td>
0888: <td>{$maxWindSpeed}</td>
0889: <td>{$directioin}{$speed}</td>
0890: </tr>
0891:
0892: EOD;
0893: //3日以内の予報円の中心座標を求める v.2.01
0894: $m = 1;
0895: while (isset($info[$m])) {
0896: if ($info[$m]['kind'] == '予報') {
0897: $lat0 = $info[$m]['latitude'];
0898: $lng0 = $info[$m]['longitude'];
0899: if ($m >= 3) break;
0900: }
0901: $m++;
0902: }
0903: $cnt++;
0904: }
0905:
0906: $table .=<<< EOD
0907: </table>
0908:
0909: EOD;
0910: //有効な台風情報の数
0911: $count = $cnt - 1;
0912:
0913: return array($dt0, $lat0, $lng0);
0914: }
解説:メイン・プログラム
1054: //気象庁防災情報XMLから台風情報を取得
1055: $infos = array();
1056: $urls = array();
1057: $items = array();
1058: $dt = $date->format('Y年m月d日H時');
1059: $table = $errmsg = '';
1060: $count = 0;
1061: $js = FALSE;
1062: $ret = getTyphoon($pgc, $infos, $urls, $errmsg);
1063: if ($ret == FALSE) {
1064: $errmsg = $pgc->getError();
1065: } else {
1066: $js = jsTyphoonMap($pgc, $infos, $errmsg);
1067: list($dt, $latitude, $longitude) = getTyphoonInfo($infos, $items, $table, $count);
1068: if ($count == 0) {
1069: $longitude = DEF_LONGITUDE;
1070: $latitude = DEF_LATITUDE;
1071: $zoom = DEF_ZOOM;
1072: $type = DEF_TYPE;
1073: $dt = $date->format('Y年m月d日H時');
1074: }
1075: }
1076:
1077: //マップ作成
1078: if (($errmsg == '') && ($js != FALSE)) {
1079: $jsmap = $pgc->drawJSMap(MAPID, $latitude, $longitude, $type, $zoom, NULL, $items, MAPSERVICE, $js, (MAP_WIDTH / 2));
1080: } else {
1081: $jsmap = $pgc->drawJSMap(MAPID, $latitude, $longitude, $type, $zoom, NULL, $items, MAPSERVICE);
1082: }
1083:
1084: //ツイート機能
1085: $message =<<< EOT
1086: 🌀台風情報 {$dt}現在
1087:
1088: (ご参考)PHPで台風情報を取得する https://www.pahoo.org/e-soul/webtech/php05/php05-15-01.shtm #台風 #台風情報 #nhk #ntv #tbs #fujitv #tvasahi
1089:
1090: EOT;
1091: mediaTweet($message, $res);
1092:
1093: //HTML BODY作成
1094: $HtmlBody = makeCommonBody($dt, $jsmap, $table, $urls, $res, $errmsg, $count);
1095:
1096: //オブジェクト解放
1097: $pgc = NULL;
1098: $date = NULL;
1099:
1100: //ブラウザ表示処理
1101: echo $HtmlHeader;
1102: echo $HtmlBody;
1103: echo $HtmlFooter;
ここまででエラーがなければ、マップを生成する。
エラーがあれば、台風情報を描かずにマップのみ生成する。
解説:ツイート機能
FALSE なら、pahooTwitterAPI クラスを読み込まず、ツイート・ボタンも表示しない。ツイート・ボタンの作成については、「HTMLとCSSでさまざまなアイコンを表示する」を参照してほしい。
画像化したいオブジェクト(ID名)は定数 TARGET で指定する。
解説:html2canvasライブラリ
0122: <script src="https://html2canvas.hertzen.com/dist/html2canvas.js"></script>
0123:
0124: <style>
0125: p.werror {
0126: color: red;
0127: }
画像化を実行するJavaScript関数は html2canvas である。
1009: <div id="{$target}" name="{$target}" style="width:{$width2}px;">
1010: <p>
1011: 🌀台風情報 {$dt}現在{$res2}
1012: {$tweet}
1013: </p>
1014: <div id="{$mapid}" style="width:{$width}px; height:{$height}px;"></div>
1015: {$table}
1016: </div>
レンダリングエンジンによって違うのかもしれないが、html2canvas ライブラリによって画像化される範囲が実際よりやや小さいため、あえてマップ領域より、幅を20ピクセル大きくした範囲を画像化範囲としている。
0269: /**
0270: * HTMLオブジェクトの画像化
0271: * @param なし
0272: * @return string JavaScriptコード
0273: */
0274: function js_html2image() {
0275: $target = TARGET;
0276: $js = '';
0277:
0278: //Googleマップの場合
0279: if (MAPSERVICE == 0) {
0280: $js .=<<< EOT
0281: google.maps.event.addListener(map, 'tilesloaded', function() {
0282: var capture = document.querySelector('#{$target}');
0283: html2canvas(capture, {useCORS: true, allowTaint:true}).then(canvas => {
0284: var base64 = canvas.toDataURL('image/png'); //画像化
0285: $('#base64').val(base64);
0286: });
0287: });
0288:
0289: EOT;
0290:
0291: //Leafletの場合(ブラウザによってはうまく動作しない)
0292: } else {
0293: $js .=<<< EOT
0294: HTMLCanvasElement.prototype.getContext = function(origFn) {
0295: return function(type, attribs) {
0296: attribs = attribs || {};
0297: attribs.preserveDrawingBuffer = true;
0298: return origFn.call(this, type, attribs);
0299: };
0300: } (HTMLCanvasElement.prototype.getContext);
0301:
0302: //HTML画像化イベント登録
0303: function html2image() {
0304: var capture = document.querySelector('#{$target}');
0305: html2canvas(capture, {useCORS: true, allowTaint:true}).then(canvas => {
0306: var base64 = canvas.toDataURL('image/png'); //画像化
0307: $('#base64').val(base64);
0308: });
0309: };
0310:
0311: //ズーム変更イベント
0312: map.on('zoomend', function() {
0313: html2image();
0314: });
0315:
0316: //マップ移動イベント
0317: map.on('moveend', function() {
0318: html2image();
0319: });
0320:
0321: //ズーム変更イベント発生
0322: var zoom = map.getZoom();
0323: map.setZoom(zoom);
0324:
0325: EOT;
0326: }
0327:
0328: return $js;
0329: }
Leafletの場合、これに相当するイベントがないため、ズーム変更完了イベントにフックし、強制的にズーム変更イベントを発生させるタイミングで画像化する。しかし、この方法だとブラウザによって、マップ画像が無い状態で画像化されてしまうことがあるようだ。もし対策方法をご存じの方がいたらお知らせいただきたい。
これらをJavaScriptとして生成するユーザー関数が js_html2image である。
準備:pahooTwitterAPI クラス
0015: class pahooTwitterAPI {
0016: var $webapi; //直前に呼び出したWebAPI URL
0017: var $error; //エラーフラグ
0018: var $errmsg; //エラーメッセージ
0019: var $errcode; //エラーコード
0020: var $responses; //直前の結果(配列)
0021:
0022: //OAuth用パラメータ
0023: // https://apps.twitter.com/
0024: var $TWTR_CONSUMER_KEY = '***************'; //Cunsumer key
0025: var $TWTR_CONSUMER_SECRET = '***************'; //Consumer secret
0026: var $TWTR_ACCESS_KEY = '***************'; //Access Token (oauth_token)
0027: var $TWTR_ACCESS_SECRET = '***************'; //Access Token Secret (oauth_token_secret)
解説:メディア付き投稿(RAWデータ)
0385: /**
0386: * メディア付き投稿(RAWデータ)
0387: * @param string $message 投稿メッセージ(UTF-8限定)
0388: * @param array $raws メディアデータ(RAWデータ配列)
0389: * @return bool TRUE:リクエスト成功/FALSE:失敗
0390: */
0391: function tweet_media_raw($message, $raws) {
0392: static $url_upload = 'https://upload.twitter.com/1.1/media/upload.json';
0393: static $url_tweet = 'https://api.twitter.com/1.1/statuses/update.json';
0394: static $method = 'POST' ;
0395:
0396: //メディアのアップロード
0397: $media_ids = '';
0398: $cnt = 0;
0399: foreach ($raws as $raw) {
0400: $media_id = $this->upload($url_upload, 'POST', 'media', $raw);
0401: if ($media_id == NULL) break;
0402: if ($cnt > 0) $media_ids .= ',';
0403: $media_ids .= $media_id;
0404: $cnt++;
0405: if ($cnt > 3) break; //最大4つまで
0406: }
0407:
0408: //ツイート
0409: if (! $this->error) {
0410: $option = array('status' => $message, 'media_ids' => $media_ids);
0411: $res = $this->request_user($url_tweet, $method, $option);
0412: } else {
0413: $res = FALSE;
0414: }
0415:
0416: return $res;
0417: }
解説:ツイート処理
0244: /**
0245: * ツイート処理
0246: * @param string $message 投稿文
0247: * @param string $res 応答メッセージ格納用
0248: * @return bool TRUE:成功/FALSE:失敗または未処理
0249: */
0250: function mediaTweet($message, &$res) {
0251: if (! TWITTER) return FALSE;
0252:
0253: $ret = TRUE;
0254: if (isset($_POST['base64']) && ($_POST['base64'] != '')) {
0255: $base64 = preg_replace('/data\:image\/png\;base64\,/ui', '', $_POST['base64']);
0256: $raws = array(base64_decode($base64));
0257: $ptw = new pahooTwitterAPI();
0258: $ptw->tweet_media_raw($message, $raws);
0259: $errmsg = $ptw->errmsg;
0260: $ret = ! $ptw->error;
0261: $ptw = NULL;
0262: if ($ret) {
0263: $res = 'ツイートしました';
0264: }
0265: }
0266: return $ret;
0267: }
ブラウザからPOSTで受け取った画像データ $_POST['base64'&$x5D; はBASE64でデコードされており、冒頭に余計なヘッダ情報が付いているので、このヘッダ情報を除き( preg_replace )、バイナリデータにデコードする( base64_decode )。
続いて pahooTwitterAPI クラスを呼び出し、tweet_media_raw メソッドを使って、メッセージと画像を一気にツイートする。
1084: //ツイート機能
1085: $message =<<< EOT
1086: 🌀台風情報 {$dt}現在
1087:
1088: (ご参考)PHPで台風情報を取得する https://www.pahoo.org/e-soul/webtech/php05/php05-15-01.shtm #台風 #台風情報 #nhk #ntv #tbs #fujitv #tvasahi
1089:
1090: EOT;
1091: mediaTweet($message, $res);
活用例
参考サイト
- 台風情報:気象庁
- JavaScriptでHTML表示を画像保存する:ぱふぅ家のホームページ
- HTMLとCSSでさまざまなアイコンを表示する:ぱふぅ家のホームページ
- Twitter API - WebAPIの登録方法:ぱふぅ家のホームページ
- PHPでTwitterに画像付きメッセージ投稿:ぱふぅ家のホームページ
- 日本付近の台風情報 - 気象庁発表:みんなの知識 ちょっと便利帳
そこで、気象庁の台風情報のサイトから、現在日本に接近している台風の情報を取得するPHPスクリプトを作成してみることにする。
2021年(令和3年)2月24日の気象庁サイト・リニューアルにより、スクレイピングによる取り出しが難しくなったため、気象庁防災情報XMLからの情報取得に変更した。あわせてキャッシュ・システムを導入した。
(2022年3月12日)気象庁防災情報XMLのhttps化に対応。キャッシュディレクトリ(定数 DIR_CACHE で指定するもの)は、ディレクトリごと消去してから新しプログラムを起動すること。