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

目次
- サンプル・プログラムの実行例
- サンプル・プログラム
- 準備:pahooGeoCode クラス
- 準備:地図サービスの選択
- 準備:キャッシュ・システム
- 準備:各種定数など
- 気象庁防災情報XMLフォーマット
- VPTW60の構造
- 解説:台風に関する情報URLを取得
- 解説:台風情報を読み込む
- 解説:台風情報を描くスクリプトを生成
- 解説:マップ描画用情報を生成
- 解説:メイン・プログラム
- 解説:SNS投稿機能
- 解説:html2canvasライブラリ
- 準備:pahooTwitterAPI クラス
- 解説:メディア付き投稿(RAWデータ)
- 解説:Twitter(現・X)へ投稿する
- 準備:pahooBlueskyAPI クラス
- 解説:Blueskyへ投稿する
- 解説:SNSへ投稿する(メイン・プログラム)
- 活用例
- 参考サイト
サンプル・プログラム
typhoon.php | サンプル・プログラム本体 |
pahooGeoCode.php | 住所・緯度・経度に関わるクラス pahooGeoCode。 使い方は「PHPで住所・ランドマークから最寄り駅を求める」「PHPで住所・ランドマークから緯度・経度を求める」などを参照。include_path が通ったディレクトリに配置すること。 |
pahooCache.php | キャッシュ処理に関わるクラス pahooCache。 キャッシュ処理に関わるクラスの使い方は「PHPで天気予報を求める」を参照。include_path が通ったディレクトリに配置すること。 |
pahooTwitterAPI.php | Twitter APIを利用するクラス pahooTwitterAPI。 使い方は「PHPでTwitterに投稿(ツイート)する」などを参照。include_path が通ったディレクトリに配置すること。 |
pahooBlueskyAPI.php | Bluesky APIに関わるクラス pahooBlueskyAPI。 使い方は「PHPでPHPでBlueskyに投稿する」などを参照。include_path が通ったディレクトリに配置すること。 |
バージョン | 更新日 | 内容 |
---|---|---|
2.7.0 | 2025/06/11 | countオプションを追加 |
2.6.0 | 2024/11/01 | Bluesky投稿機能を追加, isButton()修正 |
2.5.0 | 2024/06/21 | Twitter(現・X)ボタンを "X" に変更 |
2.4.1 | 2023/09/20 | js_html2image()--Leaflet用html2image()発火プロセス見直し |
2.4.0 | 2023/08/26 | 台風中心と予報円が入るようにズーム値を計算 |
バージョン | 更新日 | 内容 |
---|---|---|
6.5.0 | 2025/06/14 | GoogleMaps JavaScript APIの変更に対応 |
6.4.0 | 2025/03/01 | makeYOLP_GeoSelectCategory()--引数$flagWorld追加 |
6.3.3 | 2024/09/14 | $this->NOMINATIM_EMAIL 追加 |
6.3.2 | 2024/02/14 | getStaticMap() -- bug-fix |
6.3.1 | 2023/07/09 | bug-fix |
バージョン | 更新日 | 内容 |
---|---|---|
1.1.1 | 2023/02/11 | コメント追記 |
1.1 | 2021/04/08 | simplexml_load()メソッド追加 |
1.0 | 2021/04/02 | 初版 |
バージョン | 更新日 | 内容 |
---|---|---|
5.5.1 | 2024/11/23 | __construct() -- PHP8.4における応急処置 |
5.5.0 | 2024/06/21 | TwitterOAuth 7.0.0 対応 |
5.4.0 | 2024/05/18 | twitter.com → x.com 変更対応 |
5.3.0 | 2023/08/15 | tweet3() -- メディアのシャフル機能 |
5.2.1 | 2023/07/22 | bug-fix |
バージョン | 更新日 | 内容 |
---|---|---|
2.1.0 | 2025/03/20 | getUserPosts() 追加 |
2.0.1 | 2025/01/24 | getPostThread() -- 認証必要のエンドポイントに変更 |
2.0.0 | 2025/01/24 | トークンを保持するよう改良 |
1.9.0 | 2025/01/16 | getEmbedPosts() 追加 |
1.8.1 | 2024/12/11 | getOGPInformation()--リダイレクト対応 |
準備:pahooGeoCode クラス
pahooGeoCode.php
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: var $GOOGLE_MAP_ID = '*************************'; // GoogleMaps ID
50:
51: // Yahoo! JAPAN Webサービス アプリケーションID
52: // https://e.developer.yahoo.co.jp/register
53: // ※Yahoo! JAPAN Webサービスを利用しないのなら登録不要
54: var $YAHOO_APPLICATION_ID = '*****************************';
クラスについては「PHPでクラスを使ってテキストの読みやすさを調べる」を参照されたい。

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


