サンプル・プログラムの実行例
目次
サンプル・プログラム
typhoon.php | サンプル・プログラム本体 |
pahooGeoCode.php | 住所・緯度・経度に関わるクラス pahooGeoCode。 使い方は「PHPで住所・ランドマークから最寄り駅を求める」「PHPで住所・ランドマークから緯度・経度を求める」などを参照。include_path が通ったディレクトリに配置すること。 |
pahooCache.php | キャッシュ処理に関わるクラス pahooCache。 キャッシュ処理に関わるクラスの使い方は「PHPで天気予報を求める」を参照。include_path が通ったディレクトリに配置すること。 |
pahooTwitterAPI.php | Twitter APIを利用するクラス pahooTwitterAPI。 使い方は「PHPでTwitterに投稿(ツイート)する」などを参照。include_path が通ったディレクトリに配置すること。 |
バージョン | 更新日 | 内容 |
---|---|---|
2.4.1 | 2023/09/20 | js_html2image()--Leaflet用html2image()発火プロセス見直し |
2.4.0 | 2023/08/26 | 台風中心と予報円が入るようにズーム値を計算 |
2.3.0 | 2023/08/26 | 基準座標から近い方の台風を地図中心にくるように |
2.2.1 | 2023/08/10 | LIFE_CACHE_FEEDの値を変更 |
2.2 | 2022/03/10 | 気象庁防災情報XMLのhttps化に対応 |
バージョン | 更新日 | 内容 |
---|---|---|
6.3.1 | 2023/07/09 | bug-fix |
6.3.0 | 2023/07/02 | getPointsGSI()追加 |
6.2.0 | 2023/07/02 | ip2address()追加 |
6.1.0 | 2022/12/30 | ip2address()追加 |
6.0.4 | 2022/12/13 | PHP8.2対応 |
バージョン | 更新日 | 内容 |
---|---|---|
1.1.1 | 2023/02/11 | コメント追記 |
1.1 | 2021/04/08 | simplexml_load()メソッド追加 |
1.0 | 2021/04/02 | 初版 |
バージョン | 更新日 | 内容 |
---|---|---|
5.2.0 | 2023/07/17 | oembed() v2対応 |
5.1.0 | 2023/07/16 | extractMediaURL() -- file:///形式に対応 |
5.0.0 | 2023/07/02 | メソッドをTwitter API v2へ移行;v1.1は別名or廃止 |
4.9.0 | 2023/04/15 | tweet3() 追加 |
4.8.0 | 2023/01/28 | tweet2(),twitter_strcut2(),extractMediaURL()追加 |
準備: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を取得
345: /**
346: * 気象庁防災情報XMLから台風に関する情報URLを取得
347: * @param array $urls URL格納配列
348: * @param string $errmsg エラーメッセージ格納用
349: * @return bool TRUE:取得成功/FALSE:取得失敗
350: */
351: function jmaGetTyphoonURLs(&$urls, &$errmsg) {
352: //URLパターン
353: $vptw = '/https?\:\/\/www\.data\.jma\.go\.jp\/developer\/xml\/data\/([0-9\_]+)VPTW6[0-9]+\_[0-9]+\.xml/ui';
354:
355: //随時フィードの解析
356: $cnt = 0;
357: $pcc = new pahooCache(LIFE_CACHE_FEED, DIR_CACHE_FEED);
358: $xml = $pcc->simplexml_load(FEED);
359: //レスポンス・チェック
360: if ($pcc->iserror() || !isset($xml->entry)) {
361: $errmsg = '気象庁防災情報XMLにアクセスできません';
362: return FALSE;
363: }
364: foreach ($xml->entry as $node) {
365: //URLを取得
366: if (preg_match($vptw, $node->id, $arr) > 0) {
367: $urls[$cnt] = $arr[0];
368: $cnt++;
369: }
370: }
371: $pcc = NULL;
372:
373: //長期フィードの解析
374: $pcc = new pahooCache(LIFE_CACHE_FEED_L, DIR_CACHE_FEED_L);
375: $xml = $pcc->simplexml_load(FEED_L);
376: //レスポンス・チェック
377: if ($pcc->iserror() || !isset($xml->entry)) {
378: $errmsg = '気象庁防災情報XMLにアクセスできません';
379: return FALSE;
380: }
381: foreach ($xml->entry as $node) {
382: //URLを取得
383: if (preg_match($vptw, $node->id, $arr) > 0) {
384: if (array_search($arr[0], $urls) === FALSE) {
385: $urls[$cnt] = $arr[0];
386: $cnt++;
387: }
388: }
389: }
390: $pcc = NULL;
391:
392: //エラー・チェック
393: if ($cnt == 0) {
394: $errmsg = '直近の台風情報はありません';
395: return FALSE;
396: }
397:
398: //URLを日時の新しい順にソート
399: rsort($urls);
400:
401: return TRUE;
402: }
正規表現で VPTW60 を含むURLを取り出し配列に格納し、日時の新しい順に並べ替えておく。
解説:台風情報を読み込む
404: /**
405: * 新しい台風報かどうか
406: * @param object $xml 気象庁防災情報XML
407: * @param array $items 台風情報を格納する配列
408: * @return bool TRUE:新しい情報
409: */
410: function isNewTyphoon($xml, $items) {
411: $res = FALSE;
412:
413: if (isset($xml->Body->MeteorologicalInfos->MeteorologicalInfo->Item->Kind->Property->TyphoonNamePart->Number)) {
414: $num = (string)$xml->Body->MeteorologicalInfos->MeteorologicalInfo->Item->Kind->Property->TyphoonNamePart->Number;
415: if ($num != '') {
416: if (! isset($items[$num])) {
417: $res = TRUE;
418: }
419: }
420: }
421:
422: return $res;
423: }
425: /**
426: * 台風報取得(気象庁防災情報XMLから)
427: * @param object $pgc pahooGeoCodeオブジェクト
428: * @param array $items 台風情報を格納する配列
429: * @param string $urls 情報XMLのURLを格納する配列
430: * @param string $errmsg エラーメッセージ格納用
431: * @return bool TRUE:取得成功/FALSE:失敗
432: */
433: function getTyphoon($pgc, &$items, &$urls, &$errmsg) {
434: //名前空間
435: define('JMX_EB', 'http://xml.kishou.go.jp/jmaxml1/elementBasis1/');
436: //マッチングパターン
437: $pat2 = '/([\+\-][0-9]{1,2}\.[0-9]+)([\+\-][0-9]{1,3}\.[0-9]+)/ui'; //緯度・経度
438: $pat3 = '/(予報)[ ]*([01234567890-9]+)時間後/ui'; //推定|予報
439: //予報円は取得済みか否か
440: $flag_forecast = array();
441:
442: //オブジェクト生成
443: $pcc = new pahooCache(LIFE_CACHE_DATA, DIR_CACHE_DATA);
444:
445: //最新の台風に関する情報URLを取得
446: jmaGetTyphoonURLs($urls, $errmsg);
447: if ($errmsg != '') return FALSE;
448:
449: foreach ($urls as $key=>$vptw) {
450: //台風情報の取得
451: $xml = $pcc->simplexml_load($vptw);
452: //レスポンス・チェック
453: if ($pcc->iserror() || !isset($xml->Body->MeteorologicalInfos)) {
454: $errmsg = '気象庁防災情報XMLから台風情報を取得できません';
455: return FALSE;
456: }
457: $flag_f = FALSE; //予報円取得済みか否か
458:
459: //最新の台風情報
460: if (isNewTyphoon($xml, $items)) {
461: foreach ($xml->Body->MeteorologicalInfos as $infos) {
462: $cnt = 0;
463: foreach ($infos->MeteorologicalInfo as $info) {
464: //実況
465: if ($info->DateTime['type'] == '実況') {
466: foreach ($info->Item->Kind as $kind) {
467: if (isset($kind->Property->Type)) {
468: //呼称
469: if ($kind->Property->Type == '呼称') {
470: $num = (string)$kind->Property->TyphoonNamePart->Number;
471: if ($num == '') break;
472: $items[$num]['Name'] = (string)$kind->Property->TyphoonNamePart->Name;
473: $items[$num]['NameKana'] = (string)$kind->Property->TyphoonNamePart->NameKana;
474: $items[$num][$cnt]['kind'] = '実況';
475: $items[$num][$cnt]['DateTime'] = (string)$info->DateTime;
476: //その台風の予報円は未取得
477: $flag_forecast[$num] = FALSE;
478: //古い台風情報かどうか
479: $tt = time() - strtotime($info->DateTime);
480: $items[$num]['Valid'] = ($tt < SCRAP_TIME) ? TRUE : FALSE;
481: //階級
482: } else if ($kind->Property->Type == '階級') {
483: $node = $kind->Property->ClassPart->children(JMX_EB);
484: $items[$num][$cnt]['TyphoonClass'] = (string)$node->TyphoonClass;
485: $items[$num][$cnt]['AreaClass'] = (string)$node->AreaClass;
486: $items[$num][$cnt]['IntensityClass'] = (string)$node->IntensityClass;
487: //中心
488: } else if ($kind->Property->Type == '中心') {
489: $node = $kind->Property->CenterPart->children(JMX_EB);
490: //中心位置
491: $items[$num][$cnt]['Location'] = (string)$kind->Property->CenterPart->Location;
492: foreach ($node->Coordinate as $val) {
493: if (preg_match($pat2, $val, $arr) > 0) {
494: $items[$num][$cnt]['latitude'] = (float)$arr[1];
495: $items[$num][$cnt]['longitude'] = (float)$arr[2];
496: }
497: }
498: //移動速度
499: $items[$num][$cnt]['Direction'] = (string)$node->Direction;
500: foreach ($node->Speed as $val) {
501: if (isset($val->attributes()['condition'])) {
502: $items[$num][$cnt]['Speed'] = (string)$val->attributes()['condition'];
503: } else if ($val->attributes()['unit'] == 'km/h') {
504: $items[$num][$cnt]['Speed'] = (string)$val;
505: }
506: }
507: //中心気圧
508: $items[$num][$cnt]['Pressure'] = (int)$node->Pressure;
509: //風
510: } else if ($kind->Property->Type == '風') {
511: $node = $kind->Property->WindPart->children(JMX_EB);
512: foreach ($node->WindSpeed as $val) {
513: if (($val->attributes()['condition'] == '中心付近') && ($val->attributes()['unit'] == 'm/s')) {
514: $items[$num][$cnt]['centerWindSpeed'] = (int)$val;
515: } else if (($val->attributes()['type'] == '最大瞬間風速') && ($val->attributes()['unit'] == 'm/s')) {
516: $items[$num][$cnt]['maxWindSpeed'] = (int)$val;
517: }
518: }
519: //暴風域・強風域
520: foreach ($kind->Property->WarningAreaPart as $val) {
521: $key = (string)$val['type'];
522: //半径
523: $node = $val->children(JMX_EB);
524: $n = 0;
525: foreach ($node->Circle->Axes->Axis as $axis) {
526: if (isset($axis->Direction) && ($axis->Direction != '')) {
527: $items[$num][$cnt][$key][$n]['direction'] = (string)$axis->Direction;
528: } else {
529: $items[$num][$cnt][$key][$n]['direction'] = (string)$axis->Direction->attributes()['condition'];
530: }
531: foreach ($axis->Radius as $val) {
532: if ($val->attributes()['unit'] == 'km') {
533: $items[$num][$cnt][$key][$n]['radius'] = (int)$val;
534: }
535: }
536: $n++;
537: }
538: }
539: }
540: }
541: }
542: //予報
543: } else if (preg_match($pat3, (string)$info->DateTime['type'], $arr) > 0) {
544: if ($num == '') break; //ver.2.04
545: $cnt++;
546: $items[$num][$cnt]['kind'] = '予報';
547: $items[$num][$cnt]['DateTime'] = (string)$info->DateTime;
548: $flag_f = TRUE; //予報円取得済み
549: foreach ($info->Item->Kind as $kind) {
550: //階級
551: if ($kind->Property->Type == '階級') {
552: $node = $kind->Property->ClassPart->children(JMX_EB);
553: $items[$num][$cnt]['TyphoonClass'] = (string)$node->TyphoonClass;
554: $items[$num][$cnt]['AreaClass'] = isset($node->AreaClass) ? (string)$node->AreaClass : '';
555: $items[$num][$cnt]['IntensityClass'] = isset($node->IntensityClass) ? (string)$node->IntensityClass : '';
556: //中心位置(予報円)
557: } else if ($kind->Property->Type == '中心') {
558: $node = $kind->Property->CenterPart->ProbabilityCircle->children(JMX_EB);
559: foreach ($node->BasePoint as $val) {
560: if (preg_match($pat2, $val, $arr) > 0) {
561: $items[$num][$cnt]['latitude'] = (float)$arr[1];
562: $items[$num][$cnt]['longitude'] = (float)$arr[2];
563: }
564: }
565: //半径
566: $n = 0;
567: foreach ($node->Axes->Axis as $axis) {
568: if (isset($axis->Direction) && ($axis->Direction != '')) {
569: $items[$num][$cnt]['予報円'][$n]['direction'] = (string)$axis->Direction;
570: } else {
571: $items[$num][$cnt]['予報円'][$n]['direction'] = (string)$axis->Direction->attributes()['condition'];
572: }
573: foreach ($axis->Radius as $val) {
574: if ($val->attributes()['unit'] == 'km') {
575: $items[$num][$cnt]['予報円'][$n]['radius'] = (int)$val;
576: }
577: }
578: $n++;
579: }
580: //風
581: } else if ($kind->Property->Type == '風') {
582: $node = $kind->Property->WindPart->children(JMX_EB);
583: foreach ($node->WindSpeed as $val) {
584: if (($val->attributes()['condition'] == '中心付近') && ($val->attributes()['unit'] == 'm/s')) {
585: $items[$num][$cnt]['centerWindSpeed'] = (int)$val;
586: } else if (($val->attributes()['type'] == '最大瞬間風速') && ($val->attributes()['unit'] == 'm/s')) {
587: $items[$num][$cnt]['maxWindSpeed'] = (int)$val;
588: }
589: }
590: }
591: }
592: }
593: }
594: }
595: //過去の台風情報
596: } else {
597: foreach ($xml->Body->MeteorologicalInfos as $infos) {
598: foreach ($infos->MeteorologicalInfo as $info) {
599: //過去の位置
600: if ($info->DateTime['type'] == '実況') {
601: foreach ($info->Item->Kind as $kind) {
602: if (isset($kind->Property->Type)) {
603: //呼称
604: if ($kind->Property->Type == '呼称') {
605: $num = (string)$kind->Property->TyphoonNamePart->Number;
606: if ($num == '') break;
607: $cnt = 0;
608: while (isset($items[$num][$cnt])) $cnt++;
609: $items[$num][$cnt]['kind'] = '過去';
610: $items[$num][$cnt]['DateTime'] = (string)$info->DateTime;
611: //中心
612: } else if ($kind->Property->Type == '中心') {
613: $node = $kind->Property->CenterPart->children(JMX_EB);
614: //中心位置
615: foreach ($node->Coordinate as $val) {
616: if (preg_match($pat2, $val, $arr) > 0) {
617: $items[$num][$cnt]['latitude'] = (float)$arr[1];
618: $items[$num][$cnt]['longitude'] = (float)$arr[2];
619: }
620: }
621: }
622: }
623: }
624: //予報
625: } else if (isset($flag_forecast[$num]) && ! $flag_forecast[$num] && (preg_match($pat3, (string)$info->DateTime['type'], $arr) > 0)) {
626: if ($num == '') break; //ver.2.04
627: $cnt++;
628: $items[$num][$cnt]['kind'] = '予報';
629: $items[$num][$cnt]['DateTime'] = (string)$info->DateTime;
630: $flag_f = TRUE; //予報円取得済み
631: foreach ($info->Item->Kind as $kind) {
632: //階級
633: if ($kind->Property->Type == '階級') {
634: $node = $kind->Property->ClassPart->children(JMX_EB);
635: $items[$num][$cnt]['TyphoonClass'] = (string)$node->TyphoonClass;
636: $items[$num][$cnt]['AreaClass'] = isset($node->AreaClass) ? (string)$node->AreaClass : '';
637: $items[$num][$cnt]['IntensityClass'] = isset($node->IntensityClass) ? (string)$node->IntensityClass : '';
638: //中心位置(予報円)
639: } else if ($kind->Property->Type == '中心') {
640: $node = $kind->Property->CenterPart->ProbabilityCircle->children(JMX_EB);
641: foreach ($node->BasePoint as $val) {
642: if (preg_match($pat2, $val, $arr) > 0) {
643: $items[$num][$cnt]['latitude'] = (float)$arr[1];
644: $items[$num][$cnt]['longitude'] = (float)$arr[2];
645: }
646: }
647: //半径
648: $n = 0;
649: foreach ($node->Axes->Axis as $axis) {
650: if (isset($axis->Direction) && ($axis->Direction != '')) {
651: $items[$num][$cnt]['予報円'][$n]['direction'] = (string)$axis->Direction;
652: } else {
653: $items[$num][$cnt]['予報円'][$n]['direction'] = (string)$axis->Direction->attributes()['condition'];
654: }
655: foreach ($axis->Radius as $val) {
656: if ($val->attributes()['unit'] == 'km') {
657: $items[$num][$cnt]['予報円'][$n]['radius'] = (int)$val;
658: }
659: }
660: $n++;
661: }
662: //風
663: } else if ($kind->Property->Type == '風') {
664: $node = $kind->Property->WindPart->children(JMX_EB);
665: foreach ($node->WindSpeed as $val) {
666: if (($val->attributes()['condition'] == '中心付近') && ($val->attributes()['unit'] == 'm/s')) {
667: $items[$num][$cnt]['centerWindSpeed'] = (int)$val;
668: } else if (($val->attributes()['type'] == '最大瞬間風速') && ($val->attributes()['unit'] == 'm/s')) {
669: $items[$num][$cnt]['maxWindSpeed'] = (int)$val;
670: }
671: }
672: }
673: }
674: }
675: }
676: }
677: }
678: //その台風の予報円は取得済み
679: if ($flag_f) {
680: $flag_forecast[$num] = TRUE;
681: }
682: }
683: //オブジェクト解放
684: $pcc = NULL;
685:
686: return TRUE;
687: }
XMLファイルを読み込んだら、まず、実況情報か予報情報かを識別する。
予報情報の場合、ユーザー関数 isNewTyphoon により、台風の番号を参照し、まだ登録されていない情報であれば、最新の実況情報として配列に代入する。登録済みの情報であれば、過去の台風情報として配列に代入する。過去の情報は、台風の過去の進路としてマッピングするときに参照する。
なお、現在日時から実況日時を減じ、定数 SCRAP_TIME を超えていたら、古い台風情報として、配列には記録するものの、要素 ValidにFALSEを代入し、古い台風情報であることを明示する。
暴風域・強風域、予報円については、半径の値が複数存在する。たとえば、強風域が北東240km、南西200kmとなっていたら、台風の中心から北東へズレたところに強風域の中心がある。
前述の通り、VPTW60 の発表日時によっては予報円情報を含まないことがある。
そこで、台風毎に配列 $flag_forecast に予報円を取得したかどうかのフラグを持たせる。もし isNewTyphoon で取得した最新の VPTW60 に予報円情報が無ければ、過去の台風情報から予報円情報を取得する。
このため、ほぼ同じ予報取得プロセスが2箇所に書く格好になっており、美しさに欠けてしまった。
解説:台風情報を描くスクリプトを生成
734: /**
735: * 暴風域、強風域、予報円の描画スクリプトを生成する
736: * @param object $pgc pahooGeoCodeオブジェクト
737: * @param array $infos 台風情報
738: * @return string スクリプト/FALSE:生成失敗
739: */
740: function jsTyphoonMap($pgc, $infos) {
741: $js = '';
742: foreach ($infos as $info) {
743: $key = 0;
744: //台風以外ならスキップ
745: if (! isset($info[$key]['TyphoonClass'])) continue; //v.2.11
746: if (preg_match('/台風/ui', $info[$key]['TyphoonClass']) == 0) continue;
747: //古い台風情報ならスキップ
748: if ($info['Valid'] == FALSE) continue;
749:
750: //暴風域
751: list($latitude, $longitude, $radius) = shiftCircle($pgc, $info[$key]['latitude'], $info[$key]['longitude'], $info[$key]['暴風域']);
752: $radius *= 1000;
753: $js .= $pgc->jsCircle($longitude, $latitude, $radius, COLOR_WIND2, '1.0', 2, MAPSERVICE);
754: //強風域
755: list($latitude, $longitude, $radius) = shiftCircle($pgc, $info[$key]['latitude'], $info[$key]['longitude'], $info[$key]['強風域']);
756: $radius *= 1000;
757: $js .= $pgc->jsCircle($longitude, $latitude, $radius, COLOR_WIND1, '1.0', 2, MAPSERVICE);
758:
759: $key = 1;
760: $cnt = 1;
761: $lng_0 = $points[0]['longitude'] = $info[0]['longitude'];
762: $lat_0 = $points[0]['latitude'] = $info[0]['latitude'];
763: while (isset($info[$key])) {
764: //予報円
765: if ($info[$key]['kind'] == '予報') {
766: //予報円を間引くかどうか
767: $dd = $pgc->greatCircleDistance($lng_0, $lat_0, $info[$key]['longitude'], $info[$key]['latitude']);
768: if ($dd > THIN_OUT) {
769: list($latitude, $longitude, $radius) = shiftCircle($pgc, $info[$key]['latitude'], $info[$key]['longitude'], $info[$key]['予報円']);
770: $radius *= 1000;
771: $js .= $pgc->jsCircle($longitude, $latitude, $radius, COLOR_FORECAST, '1.0', 2, MAPSERVICE);
772: preg_match('/[0-9]{4}\-[0-9]{2}\-([0-9]{2})T([0-9]{2})/ui', $info[$key]['DateTime'], $arr);
773: $dt = sprintf('%d日%d時', $arr[1], $arr[2]);
774: list($lat, $lng) = $pgc->getPointDistance($longitude, $latitude, 0 - $radius - 30000, 9);
775: $js .= $pgc->jsLabel($lat, $lng, $dt, 12, COLOR_FORECAST, 'normal', MAPSERVICE);
776: $lat_0 = $info[$key]['latitude'];
777: $lng_0 = $info[$key]['longitude'];
778: }
779:
780: //過去の位置
781: } else {
782: $points[$cnt]['longitude'] = $info[$key]['longitude'];
783: $points[$cnt]['latitude'] = $info[$key]['latitude'];
784: $cnt++;
785: }
786: $key++;
787: }
788: //過去の移動経路
789: if ($js != '') {
790: $js .= $pgc->jsLine($points, COLOR_LINE, '1.0', 3, MAPSERVICE);
791: $points = array();
792: $cnt = 0;
793: if (isset($info[$key]['longitude'])) {
794: $points[0]['longitude'] = $info[$key]['longitude'];
795: $points[0]['latitude'] = $info[$key]['latitude'];
796: }
797: }
798: }
799:
800: //HTMLの画像化
801: $js .= js_html2image();
802:
803: return $js;
804: }
前述の通り、円の中心がズレている場合があり、そのためのユーザー関数 shiftCircle を呼び出して使う。
前回の予報円の中心座標 ($lat_0, $lng_0) (初回は現在の台風の中心座標)からの大圏航路距離をメソッド greatCircleDistance によって計算し、予報円の間引き条件 THIN_OUT 以下であれば予報円を描かない。
解説:マップ描画用情報を生成
806: /**
807: * 台風情報からマップ描画用情報を生成する
808: * @param array $infos 台風情報
809: * @param array $items マップ描画用情報を格納
810: * @param string $table HTML文(表形式)を格納
811: * @param int $count 有効な台風情報の数
812: * @param objct $pgc pahooGeoCodeオブジェクト
813: * @return array(日時,緯度,経度) 発表日時,予報円の最後の中心座標,地図ズーム
814: */
815: function getTyphoonInfo($infos, &$items, &$table, &$count, $pgc) {
816: //地図ズーム値=距離換算表
817: $zooms = array(10000, 5000, 3000, 1000, 500, 300);
818:
819: //台風情報一覧
820: $table =<<< EOT
821: <table class="plists">
822: <th>名称</th>
823: <th>位置</th>
824: <th>中心気圧</th>
825: <th>最大瞬間風速</th>
826: <th>進路</th>
827: </tr>
828:
829: EOT;
830:
831: $dt0 = $lat0 = $lng0 = $zoom = FALSE;
832: $distance = 99999;
833: $cnt = 1;
834: foreach ($infos as $nn=>$info) { //v.2.11
835: $key = 0; //v.2.11
836: if (! isset($info[$key]['TyphoonClass'])) continue; //v.2.11
837: if (preg_match('/台風/ui', $info[0]['TyphoonClass']) > 0) {
838: $num = sprintf('台%d', (int)substr($nn, 2, 2)); //v.2.11
839: $name = sprintf('台風%d号(%s)', (int)substr($nn, 2, 2), $info['NameKana']);
840: } else {
841: continue;
842: }
843: //古い台風情報ならスキップ
844: if ($info['Valid'] == FALSE) continue;
845:
846: $items[$cnt]['longitude'] = $info[0]['longitude'];
847: $items[$cnt]['latitude'] = $info[0]['latitude'];
848: if ($cnt == 1) {
849: preg_match('/([0-9]{4})\-([0-9]{2})\-([0-9]{2})T([0-9]{2})\:/ui', $info[0]['DateTime'], $arr);
850: $dt0 = $info[0]['DateTime'] = sprintf('%d年%d月%d日%d時', $arr[1], $arr[2], $arr[3], $arr[4]);
851: }
852: $items[$cnt]['label'] = $num;
853: $items[$cnt]['label_color'] = COLOR_NAME1;
854: $items[$cnt]['label_size'] = 16;
855: $items[$cnt]['label_weight'] = 'bold';
856: //情報ウィンドウ
857: if ($info[0]['AreaClass'] != '') {
858: $AreaClass = $info[0]['AreaClass'];
859: } else {
860: $AreaClass = '-';
861: }
862: if ($info[0]['IntensityClass'] != '') {
863: $IntensityClass = $info[0]['IntensityClass'];
864: } else {
865: $IntensityClass = '-';
866: }
867: if ($items[$cnt]['longitude'] >= 0) {
868: $lng = sprintf('東経%.1f度', $items[$cnt]['longitude']);
869: } else {
870: $lng = sprintf('西経%.1f度', 0 - $items[$cnt]['longitude']);
871: }
872: if ($items[$cnt]['latitude'] >= 0) {
873: $lat = sprintf('北緯%.1f度', $items[$cnt]['latitude']);
874: } else {
875: $lat = sprintf('南緯%.1f度', 0 - $items[$cnt]['latitude']);
876: }
877: if ($info[0]['Direction'] != '') {
878: $directioin = $info[0]['Direction'] . 'へ';
879: } else {
880: $directioin = '';
881: }
882: if (is_numeric($info[0]['Speed'])) {
883: $speed = sprintf('時速%dkm', $info[0]['Speed']);
884: } else {
885: $speed = $info[0]['Speed'];
886: }
887: if (isset($info[0]['centerWindSpeed']) && is_numeric($info[0]['centerWindSpeed'])) {
888: $centerWindSpeed = $info[0]['centerWindSpeed'] . 'メートル';
889: } else {
890: $centerWindSpeed = '-';
891: }
892: if (isset($info[0]['maxWindSpeed']) && is_numeric($info[0]['maxWindSpeed'])) {
893: $maxWindSpeed = $info[0]['maxWindSpeed'] . 'メートル';
894: } else {
895: $maxWindSpeed = '-';
896: }
897: $items[$cnt]['title'] = '';
898: $items[$cnt]['description'] =<<< EOT
899: <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 />
900: EOT;
901: //台風情報一覧
902: $table .=<<< EOT
903: <tr>
904: <td>{$name}</td>
905: <td>{$info[0]['Location']}</td>
906: <td>{$info[0]['Pressure']}hPa</td>
907: <td>{$maxWindSpeed}</td>
908: <td>{$directioin}{$speed}</td>
909: </tr>
910:
911: EOT;
912: //3日以内の予報円の中心座標を求める v.2.01
913: $m = 1;
914: while (isset($info[$m])) {
915: if ($info[$m]['kind'] == '予報') {
916: $lat1 = $info[$m]['latitude'];
917: $lng1 = $info[$m]['longitude'];
918: if ($m >= 3) break;
919: }
920: $m++;
921: }
922: //基準座標に近い方をマップ中心にする
923: $dist = $pgc->greatCircleDistance($lng1, $lat1, LONGITUDE00, LATITUDE00);
924: if ($dist < $distance) {
925: $lat0 = $lat1;
926: $lng0 = $lng1;
927: $distance = $dist;
928: //台風中心と3日後予報円の距離から地図ズーム値を決める
929: $dist = $pgc->greatCircleDistance($lng0, $lat0, $info[0]['longitude'], $info[0]['latitude']);
930: $zoom = count($zooms) + 1;
931: foreach ($zooms as $key=>$val) {
932: if ($dist >= $val) {
933: $zoom = $key + 1;
934: break;
935: }
936: }
937: }
938:
939: $cnt++;
940: }
941:
942: $table .=<<< EOT
943: </table>
944:
945: EOT;
946: //有効な台風情報の数
947: $count = $cnt - 1;
948:
949: return array($dt0, $lat0, $lng0, $zoom);
950: }
解説:メイン・プログラム
1090: //気象庁防災情報XMLから台風情報を取得
1091: $infos = array();
1092: $urls = array();
1093: $items = array();
1094: $dt = $date->format('Y年m月d日H時');
1095: $table = $errmsg = '';
1096: $count = 0;
1097: $js = FALSE;
1098: $ret = getTyphoon($pgc, $infos, $urls, $errmsg);
1099: if ($ret == FALSE) {
1100: $errmsg = $pgc->getError();
1101: } else {
1102: $js = jsTyphoonMap($pgc, $infos, $errmsg);
1103: list($dt, $latitude, $longitude, $zoom) = getTyphoonInfo($infos, $items, $table, $count, $pgc);
1104: if ($count == 0) {
1105: $longitude = DEF_LONGITUDE;
1106: $latitude = DEF_LATITUDE;
1107: $zoom = DEF_ZOOM;
1108: $type = DEF_TYPE;
1109: $dt = $date->format('Y年m月d日H時');
1110: }
1111: }
1112:
1113: //マップ作成
1114: if (($errmsg == '') && ($js != FALSE)) {
1115: $jsmap = $pgc->drawJSMap(MAPID, $latitude, $longitude, $type, $zoom, NULL, $items, MAPSERVICE, $js, (MAP_WIDTH / 2));
1116: } else {
1117: $jsmap = $pgc->drawJSMap(MAPID, $latitude, $longitude, $type, $zoom, NULL, $items, MAPSERVICE);
1118: }
1119:
1120: //ツイート機能
1121: $message =<<< EOT
1122: 🌀台風情報 {$dt}現在
1123:
1124: (ご参考)PHPで台風情報を取得する https://www.pahoo.org/e-soul/webtech/php05/php05-15-01.shtm #台風 #台風情報 #nhk #ntv #tbs #fujitv #tvasahi
1125:
1126: EOT;
1127: mediaTweet($message, $res);
1128:
1129: //HTML BODY作成
1130: $HtmlBody = makeCommonBody($dt, $jsmap, $table, $urls, $res, $errmsg, $count);
1131:
1132: //オブジェクト解放
1133: $pgc = NULL;
1134: $date = NULL;
1135:
1136: //ブラウザ表示処理
1137: echo $HtmlHeader;
1138: echo $HtmlBody;
1139: 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 である。
1045: <div id="{$target}" name="{$target}" style="width:{$width2}px;">
1046: <p>
1047: 🌀台風情報 {$dt}現在{$res2}
1048: {$tweet}
1049: </p>
1050: <div id="{$mapid}" style="width:{$width}px; height:{$height}px;"></div>
1051: {$table}
1052: </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: //html2image()を発火させるために,ズームアウトして500msec後に元に戻す.
333: var zoom = map.getZoom();
334: map.setZoom(zoom - 1);
335: setTimeout(function() {
336: map.setZoom(zoom);
337: }, 500);
338:
339: EOT;
340: }
341:
342: return $js;
343: }
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 メソッドを使って、メッセージと画像を一気にツイートする。
1120: //ツイート機能
1121: $message =<<< EOT
1122: 🌀台風情報 {$dt}現在
1123:
1124: (ご参考)PHPで台風情報を取得する https://www.pahoo.org/e-soul/webtech/php05/php05-15-01.shtm #台風 #台風情報 #nhk #ntv #tbs #fujitv #tvasahi
1125:
1126: EOT;
1127: mediaTweet($message, $res);
活用例
参考サイト
- 台風情報:気象庁
- JavaScriptでHTML表示を画像保存する:ぱふぅ家のホームページ
- HTMLとCSSでさまざまなアイコンを表示する:ぱふぅ家のホームページ
- Twitter API - WebAPIの登録方法:ぱふぅ家のホームページ
- PHPでTwitterに画像付きメッセージ投稿:ぱふぅ家のホームページ
- 日本付近の台風情報 - 気象庁発表:みんなの知識 ちょっと便利帳
そこで、気象庁の台風情報のサイトから、現在日本に接近している台風の情報を取得するPHPスクリプトを作成してみることにする。
2021年(令和3年)2月24日の気象庁サイト・リニューアルにより、スクレイピングによる取り出しが難しくなったため、気象庁防災情報XMLからの情報取得に変更した。あわせてキャッシュ・システムを導入した。
(2023年9月20日)js_html2image()--Leaflet用html2image()発火プロセス見直し
(2023年8月26日)基準座標から近い方の台風を地図中心にくるようにし,台風中心と予報円が入るようにズーム値を計算・表示するようにした.
(2023年8月10日)LIFE_CACHE_FEEDの値を変更した.