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

目次
- サンプル・プログラムの実行例
- サンプル・プログラム
- 準備: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.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 | 台風中心と予報円が入るようにズーム値を計算 |
2.3.0 | 2023/08/26 | 基準座標から近い方の台風を地図中心にくるように |
バージョン | 更新日 | 内容 |
---|---|---|
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 |
6.3.0 | 2023/07/02 | getPointsGSI()追加 |
6.2.0 | 2023/07/02 | ip2address()追加 |
バージョン | 更新日 | 内容 |
---|---|---|
1.1.1 | 2023/02/11 | コメント追記 |
1.1 | 2021/04/08 | simplexml_load()メソッド追加 |
1.0 | 2021/04/02 | 初版 |
バージョン | 更新日 | 内容 |
---|---|---|
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 |
5.2.0 | 2023/07/17 | oembed() v2対応 |
バージョン | 更新日 | 内容 |
---|---|---|
1.3.4 | 2024/10/31 | getOGPInformation() -- 文字化け対策 |
1.3.3 | 2024/10/22 | post() -- 画像投稿をカード情報投稿より優先 |
1.3.2 | 2024/10/22 | extractMediaURL() -- ローカル画像bugfix |
1.3.1 | 2024/10/20 | post() -- 返信,引用の引数仕様変更 |
1.3.0 | 2024/10/20 | getProfile, getDID, getRootParentID追加 |
準備: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:
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の登録方法」を参照されたい。
準備:地図サービスの選択
typhoon.php
68: //地図描画サービスの選択
69: // 0:Google
70: // 2:地理院地図・OSM
71: define('MAPSERVICE', 2);


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

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

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

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

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

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