準備:キャッシュ・システム
typhoon.php
106: // キャッシュ保持時間(分) 0:キャッシュしない
107: // 気象庁へのアクセス負荷軽減+台風の進路プロット
108: define('LIFE_CACHE_FEED', 15); // 高頻度 - 随時フィードに対して
109: define('LIFE_CACHE_FEED_L', 120); // 長期 - 随時フィードに対して
110: define('LIFE_CACHE_DATA', (60 * 24 * 14)); // 台風情報の保持時間(進路プロット)
111:
112: // キャッシュ・ディレクトリ
113: // 書き込み可能で,外部からアクセスされないディレクトリを指定してください.
114: define('DIR_CACHE_FEED', './pcache_typhoon1/');
115: define('DIR_CACHE_FEED_L', './pcache_typhoon2/');
116: define('DIR_CACHE_DATA', './pcache_typhoon3/');
キャッシュ保持時間、キャッシュ・ディレクトリともに、自サイトの環境に応じて変更してほしい。
LIFE_CACHE_DATA は、過去の台風の進路をプロットできるように、初期値では14日間を指定してある。
準備:各種定数など
typhoon.php
46: // 各種定数(START) ===========================================================
47: // Twitter(現・X)投稿ボタン TRUE:有効,FALSE:無効
48: define('TWITTER', FALSE);
49:
50: // Bluesky投稿ボタン TRUE:有効,FALSE:無効
51: define('BLUESKY', FALSE);
52:
53: // 住所・緯度・経度に関わるクラス:include_pathが通ったディレクトリに配置
54: require_once('pahooGeoCode.php');
55:
56: // キャッシュ処理に関わるクラス:include_pathが通ったディレクトリに配置
57: require_once('pahooCache.php');
58:
59: // Twitterクラス:include_pathが通ったディレクトリに配置
60: if (TWITTER) {
61: require_once('pahooTwitterAPI.php');
62: }
63:
64: // BlueskyAPIクラス:include_pathが通ったディレクトリに配置
65: if (BLUESKY) {
66: require_once('pahooBlueskyAPI.php');
67: define('BLUESKY_DOMAIN', 'bsky.social'); // あなたのドメインを記入
68: }
69:
70: // 画像化したいオブジェクト
71: define('TARGET', 'target');
72:
73: // 地図描画サービスの選択
74: // 0:Google
75: // 2:地理院地図・OSM
76: define('MAPSERVICE', 2);
77:
78: // 古い台風情報を捨てる条件:現在日時との差(秒数)
79: define('SCRAP_TIME', (60 * 60 * 12));
80:
81: // 予報円を間引く条件(km):次の予報円がこれより近ければ描画しない
82: define('THIN_OUT', 50);
83:
84: // マップの表示サイズ(単位:ピクセル)
85: define('MAP_WIDTH', 600);
86: define('MAP_HEIGHT', 480);
87: // マップID
88: define('MAPID', 'map_id');
89: // マップ座標
90: define('DEF_LATITUDE', 35.0); // 緯度
91: define('DEF_LONGITUDE', 137.0); // 経度
92: define('DEF_TYPE', 'GSISTD'); // マップタイプ
93: define('DEF_ZOOM', 6); // ズーム
94: // マップ描画色
95: define('COLOR_NAME1', '#FF8800'); // 台風名称の描画色
96: define('COLOR_NAME2', '#0000FF'); // 熱低名称の描画色
97: define('COLOR_LINE', '#0000FF'); // 過去経路の描画色
98: define('COLOR_WIND1', '#FFFF00'); // 強風域の描画色
99: define('COLOR_WIND2', '#FF0000'); // 暴風域の描画色
100: define('COLOR_FORECAST', '#FFFFFF'); // 予報円の描画色
101:
102: // 基準座標;台風との距離計算に使う(これは東京駅の値;変更可能)
103: define('LATITUDE00', 35.681111,); // 緯度
104: define('LONGITUDE00', 139.766667); // 経度
105:
106: // キャッシュ保持時間(分) 0:キャッシュしない
107: // 気象庁へのアクセス負荷軽減+台風の進路プロット
108: define('LIFE_CACHE_FEED', 15); // 高頻度 - 随時フィードに対して
109: define('LIFE_CACHE_FEED_L', 120); // 長期 - 随時フィードに対して
110: define('LIFE_CACHE_DATA', (60 * 24 * 14)); // 台風情報の保持時間(進路プロット)
111:
112: // キャッシュ・ディレクトリ
113: // 書き込み可能で,外部からアクセスされないディレクトリを指定してください.
114: define('DIR_CACHE_FEED', './pcache_typhoon1/');
115: define('DIR_CACHE_FEED_L', './pcache_typhoon2/');
116: define('DIR_CACHE_DATA', './pcache_typhoon3/');
117:
118: // 気象庁防災情報XML:高頻度フィード - 随時【変更不可】
119: define('FEED', 'https://www.data.jma.go.jp/developer/xml/feed/extra.xml');
120:
121: // 気象庁防災情報XML:長期フィード - 随時【変更不可】
122: define('FEED_L', 'https://www.data.jma.go.jp/developer/xml/feed/extra_l.xml');
123:
124: // 各種定数(END) ============================================================

予報円を間引く条件 THIN_OUT は、台風が停滞していたり、移動速度が極めてゆっくりの場合、予報円の密度が高すぎて地図が見にくくなる。そこで、前回予報円がこの距離以下であれば次の予報円を描かない(間引く)ことができる。0を代入すれば、すべての予報円を描くようになる。

出力結果を Twitter(現・X) に投稿することができる。投稿機能を有効化するときは、定数 TWITTER を TRUE にする。ユーザー定義クラス pahooTwitterAPI を利用するので、"pahooTwitterAPI.php" をinclude_pathが通ったディレクトリに配置すること。また、事前にアプリケーションの登録が必要になる。詳しくは「PHPでTwitter(現・X)に投稿(ツイート)する」を参照してほしい。

出力結果を Bluesky に投稿することができる。投稿機能を有効化するときは、定数 BLUESKY を TRUE にする。ユーザー定義クラス pahooBlueskyAPI を利用するので、"pahooBlueskyAPI.php" をinclude_pathが通ったディレクトリに配置すること。また、事前にアプリケーションの登録が必要になる。詳しくは「PHPでPHPでBlueskyに投稿する」を参照してほしい。

