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

目次
サンプル・プログラム
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 クラス
37: class pahooGeoCode {
38: var $items; //検索結果格納用
39: var $error; //エラー・フラグ
40: var $errmsg; //エラー・メッセージ
41: var $hits; //検索ヒット件数
42: var $webapi; //直前に呼び出したWebAPI URL
43:
44: //Google Cloud Platform APIキー
45: //https://cloud.google.com/maps-platform/
46: //※Google Maps APIを利用しないのなら登録不要
47: var $GOOGLE_API_KEY_1 = '**************************'; //HTTPリファラ用
48: var $GOOGLE_API_KEY_2 = '**************************'; //IP制限用
49:
50: //Yahoo! JAPAN Webサービス アプリケーションID
51: //https://e.developer.yahoo.co.jp/register
52: //※Yahoo! JAPAN Webサービスを利用しないのなら登録不要
53: var $YAHOO_APPLICATION_ID = '*****************************';
クラスについては「PHPでクラスを使ってテキストの読みやすさを調べる」を参照されたい。

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


準備:キャッシュ・システム
92: //キャッシュ保持時間(分) 0:キャッシュしない
93: //気象庁へのアクセス負荷軽減+台風の進路プロット
94: define('LIFE_CACHE_FEED', 15); //高頻度 - 随時フィードに対して
95: define('LIFE_CACHE_FEED_L', 120); //長期 - 随時フィードに対して
96: define('LIFE_CACHE_DATA', (60 * 24 * 14)); //台風情報の保持時間(進路プロット)
97:
98: //キャッシュ・ディレクトリ
99: //書き込み可能で,外部からアクセスされないディレクトリを指定してください.
100: define('DIR_CACHE_FEED', './pcache_typhoon1/');
101: define('DIR_CACHE_FEED_L', './pcache_typhoon2/');
102: define('DIR_CACHE_DATA', './pcache_typhoon3/');
キャッシュ保持時間、キャッシュ・ディレクトリともに、自サイトの環境に応じて変更してほしい。
LIFE_CACHE_DATA は、過去の台風の進路をプロットできるように、初期値では14日間を指定してある。
気象庁防災情報XMLフォーマット
今回は、高頻度 - 随時フィード および 長期 - 随時フィード にアクセスし、電文コード VPTW60 の台風解析・予報情報(5日予報)を取得する。
VPTW60の構造
注意すべきは、1つのXMLファイルの中に1つまたは複数の台風情報が含まれていること。
また、過去の中心位置は分からないので、キャッシュ・ファイルとして保存した VZSA50 から中心位置を拾い出し、それを台風の過去進路にする方針である。
また、VPTW60 の発表時刻によっては予報円の情報が無い。その場合、予報円情報が存在する最も新しい VPTW60 から予報円情報を取り出すことにする。
準備:マップの表示サイズなど
67: //予報円を間引く条件(km):次の予報円がこれより近ければ描画しない
68: define('THIN_OUT', 50);
69:
70: //マップの表示サイズ(単位:ピクセル)
71: define('MAP_WIDTH', 600);
72: define('MAP_HEIGHT', 480);
73: //マップID
74: define('MAPID', 'map_id');
75: //マップ座標
76: define('DEF_LATITUDE', 35.0); //緯度
77: define('DEF_LONGITUDE', 137.0); //経度
78: define('DEF_TYPE', 'GSISTD'); //マップタイプ
79: define('DEF_ZOOM', 6); //ズーム
80: //マップ描画色
81: define('COLOR_NAME1', '#FF8800'); //台風名称の描画色
82: define('COLOR_NAME2', '#0000FF'); //熱低名称の描画色
83: define('COLOR_LINE', '#0000FF'); //過去経路の描画色
84: define('COLOR_WIND1', '#FFFF00'); //強風域の描画色
85: define('COLOR_WIND2', '#FF0000'); //暴風域の描画色
86: define('COLOR_FORECAST', '#FFFFFF'); //予報円の描画色

予報円を間引く条件 THIN_OUT は、台風が停滞していたり、移動速度が極めてゆっくりの場合、予報円の密度が高すぎて地図が見にくくなる。そこで、前回予報円がこの距離以下であれば次の予報円を描かない(間引く)ことができる。0を代入すれば、すべての予報円を描くようになる。
解説:台風に関する情報URLを取得
342: /**
343: * 気象庁防災情報XMLから台風に関する情報URLを取得
344: * @param array $urls URL格納配列
345: * @param string $errmsg エラーメッセージ格納用
346: * @return bool TRUE:取得成功/FALSE:取得失敗
347: */
348: function jmaGetTyphoonURLs(&$urls, &$errmsg) {
349: //URLパターン
350: $vptw = '/https?\:\/\/www\.data\.jma\.go\.jp\/developer\/xml\/data\/([0-9\_]+)VPTW6[0-9]+\_[0-9]+\.xml/ui';
351:
352: //随時フィードの解析
353: $cnt = 0;
354: $pcc = new pahooCache(LIFE_CACHE_FEED, DIR_CACHE_FEED);
355: $xml = $pcc->simplexml_load(FEED);
356: //レスポンス・チェック
357: if ($pcc->iserror() || !isset($xml->entry)) {
358: $errmsg = '気象庁防災情報XMLにアクセスできません';
359: return FALSE;
360: }
361: foreach ($xml->entry as $node) {
362: //URLを取得
363: if (preg_match($vptw, $node->id, $arr) > 0) {
364: $urls[$cnt] = $arr[0];
365: $cnt++;
366: }
367: }
368: $pcc = NULL;
369:
370: //長期フィードの解析
371: $pcc = new pahooCache(LIFE_CACHE_FEED_L, DIR_CACHE_FEED_L);
372: $xml = $pcc->simplexml_load(FEED_L);
373: //レスポンス・チェック
374: if ($pcc->iserror() || !isset($xml->entry)) {
375: $errmsg = '気象庁防災情報XMLにアクセスできません';
376: return FALSE;
377: }
378: foreach ($xml->entry as $node) {
379: //URLを取得
380: if (preg_match($vptw, $node->id, $arr) > 0) {
381: if (array_search($arr[0], $urls) === FALSE) {
382: $urls[$cnt] = $arr[0];
383: $cnt++;
384: }
385: }
386: }
387: $pcc = NULL;
388:
389: //エラー・チェック
390: if ($cnt == 0) {
391: $errmsg = '直近の台風情報はありません';
392: return FALSE;
393: }
394:
395: //URLを日時の新しい順にソート
396: rsort($urls);
397:
398: return TRUE;
399: }
正規表現で VPTW60 を含むURLを取り出し配列に格納し、日時の新しい順に並べ替えておく。
解説:台風情報を読み込む
401: /**
402: * 新しい台風報かどうか
403: * @param object $xml 気象庁防災情報XML
404: * @param array $items 台風情報を格納する配列
405: * @return bool TRUE:新しい情報
406: */
407: function isNewTyphoon($xml, $items) {
408: $res = FALSE;
409:
410: if (isset($xml->Body->MeteorologicalInfos->MeteorologicalInfo->Item->Kind->Property->TyphoonNamePart->Number)) {
411: $num = (string)$xml->Body->MeteorologicalInfos->MeteorologicalInfo->Item->Kind->Property->TyphoonNamePart->Number;
412: if ($num != '') {
413: if (! isset($items[$num])) {
414: $res = TRUE;
415: }
416: }
417: }
418:
419: return $res;
420: }
422: /**
423: * 台風報取得(気象庁防災情報XMLから)
424: * @param object $pgc pahooGeoCodeオブジェクト
425: * @param array $items 台風情報を格納する配列
426: * @param string $urls 情報XMLのURLを格納する配列
427: * @param string $errmsg エラーメッセージ格納用
428: * @return bool TRUE:取得成功/FALSE:失敗
429: */
430: function getTyphoon($pgc, &$items, &$urls, &$errmsg) {
431: //名前空間
432: define('JMX_EB', 'http://xml.kishou.go.jp/jmaxml1/elementBasis1/');
433: //マッチングパターン
434: $pat2 = '/([\+\-][0-9]{1,2}\.[0-9]+)([\+\-][0-9]{1,3}\.[0-9]+)/ui'; //緯度・経度
435: $pat3 = '/(予報)[ ]*([01234567890-9]+)時間後/ui'; //推定|予報
436: //予報円は取得済みか否か
437: $flag_forecast = array();
438:
439: //オブジェクト生成
440: $pcc = new pahooCache(LIFE_CACHE_DATA, DIR_CACHE_DATA);
441:
442: //最新の台風に関する情報URLを取得
443: jmaGetTyphoonURLs($urls, $errmsg);
444: if ($errmsg != '') return FALSE;
445:
446: foreach ($urls as $key=>$vptw) {
447: //台風情報の取得
448: $xml = $pcc->simplexml_load($vptw);
449: //レスポンス・チェック
450: if ($pcc->iserror() || !isset($xml->Body->MeteorologicalInfos)) {
451: $errmsg = '気象庁防災情報XMLから台風情報を取得できません';
452: return FALSE;
453: }
454: $flag_f = FALSE; //予報円取得済みか否か
455:
456: //最新の台風情報
457: if (isNewTyphoon($xml, $items)) {
458: foreach ($xml->Body->MeteorologicalInfos as $infos) {
459: $cnt = 0;
460: foreach ($infos->MeteorologicalInfo as $info) {
461: //実況
462: if ($info->DateTime['type'] == '実況') {
463: foreach ($info->Item->Kind as $kind) {
464: if (isset($kind->Property->Type)) {
465: //呼称
466: if ($kind->Property->Type == '呼称') {
467: $num = (string)$kind->Property->TyphoonNamePart->Number;
468: if ($num == '') break;
469: $items[$num]['Name'] = (string)$kind->Property->TyphoonNamePart->Name;
470: $items[$num]['NameKana'] = (string)$kind->Property->TyphoonNamePart->NameKana;
471: $items[$num][$cnt]['kind'] = '実況';
472: $items[$num][$cnt]['DateTime'] = (string)$info->DateTime;
473: //その台風の予報円は未取得
474: $flag_forecast[$num] = FALSE;
475: //古い台風情報かどうか
476: $tt = time() - strtotime($info->DateTime);
477: $items[$num]['Valid'] = ($tt < SCRAP_TIME) ? TRUE : FALSE;
478: //階級
479: } else if ($kind->Property->Type == '階級') {
480: $node = $kind->Property->ClassPart->children(JMX_EB);
481: $items[$num][$cnt]['TyphoonClass'] = (string)$node->TyphoonClass;
482: $items[$num][$cnt]['AreaClass'] = (string)$node->AreaClass;
483: $items[$num][$cnt]['IntensityClass'] = (string)$node->IntensityClass;
484: //中心
485: } else if ($kind->Property->Type == '中心') {
486: $node = $kind->Property->CenterPart->children(JMX_EB);
487: //中心位置
488: $items[$num][$cnt]['Location'] = (string)$kind->Property->CenterPart->Location;
489: foreach ($node->Coordinate as $val) {
490: if (preg_match($pat2, $val, $arr) > 0) {
491: $items[$num][$cnt]['latitude'] = (float)$arr[1];
492: $items[$num][$cnt]['longitude'] = (float)$arr[2];
493: }
494: }
495: //移動速度
496: $items[$num][$cnt]['Direction'] = (string)$node->Direction;
497: foreach ($node->Speed as $val) {
498: if (isset($val->attributes()['condition'])) {
499: $items[$num][$cnt]['Speed'] = (string)$val->attributes()['condition'];
500: } else if ($val->attributes()['unit'] == 'km/h') {
501: $items[$num][$cnt]['Speed'] = (string)$val;
502: }
503: }
504: //中心気圧
505: $items[$num][$cnt]['Pressure'] = (int)$node->Pressure;
506: //風
507: } else if ($kind->Property->Type == '風') {
508: $node = $kind->Property->WindPart->children(JMX_EB);
509: foreach ($node->WindSpeed as $val) {
510: if (($val->attributes()['condition'] == '中心付近') && ($val->attributes()['unit'] == 'm/s')) {
511: $items[$num][$cnt]['centerWindSpeed'] = (int)$val;
512: } else if (($val->attributes()['type'] == '最大瞬間風速') && ($val->attributes()['unit'] == 'm/s')) {
513: $items[$num][$cnt]['maxWindSpeed'] = (int)$val;
514: }
515: }
516: //暴風域・強風域
517: foreach ($kind->Property->WarningAreaPart as $val) {
518: $key = (string)$val['type'];
519: //半径
520: $node = $val->children(JMX_EB);
521: $n = 0;
522: foreach ($node->Circle->Axes->Axis as $axis) {
523: if (isset($axis->Direction) && ($axis->Direction != '')) {
524: $items[$num][$cnt][$key][$n]['direction'] = (string)$axis->Direction;
525: } else {
526: $items[$num][$cnt][$key][$n]['direction'] = (string)$axis->Direction->attributes()['condition'];
527: }
528: foreach ($axis->Radius as $val) {
529: if ($val->attributes()['unit'] == 'km') {
530: $items[$num][$cnt][$key][$n]['radius'] = (int)$val;
531: }
532: }
533: $n++;
534: }
535: }
536: }
537: }
538: }
539: //予報
540: } else if (preg_match($pat3, (string)$info->DateTime['type'], $arr) > 0) {
541: if ($num == '') break; //ver.2.04
542: $cnt++;
543: $items[$num][$cnt]['kind'] = '予報';
544: $items[$num][$cnt]['DateTime'] = (string)$info->DateTime;
545: $flag_f = TRUE; //予報円取得済み
546: foreach ($info->Item->Kind as $kind) {
547: //階級
548: if ($kind->Property->Type == '階級') {
549: $node = $kind->Property->ClassPart->children(JMX_EB);
550: $items[$num][$cnt]['TyphoonClass'] = (string)$node->TyphoonClass;
551: $items[$num][$cnt]['AreaClass'] = isset($node->AreaClass) ? (string)$node->AreaClass : '';
552: $items[$num][$cnt]['IntensityClass'] = isset($node->IntensityClass) ? (string)$node->IntensityClass : '';
553: //中心位置(予報円)
554: } else if ($kind->Property->Type == '中心') {
555: $node = $kind->Property->CenterPart->ProbabilityCircle->children(JMX_EB);
556: foreach ($node->BasePoint as $val) {
557: if (preg_match($pat2, $val, $arr) > 0) {
558: $items[$num][$cnt]['latitude'] = (float)$arr[1];
559: $items[$num][$cnt]['longitude'] = (float)$arr[2];
560: }
561: }
562: //半径
563: $n = 0;
564: foreach ($node->Axes->Axis as $axis) {
565: if (isset($axis->Direction) && ($axis->Direction != '')) {
566: $items[$num][$cnt]['予報円'][$n]['direction'] = (string)$axis->Direction;
567: } else {
568: $items[$num][$cnt]['予報円'][$n]['direction'] = (string)$axis->Direction->attributes()['condition'];
569: }
570: foreach ($axis->Radius as $val) {
571: if ($val->attributes()['unit'] == 'km') {
572: $items[$num][$cnt]['予報円'][$n]['radius'] = (int)$val;
573: }
574: }
575: $n++;
576: }
577: //風
578: } else if ($kind->Property->Type == '風') {
579: $node = $kind->Property->WindPart->children(JMX_EB);
580: foreach ($node->WindSpeed as $val) {
581: if (($val->attributes()['condition'] == '中心付近') && ($val->attributes()['unit'] == 'm/s')) {
582: $items[$num][$cnt]['centerWindSpeed'] = (int)$val;
583: } else if (($val->attributes()['type'] == '最大瞬間風速') && ($val->attributes()['unit'] == 'm/s')) {
584: $items[$num][$cnt]['maxWindSpeed'] = (int)$val;
585: }
586: }
587: }
588: }
589: }
590: }
591: }
592: //過去の台風情報
593: } else {
594: foreach ($xml->Body->MeteorologicalInfos as $infos) {
595: foreach ($infos->MeteorologicalInfo as $info) {
596: //過去の位置
597: if ($info->DateTime['type'] == '実況') {
598: foreach ($info->Item->Kind as $kind) {
599: if (isset($kind->Property->Type)) {
600: //呼称
601: if ($kind->Property->Type == '呼称') {
602: $num = (string)$kind->Property->TyphoonNamePart->Number;
603: if ($num == '') break;
604: $cnt = 0;
605: while (isset($items[$num][$cnt])) $cnt++;
606: $items[$num][$cnt]['kind'] = '過去';
607: $items[$num][$cnt]['DateTime'] = (string)$info->DateTime;
608: //中心
609: } else if ($kind->Property->Type == '中心') {
610: $node = $kind->Property->CenterPart->children(JMX_EB);
611: //中心位置
612: foreach ($node->Coordinate as $val) {
613: if (preg_match($pat2, $val, $arr) > 0) {
614: $items[$num][$cnt]['latitude'] = (float)$arr[1];
615: $items[$num][$cnt]['longitude'] = (float)$arr[2];
616: }
617: }
618: }
619: }
620: }
621: //予報
622: } else if (isset($flag_forecast[$num]) && ! $flag_forecast[$num] && (preg_match($pat3, (string)$info->DateTime['type'], $arr) > 0)) {
623: if ($num == '') break; //ver.2.04
624: $cnt++;
625: $items[$num][$cnt]['kind'] = '予報';
626: $items[$num][$cnt]['DateTime'] = (string)$info->DateTime;
627: $flag_f = TRUE; //予報円取得済み
628: foreach ($info->Item->Kind as $kind) {
629: //階級
630: if ($kind->Property->Type == '階級') {
631: $node = $kind->Property->ClassPart->children(JMX_EB);
632: $items[$num][$cnt]['TyphoonClass'] = (string)$node->TyphoonClass;
633: $items[$num][$cnt]['AreaClass'] = isset($node->AreaClass) ? (string)$node->AreaClass : '';
634: $items[$num][$cnt]['IntensityClass'] = isset($node->IntensityClass) ? (string)$node->IntensityClass : '';
635: //中心位置(予報円)
636: } else if ($kind->Property->Type == '中心') {
637: $node = $kind->Property->CenterPart->ProbabilityCircle->children(JMX_EB);
638: foreach ($node->BasePoint as $val) {
639: if (preg_match($pat2, $val, $arr) > 0) {
640: $items[$num][$cnt]['latitude'] = (float)$arr[1];
641: $items[$num][$cnt]['longitude'] = (float)$arr[2];
642: }
643: }
644: //半径
645: $n = 0;
646: foreach ($node->Axes->Axis as $axis) {
647: if (isset($axis->Direction) && ($axis->Direction != '')) {
648: $items[$num][$cnt]['予報円'][$n]['direction'] = (string)$axis->Direction;
649: } else {
650: $items[$num][$cnt]['予報円'][$n]['direction'] = (string)$axis->Direction->attributes()['condition'];
651: }
652: foreach ($axis->Radius as $val) {
653: if ($val->attributes()['unit'] == 'km') {
654: $items[$num][$cnt]['予報円'][$n]['radius'] = (int)$val;
655: }
656: }
657: $n++;
658: }
659: //風
660: } else if ($kind->Property->Type == '風') {
661: $node = $kind->Property->WindPart->children(JMX_EB);
662: foreach ($node->WindSpeed as $val) {
663: if (($val->attributes()['condition'] == '中心付近') && ($val->attributes()['unit'] == 'm/s')) {
664: $items[$num][$cnt]['centerWindSpeed'] = (int)$val;
665: } else if (($val->attributes()['type'] == '最大瞬間風速') && ($val->attributes()['unit'] == 'm/s')) {
666: $items[$num][$cnt]['maxWindSpeed'] = (int)$val;
667: }
668: }
669: }
670: }
671: }
672: }
673: }
674: }
675: //その台風の予報円は取得済み
676: if ($flag_f) {
677: $flag_forecast[$num] = TRUE;
678: }
679: }
680: //オブジェクト解放
681: $pcc = NULL;
682:
683: return TRUE;
684: }

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

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

前述の通り、VPTW60 の発表日時によっては予報円情報を含まないことがある。
そこで、台風毎に配列 $flag_forecast に予報円を取得したかどうかのフラグを持たせる。もし isNewTyphoon で取得した最新の VPTW60 に予報円情報が無ければ、過去の台風情報から予報円情報を取得する。
このため、ほぼ同じ予報取得プロセスが2箇所に書く格好になっており、美しさに欠けてしまった。
解説:台風情報を描くスクリプトを生成
731: /**
732: * 暴風域、強風域、予報円の描画スクリプトを生成する
733: * @param object $pgc pahooGeoCodeオブジェクト
734: * @param array $infos 台風情報
735: * @return string スクリプト/FALSE:生成失敗
736: */
737: function jsTyphoonMap($pgc, $infos) {
738: $js = '';
739: foreach ($infos as $info) {
740: $key = 0;
741: //台風以外ならスキップ
742: if (! isset($info[$key]['TyphoonClass'])) continue; //v.2.11
743: if (preg_match('/台風/ui', $info[$key]['TyphoonClass']) == 0) continue;
744: //古い台風情報ならスキップ
745: if ($info['Valid'] == FALSE) continue;
746:
747: //暴風域
748: list($latitude, $longitude, $radius) = shiftCircle($pgc, $info[$key]['latitude'], $info[$key]['longitude'], $info[$key]['暴風域']);
749: $radius *= 1000;
750: $js .= $pgc->jsCircle($longitude, $latitude, $radius, COLOR_WIND2, '1.0', 2, MAPSERVICE);
751: //強風域
752: list($latitude, $longitude, $radius) = shiftCircle($pgc, $info[$key]['latitude'], $info[$key]['longitude'], $info[$key]['強風域']);
753: $radius *= 1000;
754: $js .= $pgc->jsCircle($longitude, $latitude, $radius, COLOR_WIND1, '1.0', 2, MAPSERVICE);
755:
756: $key = 1;
757: $cnt = 1;
758: $lng_0 = $points[0]['longitude'] = $info[0]['longitude'];
759: $lat_0 = $points[0]['latitude'] = $info[0]['latitude'];
760: while (isset($info[$key])) {
761: //予報円
762: if ($info[$key]['kind'] == '予報') {
763: //予報円を間引くかどうか
764: $dd = $pgc->greatCircleDistance($lng_0, $lat_0, $info[$key]['longitude'], $info[$key]['latitude']);
765: if ($dd > THIN_OUT) {
766: list($latitude, $longitude, $radius) = shiftCircle($pgc, $info[$key]['latitude'], $info[$key]['longitude'], $info[$key]['予報円']);
767: $radius *= 1000;
768: $js .= $pgc->jsCircle($longitude, $latitude, $radius, COLOR_FORECAST, '1.0', 2, MAPSERVICE);
769: preg_match('/[0-9]{4}\-[0-9]{2}\-([0-9]{2})T([0-9]{2})/ui', $info[$key]['DateTime'], $arr);
770: $dt = sprintf('%d日%d時', $arr[1], $arr[2]);
771: list($lat, $lng) = $pgc->getPointDistance($longitude, $latitude, 0 - $radius - 30000, 9);
772: $js .= $pgc->jsLabel($lat, $lng, $dt, 12, COLOR_FORECAST, 'normal', MAPSERVICE);
773: $lat_0 = $info[$key]['latitude'];
774: $lng_0 = $info[$key]['longitude'];
775: }
776:
777: //過去の位置
778: } else {
779: $points[$cnt]['longitude'] = $info[$key]['longitude'];
780: $points[$cnt]['latitude'] = $info[$key]['latitude'];
781: $cnt++;
782: }
783: $key++;
784: }
785: //過去の移動経路
786: if ($js != '') {
787: $js .= $pgc->jsLine($points, COLOR_LINE, '1.0', 3, MAPSERVICE);
788: $points = array();
789: $cnt = 0;
790: if (isset($info[$key]['longitude'])) {
791: $points[0]['longitude'] = $info[$key]['longitude'];
792: $points[0]['latitude'] = $info[$key]['latitude'];
793: }
794: }
795: }
796:
797: //HTMLの画像化
798: $js .= js_html2image();
799:
800: return $js;
801: }
前述の通り、円の中心がズレている場合があり、そのためのユーザー関数 shiftCircle を呼び出して使う。

前回の予報円の中心座標 ($lat_0, $lng_0) (初回は現在の台風の中心座標)からの大圏航路距離をメソッド greatCircleDistance によって計算し、予報円の間引き条件 THIN_OUT 以下であれば予報円を描かない。
解説:マップ描画用情報を生成
803: /**
804: * 台風情報からマップ描画用情報を生成する
805: * @param array $infos 台風情報
806: * @param array $items マップ描画用情報を格納
807: * @param string $table HTML文(表形式)を格納
808: * @param int $count 有効な台風情報の数
809: * @param objct $pgc pahooGeoCodeオブジェクト
810: * @return array(日時,緯度,経度) 発表日時,予報円の最後の中心座標,地図ズーム
811: */
812: function getTyphoonInfo($infos, &$items, &$table, &$count, $pgc) {
813: //地図ズーム値=距離換算表
814: $zooms = array(10000, 5000, 3000, 1000, 500, 300);
815:
816: //台風情報一覧
817: $table =<<< EOT
818: <table class="plists">
819: <th>名称</th>
820: <th>位置</th>
821: <th>中心気圧</th>
822: <th>最大瞬間風速</th>
823: <th>進路</th>
824: </tr>
825:
826: EOT;
827:
828: $dt0 = $lat0 = $lng0 = $zoom = FALSE;
829: $distance = 99999;
830: $cnt = 1;
831: foreach ($infos as $nn=>$info) { //v.2.11
832: $key = 0; //v.2.11
833: if (! isset($info[$key]['TyphoonClass'])) continue; //v.2.11
834: if (preg_match('/台風/ui', $info[0]['TyphoonClass']) > 0) {
835: $num = sprintf('台%d', (int)substr($nn, 2, 2)); //v.2.11
836: $name = sprintf('台風%d号(%s)', (int)substr($nn, 2, 2), $info['NameKana']);
837: } else {
838: continue;
839: }
840: //古い台風情報ならスキップ
841: if ($info['Valid'] == FALSE) continue;
842:
843: $items[$cnt]['longitude'] = $info[0]['longitude'];
844: $items[$cnt]['latitude'] = $info[0]['latitude'];
845: if ($cnt == 1) {
846: preg_match('/([0-9]{4})\-([0-9]{2})\-([0-9]{2})T([0-9]{2})\:/ui', $info[0]['DateTime'], $arr);
847: $dt0 = $info[0]['DateTime'] = sprintf('%d年%d月%d日%d時', $arr[1], $arr[2], $arr[3], $arr[4]);
848: }
849: $items[$cnt]['label'] = $num;
850: $items[$cnt]['label_color'] = COLOR_NAME1;
851: $items[$cnt]['label_size'] = 16;
852: $items[$cnt]['label_weight'] = 'bold';
853: //情報ウィンドウ
854: if ($info[0]['AreaClass'] != '') {
855: $AreaClass = $info[0]['AreaClass'];
856: } else {
857: $AreaClass = '-';
858: }
859: if ($info[0]['IntensityClass'] != '') {
860: $IntensityClass = $info[0]['IntensityClass'];
861: } else {
862: $IntensityClass = '-';
863: }
864: if ($items[$cnt]['longitude'] >= 0) {
865: $lng = sprintf('東経%.1f度', $items[$cnt]['longitude']);
866: } else {
867: $lng = sprintf('西経%.1f度', 0 - $items[$cnt]['longitude']);
868: }
869: if ($items[$cnt]['latitude'] >= 0) {
870: $lat = sprintf('北緯%.1f度', $items[$cnt]['latitude']);
871: } else {
872: $lat = sprintf('南緯%.1f度', 0 - $items[$cnt]['latitude']);
873: }
874: if ($info[0]['Direction'] != '') {
875: $directioin = $info[0]['Direction'] . 'へ';
876: } else {
877: $directioin = '';
878: }
879: if (is_numeric($info[0]['Speed'])) {
880: $speed = sprintf('時速%dkm', $info[0]['Speed']);
881: } else {
882: $speed = $info[0]['Speed'];
883: }
884: if (isset($info[0]['centerWindSpeed']) && is_numeric($info[0]['centerWindSpeed'])) {
885: $centerWindSpeed = $info[0]['centerWindSpeed'] . 'メートル';
886: } else {
887: $centerWindSpeed = '-';
888: }
889: if (isset($info[0]['maxWindSpeed']) && is_numeric($info[0]['maxWindSpeed'])) {
890: $maxWindSpeed = $info[0]['maxWindSpeed'] . 'メートル';
891: } else {
892: $maxWindSpeed = '-';
893: }
894: $items[$cnt]['title'] = '';
895: $items[$cnt]['description'] =<<< EOT
896: <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 />
897: EOT;
898: //台風情報一覧
899: $table .=<<< EOT
900: <tr>
901: <td>{$name}</td>
902: <td>{$info[0]['Location']}</td>
903: <td>{$info[0]['Pressure']}hPa</td>
904: <td>{$maxWindSpeed}</td>
905: <td>{$directioin}{$speed}</td>
906: </tr>
907:
908: EOT;
909: //3日以内の予報円の中心座標を求める v.2.01
910: $m = 1;
911: while (isset($info[$m])) {
912: if ($info[$m]['kind'] == '予報') {
913: $lat1 = $info[$m]['latitude'];
914: $lng1 = $info[$m]['longitude'];
915: if ($m >= 3) break;
916: }
917: $m++;
918: }
919: //基準座標に近い方をマップ中心にする
920: $dist = $pgc->greatCircleDistance($lng1, $lat1, LONGITUDE00, LATITUDE00);
921: if ($dist < $distance) {
922: $lat0 = $lat1;
923: $lng0 = $lng1;
924: $distance = $dist;
925: //台風中心と3日後予報円の距離から地図ズーム値を決める
926: $dist = $pgc->greatCircleDistance($lng0, $lat0, $info[0]['longitude'], $info[0]['latitude']);
927: $zoom = count($zooms) + 1;
928: foreach ($zooms as $key=>$val) {
929: if ($dist >= $val) {
930: $zoom = $key + 1;
931: break;
932: }
933: }
934: }
935:
936: $cnt++;
937: }
938:
939: $table .=<<< EOT
940: </table>
941:
942: EOT;
943: //有効な台風情報の数
944: $count = $cnt - 1;
945:
946: return array($dt0, $lat0, $lng0, $zoom);
947: }
解説:メイン・プログラム
1087: //気象庁防災情報XMLから台風情報を取得
1088: $infos = array();
1089: $urls = array();
1090: $items = array();
1091: $dt = $date->format('Y年m月d日H時');
1092: $table = $errmsg = '';
1093: $count = 0;
1094: $js = FALSE;
1095: $ret = getTyphoon($pgc, $infos, $urls, $errmsg);
1096: if ($ret == FALSE) {
1097: $errmsg = $pgc->getError();
1098: } else {
1099: $js = jsTyphoonMap($pgc, $infos, $errmsg);
1100: list($dt, $latitude, $longitude, $zoom) = getTyphoonInfo($infos, $items, $table, $count, $pgc);
1101: if ($count == 0) {
1102: $longitude = DEF_LONGITUDE;
1103: $latitude = DEF_LATITUDE;
1104: $zoom = DEF_ZOOM;
1105: $type = DEF_TYPE;
1106: $dt = $date->format('Y年m月d日H時');
1107: }
1108: }
1109:
1110: //マップ作成
1111: if (($errmsg == '') && ($js != FALSE)) {
1112: $jsmap = $pgc->drawJSMap(MAPID, $latitude, $longitude, $type, $zoom, NULL, $items, MAPSERVICE, $js, (MAP_WIDTH / 2));
1113: } else {
1114: $jsmap = $pgc->drawJSMap(MAPID, $latitude, $longitude, $type, $zoom, NULL, $items, MAPSERVICE);
1115: }
1116:
1117: //ツイート機能
1118: $message =<<< EOT
1119: 🌀台風情報 {$dt}現在
1120:
1121: (ご参考)PHPで台風情報を取得する https://www.pahoo.org/e-soul/webtech/php05/php05-15-01.shtm #台風 #台風情報 #nhk #ntv #tbs #fujitv #tvasahi
1122:
1123: EOT;
1124: mediaTweet($message, $res);
1125:
1126: //HTML BODY作成
1127: $HtmlBody = makeCommonBody($dt, $jsmap, $table, $urls, $res, $errmsg, $count);
1128:
1129: //オブジェクト解放
1130: $pgc = NULL;
1131: $date = NULL;
1132:
1133: //ブラウザ表示処理
1134: echo $HtmlHeader;
1135: echo $HtmlBody;
1136: echo $HtmlFooter;
ここまででエラーがなければ、マップを生成する。
エラーがあれば、台風情報を描かずにマップのみ生成する。
解説:ツイート機能
42: //ツイート・ボタン TRUE:有効,FALSE:無効
43: define('TWITTER', FALSE);
44:
45: //住所・緯度・経度に関わるクラス:include_pathが通ったディレクトリに配置
46: require_once('pahooGeoCode.php');
47:
48: //キャッシュ処理に関わるクラス:include_pathが通ったディレクトリに配置
49: require_once('pahooCache.php');
50:
51: //Twitterクラス:include_pathが通ったディレクトリに配置
52: if (TWITTER) {
53: require_once('pahooTwitterAPI.php');
54: }
55:
56: //画像化したいオブジェクト
57: define('TARGET', 'target');
FALSE なら、pahooTwitterAPI クラスを読み込まず、ツイート・ボタンも表示しない。ツイート・ボタンの作成については、「HTMLとCSSでさまざまなアイコンを表示する」を参照してほしい。
画像化したいオブジェクト(ID名)は定数 TARGET で指定する。
解説:html2canvasライブラリ
133: <script src="https://html2canvas.hertzen.com/dist/html2canvas.js"></script>
134:
135: <style>
136: p.werror {
137: color: red;
138: }
画像化を実行するJavaScript関数は html2canvas である。
1042: <div id="{$target}" name="{$target}" style="width:{$width2}px;">
1043: <p>
1044: 🌀台風情報 {$dt}現在{$res2}
1045: {$tweet}
1046: </p>
1047: <div id="{$mapid}" style="width:{$width}px; height:{$height}px;"></div>
1048: {$table}
1049: </div>
レンダリングエンジンによって違うのかもしれないが、html2canvas ライブラリによって画像化される範囲が実際よりやや小さいため、あえてマップ領域より、幅を20ピクセル大きくした範囲を画像化範囲としている。
280: /**
281: * HTMLオブジェクトの画像化
282: * @param なし
283: * @return string JavaScriptコード
284: */
285: function js_html2image() {
286: $target = TARGET;
287: $js = '';
288:
289: //Googleマップの場合
290: if (MAPSERVICE == 0) {
291: $js .=<<< EOT
292: google.maps.event.addListener(map, 'tilesloaded', function() {
293: var capture = document.querySelector('#{$target}');
294: html2canvas(capture, {useCORS: true, allowTaint:true}).then(canvas => {
295: var base64 = canvas.toDataURL('image/png'); //画像化
296: $('#base64').val(base64);
297: });
298: });
299:
300: EOT;
301:
302: //Leafletの場合(ブラウザによってはうまく動作しない)
303: } else {
304: $js .=<<< EOT
305: HTMLCanvasElement.prototype.getContext = function(origFn) {
306: return function(type, attribs) {
307: attribs = attribs || {};
308: attribs.preserveDrawingBuffer = true;
309: return origFn.call(this, type, attribs);
310: };
311: } (HTMLCanvasElement.prototype.getContext);
312:
313: //HTML画像化イベント登録
314: function html2image() {
315: var capture = document.querySelector('#{$target}');
316: html2canvas(capture, {useCORS: true, allowTaint:true}).then(canvas => {
317: var base64 = canvas.toDataURL('image/png'); //画像化
318: $('#base64').val(base64);
319: });
320: };
321:
322: //ズーム変更イベント
323: map.on('zoomend', function() {
324: html2image();
325: });
326:
327: //マップ移動イベント
328: map.on('moveend', function() {
329: html2image();
330: });
331:
332: //ズーム変更イベント発生
333: var zoom = map.getZoom();
334: map.setZoom(zoom);
335:
336: EOT;
337: }
338:
339: return $js;
340: }
Leafletの場合、これに相当するイベントがないため、ズーム変更完了イベントにフックし、強制的にズーム変更イベントを発生させるタイミングで画像化する。しかし、この方法だとブラウザによって、マップ画像が無い状態で画像化されてしまうことがあるようだ。もし対策方法をご存じの方がいたらお知らせいただきたい。
これらをJavaScriptとして生成するユーザー関数が js_html2image である。
準備:pahooTwitterAPI クラス
19: class pahooTwitterAPI {
20: var $responses; //応答データ
21: var $webapi; //直前に呼び出したWebAPI URL
22: var $error; //エラーフラグ
23: var $errmsg; //エラーメッセージ
24: var $errcode; //エラーコード
25: var $connection;
26:
27: //OAuth用パラメータ
28: // https://apps.twitter.com/
29: var $TWTR_CONSUMER_KEY = '***************'; //Cunsumer key
30: var $TWTR_CONSUMER_SECRET = '***************'; //Consumer secret
31: var $TWTR_ACCESS_KEY = '***************'; //Access Token (oauth_token)
32: var $TWTR_ACCESS_SECRET = '***************'; //Access Token Secret (oauth_token_secret)
解説:メディア付き投稿(RAWデータ)
584: /**
585: * バイナリデータを使ったメディア付きメッセージをツイートする.
586: * Tweetet API v2 を使用する.
587: * @param string $message 投稿メッセージ(UTF-8限定)
588: * @param array $items メディアデータ(バイナリデータ配列)
589: * @return bool TRUE:リクエスト成功/FALSE:失敗
590: */
591: function tweet_media_raw($message, $items) {
592: //メディアのアップロード
593: $media_ids = array();
594: $cnt = 0;
595: //Tweetet API v1.1 を使用する(v2にメディアアップロードが未実装のため)
596: $this->connection->setApiVersion('1.1');
597: foreach ($items as $data) {
598: $tmpname = $this->saveTempFile($data);
599: $media = $this->connection->upload('media/upload', ['media' => $tmpname]);
600: unlink($tmpname);
601: if (! isset($media->media_id_string)) break; //処理失敗
602: $media_ids[] = (string)$media->media_id_string;
603: $cnt++;
604: if ($cnt > 3) break; //最大4つまで
605: }
606:
607: //メディア付きツイート(Tweetet API v2 を使用する)
608: $this->connection->setApiVersion('2');
609: $option = [
610: 'text' => $message,
611: 'media' => [
612: 'media_ids' => $media_ids
613: ]
614: ];
615: $status = $this->connection->post('tweets', $option, TRUE);
616: $this->webapi = 'https://api.twitter.com/2/tweets';
617:
618: //処理に成功した.
619: if ($this->isSuccess()) {
620: $this->responses = $status->data;
621: $this->errcode = NULL;
622: $this->errmsg = '';
623: $this->error = FALSE;
624: $res = TRUE;
625: //処理に失敗した.
626: } else {
627: if ($this->isAuthError() == FALSE) {
628: $this->errmsg = $status->detail;
629: $this->error = TRUE;
630: }
631: $res = FALSE;
632: }
633: return $res;
634: }
解説:ツイート処理
255: /**
256: * ツイート処理
257: * @param string $message 投稿文
258: * @param string $res 応答メッセージ格納用
259: * @return bool TRUE:成功/FALSE:失敗または未処理
260: */
261: function mediaTweet($message, &$res) {
262: if (! TWITTER) return FALSE;
263:
264: $ret = TRUE;
265: if (isset($_POST['base64']) && ($_POST['base64'] != '')) {
266: $base64 = preg_replace('/data\:image\/png\;base64\,/ui', '', $_POST['base64']);
267: $raws = array(base64_decode($base64));
268: $ptw = new pahooTwitterAPI();
269: $ptw->tweet_media_raw($message, $raws);
270: $errmsg = $ptw->errmsg;
271: $ret = ! $ptw->error;
272: $ptw = NULL;
273: if ($ret) {
274: $res = 'ツイートしました';
275: }
276: }
277: return $ret;
278: }
ブラウザからPOSTで受け取った画像データ $_POST['base64'&$x5D; はBASE64でデコードされており、冒頭に余計なヘッダ情報が付いているので、このヘッダ情報を除き( preg_replace )、バイナリデータにデコードする( base64_decode )。
続いて pahooTwitterAPI クラスを呼び出し、tweet_media_raw メソッドを使って、メッセージと画像を一気にツイートする。
1117: //ツイート機能
1118: $message =<<< EOT
1119: 🌀台風情報 {$dt}現在
1120:
1121: (ご参考)PHPで台風情報を取得する https://www.pahoo.org/e-soul/webtech/php05/php05-15-01.shtm #台風 #台風情報 #nhk #ntv #tbs #fujitv #tvasahi
1122:
1123: EOT;
1124: mediaTweet($message, $res);
活用例
参考サイト
- 台風情報:気象庁
- JavaScriptでHTML表示を画像保存する:ぱふぅ家のホームページ
- HTMLとCSSでさまざまなアイコンを表示する:ぱふぅ家のホームページ
- Twitter API - WebAPIの登録方法:ぱふぅ家のホームページ
- PHPでTwitterに画像付きメッセージ投稿:ぱふぅ家のホームページ
- 日本付近の台風情報 - 気象庁発表:みんなの知識 ちょっと便利帳
そこで、気象庁の台風情報のサイトから、現在日本に接近している台風の情報を取得するPHPスクリプトを作成してみることにする。
2021年(令和3年)2月24日の気象庁サイト・リニューアルにより、スクレイピングによる取り出しが難しくなったため、気象庁防災情報XMLからの情報取得に変更した。あわせてキャッシュ・システムを導入した。
(2023年8月26日)基準座標から近い方の台風を地図中心にくるようにし,台風中心と予報円が入るようにズーム値を計算・表示するようにした.
(2023年8月10日)LIFE_CACHE_FEEDの値を変更した.