コンテンツを描画しているのはクライアントにあるブラウザ(レンダリングエンジン)であることから、クライアント側で画像を作成し、サーバ側で SNS の API をコールするという方針とした。
- ブラウザはサーバにコンテンツ描画をリクエストする。
- サーバはデータサイトからデータ取得する。(サーバキャッシュにデータがあればそれを利用する)
- サーバはコンテンツ描画スクリプトを生成する。
- サーバはブラウザへレスポンス(HTML文)を返す。
- ブラウザはコンテンツをレンダリングする。
- ブラウザはレンダリングしたコンテンツを画像データとしてサーバへアップロードする。
- サーバは SNS の API を使ってツイートする。
- サーバは SNS へメッセージと画像を送る。
- サーバはブラウザへレスポンス(HTML文)を返す。
解説:html2canvasライブラリ
typhoon.php
142: <script src="https://html2canvas.hertzen.com/dist/html2canvas.js"></script>
143:
144: <style>
145: p.werror {
146: color: red;
147: }
画像化を実行するJavaScript関数は html2canvas である。
typhoon.php
1106: <div id="{$target}" name="{$target}" style="width:{$width2}px;">
1107: <p>
1108: 🌀台風情報 {$dt}現在{$res2}
1109: {$tweet}{$bluesky}
1110: </p>
1111: <div id="{$mapid}" style="width:{$width}px; height:{$height}px;"></div>
1112: {$table}
1113: </div>
レンダリングエンジンによって違うのかもしれないが、html2canvas ライブラリによって画像化される範囲が実際よりやや小さいため、あえてマップ領域より、幅を20ピクセル大きくした範囲を画像化範囲としている。
typhoon.php
328: /**
329: * HTMLオブジェクトの画像化
330: * @param なし
331: * @return string JavaScriptコード
332: */
333: function js_html2image() {
334: $target = TARGET;
335: $js = '';
336:
337: //Googleマップの場合
338: if (MAPSERVICE == 0) {
339: $js .=<<< EOT
340: google.maps.event.addListener(map, 'tilesloaded', function() {
341: var capture = document.querySelector('#{$target}');
342: html2canvas(capture, {useCORS: true, allowTaint:true}).then(canvas => {
343: var base64 = canvas.toDataURL('image/png'); //画像化
344: $('#base64').val(base64);
345: });
346: });
347:
348: EOT;
349:
350: //Leafletの場合(ブラウザによってはうまく動作しない)
351: } else {
352: $js .=<<< EOT
353: HTMLCanvasElement.prototype.getContext = function(origFn) {
354: return function(type, attribs) {
355: attribs = attribs || {};
356: attribs.preserveDrawingBuffer = true;
357: return origFn.call(this, type, attribs);
358: };
359: } (HTMLCanvasElement.prototype.getContext);
360:
361: //HTML画像化イベント登録
362: function html2image() {
363: var capture = document.querySelector('#{$target}');
364: html2canvas(capture, {useCORS: true, allowTaint:true}).then(canvas => {
365: var base64 = canvas.toDataURL('image/png'); //画像化
366: $('#base64').val(base64);
367: });
368: };
369:
370: //ズーム変更イベント
371: map.on('zoomend', function() {
372: html2image();
373: });
374:
375: //マップ移動イベント
376: map.on('moveend', function() {
377: html2image();
378: });
379:
380: //html2image()を発火させるために,ズームアウトして500msec後に元に戻す.
381: var zoom = map.getZoom();
382: map.setZoom(zoom - 1);
383: setTimeout(function() {
384: map.setZoom(zoom);
385: }, 500);
386:
387: EOT;
388: }
389:
390: return $js;
391: }
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
586: /**
587: * バイナリデータを使ったメディア付きメッセージをツイートする.
588: * Tweetet API v2 を使用する.
589: * @param string $message 投稿メッセージ(UTF-8限定)
590: * @param array $items メディアデータ(バイナリデータ配列)
591: * @return bool TRUE:リクエスト成功/FALSE:失敗
592: */
593: function tweet_media_raw($message, $items) {
594: //メディアのアップロード
595: $media_ids = array();
596: $cnt = 0;
597: //Tweetet API v1.1 を使用する(v2にメディアアップロードが未実装のため)
598: $this->connection->setApiVersion('1.1');
599: foreach ($items as $data) {
600: $tmpname = $this->saveTempFile($data);
601: // $media = $this->connection->upload('media/upload', ['media' => $tmpname]);
602: $media = $this->connection->upload('media/upload', ['media' => $tmpname], ['chunkedUpload' => true]); //twitteroauth 7.0.0 対応
603: unlink($tmpname);
604: if (! isset($media->media_id_string)) break; //処理失敗
605: $media_ids[] = (string)$media->media_id_string;
606: $cnt++;
607: if ($cnt > 3) break; //最大4つまで
608: }
609:
610: //メディア付きツイート(Tweetet API v2 を使用する)
611: $this->connection->setApiVersion('2');
612: $option = [
613: 'text' => $message,
614: 'media' => [
615: 'media_ids' => $media_ids
616: ]
617: ];
618: // $status = $this->connection->post('tweets', $option, TRUE);
619: $status = $this->connection->post('tweets', $option, ['jsonPayload' => true]); //twitteroauth 7.0.0 対応
620: $this->webapi = 'https://api.twitter.com/2/tweets';
621:
622: //処理に成功した.
623: if ($this->isSuccess()) {
624: $this->responses = $status->data;
625: $this->errcode = NULL;
626: $this->errmsg = '';
627: $this->error = FALSE;
628: $res = TRUE;
629: //処理に失敗した.
630: } else {
631: if ($this->isAuthError() == FALSE) {
632: $this->errmsg = $status->detail;
633: $this->error = TRUE;
634: }
635: $res = FALSE;
636: }
637: return $res;
638: }
解説:Twitter(現・X)へ投稿する
typhoon.php
303: /**
304: * ツイート処理
305: * @param string $message 投稿文
306: * @param string $res 応答メッセージ格納用
307: * @return bool TRUE:成功/FALSE:失敗または未処理
308: */
309: function mediaTweet($message, &$res) {
310: if (! TWITTER) return FALSE;
311:
312: $ret = TRUE;
313: if (isset($_POST['base64']) && ($_POST['base64'] != '')) {
314: $base64 = preg_replace('/data\:image\/png\;base64\,/ui', '', $_POST['base64']);
315: $raws = array(base64_decode($base64));
316: $ptw = new pahooTwitterAPI();
317: $ptw->tweet_media_raw($message, $raws);
318: $errmsg = $ptw->errmsg;
319: $ret = ! $ptw->error;
320: $ptw = NULL;
321: if ($ret) {
322: $res = 'ツイートしました';
323: }
324: }
325: return $ret;
326: }
ブラウザからPOSTで受け取った画像データ $_POST['base64'&$x5D; は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:
21: const INTERNAL_ENCODING = 'UTF-8'; //内部エンコーディング
22: const MAX_MESSAGE_LEN = 300; // 投稿可能なメッセージ文字数
23: const URL_LEN = 30; // メッセージ中のURL文字数(相当)
24: const MAX_IMAGE_WIDTH = 1200; // 投稿可能な最大画像幅(ピクセル)
25: const MAX_IMAGE_HEIGHT = 900; // 投稿可能な最大画像高さ(ピクセル)
26: // これより大きいときは自動縮小する
27:
28: // Bluesky API アプリパスワード
29: // https://bsky.app/
30: var $BLUESKY_HANDLE = '***************'; // ハンドル名
31: var $BLUESKY_PASSWORD = '***************'; // アプリケーション・パスワード
解説:Blueskyへ投稿する
typhoon.php
276: /**
277: * Bluesky投稿処理
278: * @param string $message 投稿文
279: * @param string $res 応答メッセージ格納用
280: * @return bool TRUE:成功/FALSE:失敗または未処理
281: */
282: function mediaBluesky($message, &$res) {
283: if (! BLUESKY) return FALSE;
284:
285: $ret = TRUE;
286: if (isset($_POST['base64']) && ($_POST['base64'] != '')) {
287: $base64 = preg_replace('/data\:image\/png\;base64\,/ui', '', $_POST['base64']);
288: $raws = array(base64_decode($base64));
289: $pbs = new pahooBlueskyAPI(BLUESKY_DOMAIN);
290: $res = $pbs->createSession();
291: $pbs->post($message, FALSE, NULL, NULL, $raws);
292: $errmsg = $pbs->geterror();
293: $ret = ! $pbs->iserror();
294: $res = $pbs->deleteSession();
295: $pbs = NULL;
296: if ($ret) {
297: $res = 'Blueskyに投稿しました';
298: }
299: }
300: return $ret;
301: }
ブラウザからPOSTで受け取った画像データ $_POST['base64'&$x5D; はBASE64でデコードされており、冒頭に余計なヘッダ情報が付いているので、このヘッダ情報を除き( preg_replace )、バイナリデータにデコードする( base64_decode )。
続いて pahooBlueskyAPI クラスを呼び出し、post メソッドを使って、メッセージと画像を一気に投稿する。
解説:SNSへ投稿する(メイン・プログラム)
typhoon.php
1182: // Twitter(現・X)、Blueskyへ投稿する.
1183: $message =<<< EOT
1184: 🌀台風情報 {$dt}現在
1185:
1186: (ご参考)PHPで台風情報を取得する https://www.pahoo.org/e-soul/webtech/php05/php05-15-01.shtm #台風 #台風情報 #nhk #ntv #tbs #fujitv #tvasahi
1187:
1188: EOT;
1189: if (TWITTER && isButton('tweet')) {
1190: mediaTweet($message, $res);
1191: }
1192: if (BLUESKY && isButton('bluesky')) {
1193: mediaBluesky($message, $res);
1194: }
1195:
1196: // 表示コンテンツを作成する.
活用例
参考サイト
- 台風情報:気象庁
- 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" に変更