Twitter(現・X) や Bluesky のボタン・アイコンについては、「HTMLとCSSでさまざまなアイコンを表示する」を参照して欲しい。
気象庁防災情報XMLフォーマット
今回は、高頻度 - 随時フィード および 長期 - 随時フィード にアクセスし、電文コード VPTW60 の台風解析・予報情報(5日予報)を取得する。
VPTW60の構造
注意すべきは、1つのXMLファイルの中に1つまたは複数の台風情報が含まれていること。
また、過去の中心位置は分からないので、キャッシュ・ファイルとして保存した VZSA50 から中心位置を拾い出し、それを台風の過去進路にする方針である。
また、VPTW60 の発表時刻によっては予報円の情報が無い。その場合、予報円情報が存在する最も新しい VPTW60 から予報円情報を取り出すことにする。
解説:台風に関する情報URLを取得
typhoon.php
400: /**
401: * 気象庁防災情報XMLから台風に関する情報URLを取得
402: * @param array $urls URL格納配列
403: * @param string $errmsg エラーメッセージ格納用
404: * @return bool TRUE:取得成功/FALSE:取得失敗
405: */
406: function jmaGetTyphoonURLs(&$urls, &$errmsg) {
407: // URLパターン
408: $vptw = '/https?\:\/\/www\.data\.jma\.go\.jp\/developer\/xml\/data\/([0-9\_]+)VPTW6[0-9]+\_[0-9]+\.xml/ui';
409:
410: // 随時フィードの解析
411: $cnt = 0;
412: $pcc = new pahooCache(LIFE_CACHE_FEED, DIR_CACHE_FEED);
413: $xml = $pcc->simplexml_load(FEED);
414: // レスポンス・チェック
415: if ($pcc->iserror() || !isset($xml->entry)) {
416: $errmsg = '気象庁防災情報XMLにアクセスできません';
417: return FALSE;
418: }
419: foreach ($xml->entry as $node) {
420: // URLを取得
421: if (preg_match($vptw, $node->id, $arr) > 0) {
422: $urls[$cnt] = $arr[0];
423: $cnt++;
424: }
425: }
426: $pcc = NULL;
427:
428: // 長期フィードの解析
429: $pcc = new pahooCache(LIFE_CACHE_FEED_L, DIR_CACHE_FEED_L);
430: $xml = $pcc->simplexml_load(FEED_L);
431: // レスポンス・チェック
432: if ($pcc->iserror() || !isset($xml->entry)) {
433: $errmsg = '気象庁防災情報XMLにアクセスできません';
434: return FALSE;
435: }
436: foreach ($xml->entry as $node) {
437: // URLを取得
438: if (preg_match($vptw, $node->id, $arr) > 0) {
439: if (array_search($arr[0], $urls) === FALSE) {
440: $urls[$cnt] = $arr[0];
441: $cnt++;
442: }
443: }
444: }
445: $pcc = NULL;
446:
447: // エラー・チェック
448: if ($cnt == 0) {
449: $errmsg = '直近の台風情報はありません';
450: return FALSE;
451: }
452:
453: // URLを日時の新しい順にソート
454: rsort($urls);
455:
456: return TRUE;
457: }
正規表現で VPTW60 を含むURLを取り出し配列に格納し、日時の新しい順に並べ替えておく。
解説:台風情報を読み込む
typhoon.php
459: /**
460: * 新しい台風報かどうか
461: * @param object $xml 気象庁防災情報XML
462: * @param array $items 台風情報を格納する配列
463: * @return bool TRUE:新しい情報
464: */
465: function isNewTyphoon($xml, $items) {
466: $res = FALSE;
467:
468: if (isset($xml->Body->MeteorologicalInfos->MeteorologicalInfo->Item->Kind->Property->TyphoonNamePart->Number)) {
469: $num = (string)$xml->Body->MeteorologicalInfos->MeteorologicalInfo->Item->Kind->Property->TyphoonNamePart->Number;
470: if ($num != '') {
471: if (! isset($items[$num])) {
472: $res = TRUE;
473: }
474: }
475: }
476:
477: return $res;
478: }
typhoon.php
480: /**
481: * 台風報取得(気象庁防災情報XMLから)
482: * @param object $pgc pahooGeoCodeオブジェクト
483: * @param array $items 台風情報を格納する配列
484: * @param string $urls 情報XMLのURLを格納する配列
485: * @param string $errmsg エラーメッセージ格納用
486: * @return bool TRUE:取得成功/FALSE:失敗
487: */
488: function getTyphoon($pgc, &$items, &$urls, &$errmsg) {
489: // 名前空間
490: define('JMX_EB', 'http://xml.kishou.go.jp/jmaxml1/elementBasis1/');
491: // マッチングパターン
492: $pat2 = '/([\+\-][0-9]{1,2}\.[0-9]+)([\+\-][0-9]{1,3}\.[0-9]+)/ui'; // 緯度・経度
493: $pat3 = '/(予報)[ ]*([01234567890-9]+)時間後/ui'; // 推定|予報
494: // 予報円は取得済みか否か
495: $flag_forecast = array();
496:
497: // オブジェクト生成
498: $pcc = new pahooCache(LIFE_CACHE_DATA, DIR_CACHE_DATA);
499:
500: // 最新の台風に関する情報URLを取得
501: jmaGetTyphoonURLs($urls, $errmsg);
502: if ($errmsg != '') return FALSE;
503:
504: foreach ($urls as $key=>$vptw) {
505: // 台風情報の取得
506: $xml = $pcc->simplexml_load($vptw);
507: // レスポンス・チェック
508: if ($pcc->iserror() || !isset($xml->Body->MeteorologicalInfos)) {
509: $errmsg = '気象庁防災情報XMLから台風情報を取得できません';
510: return FALSE;
511: }
512: $flag_f = FALSE; // 予報円取得済みか否か
513:
514: // 最新の台風情報
515: if (isNewTyphoon($xml, $items)) {
516: foreach ($xml->Body->MeteorologicalInfos as $infos) {
517: $cnt = 0;
518: foreach ($infos->MeteorologicalInfo as $info) {
519: // 実況
520: if ($info->DateTime['type'] == '実況') {
521: foreach ($info->Item->Kind as $kind) {
522: if (isset($kind->Property->Type)) {
523: // 呼称
524: if ($kind->Property->Type == '呼称') {
525: $num = (string)$kind->Property->TyphoonNamePart->Number;
526: if ($num == '') break;
527: $items[$num]['Name'] = (string)$kind->Property->TyphoonNamePart->Name;
528: $items[$num]['NameKana'] = (string)$kind->Property->TyphoonNamePart->NameKana;
529: $items[$num][$cnt]['kind'] = '実況';
530: $items[$num][$cnt]['DateTime'] = (string)$info->DateTime;
531: // その台風の予報円は未取得
532: $flag_forecast[$num] = FALSE;
533: // 古い台風情報かどうか
534: $tt = time() - strtotime($info->DateTime);
535: $items[$num]['Valid'] = ($tt < SCRAP_TIME) ? TRUE : FALSE;
536: // 階級
537: } else if ($kind->Property->Type == '階級') {
538: $node = $kind->Property->ClassPart->children(JMX_EB);
539: $items[$num][$cnt]['TyphoonClass'] = (string)$node->TyphoonClass;
540: $items[$num][$cnt]['AreaClass'] = (string)$node->AreaClass;
541: $items[$num][$cnt]['IntensityClass'] = (string)$node->IntensityClass;
542: // 中心
543: } else if ($kind->Property->Type == '中心') {
544: $node = $kind->Property->CenterPart->children(JMX_EB);
545: // 中心位置
546: $items[$num][$cnt]['Location'] = (string)$kind->Property->CenterPart->Location;
547: foreach ($node->Coordinate as $val) {
548: if (preg_match($pat2, $val, $arr) > 0) {
549: $items[$num][$cnt]['latitude'] = (float)$arr[1];
550: $items[$num][$cnt]['longitude'] = (float)$arr[2];
551: }
552: }
553: // 移動速度
554: $items[$num][$cnt]['Direction'] = (string)$node->Direction;
555: foreach ($node->Speed as $val) {
556: if (isset($val->attributes()['condition'])) {
557: $items[$num][$cnt]['Speed'] = (string)$val->attributes()['condition'];
558: } else if ($val->attributes()['unit'] == 'km/h') {
559: $items[$num][$cnt]['Speed'] = (string)$val;
560: }
561: }
562: // 中心気圧
563: $items[$num][$cnt]['Pressure'] = (int)$node->Pressure;
564: // 風
565: } else if ($kind->Property->Type == '風') {
566: $node = $kind->Property->WindPart->children(JMX_EB);
567: foreach ($node->WindSpeed as $val) {
568: if (($val->attributes()['condition'] == '中心付近') && ($val->attributes()['unit'] == 'm/s')) {
569: $items[$num][$cnt]['centerWindSpeed'] = (int)$val;
570: } else if (($val->attributes()['type'] == '最大瞬間風速') && ($val->attributes()['unit'] == 'm/s')) {
571: $items[$num][$cnt]['maxWindSpeed'] = (int)$val;
572: }
573: }
574: // 暴風域・強風域
575: foreach ($kind->Property->WarningAreaPart as $val) {
576: $key = (string)$val['type'];
577: // 半径
578: $node = $val->children(JMX_EB);
579: $n = 0;
580: foreach ($node->Circle->Axes->Axis as $axis) {
581: if (isset($axis->Direction) && ($axis->Direction != '')) {
582: $items[$num][$cnt][$key][$n]['direction'] = (string)$axis->Direction;
583: } else {
584: $items[$num][$cnt][$key][$n]['direction'] = (string)$axis->Direction->attributes()['condition'];
585: }
586: foreach ($axis->Radius as $val) {
587: if ($val->attributes()['unit'] == 'km') {
588: $items[$num][$cnt][$key][$n]['radius'] = (int)$val;
589: }
590: }
591: $n++;
592: }
593: }
594: }
595: }
596: }
597: // 予報
598: } else if (preg_match($pat3, (string)$info->DateTime['type'], $arr) > 0) {
599: if ($num == '') break; // ver.2.04
600: $cnt++;
601: $items[$num][$cnt]['kind'] = '予報';
602: $items[$num][$cnt]['DateTime'] = (string)$info->DateTime;
603: $flag_f = TRUE; // 予報円取得済み
604: foreach ($info->Item->Kind as $kind) {
605: // 階級
606: if ($kind->Property->Type == '階級') {
607: $node = $kind->Property->ClassPart->children(JMX_EB);
608: $items[$num][$cnt]['TyphoonClass'] = (string)$node->TyphoonClass;
609: $items[$num][$cnt]['AreaClass'] = isset($node->AreaClass) ? (string)$node->AreaClass : '';
610: $items[$num][$cnt]['IntensityClass'] = isset($node->IntensityClass) ? (string)$node->IntensityClass : '';
611: // 中心位置(予報円)
612: } else if ($kind->Property->Type == '中心') {
613: $node = $kind->Property->CenterPart->ProbabilityCircle->children(JMX_EB);
614: foreach ($node->BasePoint as $val) {
615: if (preg_match($pat2, $val, $arr) > 0) {
616: $items[$num][$cnt]['latitude'] = (float)$arr[1];
617: $items[$num][$cnt]['longitude'] = (float)$arr[2];
618: }
619: }
620: // 半径
621: $n = 0;
622: foreach ($node->Axes->Axis as $axis) {
623: if (isset($axis->Direction) && ($axis->Direction != '')) {
624: $items[$num][$cnt]['予報円'][$n]['direction'] = (string)$axis->Direction;
625: } else {
626: $items[$num][$cnt]['予報円'][$n]['direction'] = (string)$axis->Direction->attributes()['condition'];
627: }
628: foreach ($axis->Radius as $val) {
629: if ($val->attributes()['unit'] == 'km') {
630: $items[$num][$cnt]['予報円'][$n]['radius'] = (int)$val;
631: }
632: }
633: $n++;
634: }
635: // 風
636: } else if ($kind->Property->Type == '風') {
637: $node = $kind->Property->WindPart->children(JMX_EB);
638: foreach ($node->WindSpeed as $val) {
639: if (($val->attributes()['condition'] == '中心付近') && ($val->attributes()['unit'] == 'm/s')) {
640: $items[$num][$cnt]['centerWindSpeed'] = (int)$val;
641: } else if (($val->attributes()['type'] == '最大瞬間風速') && ($val->attributes()['unit'] == 'm/s')) {
642: $items[$num][$cnt]['maxWindSpeed'] = (int)$val;
643: }
644: }
645: }
646: }
647: }
648: }
649: }
650: // 過去の台風情報
651: } else {
652: foreach ($xml->Body->MeteorologicalInfos as $infos) {
653: foreach ($infos->MeteorologicalInfo as $info) {
654: // 過去の位置
655: if ($info->DateTime['type'] == '実況') {
656: foreach ($info->Item->Kind as $kind) {
657: if (isset($kind->Property->Type)) {
658: // 呼称
659: if ($kind->Property->Type == '呼称') {
660: $num = (string)$kind->Property->TyphoonNamePart->Number;
661: if ($num == '') break;
662: $cnt = 0;
663: while (isset($items[$num][$cnt])) $cnt++;
664: $items[$num][$cnt]['kind'] = '過去';
665: $items[$num][$cnt]['DateTime'] = (string)$info->DateTime;
666: // 中心
667: } else if ($kind->Property->Type == '中心') {
668: $node = $kind->Property->CenterPart->children(JMX_EB);
669: // 中心位置
670: foreach ($node->Coordinate as $val) {
671: if (preg_match($pat2, $val, $arr) > 0) {
672: $items[$num][$cnt]['latitude'] = (float)$arr[1];
673: $items[$num][$cnt]['longitude'] = (float)$arr[2];
674: }
675: }
676: }
677: }
678: }
679: // 予報
680: } else if (isset($flag_forecast[$num]) && ! $flag_forecast[$num] && (preg_match($pat3, (string)$info->DateTime['type'], $arr) > 0)) {
681: if ($num == '') break; // ver.2.04
682: $cnt++;
683: $items[$num][$cnt]['kind'] = '予報';
684: $items[$num][$cnt]['DateTime'] = (string)$info->DateTime;
685: $flag_f = TRUE; // 予報円取得済み
686: foreach ($info->Item->Kind as $kind) {
687: // 階級
688: if ($kind->Property->Type == '階級') {
689: $node = $kind->Property->ClassPart->children(JMX_EB);
690: $items[$num][$cnt]['TyphoonClass'] = (string)$node->TyphoonClass;
691: $items[$num][$cnt]['AreaClass'] = isset($node->AreaClass) ? (string)$node->AreaClass : '';
692: $items[$num][$cnt]['IntensityClass'] = isset($node->IntensityClass) ? (string)$node->IntensityClass : '';
693: // 中心位置(予報円)
694: } else if ($kind->Property->Type == '中心') {
695: $node = $kind->Property->CenterPart->ProbabilityCircle->children(JMX_EB);
696: foreach ($node->BasePoint as $val) {
697: if (preg_match($pat2, $val, $arr) > 0) {
698: $items[$num][$cnt]['latitude'] = (float)$arr[1];
699: $items[$num][$cnt]['longitude'] = (float)$arr[2];
700: }
701: }
702: // 半径
703: $n = 0;
704: foreach ($node->Axes->Axis as $axis) {
705: if (isset($axis->Direction) && ($axis->Direction != '')) {
706: $items[$num][$cnt]['予報円'][$n]['direction'] = (string)$axis->Direction;
707: } else {
708: $items[$num][$cnt]['予報円'][$n]['direction'] = (string)$axis->Direction->attributes()['condition'];
709: }
710: foreach ($axis->Radius as $val) {
711: if ($val->attributes()['unit'] == 'km') {
712: $items[$num][$cnt]['予報円'][$n]['radius'] = (int)$val;
713: }
714: }
715: $n++;
716: }
717: // 風
718: } else if ($kind->Property->Type == '風') {
719: $node = $kind->Property->WindPart->children(JMX_EB);
720: foreach ($node->WindSpeed as $val) {
721: if (($val->attributes()['condition'] == '中心付近') && ($val->attributes()['unit'] == 'm/s')) {
722: $items[$num][$cnt]['centerWindSpeed'] = (int)$val;
723: } else if (($val->attributes()['type'] == '最大瞬間風速') && ($val->attributes()['unit'] == 'm/s')) {
724: $items[$num][$cnt]['maxWindSpeed'] = (int)$val;
725: }
726: }
727: }
728: }
729: }
730: }
731: }
732: }
733: // その台風の予報円は取得済み
734: if ($flag_f) {
735: $flag_forecast[$num] = TRUE;
736: }
737: }
738: // オブジェクト解放
739: $pcc = NULL;
740:
741: return TRUE;
742: }

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

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

