サンプル・プログラムの実行例
目次
- サンプル・プログラムの実行例
- サンプル・プログラムのダウンロード
- 準備:PHP の https対応
- 準備:pahooInputData 関数群
- 準備:pahooGeoCode クラス
- 準備:地図サービスの選択
- 準備:pahooCache クラス
- 準備:各種定数など
- 気象庁防災情報XMLフォーマット
- VZSA50の構造
- 解説:最新の地上実況図URLを取得
- 解説:地上実況図を読み込む
- 解説:前線記号
- 解説:前線を描く(Googleマップ)
- 解説:前線を描く(Leaflet)
- 解説:ラベルを描く(Googleマップ)
- 解説:ラベルを描く(Leaflet)
- 解説:天気図描画スクリプトを生成する
- 解説:SNS投稿機能
- 解説:html2canvasライブラリ
- 準備:pahooTwitterAPI クラス
- 解説:メディア付き投稿(RAWデータ)
- 解説:Twitter(現・X)へ投稿する
- 準備:pahooBlueskyAPI クラス
- 解説:Blueskyへ投稿する
- 解説:SNSへ投稿する(メイン・プログラム)
- 参考サイト
サンプル・プログラムのダウンロード
| weatherMap.php | サンプル・プログラム本体。 |
| .pahooEnv | クラウドサービスを利用するためのアカウント情報などを記入する .env ファイル。 使い方は「各種クラウド連携サービス(WebAPI)の登録方法」を参照。include_path が通ったディレクトリに配置すること。 |
| pahooInputData.php | データ入力に関わる関数群。 使い方は「数値入力とバリデーション」「文字入力とバリデーション」などを参照。include_path が通ったディレクトリに配置すること。 |
| 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 が通ったディレクトリに配置すること。 |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 1.8.0 | 2026/01/12 | mediaBluesky - 投稿URL表示機能を追加 |
| 1.7.0 | 2025/08/13 | pahooInputData.php導入 |
| 1.6.0 | 2024/11/02 | Bluesky投稿機能を追加 |
| 1.5.0 | 2024/06/21 | Twitter(現・X)ボタンを "X" に変更 |
| 1.4.1 | 2023/09/20 | js_html2image()--Leaflet用html2image()発火プロセス見直し |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 2.0.1 | 2025/08/11 | getParam() bug-fix |
| 2.0.0 | 2025/08/11 | pahooLoadEnv() 追加 |
| 1.9.0 | 2025/07/26 | getParam() 引数に$trim追加 |
| 1.8.1 | 2025/03/15 | validRegexPattern() debug |
| 1.8.0 | 2024/11/12 | validRegexPattern() 追加 |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 6.9.1 | 2025/11/25 | PHP8.5対応:double→float |
| 6.9.0 | 2025/09/21 | jsPolygon, jsPolygon_Gmap, jsPolygon_Leaflet, loadGeoJSON, getPrefBorderList を追加 |
| 6.8.0 | 2025/08/10 | アクセスキーなどを ".pahooEnd" に分離 |
| 6.7.1 | 2025/07/26 | jsLine_Gmap() - bug-fix |
| 6.7.0 | 2025/07/20 | drawJSmap,drawGMap -- 引数 $markerLevel 追加 |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 1.3.0 | 2025/12/06 | PHP8.5対応:curl_closeを使わない |
| 1.2.0 | 2025/09/06 | cLoad() HTTPヘッダを送信できるようにした |
| 1.1.3 | 2025/08/10 | var→public |
| 1.1.2 | 2023/07/22 | bug-fix |
| 1.1.1 | 2023/02/11 | コメント追記 |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 5.6.1 | 2025/11/30 | PHP8.5対応:curl_closeを使わないようにした |
| 5.6.0 | 2025/08/13 | .pahooEnv導入 |
| 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 変更対応 |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 2.7.1 | 2025/11/22 | PHP8.5対応:curl_close,imagedestroyを実行しないようにした |
| 2.7.0 | 2025/08/17 | reductImage,uploadBlob仕様変更←画像に余計な空白が入らないようにするため |
| 2.6.0 | 2025/08/14 | .pahooEnv 導入 |
| 2.5.1 | 2025/08/10 | uploadBlob() -- bug-fix |
| 2.5.0 | 2025/08/02 | getOGPInformation() -- og:imageがないページに対応 |
準備:PHP の https対応
Windowsでは、"php.ini" の下記の行を有効化する。
extension=php_openssl.dllLinuxでは --with-openssl=/usr オプションを付けて再ビルドする。→OpenSSLインストール手順
これで準備は完了だ。
準備:pahooInputData 関数群
また、各種クラウドサービスに登録したときに取得するアカウント情報、アプリケーションパスワードなどを登録した .pahooEnv ファイルから読み込む関数 pahooLoadEnv を備えている。こちらについては、「各種クラウド連携サービス(WebAPI)の登録方法」をご覧いただきたい。
準備:pahooGeoCode クラス
pahooGeoCode.php
41: class pahooGeoCode {
42: public $items; // 検索結果格納用
43: public $error; // エラー・フラグ
44: public $errmsg; // エラー・メッセージ
45: public $hits; // 検索ヒット件数
46: public $webapi; // 直前に呼び出したWebAPI URL
47:
48: // 都道府県境界線データ
49: // SimpleMaps.com is a product of Pareto Software, LLC. © 2010-2025
50: // https://simplemaps.com/gis/country/jp
51: // ※各自の環境に合わせて設定すること
52: public $GeoJsonJP = __DIR__ . '/jp.json';
53:
54: // -- 以下のデータは .env ファイルに記述可能
55: // Google Cloud Platform APIキー
56: // https://cloud.google.com/maps-platform/
57: // ※Google Maps APIを利用しないのなら登録不要
58: public $GOOGLE_API_KEY_1 = ''; // HTTPリファラ用
59: public $GOOGLE_API_KEY_2 = ''; // IP制限用
60: public $GOOGLE_MAP_ID = ''; // GoogleMaps ID
61:
62: // Yahoo! JAPAN Webサービス アプリケーションID
63: // https://e.developer.yahoo.co.jp/register
64: // ※Yahoo! JAPAN Webサービスを利用しないのなら登録不要
65: public $YAHOO_APPLICATION_ID = '';
66:
67: // OSM Nominatim Search API利用時に知らせるメールアドレス
68: // https://wiki.openstreetmap.org/wiki/JA:Nominatim#.E6.A4.9C.E7.B4.A2
69: // ※OSM Nominatim Search APIを利用しないのなら登録不要
70: public $NOMINATIM_EMAIL = '';
71:
72: // IP2Location.io APIキー
73: // https://www.ip2location.io/
74: // ※IP2Location.ioを利用しないのなら登録不要
75: public $IP2LOCATION_API_KEY = '';
地図や住所検索として Google を利用するのであれば Google Cloud Platform APIキー とマップID が必要で、その入手方法は「Google Cloud Platform - WebAPIの登録方法」を、Yahoo!JAPAN を利用するのであれば Yahoo! JAPAN Webサービス アプリケーションIDが必要で、その入手方法は「Yahoo!JAPAN デベロッパーネットワーク - WebAPIの登録方法」を、IP2Location.ioを利用するのであれば「PHPでIPアドレスやホスト名から住所を求める」を、それぞれ参照されたい。
PHPのクラスについては「PHPでクラスを使ってテキストの読みやすさを調べる」を参照されたい。
準備:地図サービスの選択
weatherMap.php
66: // 地図描画サービスの選択
67: // 0:Google
68: // 2:地理院地図・OSM
69: define('MAPSERVICE', 2);
70:
71: // マップの表示サイズ(単位:ピクセル)
72: define('MAP_WIDTH', 600);
73: define('MAP_HEIGHT', 480);
74: // マップID
75: define('MAPID', 'map_id');
76: // 初期値
77: define('DEF_LATITUDE', 35.0); // 緯度
78: define('DEF_LONGITUDE', 137.0); // 経度
79: define('DEF_TYPE', 'GSISTD'); // マップタイプ
80: define('DEF_ZOOM', 4); // ズーム
81:
82: define('SEMICIRCLE', 30); // 半円を代替する多角形頂点数
住所検索サービスは、Google、Yahoo!JAPAN、HeartRails Geo API、OSM Nominatim Search API から選べる。あらかじめ、定数 GEOSERVICE に値を設定すること。
準備:pahooCache クラス
pahooCache.php
13: class pahooCache {
14: const LIFE_CACHE = (2 * 60); // キャッシュ保持時間(デフォルト;分)
15: const DEF_DIRCACHE = './pcache/'; // キャッシュ・ディレクトリ(デフォルト)
16:
17: public $lifeCache; // キャッシュ保持時間(分)(0:キャッシュしない)
18: public $dirCache; // キャッシュ用ディレクトリ
19: public $httpHeader; // HTTPヘッダ(空文字の時は何も送らない)
20: public $error; // エラーフラグ
21: public $errmsg; // エラーメッセージ
22: public $debug; // デバッグ用ファイル名
23:
24: /**
25: * コンストラクタ
26: * 参考サイト https://www.pahoo.org/e-soul/webtech/php06/php06-72-01.shtm
27: * @param int $life キャッシュ保持時間(分)(省略可能)
28: * @param string $dir キャッシュ・ディレクトリ(省略可能)
29: * @param array $httpHeader httpヘッダに渡す配列(省略可能)
30: * USER AGENT偽装に用いることを想定
31: * (例)
32: * array(
33: * 'User-Agent: Mozilla/5.0(Windows NT 10.0; Win64; x64) pahooAppy/pahoo.org AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36',
34: * 'Accept-Language: ja-JP'
35: * );
36: * @return なし
37: */
38: function __construct($life=self::LIFE_CACHE, $dir=self::DEF_DIRCACHE, $httpHeader=NULL) {
39: if ($life < 0) {
40: $life = 0;
41: }
42: if (preg_match('/\/$/ui', $dir) == 0) {
43: $dir = $dir . '/';
44: }
45: $this->error = FALSE;
46: $this->errmsg = '';
47: $this->debug = '';
48: $this->lifeCache = $life;
49: $this->dirCache = $dir;
50: $this->httpHeader = $httpHeader;
51:
52: // PHP5以上であることを調べる.
53: if (! $this->isphp5over()) {
54: $this->error = TRUE;
55: $this->errmsg = '動作にはPHP5以上が必要です';
56: return;
57: }
58:
59: // キャッシュ・ディレクトリが無ければ作成する.
60: if (! is_dir($this->dirCache)) {
61: $res = mkdir($this->dirCache, 0744);
62: if ($res == FALSE) {
63: $this->error = TRUE;
64: $this->errmsg = 'キャッシュ・ディレクトリ "' . $this->$dirCache . '" の作成に失敗しました';
65: return;
66: }
67: }
68: }
そこで、頻繁に変更がないデータについては、一度取り込んだら、こちら側のサーバのローカルストレージにキャッシュしておく仕組みを用意した。それが pahooCacheクラス である。同梱のクラス・ファイル "pahooCache.php" は include_path が通ったディレクトリに配置してほしい。他のプログラムでも pahooCacheクラス を利用するが、常に最新のクラス・ファイルを1つ配置すればよい。
pahooCacheクラス の注意ポイントは、キャッシュ時間(単位:分)とキャッシュを保存するディレクトリをコンストラクタで指定している点だ。これらはプログラムによって変わるものである。インスタンス化するときの値は、pahooCacheクラス を利用するメイン・プログラムの方で解説する。
サイトによっては、User-Agent などを必要とすることがあるだろう。そこで、第3引数に HTTPヘッダ として送信するデータを配列で渡すことができるようにした。配列の構造はコメントを参照していただきたい。
PHPのクラスについては「PHPでクラスを使ってテキストの読みやすさを調べる」を参照されたい。
準備:各種定数など
weatherMap.php
55: // 各種定数(START) ===========================================================
56:
57: // Twitter(現・X)投稿ボタン TRUE:有効,FALSE:無効
58: define('TWITTER', FALSE);
59:
60: // Bluesky投稿ボタン TRUE:有効,FALSE:無効
61: define('BLUESKY', FALSE);
62:
63: // 画像化したいオブジェクト
64: define('TARGET', 'target');
65:
66: // 地図描画サービスの選択
67: // 0:Google
68: // 2:地理院地図・OSM
69: define('MAPSERVICE', 2);
70:
71: // マップの表示サイズ(単位:ピクセル)
72: define('MAP_WIDTH', 600);
73: define('MAP_HEIGHT', 480);
74: // マップID
75: define('MAPID', 'map_id');
76: // 初期値
77: define('DEF_LATITUDE', 35.0); // 緯度
78: define('DEF_LONGITUDE', 137.0); // 経度
79: define('DEF_TYPE', 'GSISTD'); // マップタイプ
80: define('DEF_ZOOM', 4); // ズーム
81:
82: define('SEMICIRCLE', 30); // 半円を代替する多角形頂点数
83:
84: // キャッシュ保持時間(分) 0:キャッシュしない
85: // 気象庁へのアクセス負荷軽減のため,60分以上のキャッシュ保持をお勧めします.
86: define('LIFE_CACHE', 120);
87:
88: // キャッシュ・ディレクトリ
89: // 書き込み可能で,外部からアクセスされないディレクトリを指定してください.
90: define('DIR_CACHE', './pcache/');
91:
92: // 気象庁防災情報XML:高頻度フィード - 定時配信(変更不可)
93: define('FEED_REGULAR', 'https://www.data.jma.go.jp/developer/xml/feed/regular.xml');
94:
95: // 住所・緯度・経度に関わるクラス:include_pathが通ったディレクトリに配置
96: require_once('pahooGeoCode.php');
97:
98: // キャッシュ処理に関わるクラス:include_pathが通ったディレクトリに配置
99: require_once('pahooCache.php');
100:
101: // TwitterAPIクラス:include_pathが通ったディレクトリに配置
102: if (TWITTER) {
103: require_once('pahooTwitterAPI.php');
104: }
105:
106: // BlueskyAPIクラス:include_pathが通ったディレクトリに配置
107: if (BLUESKY) {
108: require_once('pahooBlueskyAPI.php');
109: define('BLUESKY_DOMAIN', 'bsky.social'); // あなたのドメインを記入
110: }
111: // 各種定数(END) ============================================================
出力結果を 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フォーマット
今回は、高頻度 - 定時更新フィードにアクセスし、電文コード VZSA50 の地上実況図を取得する。
VZSA50の構造
注意すべきは、1つのXMLファイルの中に1つまたは複数の予報地点が含まれていること。さらに、天気予報(アイコン画像を含む)と降水確率は「地方」に紐付いているが、最低気温・最高気温は「予報地点」に紐付いているということである。地方と予報地点は1対N対応のため、後述する予報値地点データファイルに用意しておくことにした。
また、TimeDefine タグに示される7日分の情報が格納されているのだが、XML情報の取得時間帯によって、1つ目(refID=1)の情報が、今日になるか明日になるか分かれる。そこで、TimeDefine を取得して、現在日時(PCの内蔵時計)と比較して決定することにした。
降水確率や気温は、XML情報の取得タイミングによっては、1つ目(refID=1)~3つ目(refID=3)が「値なし」になっていることがある。そこで、後述する情報XML VPFW50 から、今日~明後日(または明日~明後日)の情報を取り出し、補完することにする。
解説:最新の地上実況図URLを取得
weatherMap.php
359: /**
360: * 気象庁防災情報XMLから最新の地上実況図URLを取得
361: * @param object $pcc pahooCacheオブジェクト
362: * @return string 地上実況図URL/FALSE:取得失敗
363: */
364: function jmaGetWeatherMapURL($pcc) {
365: // URLパターン
366: $vzsa50 = '/https?\:\/\/www\.data\.jma\.go\.jp\/developer\/xml\/data\/([0-9\_]+)VZSA50\_([0-9\_]+)\.xml/ui';
367:
368: $xml = $pcc->simplexml_load(FEED_REGULAR);
369: // レスポンス・チェック
370: if ($pcc->iserror() || !isset($xml->entry)) {
371: return FALSE;
372: }
373:
374: // フィード(XMLファイル)解析
375: $vzsa50_url = $vzsa50_dt = '';
376: $res = FALSE;
377: foreach ($xml->entry as $node) {
378: // 日時がより新しいURLを採用
379: if (preg_match($vzsa50, $node->id, $arr) > 0) {
380: if ($arr[1] > $vzsa50_dt) {
381: $vzsa50_url = $arr[0];
382: $vzsa50_dt = $arr[1];
383: $res = TRUE;
384: }
385: }
386: }
387:
388: // エラー・チェック
389: if (! $res) {
390: return FALSE;
391: }
392:
393: return $vzsa50_url;
394: }
URLを正規表現で分解し、配信日時 yyyymmddhhmmss が最も大きく、VZSA50 を含むURLを返す。
解説:地上実況図を読み込む
weatherMap.php
396: /**
397: * 気象庁防災情報XMLから地上実況図を取得
398: * @param string $url 地上実況図URL
399: * @param string $dt 報告日時格納用
400: * @param array $items 情報を格納する配列
401: * @param object $pcc pahooCacheオブジェクト
402: * @return bool TRUE:取得成功/FALSE:失敗
403: */
404: function jmaGetIsobar($url, &$dt, &$items, $pcc) {
405: // 名前空間
406: define('JMX_EB', 'http://xml.kishou.go.jp/jmaxml1/elementBasis1/');
407:
408: $xml = $pcc->simplexml_load($url);
409: // レスポンス・チェック
410: if ($pcc->iserror() || !isset($xml->Body->MeteorologicalInfos)) {
411: return FALSE;
412: }
413:
414: // 報告日時
415: $pat = '/([0-9]+)\-([0-9]+)\-([0-9]+)T([0-9]+)\:/ui';
416: $dt = (string)$xml->Body->MeteorologicalInfos->MeteorologicalInfo->DateTime;
417: if (preg_match($pat, $dt, $arr) > 0) {
418: $dt = sprintf('%d年%d月%d日 %d時', $arr[1], $arr[2], $arr[3], $arr[4]);
419: } else {
420: $dt = '';
421: }
422:
423: // 等圧線情報
424: $res = FALSE;
425: $cnt = 0;
426: foreach ($xml->Body->MeteorologicalInfos->MeteorologicalInfo->Item as $item) {
427: $Property = $item->Kind->Property;
428: // 等圧線
429: if ($Property->Type == '等圧線') {
430: $IsobarPart = $Property->IsobarPart->children(JMX_EB);
431: $items[$cnt]['type'] = (string)$Property->Type;
432: $items[$cnt]['pressure'] = (int)$IsobarPart->Pressure;
433: $bar = (string)$IsobarPart->Line;
434: $arr = preg_split('/\//ui', $bar);
435: // 緯度・経度
436: $i = 0;
437: foreach ($arr as $ss) {
438: if (preg_match('/([\+\-]+[0-9\.]+)([\+\-]+[0-9\.]+)/ui', $ss, $arr2) > 0) {
439: $items[$cnt]['point'][$i]['latitude'] = (float)$arr2[1];
440: $items[$cnt]['point'][$i]['longitude'] = (float)$arr2[2];
441: $i++;
442: }
443: }
444: $res = TRUE;
445: $cnt++;
446: // 前線
447: } else if (preg_match('/前線/ui', $Property->Type) > 0) {
448: $CoordinatePart = $Property->CoordinatePart->children(JMX_EB);
449: $items[$cnt]['type'] = (string)$Property->Type;
450: $bar = (string)$CoordinatePart->Line;
451: $arr = preg_split('/\//ui', $bar);
452: // 緯度・経度
453: $i = 0;
454: foreach ($arr as $ss) {
455: if (preg_match('/([\+\-]+[0-9\.]+)([\+\-]+[0-9\.]+)/ui', $ss, $arr2) > 0) {
456: $items[$cnt]['point'][$i]['latitude'] = (float)$arr2[1];
457: $items[$cnt]['point'][$i]['longitude'] = (float)$arr2[2];
458: $i++;
459: }
460: }
461: $res = TRUE;
462: $cnt++;
463: // 気圧
464: } else if (preg_match('/気圧|台風|熱帯/ui', $Property->Type) > 0) {
465: $items[$cnt]['type'] = (string)$Property->Type;
466: $CenterPart = $Property->CenterPart->children(JMX_EB);
467: if (preg_match('/([\+\-]+[0-9\.]+)([\+\-]+[0-9\.]+)/ui', (string)$CenterPart->Coordinate, $arr2) > 0) {
468: $items[$cnt]['latitude'] = (float)$arr2[1];
469: $items[$cnt]['longitude'] = (float)$arr2[2];
470: }
471: $items[$cnt]['pressure'] = (int)$CenterPart->Pressure;
472: $res = TRUE;
473: $cnt++;
474: }
475: }
476:
477: return TRUE;
478: }
XMLファイルを読み込んだら、まず、報告日時を $dt へ格納する。
次に、Type要素の値に応じて、等圧線、前線、気圧の位置情報を配列 $items へ格納していく。
解説:前線記号
寒冷前線系の三角形と、温暖前線系の半円を描く必要がある。いろいろ試行錯誤したのだが、結局、マップサービスの多角形描画機能を使って座標を計算して描かせることにした。
weatherMap.php
502: /**
503: * 二等辺三角形座標(寒冷前線用)
504: * @param float $lat0, $lng0 底辺の中心座標
505: * @param float $lat1, $lng2 底辺の一方の座標
506: * @param int $way 0:寒冷前線・停滞前線,1:閉塞前線
507: * @return array($lat, $lng) 頂点の座標
508: */
509: function isoTriangle($lat0, $lng0, $lat1, $lng1, $way=0) {
510: $angle = ($way == 0) ? 270 : 90;
511:
512: if ($lng1 - $lng0 == 0) {
513: $t = 0.0;
514: } else {
515: $t = rad2deg(atan(($lat1 - $lat0) / ($lng1 - $lng0)));
516: }
517: $r = sqrt(pow(($lng1 - $lng0), 2) + pow(($lat1 - $lat0), 2));
518: $lat = $r * sin(deg2rad($t + $angle)) + $lat0;
519: $lng = $r * cos(deg2rad($t + $angle)) + $lng0;
520:
521: return array($lat, $lng);
522: }
座標系は緯度、経度から成る球面座標系だが、マップサービスがメルカトル図法であることから、平面座標系であると近似して計算式を立てた。
ここで頂点 $ B(lat1, lng1) $ の座標から、∠BCOは $ \displaystyle t\ =\ tan^{-1}(\frac{lat_1\ -\ lat_0}{lng_1\ -\ lng_0}) $ である。
辺COの長さは $ \displaystyle r\ =\ \sqrt{(lng_1\ -\ lng_0)^2\ +\ (lat_1\ -\ lat_0)^2} $ であるから、頂点 $ B(lat, lng) $ の座標は $$ \displaystyle \begin{align} lat\ &=\ r\ sin(t\ +\ \frac{3}{2}\pi)\ +\ lat_1 \\ lng\ &=\ r\ cos(t\ +\ \frac{3}{2}\pi)\ +\ lng_1 \end{align} $$
なお、底辺ABは前線であることから必ずしも直線ではなく、頂点A,B,Cを含む多角形としてマップに描画する。
weatherMap.php
524: /**
525: * 半円弧座標(温暖前線記号):多角形で近似する
526: * @param float $lat0, $lng0 中心座標
527: * @param float $lat1, $lng2 円弧の始点座標
528: * @param int $n 多角形の頂点数
529: * @param array points 円弧の座標を格納する
530: * @return なし
531: */
532: function semiCircle($lat0, $lng0, $lat1, $lng1, $n, &$points) {
533: if ($lng1 - $lng0 == 0) {
534: $t = 0.0;
535: } else {
536: $t = rad2deg(atan(($lat1 - $lat0) / ($lng1 - $lng0)));
537: }
538: $r = sqrt(pow(($lng1 - $lng0), 2) + pow(($lat1 - $lat0), 2));
539: for ($i = 0; $i < $n; $i++) {
540: $points[$i]['latitude'] = $r * sin(deg2rad($t + 180 / $n * $i)) + $lat0;
541: $points[$i]['longitude'] = $r * cos(deg2rad($t + 180 / $n * $i)) + $lng0;
542: }
543: }
多角形の頂点座標の求め方は、isoTriangle 関数と同様で、頂点数がN個になったとして計算する。
解説:前線を描く(Googleマップ)
weatherMap.php
601: /**
602: * 前線描画用スクリプト作成:Googleマップ
603: * @param array $item 前線の座標
604: * @param int $kind 前線の種類(0:温暖前線,1:寒冷前線,2:停滞前線,
605: * 3:閉塞前線)
606: * @return string 描画用スクリプト
607: */
608: function jsStationaryFront_gmap($item, $kind) {
609: static $table_color1 = array('red', 'blue', 'red', 'purple');
610: static $table_color2 = array('red', 'blue', 'blue', 'purple');
611: static $table_angle = array(0, 0, 0, 1);
612:
613: $i = 0;
614: $flag = TRUE;
615: $js = '';
616: while ($flag) {
617: // 前線(温暖)
618: $ss = '';
619: $cnt = 0;
620: for ($j = $i; $j <= $i + 10; $j++) {
621: if (! isset($item['point'][$j])) {
622: $flag = FALSE;
623: break;
624: }
625: if ($cnt > 0) $ss .= ",\n";
626: $ss .= "\t\t{ lat: {$item['point'][$j]['latitude']}, lng: {$item['point'][$j]['longitude']} }";
627: $cnt++;
628: }
629: $js .=<<< EOT
630: new google.maps.Polyline({
631: map: map,
632: path: [
633: {$ss}
634: ],
635: strokeColor: '{$table_color1[$kind]}',
636: strokeOpacity: 1.0,
637: strokeWeight: 2,
638: });
639:
640: EOT;
641: $i += 10;
642:
643: if (isset($item['point'][$i + 20])) {
644: // 温暖部
645: $ss = '';
646: if ($kind != 1) {
647: $points = array();
648: semiCircle($item['point'][$i + 5]['latitude'], $item['point'][$i + 5]['longitude'], $item['point'][$i + 10]['latitude'], $item['point'][$i + 10]['longitude'], SEMICIRCLE, $points);
649: $cnt = 0;
650: foreach ($points as $point) {
651: if ($cnt > 0) $ss .= ",\n";
652: $ss .= "\t\t{ lat: {$point['latitude']}, lng: {$point['longitude']} }";
653: $cnt++;
654: }
655: for ($j = $i; $j <= $i + 10; $j++) {
656: if ($cnt > 0) $ss .= ",\n";
657: $ss .= "\t\t{ lat: {$item['point'][$j]['latitude']}, lng: {$item['point'][$j]['longitude']} }";
658: $cnt++;
659: }
660: $js .=<<< EOT
661: new google.maps.Polygon({
662: map: map,
663: paths: [
664: {$ss}
665: ],
666: strokeColor: '{$table_color1[$kind]}',
667: strokeOpacity: 1.0,
668: strokeWeight: 2,
669: fillColor: '{$table_color1[$kind]}',
670: fillOpacity: 1.0,
671: });
672:
673: EOT;
674: } else {
675: $ss = '';
676: $cnt = 0;
677: for ($j = $i; $j <= $i + 10; $j++) {
678: if ($cnt > 0) $ss .= ",\n";
679: $ss .= "\t\t{ lat: {$item['point'][$j]['latitude']}, lng: {$item['point'][$j]['longitude']} }";
680: $cnt++;
681: }
682: $js .=<<< EOT
683: new google.maps.Polyline({
684: map: map,
685: path: [
686: {$ss}
687: ],
688: strokeColor: '{$table_color1[$kind]}',
689: strokeOpacity: 1.0,
690: strokeWeight: 2,
691: });
692:
693: EOT;
694: }
695:
696: // 寒冷部
697: if ($kind != 0) {
698: list($lat, $lng) = isoTriangle($item['point'][$i + 15]['latitude'], $item['point'][$i + 15]['longitude'], $item['point'][$i + 19]['latitude'], $item['point'][$i + 19]['longitude'], $table_angle[$kind]);
699: $ss = "\t\t{ lat: {$lat}, lng: {$lng} }";
700: $cnt = 1;
701: for ($j = $i + 10; $j <= $i + 20; $j++) {
702: if ($cnt > 0) $ss .= ",\n";
703: $ss .= "\t\t{ lat: {$item['point'][$j]['latitude']}, lng: {$item['point'][$j]['longitude']} }";
704: $cnt++;
705: }
706: $js .=<<< EOT
707: new google.maps.Polygon({
708: map: map,
709: paths: [
710: {$ss}
711: ],
712: strokeColor: '{$table_color2[$kind]}',
713: strokeOpacity: 1.0,
714: strokeWeight: 1,
715: fillColor: '{$table_color2[$kind]}',
716: fillOpacity: 1.0
717: });
718:
719: EOT;
720: } else {
721: $ss = '';
722: $cnt = 0;
723: for ($j = $i + 10; $j <= $i + 20; $j++) {
724: if ($cnt > 0) $ss .= ",\n";
725: $ss .= "\t\t{ lat: {$item['point'][$j]['latitude']}, lng: {$item['point'][$j]['longitude']} }";
726: $cnt++;
727: }
728: $js .=<<< EOT
729: new google.maps.Polyline({
730: map: map,
731: path: [
732: {$ss}
733: ],
734: strokeColor: '{$table_color1[$kind]}',
735: strokeOpacity: 1.0,
736: strokeWeight: 2,
737: });
738:
739: EOT;
740:
741: }
742: $i += 20;
743: }
744:
745: // 前線(寒冷)
746: $cnt = 0;
747: $ss = '';
748: for ($j = $i; $j <= $i + 10; $j++) {
749: if (! isset($item['point'][$j])) {
750: $flag = FALSE;
751: break;
752: }
753: if ($cnt > 0) $ss .= ",\n";
754: $ss .= "\t\t{ lat: {$item['point'][$j]['latitude']}, lng: {$item['point'][$j]['longitude']} }";
755: $cnt++;
756: }
757: $js .=<<< EOT
758: new google.maps.Polyline({
759: map: map,
760: path: [
761: {$ss}
762: ],
763: strokeColor: '{$table_color2[$kind]}',
764: strokeOpacity: 1.0,
765: strokeWeight: 2,
766: });
767:
768: EOT;
769: $i += 10;
770: }
771: return $js;
772: }
温暖前線、寒冷前線、停滞前線、閉塞前線の識別を変数 $kind にもたせて、すべて1つの関数で描画を行う。
基本形は停滞前線で、他の3つの前線は次のようにして描く。
- 温暖前線‥‥寒冷前線記号は描かず、替わりに赤い直線(温暖前線)を引く。
- 寒冷前線‥‥温暖前線記号は描かず、替わりに青い直線(寒冷前線)を引く。
- 閉塞前線‥‥寒冷前線記号の向きを180度逆転し、紫色で描く。
解説:前線を描く(Leaflet)
weatherMap.php
775: /**
776: * 前線描画用スクリプト作成:Leaflet
777: * @param array $item 前線の座標
778: * @param int $kind 前線の種類(0:温暖前線,1:寒冷前線,2:停滞前線,
779: * 3:閉塞前線)
780: * @return string 描画用スクリプト
781: */
782: function jsStationaryFront_leaflet($item, $kind) {
783: static $table_color1 = array('red', 'blue', 'red', 'purple');
784: static $table_color2 = array('red', 'blue', 'blue', 'purple');
785: static $table_angle = array(0, 0, 0, 1);
786:
787: $i = 0;
788: $flag = TRUE;
789: $js = '';
790: while ($flag) {
791: // 前線(温暖)
792: $ss = '';
793: for ($j = $i; $j <= $i + 10; $j++) {
794: if (! isset($item['point'][$j])) {
795: $flag = FALSE;
796: break;
797: }
798: $ss .=<<< EOT
799: [{$item['point'][$j]['latitude']}, {$item['point'][$j]['longitude']}],
800:
801: EOT;
802: }
803: $js .=<<< EOT
804: L.polyline([
805: {$ss}
806: ], {
807: color: '{$table_color1[$kind]}',
808: opacity: 1.0,
809: weight: 2
810: }
811: ).addTo(map);
812:
813: EOT;
814: $i += 10;
815:
816: if (isset($item['point'][$i + 20])) {
817: // 温暖部
818: $ss = '';
819: if ($kind != 1) {
820: $points = array();
821: semiCircle($item['point'][$i + 5]['latitude'], $item['point'][$i + 5]['longitude'], $item['point'][$i + 10]['latitude'], $item['point'][$i + 10]['longitude'], SEMICIRCLE, $points);
822: foreach ($points as $point) {
823: $ss .=<<< EOT
824: [{$point['latitude']}, {$point['longitude']}],
825:
826: EOT;
827: }
828: for ($j = $i + 0; $j <= $i + 10; $j++) {
829: $ss .=<<< EOT
830: [{$item['point'][$j]['latitude']}, {$item['point'][$j]['longitude']}],
831:
832: EOT;
833: }
834: $js .=<<< EOT
835: L.polygon([
836: {$ss}
837: ], {
838: color: '{$table_color1[$kind]}',
839: fillColor: '{$table_color1[$kind]}',
840: fillOpacity: 1.0,
841: }
842: ).addTo(map);
843:
844: EOT;
845: } else {
846: for ($j = $i + 0; $j <= $i + 10; $j++) {
847: $ss .=<<< EOT
848: [{$item['point'][$j]['latitude']}, {$item['point'][$j]['longitude']}],
849:
850: EOT;
851: }
852: $js .=<<< EOT
853: L.polyline([
854: {$ss}
855: ], {
856: color: '{$table_color1[$kind]}',
857: opacity: 1.0,
858: weight: 2
859: }
860: ).addTo(map);
861:
862: EOT;
863: }
864:
865: // 寒冷部
866: if ($kind != 0) {
867: list($lat, $lng) = isoTriangle($item['point'][$i + 15]['latitude'], $item['point'][$i + 15]['longitude'], $item['point'][$i + 19]['latitude'], $item['point'][$i + 19]['longitude'], $table_angle[$kind]);
868: $ss =<<< EOT
869: [{$lat}, {$lng}],
870:
871: EOT;
872: for ($j = $i + 10; $j <= $i + 20; $j++) {
873: $ss .=<<< EOT
874: [{$item['point'][$j]['latitude']}, {$item['point'][$j]['longitude']}],
875:
876: EOT;
877: }
878: $js .=<<< EOT
879: L.polygon([
880: {$ss}
881: ], {
882: color: '{$table_color2[$kind]}',
883: fillColor: '{$table_color2[$kind]}',
884: fillOpacity: 1.0,
885: }
886: ).addTo(map);
887:
888: EOT;
889: } else {
890: $ss = '';
891: for ($j = $i + 10; $j <= $i + 20; $j++) {
892: $ss .=<<< EOT
893: [{$item['point'][$j]['latitude']}, {$item['point'][$j]['longitude']}],
894:
895: EOT;
896: }
897: $js .=<<< EOT
898: L.polyline([
899: {$ss}
900: ], {
901: color: '{$table_color1[$kind]}',
902: opacity: 1.0,
903: weight: 2
904: }
905: ).addTo(map);
906:
907: EOT;
908: }
909: $i += 20;
910: }
911: // 前線(寒冷)
912: $ss = '';
913: for ($j = $i; $j <= $i + 10; $j++) {
914: if (! isset($item['point'][$j])) {
915: $flag = FALSE;
916: break;
917: }
918: $ss .=<<< EOT
919: [{$item['point'][$j]['latitude']}, {$item['point'][$j]['longitude']}],
920:
921: EOT;
922: }
923: $js .=<<< EOT
924: L.polyline([
925: {$ss}
926: ], {
927: color: '{$table_color2[$kind]}',
928: opacity: 1.0,
929: weight: 2
930: }
931: ).addTo(map);
932:
933: EOT;
934: $i += 10;
935: }
936:
937: return $js;
938: }
アルゴリズムは、Googleマップ用の jsStationaryFront_gmap とほぼ同じで、多角形や直線を描く命令部分を変えている。
解説:ラベルを描く(Googleマップ)
weatherMap.php
545: /**
546: * ラベル表示用スクリプト作成:Googleマップ
547: * @param float $latitude, $longitude ラベル表示座標
548: * @param string $label ラベル
549: * @param int $size フォントサイズ(pt)
550: * @param string $color フォントカラー
551: * @param string $weight 太さ
552: * @return string 描画用スクリプト
553: */
554: function jsLabel_gmap($latitude, $longitude, $label, $size=14, $color='#FF0000', $weight="normal") {
555: $js =<<< EOT
556: new google.maps.Marker({
557: map: map,
558: position: new google.maps.LatLng({$latitude}, {$longitude}),
559: icon: {
560: url: 'https://www.pahoo.org/common/space.gif'
561: },
562: label: {
563: text: '{$label}',
564: color: '{$color}',
565: fontSize: '{$size}px',
566: fontWeight: '{$weight}'
567: }
568: });
569:
570: EOT;
571: return $js;
572: }
アイコン表示命令を利用し、アイコン画像を表示させないようにしている。
解説:ラベルを描く(Leaflet)
weatherMap.php
574: /**
575: * ラベル表示用スクリプト作成:Leaflet
576: * @param float $latitude, $longitude ラベル表示座標
577: * @param string $label ラベル
578: * @param int $size フォントサイズ(pt)
579: * @param string $color フォントカラー
580: * @param string $weight 太さ
581: * @return string 描画用スクリプト
582: */
583: function jsLabel_leaflet($latitude, $longitude, $label, $size=14, $color='#FF0000', $weight="normal") {
584: $js =<<< EOT
585: new L.marker(
586: [{$latitude}, {$longitude}],
587: {
588: icon:
589: new L.divIcon({
590: html: '<span style="color:{$color}; font-size:{$size}px; font-weight:{$weight};">{$label}</span>',
591: iconSize: [0, 0],
592: iconAnchor: [{$size}, {$size}],
593: })
594: }
595: ).addTo(map);
596:
597: EOT;
598: return $js;
599: }
前述の jsLabel_gmap と同様、アイコン表示命令を利用し、アイコン画像を表示させないようにしている。
解説:天気図描画スクリプトを生成する
weatherMap.php
940: /**
941: * 天気図描画スクリプトを生成する
942: * @param string $url 地上実況図URL
943: * @param object $pgc pahooGeoCodeオブジェクト
944: * @param object $pcc pahooCacheオブジェクト
945: * @param string $dt 報告日時格納用
946: * @param string $errmsg エラーメッセージ格納用
947: * @return string スクリプト/FALSE:生成失敗
948: */
949: function jsWeatherMap($url, $pgc, $pcc, &$dt, &$errmsg) {
950: static $table = array('温暖前線', '寒冷前線', '停滞前線', '閉塞前線');
951: $errmsg = '';
952: $js = '';
953: $items = array();
954: $res = jmaGetIsobar($url, $dt, $items, $pcc);
955: if ($res == FALSE) {
956: $errmsg = '気象庁防災情報XMLから地上実況図を取得できません';
957: }
958:
959: // スクリプト生成
960: foreach ($items as $key=>$item) {
961: if ($item['type'] == '等圧線') {
962: $js .= $pgc->jsLine($item['point'], 'black', 0.5, 0.5, MAPSERVICE);
963: } else if (preg_match('/前線/ui', $item['type']) > 0) {
964: $kind = array_search($item['type'], $table);
965: if (MAPSERVICE == 0) {
966: $js .= jsStationaryFront_gmap($item, $kind);
967: } else {
968: $js .= jsStationaryFront_leaflet($item, $kind);
969: }
970: } else if (preg_match('/気圧|台風|熱帯/ui', $item['type']) > 0) {
971: switch ($item['type']) {
972: case '高気圧':
973: $label = '高';
974: $color = 'blue';
975: break;
976: case '低気圧':
977: $label = '低';
978: $color = 'red';
979: break;
980: case '台風':
981: $label = '台';
982: $color = 'magenta';
983: break;
984: case '熱帯低気圧':
985: $label = '熱';
986: $color = 'magenta';
987: break;
988: }
989: if (MAPSERVICE == 0) {
990: $js .= jsLabel_gmap($item['latitude'], $item['longitude'], $label, 18, $color, 'bold');
991: $js .= jsLabel_gmap($item['latitude'] - 1.3, $item['longitude'], $item['pressure'], 16, 'black', 'bold%');
992: } else {
993: $js .= jsLabel_leaflet($item['latitude'], $item['longitude'], $label, 18, $color, 'bold');
994: $js .= jsLabel_leaflet($item['latitude'] - 1.3, $item['longitude'], $item['pressure'], 16, 'black', 'bold');
995: }
996: }
997: }
998:
999: // HTMLの画像化
1000: $js .= js_html2image();
1001:
1002: return $js;
1003: }
解説:SNS投稿機能
コンテンツを描画しているのはクライアントにあるブラウザ(レンダリングエンジン)であることから、クライアント側で画像を作成し、サーバ側で SNS の API をコールするという方針とした。
- ブラウザはサーバにコンテンツ描画をリクエストする。
- サーバはデータサイトからデータ取得する。(サーバキャッシュにデータがあればそれを利用する)
- サーバはコンテンツ描画スクリプトを生成する。
- サーバはブラウザへレスポンス(HTML文)を返す。
- ブラウザはコンテンツをレンダリングする。
- ブラウザはレンダリングしたコンテンツを画像データとしてサーバへアップロードする。
- サーバは SNS の API を使ってツイートする。
- サーバは SNS へメッセージと画像を送る。
- サーバはブラウザへレスポンス(HTML文)を返す。
解説:html2canvasライブラリ
weatherMap.php
136: <script src="https://html2canvas.hertzen.com/dist/html2canvas.js"></script>
137:
画像化を実行するJavaScript関数は html2canvas である。
weatherMap.php
1098: <div id="{$target}" name="{$target}" style="width:{$width2}px;">
1099: <p>
1100: 🌞天気図 {$dt}現在
1101: {$tweet}{$bluesky}{$tweet_bluesky}
1102: </p>
1103: <div id="{$mapid}" style="width:{$width}px; height:{$height}px;"></div>
1104: </div>
レンダリングエンジンによって違うのかもしれないが、html2canvas ライブラリによって画像化される範囲が実際よりやや小さいため、あえてマップ領域より、幅を20ピクセル大きくした範囲を画像化範囲としている。
weatherMap.php
290: /**
291: * HTMLオブジェクトの画像化
292: * @param なし
293: * @return string JavaScriptコード
294: */
295: function js_html2image() {
296: $target = TARGET;
297: $js = '';
298:
299: // Googleマップの場合
300: if (MAPSERVICE == 0) {
301: $js .=<<< EOT
302: google.maps.event.addListener(map, 'tilesloaded', function() {
303: var capture = document.querySelector('#{$target}');
304: html2canvas(capture, {useCORS: true, allowTaint:true}).then(canvas => {
305: var base64 = canvas.toDataURL('image/png'); // 画像化
306: $('#base64').val(base64);
307: });
308: });
309:
310: EOT;
311:
312: // Leafletの場合(ブラウザによってはうまく動作しない)
313: } else {
314: $js .=<<< EOT
315: HTMLCanvasElement.prototype.getContext = function(origFn) {
316: return function(type, attribs) {
317: attribs = attribs || {};
318: attribs.preserveDrawingBuffer = true;
319: return origFn.call(this, type, attribs);
320: };
321: } (HTMLCanvasElement.prototype.getContext);
322:
323: // HTML画像化イベント登録
324: function html2image() {
325: var capture = document.querySelector('#{$target}');
326: html2canvas(capture, {useCORS: true, allowTaint:true}).then(canvas => {
327: var base64 = canvas.toDataURL('image/png'); // 画像化
328: $('#base64').val(base64);
329: document.body.innerHTML += '<img src="' + base64 + '"/>';
330: });
331: };
332:
333: // ズーム変更イベント
334: map.on('zoomend', function() {
335: html2image();
336: });
337:
338: // マップ移動イベント
339: map.on('moveend', function() {
340: html2image();
341: });
342:
343: // html2image()を発火させるために,ズームアウトして500msec後に元に戻す.
344: /**
345: var zoom = map.getZoom();
346: map.setZoom(zoom - 1);
347: setTimeout(function() {
348: map.setZoom(zoom);
349: }, 500);
350: map.setZoom(zoom);
351: **/
352:
353: EOT;
354: }
355:
356: return $js;
357: }
Leaflet の場合、これに相当するイベントがないため、ズーム変更完了イベントにフックし、強制的にズーム変更イベントを発生させるタイミングで画像化する。しかし、Leaflet では、画像を描画するレイヤ構造の関係で、html2canvas ライブラリでは位置ずれが起きることがわかっている。その場合は、Googleマップを選択してほしい。また、対策方法をご存じの方がいたらお知らせいただきたい。
これらをJavaScriptとして生成するユーザー関数が js_html2image である。
準備:pahooTwitterAPI クラス
pahooTwitterAPI.php
解説:メディア付き投稿(RAWデータ)
pahooTwitterAPI.php
610: /**
611: * バイナリデータを使ったメディア付きメッセージをツイートする.
612: * Tweetet API v2 を使用する.
613: * @param string $message 投稿メッセージ(UTF-8限定)
614: * @param array $items メディアデータ(バイナリデータ配列)
615: * @return bool TRUE:リクエスト成功/FALSE:失敗
616: */
617: function tweet_media_raw($message, $items) {
618: // メディアのアップロード
619: $media_ids = array();
620: $cnt = 0;
621: // Tweetet API v1.1 を使用する(v2にメディアアップロードが未実装のため)
622: $this->connection->setApiVersion('1.1');
623: foreach ($items as $data) {
624: $tmpname = $this->saveTempFile($data);
625: // $media = $this->connection->upload('media/upload', ['media' => $tmpname]);
626: $media = $this->connection->upload('media/upload', ['media' => $tmpname], ['chunkedUpload' => true]); // twitteroauth 7.0.0 対応
627: unlink($tmpname);
628: if (! isset($media->media_id_string)) break; // 処理失敗
629: $media_ids[] = (string)$media->media_id_string;
630: $cnt++;
631: if ($cnt > 3) break; // 最大4つまで
632: }
633:
634: // メディア付きツイート(Tweetet API v2 を使用する)
635: $this->connection->setApiVersion('2');
636: $option = [
637: 'text' => $message,
638: 'media' => [
639: 'media_ids' => $media_ids
640: ]
641: ];
642: // $status = $this->connection->post('tweets', $option, TRUE);
643: $status = $this->connection->post('tweets', $option, ['jsonPayload' => true]); // twitteroauth 7.0.0 対応
644: $this->webapi = 'https://api.twitter.com/2/tweets';
645:
646: // 処理に成功した.
647: if ($this->isSuccess()) {
648: $this->responses = $status->data;
649: $this->errcode = NULL;
650: $this->errmsg = '';
651: $this->error = FALSE;
652: $res = TRUE;
653: // 処理に失敗した.
654: } else {
655: if ($this->isAuthError() == FALSE) {
656: $this->errmsg = $status->detail;
657: $this->error = TRUE;
658: }
659: $res = FALSE;
660: }
661: return $res;
662: }
解説:Twitter(現・X)へ投稿する
weatherMap.php
233: /**
234: * Twitter(現・X)投稿
235: * @param string $message 投稿文
236: * @param string $res 応答メッセージ格納用
237: * @return bool TRUE:成功/FALSE:失敗または未処理
238: */
239: function mediaTweet($message, &$res) {
240: if (! TWITTER) return FALSE;
241:
242: $ret = TRUE;
243: if (isset($_POST['base64']) && ($_POST['base64'] != '')) {
244: $base64 = preg_replace('/data\:image\/png\;base64\,/ui', '', $_POST['base64']);
245: $raws = array(base64_decode($base64));
246: $ptw = new pahooTwitterAPI();
247: $ptw->tweet_media_raw($message, $raws);
248: $errmsg = $ptw->errmsg;
249: $ret = ! $ptw->error;
250: $ptw = NULL;
251: if ($ret) {
252: $res = 'ツイートしました';
253: }
254: }
255: return $ret;
256: }
ブラウザからPOSTで受け取った画像データ $_POST['base64'] はBASE64でデコードされており、冒頭に余計なヘッダ情報が付いているので、このヘッダ情報を除き( preg_replace )、バイナリデータにデコードする( base64_decode )。
続いて pahooTwitterAPI クラスを呼び出し、tweet_media_raw メソッドを使って、メッセージと画像を一気に投稿する。
準備:pahooBlueskyAPI クラス
pahooBlueskyAPI.php
解説:Blueskyへ投稿する
typhoon.php
253: /**
254: * Bluesky投稿
255: * @param string $message 投稿文
256: * @param string $res 応答メッセージ格納用
257: * @return bool TRUE:成功/FALSE:失敗または未処理
258: */
259: function mediaBluesky($message, &$res) {
260: if (! BLUESKY) return FALSE;
261:
262: $ret = TRUE;
263: if (isset($_POST['base64']) && ($_POST['base64'] != '')) {
264: $base64 = preg_replace('/data\:image\/png\;base64\,/ui', '', $_POST['base64']);
265: $raws = array(base64_decode($base64));
266: $pbs = new pahooBlueskyAPI(BLUESKY_DOMAIN);
267: $res = $pbs->createSession();
268: $pbs->post($message, FALSE, NULL, NULL, $raws);
269: $errmsg = $pbs->geterror();
270: $ret = ! $pbs->iserror();
271: $res = $pbs->deleteSession();
272: $pbs = NULL;
273: if ($ret) {
274: $res = 'Blueskyに投稿しました';
275: }
276: }
277: return $ret;
278: }
ブラウザからPOSTで受け取った画像データ $_POST['base64'] はBASE64でデコードされており、冒頭に余計なヘッダ情報が付いているので、このヘッダ情報を除き( preg_replace )、バイナリデータにデコードする( base64_decode )。
続いて pahooBlueskyAPI クラスを呼び出し、post メソッドを使って、メッセージと画像を一気に投稿する。
解説:SNSへ投稿する(メイン・プログラム)
weatherMap.php
1152: // Twitter(現・X)、Blueskyへ投稿
1153: $message =<<< EOT
1154: 🌞天気図 {$dt}現在
1155:
1156: (ご参考)PHPで天気図を描く https://www.pahoo.org/e-soul/webtech/php06/php06-73-01.shtm
1157:
1158: EOT;
1159:
1160: if (TWITTER && isButton('tweet')) {
1161: mediaTweet($message, $res);
1162: }
1163: if (BLUESKY && isButton('bluesky')) {
1164: mediaBluesky($message, $res);
1165: }
1166:
1167: // 表示コンテンツを作成する.
参考サイト
- 気象庁防災情報XMLフォーマット 情報提供ページ
- PHPで天気予報を求める:ぱふぅ家のホームページ
- PHPで住所・ランドマークから最寄り駅を求める:ぱふぅ家のホームページ
- JavaScriptでHTML表示を画像保存する:ぱふぅ家のホームページ
- HTMLとCSSでさまざまなアイコンを表示する:ぱふぅ家のホームページ
- Twitter API - WebAPIの登録方法:ぱふぅ家のホームページ
- Bluesky API - WebAPIの登録方法:ぱふぅ家のホームページ
- PHPでTwitterに画像付きメッセージ投稿:ぱふぅ家のホームページ
- PHPでBlueskyに投稿する:ぱふぅ家のホームページ

そこで今回は、これらの情報をリアルタイムに取得し、Googleマップなどのマップに天気図を描くPHPプログラムを作ってみることにする。オプション機能として、天気図を含めて Twitter(現・X)や Blueckyに投稿できる機能を加えた。
(2026年1月12日)投稿URL表示機能を追加
(2025年8月13日)GoogleMaps JavaScript APIの変更に対応, pahooInputData.php導入