PHPで台風情報を取得する

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

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

(2023年9月20日)js_html2image()--Leaflet用html2image()発火プロセス見直し
(2023年8月26日)基準座標から近い方の台風を地図中心にくるようにし,台風中心と予報円が入るようにズーム値を計算・表示するようにした.
(2023年8月10日)LIFE_CACHE_FEEDの値を変更した.

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

PHPで台風情報を取得する

目次

サンプル・プログラム

圧縮ファイルの内容
typhoon.phpサンプル・プログラム本体
pahooGeoCode.php住所・緯度・経度に関わるクラス pahooGeoCode。
使い方は「PHPで住所・ランドマークから最寄り駅を求める」「PHPで住所・ランドマークから緯度・経度を求める」などを参照。include_path が通ったディレクトリに配置すること。
pahooCache.phpキャッシュ処理に関わるクラス pahooCache。
キャッシュ処理に関わるクラスの使い方は「PHPで天気予報を求める」を参照。include_path が通ったディレクトリに配置すること。
pahooTwitterAPI.phpTwitter APIを利用するクラス pahooTwitterAPI。
使い方は「PHPでTwitterに投稿(ツイート)する」などを参照。include_path が通ったディレクトリに配置すること。
typhoon.php 更新履歴
バージョン 更新日 内容
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化に対応
pahooGeoCode.php 更新履歴
バージョン 更新日 内容
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対応
pahooCache.php 更新履歴
バージョン 更新日 内容
1.1.1 2023/02/11 コメント追記
1.1 2021/04/08 simplexml_load()メソッド追加
1.0 2021/04/02 初版
pahooTwitterAPI.php 更新履歴
バージョン 更新日 内容
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 = '*****************************';

地図サービスを利用するために、クラスファイル "pahooGeoCode.php" を使用する。組み込み関数  require_once  を使って読めるディレクトリに配置する。ディレクトリは、設定ファイル php.ini に記述されているオプション設定 include_path に設定しておく。
クラスについては「PHPでクラスを使ってテキストの読みやすさを調べる」を参照されたい。

地図や住所検索として Google を利用するのであれば、Google Cloud Platform APIキー が必要で、その入手方法は「Google Cloud Platform - WebAPIの登録方法」を参照されたい。

準備:地図サービスの選択

  59: //地図描画サービスの選択
  60: //    0:Google
  61: //    2:地理院地図・OSM
  62: define('MAPSERVICE', 2);

表示する地図は、Googleマップ地理院地図・オープンストリートマップ(OSM)から選べる。あらかじめ、定数 MAPSERVICE に値を設定すること。
PHPで台風情報を取得する
地理院地図表示
PHPで台風情報を取得する
オープンストリートマップ表示

準備:キャッシュ・システム

  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/');

気象庁サイトへ負荷を掛けないこと、また、過去の台風の進路をプロットすることを目的として、キャッシュ・クラス pahooCache を導入した。使用方法については、「PHPで天気予報を求める - キャッシュ・システム」を参照いただきたい。
キャッシュ保持時間、キャッシュ・ディレクトリともに、自サイトの環境に応じて変更してほしい。
LIFE_CACHE_DATA は、過去の台風の進路をプロットできるように、初期値では14日間を指定してある。

気象庁防災情報XMLフォーマット

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

VPTW60の構造

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

準備:マップの表示サイズなど

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

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

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

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

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

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

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

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;

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

解説:ツイート機能

表示した天気図(マップ)を画像として、メッセージと一緒にボタン1つでツイートする機能を追加した。流れは「PHPでCOVID-19情報をグラフ表示」で解説したとおりである。

  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');

ツイート機能を使うかどうかは、定数 TWITTER で指定する。
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: }

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

1045: <div id="{$target}" name="{$target}" style="width:{$width2}px;">
1046: <p>
1047: 🌀台風情報 {$dt}現在{$res2}
1048: &nbsp;{$tweet}
1049: </p>
1050: <div id="{$mapid}" style="width:{$width}px; height:{$height}px;"></div>
1051: {$table}
1052: </div>

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

html2canvas を呼び出すタイミングだが、Googleマップの場合は tilesloadedイベントにフックする。
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)

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

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

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

解説:ツイート処理

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

メッセージと画像をツイート処理するのはサーバ側のPHPユーザー関数 mediaTweet で処理する。
ブラウザから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);

ユーザー関数 mediaTweet を呼び出しメインプログラム側のコードは上述の通りである。メッセージの内容は自由に変更していただいて構わない。

活用例

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

参考サイト

(この項おわり)
header