前述の通り、VPTW60 の発表日時によっては予報円情報を含まないことがある。
そこで、台風毎に配列 $flag_forecast に予報円を取得したかどうかのフラグを持たせる。もし isNewTyphoon で取得した最新の VPTW60 に予報円情報が無ければ、過去の台風情報から予報円情報を取得する。
このため、ほぼ同じ予報取得プロセスが2箇所に書く格好になっており、美しさに欠けてしまった。
解説:台風情報を描くスクリプトを生成
typhoon.php
789: /**
790: * 暴風域、強風域、予報円の描画スクリプトを生成する
791: * @param object $pgc pahooGeoCodeオブジェクト
792: * @param array $infos 台風情報
793: * @return string スクリプト/FALSE:生成失敗
794: */
795: function jsTyphoonMap($pgc, $infos) {
796: $js = '';
797: foreach ($infos as $info) {
798: $key = 0;
799: // 台風以外ならスキップ
800: if (! isset($info[$key]['TyphoonClass'])) continue; // v.2.11
801: if (preg_match('/台風/ui', $info[$key]['TyphoonClass']) == 0) continue;
802: // 古い台風情報ならスキップ
803: if ($info['Valid'] == FALSE) continue;
804:
805: // 暴風域
806: list($latitude, $longitude, $radius) = shiftCircle($pgc, $info[$key]['latitude'], $info[$key]['longitude'], $info[$key]['暴風域']);
807: $radius *= 1000;
808: $js .= $pgc->jsCircle($longitude, $latitude, $radius, COLOR_WIND2, '1.0', 2, MAPSERVICE);
809: // 強風域
810: list($latitude, $longitude, $radius) = shiftCircle($pgc, $info[$key]['latitude'], $info[$key]['longitude'], $info[$key]['強風域']);
811: $radius *= 1000;
812: $js .= $pgc->jsCircle($longitude, $latitude, $radius, COLOR_WIND1, '1.0', 2, MAPSERVICE);
813:
814: $key = 1;
815: $cnt = 1;
816: $lng_0 = $points[0]['longitude'] = $info[0]['longitude'];
817: $lat_0 = $points[0]['latitude'] = $info[0]['latitude'];
818: while (isset($info[$key])) {
819: // 予報円
820: if ($info[$key]['kind'] == '予報') {
821: // 予報円を間引くかどうか
822: $dd = $pgc->greatCircleDistance($lng_0, $lat_0, $info[$key]['longitude'], $info[$key]['latitude']);
823: if ($dd > THIN_OUT) {
824: list($latitude, $longitude, $radius) = shiftCircle($pgc, $info[$key]['latitude'], $info[$key]['longitude'], $info[$key]['予報円']);
825: $radius *= 1000;
826: $js .= $pgc->jsCircle($longitude, $latitude, $radius, COLOR_FORECAST, '1.0', 2, MAPSERVICE);
827: preg_match('/[0-9]{4}\-[0-9]{2}\-([0-9]{2})T([0-9]{2})/ui', $info[$key]['DateTime'], $arr);
828: $dt = sprintf('%d日%d時', $arr[1], $arr[2]);
829: list($lat, $lng) = $pgc->getPointDistance($longitude, $latitude, 0 - $radius - 30000, 9);
830: $js .= $pgc->jsLabel($lat, $lng, $dt, 12, COLOR_FORECAST, 'normal', MAPSERVICE);
831: $lat_0 = $info[$key]['latitude'];
832: $lng_0 = $info[$key]['longitude'];
833: }
834:
835: // 過去の位置
836: } else {
837: $points[$cnt]['longitude'] = $info[$key]['longitude'];
838: $points[$cnt]['latitude'] = $info[$key]['latitude'];
839: $cnt++;
840: }
841: $key++;
842: }
843: // 過去の移動経路
844: if ($js != '') {
845: $js .= $pgc->jsLine($points, COLOR_LINE, '1.0', 3, MAPSERVICE);
846: $points = array();
847: $cnt = 0;
848: if (isset($info[$key]['longitude'])) {
849: $points[0]['longitude'] = $info[$key]['longitude'];
850: $points[0]['latitude'] = $info[$key]['latitude'];
851: }
852: }
853: }
854:
855: // HTMLの画像化
856: $js .= js_html2image();
857:
858: return $js;
859: }
前述の通り、円の中心がズレている場合があり、そのためのユーザー関数 shiftCircle を呼び出して使う。

前回の予報円の中心座標 ($lat_0, $lng_0) (初回は現在の台風の中心座標)からの大圏航路距離をメソッド greatCircleDistance によって計算し、予報円の間引き条件 THIN_OUT 以下であれば予報円を描かない。
解説:マップ描画用情報を生成
typhoon.php
861: /**
862: * 台風情報からマップ描画用情報を生成する
863: * @param array $infos 台風情報
864: * @param array $items マップ描画用情報を格納
865: * @param string $table HTML文(表形式)を格納
866: * @param int $count 有効な台風情報の数
867: * @param objct $pgc pahooGeoCodeオブジェクト
868: * @return array(日時,緯度,経度) 発表日時,予報円の最後の中心座標,地図ズーム
869: */
870: function getTyphoonInfo($infos, &$items, &$table, &$count, $pgc) {
871: // 地図ズーム値=距離換算表
872: $zooms = array(10000, 5000, 3000, 1000, 500, 300);
873:
874: // 台風情報一覧
875: $table =<<< EOT
876: <table class="plists">
877: <th>名称</th>
878: <th>位置</th>
879: <th>中心気圧</th>
880: <th>最大瞬間風速</th>
881: <th>進路</th>
882: </tr>
883:
884: EOT;
885:
886: $dt0 = $lat0 = $lng0 = $zoom = FALSE;
887: $distance = 99999;
888: $cnt = 1;
889: foreach ($infos as $nn=>$info) { // v.2.11
890: $key = 0; // v.2.11
891: if (! isset($info[$key]['TyphoonClass'])) continue; // v.2.11
892: if (preg_match('/台風/ui', $info[0]['TyphoonClass']) > 0) {
893: $num = sprintf('台%d', (int)substr($nn, 2, 2)); // v.2.11
894: $name = sprintf('台風%d号(%s)', (int)substr($nn, 2, 2), $info['NameKana']);
895: } else {
896: continue;
897: }
898: // 古い台風情報ならスキップ
899: if ($info['Valid'] == FALSE) continue;
900:
901: $items[$cnt]['longitude'] = $info[0]['longitude'];
902: $items[$cnt]['latitude'] = $info[0]['latitude'];
903: if ($cnt == 1) {
904: preg_match('/([0-9]{4})\-([0-9]{2})\-([0-9]{2})T([0-9]{2})\:/ui', $info[0]['DateTime'], $arr);
905: $dt0 = $info[0]['DateTime'] = sprintf('%d年%d月%d日%d時', $arr[1], $arr[2], $arr[3], $arr[4]);
906: }
907: $items[$cnt]['label'] = $num;
908: $items[$cnt]['label_color'] = COLOR_NAME1;
909: $items[$cnt]['label_size'] = 16;
910: $items[$cnt]['label_weight'] = 'bold';
911: // 情報ウィンドウ
912: if ($info[0]['AreaClass'] != '') {
913: $AreaClass = $info[0]['AreaClass'];
914: } else {
915: $AreaClass = '-';
916: }
917: if ($info[0]['IntensityClass'] != '') {
918: $IntensityClass = $info[0]['IntensityClass'];
919: } else {
920: $IntensityClass = '-';
921: }
922: if ($items[$cnt]['longitude'] >= 0) {
923: $lng = sprintf('東経%.1f度', $items[$cnt]['longitude']);
924: } else {
925: $lng = sprintf('西経%.1f度', 0 - $items[$cnt]['longitude']);
926: }
927: if ($items[$cnt]['latitude'] >= 0) {
928: $lat = sprintf('北緯%.1f度', $items[$cnt]['latitude']);
929: } else {
930: $lat = sprintf('南緯%.1f度', 0 - $items[$cnt]['latitude']);
931: }
932: if ($info[0]['Direction'] != '') {
933: $directioin = $info[0]['Direction'] . 'へ';
934: } else {
935: $directioin = '';
936: }
937: if (is_numeric($info[0]['Speed'])) {
938: $speed = sprintf('時速%dkm', $info[0]['Speed']);
939: } else {
940: $speed = $info[0]['Speed'];
941: }
942: if (isset($info[0]['centerWindSpeed']) && is_numeric($info[0]['centerWindSpeed'])) {
943: $centerWindSpeed = $info[0]['centerWindSpeed'] . 'メートル';
944: } else {
945: $centerWindSpeed = '-';
946: }
947: if (isset($info[0]['maxWindSpeed']) && is_numeric($info[0]['maxWindSpeed'])) {
948: $maxWindSpeed = $info[0]['maxWindSpeed'] . 'メートル';
949: } else {
950: $maxWindSpeed = '-';
951: }
952: $items[$cnt]['title'] = '';
953: $items[$cnt]['description'] =<<< EOT
954: <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 />
955: EOT;
956: // 台風情報一覧
957: $table .=<<< EOT
958: <tr>
959: <td>{$name}</td>
960: <td>{$info[0]['Location']}</td>
961: <td>{$info[0]['Pressure']}hPa</td>
962: <td>{$maxWindSpeed}</td>
963: <td>{$directioin}{$speed}</td>
964: </tr>
965:
966: EOT;
967: // 3日以内の予報円の中心座標を求める v.2.01
968: $m = 1;
969: while (isset($info[$m])) {
970: if ($info[$m]['kind'] == '予報') {
971: $lat1 = $info[$m]['latitude'];
972: $lng1 = $info[$m]['longitude'];
973: if ($m >= 3) break;
974: }
975: $m++;
976: }
977: // 基準座標に近い方をマップ中心にする
978: $dist = $pgc->greatCircleDistance($lng1, $lat1, LONGITUDE00, LATITUDE00);
979: if ($dist < $distance) {
980: $lat0 = $lat1;
981: $lng0 = $lng1;
982: $distance = $dist;
983: // 台風中心と3日後予報円の距離から地図ズーム値を決める
984: $dist = $pgc->greatCircleDistance($lng0, $lat0, $info[0]['longitude'], $info[0]['latitude']);
985: $zoom = count($zooms) + 1;
986: foreach ($zooms as $key=>$val) {
987: if ($dist >= $val) {
988: $zoom = $key + 1;
989: break;
990: }
991: }
992: }
993:
994: $cnt++;
995: }
996:
997: $table .=<<< EOT
998: </table>
999:
1000: EOT;
1001: // 有効な台風情報の数
1002: $count = $cnt - 1;
1003:
1004: return array($dt0, $lat0, $lng0, $zoom);
1005: }
解説:メイン・プログラム
typhoon.php
1161: // 気象庁防災情報XMLから台風情報を取得
1162: $infos = array();
1163: $urls = array();
1164: $items = array();
1165: $dt = $date->format('Y年m月d日H時');
1166: $table = $errmsg = '';
1167: $count = 0;
1168: $js = FALSE;
1169: $ret = getTyphoon($pgc, $infos, $urls, $errmsg);
1170: if ($ret == FALSE) {
1171: $errmsg = $pgc->getError();
1172: } else {
1173: $js = jsTyphoonMap($pgc, $infos, $errmsg);
1174: list($dt, $latitude, $longitude, $zoom) = getTyphoonInfo($infos, $items, $table, $count, $pgc);
1175: if ($count == 0) {
1176: $longitude = DEF_LONGITUDE;
1177: $latitude = DEF_LATITUDE;
1178: $zoom = DEF_ZOOM;
1179: $type = DEF_TYPE;
1180: $dt = $date->format('Y年m月d日H時');
1181: }
1182: }
1183:
1184: // 現在の台風の数だけ表示
1185: if ($countOnly) {
1186: echo $count;
1187: exit(0);
1188: }
1189:
1190: // マップ作成
1191: if (($errmsg == '') && ($js != FALSE)) {
1192: $jsmap = $pgc->drawJSMap(MAPID, $latitude, $longitude, $type, $zoom, NULL, $items, MAPSERVICE, $js, (MAP_WIDTH / 2));
1193: } else {
1194: $jsmap = $pgc->drawJSMap(MAPID, $latitude, $longitude, $type, $zoom, NULL, $items, MAPSERVICE);
1195: }
1196:
1197: // Twitter(現・X)、Blueskyへ投稿する.
1198: $message =<<< EOT
1199: 🌀台風情報 {$dt}現在
1200:
1201: (ご参考)PHPで台風情報を取得する https://www.pahoo.org/e-soul/webtech/php05/php05-15-01.shtm #台風 #台風情報 #nhk #ntv #tbs #fujitv #tvasahi
1202:
1203: EOT;
1204: if (TWITTER && isButton('tweet')) {
1205: mediaTweet($message, $res);
1206: }
1207: if (BLUESKY && isButton('bluesky')) {
1208: mediaBluesky($message, $res);
1209: }
1210:
1211: // 表示コンテンツを作成する.
1212: $HtmlBody = makeCommonBody($dt, $jsmap, $table, $urls, $res, $errmsg, $count);
1213:
1214: // ブラウザに表示する.
1215: echo $HtmlHeader;
1216: echo $HtmlBody;
1217: echo $HtmlFooter;
ここまででエラーがなければ、マップを生成する。
エラーがあれば、台風情報を描かずにマップのみ生成する。
解説:SNS投稿機能

コンテンツを描画しているのはクライアントにあるブラウザ(レンダリングエンジン)であることから、クライアント側で画像を作成し、サーバ側で SNS の API をコールするという方針とした。
- ブラウザはサーバにコンテンツ描画をリクエストする。
- サーバはデータサイトからデータ取得する。(サーバキャッシュにデータがあればそれを利用する)
- サーバはコンテンツ描画スクリプトを生成する。
- サーバはブラウザへレスポンス(HTML文)を返す。
- ブラウザはコンテンツをレンダリングする。
- ブラウザはレンダリングしたコンテンツを画像データとしてサーバへアップロードする。
- サーバは SNS の API を使ってツイートする。
- サーバは SNS へメッセージと画像を送る。
- サーバはブラウザへレスポンス(HTML文)を返す。
解説:html2canvasライブラリ
typhoon.php
149: <script src="https://html2canvas.hertzen.com/dist/html2canvas.js"></script>
150:
151: <style>
152: p.werror {
153: color: red;
154: }
画像化を実行するJavaScript関数は html2canvas である。
typhoon.php
1114: <div id="{$target}" name="{$target}" style="width:{$width2}px;">
1115: <p>
1116: 🌀台風情報 {$dt}現在{$res2}
1117: {$tweet}{$bluesky}
1118: </p>
1119: <div id="{$mapid}" style="width:{$width}px; height:{$height}px;"></div>
1120: {$table}
1121: </div>
レンダリングエンジンによって違うのかもしれないが、html2canvas ライブラリによって画像化される範囲が実際よりやや小さいため、あえてマップ領域より、幅を20ピクセル大きくした範囲を画像化範囲としている。
typhoon.php
335: /**
336: * HTMLオブジェクトの画像化
337: * @param なし
338: * @return string JavaScriptコード
339: */
340: function js_html2image() {
341: $target = TARGET;
342: $js = '';
343:
344: // Googleマップの場合
345: if (MAPSERVICE == 0) {
346: $js .=<<< EOT
347: google.maps.event.addListener(map, 'tilesloaded', function() {
348: var capture = document.querySelector('#{$target}');
349: html2canvas(capture, {useCORS: true, allowTaint:true}).then(canvas => {
350: var base64 = canvas.toDataURL('image/png'); // 画像化
351: $('#base64').val(base64);
352: });
353: });
354:
355: EOT;
356:
357: // Leafletの場合(ブラウザによってはうまく動作しない)
358: } else {
359: $js .=<<< EOT
360: HTMLCanvasElement.prototype.getContext = function(origFn) {
361: return function(type, attribs) {
362: attribs = attribs || {};
363: attribs.preserveDrawingBuffer = true;
364: return origFn.call(this, type, attribs);
365: };
366: } (HTMLCanvasElement.prototype.getContext);
367:
368: // HTML画像化イベント登録
369: function html2image() {
370: var capture = document.querySelector('#{$target}');
371: html2canvas(capture, {useCORS: true, allowTaint:true}).then(canvas => {
372: var base64 = canvas.toDataURL('image/png'); // 画像化
373: $('#base64').val(base64);
374: });
375: };
376:
377: // ズーム変更イベント
378: map.on('zoomend', function() {
379: html2image();
380: });
381:
382: // マップ移動イベント
383: map.on('moveend', function() {
384: html2image();
385: });
386:
387: // html2image()を発火させるために,ズームアウトして500msec後に元に戻す.
388: var zoom = map.getZoom();
389: map.setZoom(zoom - 1);
390: setTimeout(function() {
391: map.setZoom(zoom);
392: }, 500);
393:
394: EOT;
395: }
396:
397: return $js;
398: }
Leaflet の場合、これに相当するイベントがないため、ズーム変更完了イベントにフックし、強制的にズーム変更イベントを発生させるタイミングで画像化する。しかし、Leaflet では、画像を描画するレイヤ構造の関係で、html2canvas ライブラリでは位置ずれが起きることがわかっている。その場合は、Googleマップを選択してほしい。また、対策方法をご存じの方がいたらお知らせいただきたい。
これらをJavaScriptとして生成するユーザー関数が js_html2image である。
準備:pahooTwitterAPI クラス
pahooTwitterAPI.php
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データ)
pahooTwitterAPI.php
587: /**
588: * バイナリデータを使ったメディア付きメッセージをツイートする.
589: * Tweetet API v2 を使用する.
590: * @param string $message 投稿メッセージ(UTF-8限定)
591: * @param array $items メディアデータ(バイナリデータ配列)
592: * @return bool TRUE:リクエスト成功/FALSE:失敗
593: */
594: function tweet_media_raw($message, $items) {
595: //メディアのアップロード
596: $media_ids = array();
597: $cnt = 0;
598: //Tweetet API v1.1 を使用する(v2にメディアアップロードが未実装のため)
599: $this->connection->setApiVersion('1.1');
600: foreach ($items as $data) {
601: $tmpname = $this->saveTempFile($data);
602: // $media = $this->connection->upload('media/upload', ['media' => $tmpname]);
603: $media = $this->connection->upload('media/upload', ['media' => $tmpname], ['chunkedUpload' => true]); //twitteroauth 7.0.0 対応
604: unlink($tmpname);
605: if (! isset($media->media_id_string)) break; //処理失敗
606: $media_ids[] = (string)$media->media_id_string;
607: $cnt++;
608: if ($cnt > 3) break; //最大4つまで
609: }
610:
611: //メディア付きツイート(Tweetet API v2 を使用する)
612: $this->connection->setApiVersion('2');
613: $option = [
614: 'text' => $message,
615: 'media' => [
616: 'media_ids' => $media_ids
617: ]
618: ];
619: // $status = $this->connection->post('tweets', $option, TRUE);
620: $status = $this->connection->post('tweets', $option, ['jsonPayload' => true]); //twitteroauth 7.0.0 対応
621: $this->webapi = 'https://api.twitter.com/2/tweets';
622:
623: //処理に成功した.
624: if ($this->isSuccess()) {
625: $this->responses = $status->data;
626: $this->errcode = NULL;
627: $this->errmsg = '';
628: $this->error = FALSE;
629: $res = TRUE;
630: //処理に失敗した.
631: } else {
632: if ($this->isAuthError() == FALSE) {
633: $this->errmsg = $status->detail;
634: $this->error = TRUE;
635: }
636: $res = FALSE;
637: }
638: return $res;
639: }
解説:Twitter(現・X)へ投稿する
typhoon.php
310: /**
311: * Twitter(現・X)投稿
312: * @param string $message 投稿文
313: * @param string $res 応答メッセージ格納用
314: * @return bool TRUE:成功/FALSE:失敗または未処理
315: */
316: function mediaTweet($message, &$res) {
317: if (! TWITTER) return FALSE;
318:
319: $ret = TRUE;
320: if (isset($_POST['base64']) && ($_POST['base64'] != '')) {
321: $base64 = preg_replace('/data\:image\/png\;base64\,/ui', '', $_POST['base64']);
322: $raws = array(base64_decode($base64));
323: $ptw = new pahooTwitterAPI();
324: $ptw->tweet_media_raw($message, $raws);
325: $errmsg = $ptw->errmsg;
326: $ret = ! $ptw->error;
327: $ptw = NULL;
328: if ($ret) {
329: $res = 'ツイートしました';
330: }
331: }
332: return $ret;
333: }
ブラウザからPOSTで受け取った画像データ $_POST['base64'] はBASE64でデコードされており、冒頭に余計なヘッダ情報が付いているので、このヘッダ情報を除き( preg_replace )、バイナリデータにデコードする( base64_decode )。
続いて pahooTwitterAPI クラスを呼び出し、tweet_media_raw メソッドを使って、メッセージと画像を一気に投稿する。
準備:pahooBlueskyAPI クラス
pahooBlueskyAPI.php
15: class pahooBlueskyAPI {
16: var $pds; // PDSドメイン
17: var $webapi; // 直前に呼び出したWebAPI URL
18: var $errmsg; // エラーメッセージ
19: var $accessJwt; // accessJwt
20: var $refreshJwt; // refreshJwt
21:
22: const INTERNAL_ENCODING = 'UTF-8'; // 内部エンコーディング
23: const MAX_MESSAGE_LEN = 300; // 投稿可能なメッセージ文字数
24: const URL_LEN = 23; // メッセージ中のURL文字数(相当)
25: const MAX_IMAGE_WIDTH = 1200; // 投稿可能な最大画像幅(ピクセル)
26: const MAX_IMAGE_HEIGHT = 630; // 投稿可能な最大画像高さ(ピクセル)
27: // これより大きいときは自動縮小する
28: // トークンを保存するファイル名
29: // 秘匿性を保つことができ、かつ、PHPプログラムから読み書き可能であること
30: const FILENAME_TOKEN = './.token';
31:
32: // Bluesky API アプリパスワード
33: // https://bsky.app/
34: var $BLUESKY_HANDLE = '***************'; // ハンドル名
35: var $BLUESKY_PASSWORD = '***************'; // アプリケーション・パスワード
解説:Blueskyへ投稿する
typhoon.php
283: /**
284: * Bluesky投稿
285: * @param string $message 投稿文
286: * @param string $res 応答メッセージ格納用
287: * @return bool TRUE:成功/FALSE:失敗または未処理
288: */
289: function mediaBluesky($message, &$res) {
290: if (! BLUESKY) return FALSE;
291:
292: $ret = TRUE;
293: if (isset($_POST['base64']) && ($_POST['base64'] != '')) {
294: $base64 = preg_replace('/data\:image\/png\;base64\,/ui', '', $_POST['base64']);
295: $raws = array(base64_decode($base64));
296: $pbs = new pahooBlueskyAPI(BLUESKY_DOMAIN);
297: $res = $pbs->createSession();
298: $pbs->post($message, FALSE, NULL, NULL, $raws);
299: $errmsg = $pbs->geterror();
300: $ret = ! $pbs->iserror();
301: $res = $pbs->deleteSession();
302: $pbs = NULL;
303: if ($ret) {
304: $res = 'Blueskyに投稿しました';
305: }
306: }
307: return $ret;
308: }
ブラウザからPOSTで受け取った画像データ $_POST['base64'] はBASE64でデコードされており、冒頭に余計なヘッダ情報が付いているので、このヘッダ情報を除き( preg_replace )、バイナリデータにデコードする( base64_decode )。
続いて pahooBlueskyAPI クラスを呼び出し、post メソッドを使って、メッセージと画像を一気に投稿する。
解説:SNSへ投稿する(メイン・プログラム)
typhoon.php
1197: // Twitter(現・X)、Blueskyへ投稿する.
1198: $message =<<< EOT
1199: 🌀台風情報 {$dt}現在
1200:
1201: (ご参考)PHPで台風情報を取得する https://www.pahoo.org/e-soul/webtech/php05/php05-15-01.shtm #台風 #台風情報 #nhk #ntv #tbs #fujitv #tvasahi
1202:
1203: EOT;
1204: if (TWITTER && isButton('tweet')) {
1205: mediaTweet($message, $res);
1206: }
1207: if (BLUESKY && isButton('bluesky')) {
1208: mediaBluesky($message, $res);
1209: }
1210:
1211: // 表示コンテンツを作成する.
活用例
参考サイト
- 台風情報:気象庁
- JavaScriptでHTML表示を画像保存する:ぱふぅ家のホームページ
- HTMLとCSSでさまざまなアイコンを表示する:ぱふぅ家のホームページ
- Twitter API - WebAPIの登録方法:ぱふぅ家のホームページ
- Bluesky API - WebAPIの登録方法:ぱふぅ家のホームページ
- PHPでTwitterに画像付きメッセージ投稿:ぱふぅ家のホームページ
- PHPでBlueskyに投稿する:ぱふぅ家のホームページ
- 日本付近の台風情報 - 気象庁発表:みんなの知識 ちょっと便利帳
そこで、気象庁の台風情報のサイトから、現在日本に接近している台風の情報を取得するPHPスクリプトを作成してみることにする。
2021年(令和3年)2月24日の気象庁サイト・リニューアルにより、スクレイピングによる取り出しが難しくなったため、気象庁防災情報XMLからの情報取得に変更した。あわせてキャッシュ・システムを導入した。
(2024年11月1日)Bluesky投稿機能を追加
(2024年6月21日)TwitterOAuth 7.0.0 対応,Twitter(現・X)ボタンを "X" に変更