サンプル・プログラムの実行例
目次
- サンプル・プログラムの実行例
- サンプル・プログラム
- プログラムの方針
- 準備:PHP の https対応
- 準備:pahooGeoCode クラス
- 準備:pahooCache クラス
- 準備:pahooInputData 関数群
- 気象庁防災情報XMLフォーマット
- VXSE53の構造
- 準備:各種定数など
- 解説:気象庁防災情報XMLから最新の震源・震度に関する情報URLを取得
- 解説:地震情報の取り出し
- 解説:地震情報をマッピング情報に変換
- 解説:地図描画について
- 解説:オーバーレイ表示(Leaflet選択時のみ)
- 解説:表示とURLパラメータ
- 解説:SNS投稿機能
- 解説:html2canvasライブラリ
- 準備:pahooTwitterAPI クラス
- 解説:メディア付き投稿(RAWデータ)
- 解説:Twitter(現・X)へ投稿する
- 準備:pahooBlueskyAPI クラス
- 解説:Blueskyへ投稿する
- 解説:SNSへ投稿する(メイン・プログラム)
- 活用例
- 参考サイト
サンプル・プログラム
| earthquake.php | サンプル・プログラム本体。 |
| pahooGeoCode.php | 住所・緯度・経度に関わるクラス pahooGeoCode。 使い方は「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 が通ったディレクトリに配置すること。 |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 5.8.0 | 2026/01/12 | mediaBluesky - 投稿URL表示機能を追加 |
| 5.7.0 | 2025/08/24 | .pahooEnv導入 |
| 5.6.0 | 2024/12/08 | Bluesky投稿機能を追加, isButton()修正 |
| 5.5.0 | 2024/06/22 | Twitter(現・X)ボタンを "X" に変更 |
| 5.4.1 | 2023/09/20 | js_html2image()--Leaflet用html2image()発火プロセス見直し |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 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インストール手順
これで準備は完了だ。
準備: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でクラスを使ってテキストの読みやすさを調べる」を参照されたい。
準備: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でクラスを使ってテキストの読みやすさを調べる」を参照されたい。
準備:pahooInputData 関数群
また、各種クラウドサービスに登録したときに取得するアカウント情報、アプリケーションパスワードなどを登録した .pahooEnv ファイルから読み込む関数 pahooLoadEnv を備えている。こちらについては、「各種クラウド連携サービス(WebAPI)の登録方法」をご覧いただきたい。
気象庁防災情報XMLフォーマット
VXSE53の構造
ここから必要な情報を取り出す。
準備:各種定数など
earthquake.php
52: // 各種定数(START) ===========================================================
53:
54: // Twitter(現・X)投稿ボタン TRUE:有効,FALSE:無効
55: define('TWITTER', FALSE);
56:
57: // Bluesky投稿ボタン TRUE:有効,FALSE:無効
58: define('BLUESKY', FALSE);
59:
60: // 画像化したいオブジェクト
61: define('TARGET', 'target');
62:
63: // 地図描画サービスの選択
64: // 0:Google
65: // 2:地理院地図・OSM
66: define('MAPSERVICE', 2);
67:
68: // マップの表示サイズ(単位:ピクセル)
69: define('MAP_WIDTH', 600);
70: define('MAP_HEIGHT', 400);
71: // マップID
72: define('MAPID', 'map_id');
73: // 初期値
74: define('DEF_ZOOM', 6); // ズーム
75: define('DEF_TYPE', 'ROADMAP'); // マップタイプ
76: define('DEF_OVERLAYS', 'GSIELEV,GSIFAULT'); // オーバーレイ(Leaflet使用時のみ)
77: define('INFO_WIDTH', (MAP_WIDTH * 0.75)); // 情報ウィンドウの最大幅
78: define('INFO_OFFSET_X', +20); // 情報ウィンドウのオフセット位置(X)
79: define('INFO_OFFSET_Y', -20); // 情報ウィンドウのオフセット位置(Y)
80:
81: // Spinner - jQuery UI を使用するかどうか
82: define('USESPINNER', TRUE);
83:
84: define('DEF_NUMBER', 1); // 求めたい震源の数(初期値)
85: define('MAX_NUMBER', 50); // 求めたい震源の数(最大)
86:
87: // キャッシュ保持時間(分) 0:キャッシュしない
88: // 気象庁へのアクセス負荷軽減のため,適切な時間を設定してください.
89: define('LIFE_CACHE_FEED', 5); // 高頻度 - 地震火山フィードに対して
90: define('LIFE_CACHE_FEED_L', 120); // 長期 - 地震火山フィードに対して
91: define('LIFE_CACHE_DATA', 720); // 地震情報に対して
92:
93: // キャッシュ・ディレクトリ
94: // 書き込み可能で,外部からアクセスされないディレクトリを指定してください.
95: // 天気予報系のプログラムとは別のディレクトリを指定してください.
96: define('DIR_CACHE_FEED', './pcache1/');
97: define('DIR_CACHE_FEED_L', './pcache2/');
98: define('DIR_CACHE_DATA', './pcache3/');
99:
100: // 気象庁防災情報XML:高頻度フィード - 地震火山【変更不可】
101: define('FEED', 'https://www.data.jma.go.jp/developer/xml/feed/eqvol.xml');
102:
103: // 気象庁防災情報XML:長期フィード - 地震火山【変更不可】
104: define('FEED_L', 'https://www.data.jma.go.jp/developer/xml/feed/eqvol_l.xml');
105:
106: // 住所・緯度・経度に関わるクラス:include_pathが通ったディレクトリに配置
107: require_once('pahooGeoCode.php');
108:
109: // キャッシュ処理に関わるクラス:include_pathが通ったディレクトリに配置
110: require_once('pahooCache.php');
111:
112: // Twitterクラス:include_pathが通ったディレクトリに配置
113: if (TWITTER) {
114: require_once('pahooTwitterAPI.php');
115: }
116:
117: // BlueskyAPIクラス:include_pathが通ったディレクトリに配置
118: if (BLUESKY) {
119: require_once('pahooBlueskyAPI.php');
120: define('BLUESKY_DOMAIN', 'bsky.social'); // あなたのドメインを記入
121: }
122: // 各種定数(END) ===============================================================
表示する地図は、Googleマップ、地理院地図・オープンストリートマップ(OSM)から選べる。あらかじめ、定数 MAPSERVIC に値を設定すること。
出力結果を 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から最新の震源・震度に関する情報URLを取得
earthquake.php
412: /**
413: * 気象庁防災情報XMLから震源・震度に関する情報URLを取得
414: * @param int $mode 0:最新1件のみ取得,1:すべての情報URL取得
415: * @param array $urls URL格納配列
416: * @param string $errmsg エラーメッセージ格納用
417: * @return bool TRUE:取得成功/FALSE:取得失敗
418: */
419: function jma_getLastEarthquakeURLs($mode, &$urls, &$errmsg) {
420: // URLパターン
421: $vxse53 = '/https?\:\/\/www\.data\.jma\.go\.jp\/developer\/xml\/data\/([0-9\_]+)VXSE53\_[0-9]+\.xml/ui';
422:
423: // 随時フィードの解析
424: $cnt = 0;
425: $pcc = new pahooCache(LIFE_CACHE_FEED, DIR_CACHE_FEED);
426: $xml = $pcc->simplexml_load(FEED);
427: // レスポンス・チェック
428: if ($pcc->iserror() || !isset($xml->entry)) {
429: $errmsg = '気象庁防災情報XMLにアクセスできません';
430: return FALSE;
431: }
432: foreach ($xml->entry as $node) {
433: // URLを取得
434: if (preg_match($vxse53, $node->id, $arr) > 0) {
435: $urls[$cnt] = $arr[0];
436: $cnt++;
437: }
438: }
439: $pcc = NULL;
440:
441: // 長期フィードの解析
442: $pcc = new pahooCache(LIFE_CACHE_FEED_L, DIR_CACHE_FEED_L);
443: $xml = $pcc->simplexml_load(FEED_L);
444: // レスポンス・チェック
445: if ($pcc->iserror() || !isset($xml->entry)) {
446: $errmsg = '気象庁防災情報XMLにアクセスできません';
447: return FALSE;
448: }
449: foreach ($xml->entry as $node) {
450: // URLを取得
451: if (preg_match($vxse53, $node->id, $arr) > 0) {
452: if (array_search($arr[0], $urls) === FALSE) {
453: $urls[$cnt] = $arr[0];
454: $cnt++;
455: }
456: }
457: }
458: $pcc = NULL;
459:
460: // エラー・チェック
461: if ($cnt == 0) {
462: $errmsg = '直近の地震情報はありません';
463: return FALSE;
464: }
465:
466: // URLを日時の新しい順にソート
467: rsort($urls);
468:
469: return TRUE;
470: }
VXSE53 を含むURLを配列 $urls に格納していき、最後に配列 $urls を大きい順にソートすることで、ひづけんの新しい順にソートしたことになる。
解説:地震情報の取り出し
earthquake.php
472: /**
473: * 地震情報取得(気象庁防災情報XMLから)
474: * @param object $pgc pahooGeoCodeオブジェクト
475: * @param array $items 地震情報を格納する配列
476: * @param string $urls 情報XMLのURLを格納する配列
477: * @param string $errmsg エラーメッセージ格納用
478: * @param int $count 取得件数(省略時=1)
479: * @return bool TRUE:取得成功/FALSE:失敗
480: */
481: function get_earthquake($pgc, &$items, &$urls, &$errmsg, $count=1) {
482: // 名前空間
483: define('JMX_EB', 'http://xml.kishou.go.jp/jmaxml1/elementBasis1/');
484: // マッチングパターン
485: $pat1 = '/([0-9]+)\-([0-9]+)\-([0-9]+)T([0-9]+)\:([0-9]+)/ui'; // 年月日時分
486: $pat2 = '/([\+\-][0-9\.]+)([\+\-][0-9\.]+)([\-\+\/])([0-9]*)/ui'; // 緯度・経度・深さ
487:
488: // オブジェクト生成
489: $pcc = new pahooCache(LIFE_CACHE_DATA, DIR_CACHE_DATA);
490:
491: // 最新の震源・震度に関する情報URLを取得
492: $urls = array();
493: $mode = ($count == 1) ? 0 : 1;
494: jma_getLastEarthquakeURLs($mode, $urls, $errmsg);
495: if ($errmsg != '') return FALSE;
496:
497: foreach ($urls as $key=>$vxse53) {
498: // 取得件数が上限だったらループ脱出
499: if ($key >= $count) break;
500:
501: // 震源情報の取得
502: $xml = $pcc->simplexml_load($vxse53);
503: // レスポンス・チェック
504: if ($pcc->iserror() || !isset($xml->Body->Earthquake)) {
505: $errmsg = '気象庁防災情報XMLから最新の震源・震度情報を取得できません';
506: return FALSE;
507: }
508:
509: // 地震発生日時の取得
510: if ((preg_match($pat1, (string)$xml->Body->Earthquake->OriginTime, $arr) > 0) && isset($arr[5])) {
511: $items[$key]['year'] = (int)$arr[1];
512: $items[$key]['month'] = (int)$arr[2];
513: $items[$key]['day'] = (int)$arr[3];
514: $items[$key]['hour'] = (int)$arr[4];
515: $items[$key]['minuite'] = (int)$arr[5];
516: } else {
517: $errmsg = '気象庁防災情報XMLから最新の震源・震度情報を取得できません';
518: return FALSE;
519: }
520:
521: // 震源地の取得
522: if (isset($xml->Body->Earthquake->Hypocenter->Area->Name)) {
523: $items[$key]['location'] = $xml->Body->Earthquake->Hypocenter->Area->Name;
524: } else {
525: $items[$key]['location'] = '不明';
526: }
527: if ($items[$key]['location'] == '') {
528: $items[$key]['location'] = '不明';
529: }
530: $node = $xml->Body->Earthquake->Hypocenter->Area->children(JMX_EB);
531: if (preg_match($pat2, (string)$node->Coordinate, $arr)) {
532: if (isset($arr[1]) && isset($arr[2])) {
533: list($items[$key]['longitude'], $items[$key]['latitude']) = $pgc->tokyo_wgs84((float)$arr[2], (float)$arr[1]);
534: } else {
535: $items[$key]['latitude'] = $items[$key]['longitude'] = '不明';
536: }
537: if (isset($arr[3]) && ($arr[3] == '/')) {
538: $items[$key]['depth'] = '不明';
539: } else if (isset($arr[4])) {
540: $items[$key]['depth'] = (float)$arr[4];
541: } else {
542: $items[$key]['depth'] = '不明';
543: }
544: }
545:
546: // マグニチュードの取得
547: $node = $xml->Body->Earthquake->children(JMX_EB);
548: if (isset($node->Magnitude)) {
549: $items[$key]['magnitude'] = (float)$node->Magnitude;
550: } else {
551: $items[$key]['magnitude'] = '不明';
552: }
553:
554: // 震度を取得
555: if (isset($xml->Body->Intensity->Observation->MaxInt)) {
556: $items[$key]['maxintensity'] = (int)$xml->Body->Intensity->Observation->MaxInt;
557: } else {
558: $items[$key]['maxintensity'] = '不明';
559: }
560: }
561:
562: // オブジェクト解法
563: $pcc = NULL;
564:
565: return TRUE;
566: }
pahooGeoCode.php
616: /**
617: * 日本測地系を世界測地系に変換する
618: * @param float $long 経度(日本測地系)
619: * @param float $lat 緯度(日本測地系)
620: * @return float array(経度,緯度)(世界測地系)
621: */
622: function tokyo_wgs84($long, $lat) {
623: $glong = $long - $lat * 0.000046038 - $long * 0.000083043 + 0.010040;
624: $glat = $lat - $lat * 0.00010695 + $long * 0.000017464 + 0.0046017;
625: return array($glong, $glat);
626: }
解説:地震情報をマッピング情報に変換
earthquake.php
579: /**
580: * 地震情報をマッピング情報に変換
581: * @param array $items 地震情報を格納した配列
582: * @param array $points マッピング情報を格納する配列
583: * @return int 変換したマッピング情報の件数
584: */
585: function info2points(&$items, &$points) {
586: $i = 0;
587: $j = 0;
588: foreach ($items as $i=>$item) {
589: // 同じマッピング位置があるかどうか
590: $flag = FALSE;
591: for ($k = 0; $k < $j; $k++) {
592: if (($items[$i]['latitude'] == $points[$k + 1]['latitude']) && ($items[$i]['longitude'] == $points[$k + 1]['longitude'])) {
593: $flag = TRUE;
594: $points[$k + 1]['description'] .= '<br /><hr />';
595: break;
596: }
597: }
598: // 新規のマッピング位置
599: if (! $flag) {
600: $k = $j;
601: $points[$k + 1]['latitude'] = $items[$i]['latitude'];
602: $points[$k + 1]['longitude'] = $items[$i]['longitude'];
603: $points[$k + 1]['title'] = '';
604: $points[$k + 1]['description'] = '';
605: $j++;
606: }
607: $items[$i]['id'] = num2alpha($k + 1);
608: $dt = makeDateTime($items[$i]);
609: $latitude = sprintf('%.1f', $items[$i]['latitude']);
610: $longitude = sprintf('%.1f', $items[$i]['longitude']);
611: if (is_numeric($items[$i]['magnitude'])) {
612: $magnitude = sprintf('%.1f', $items[$i]['magnitude']);
613: } else {
614: $magnitude = $items[$i]['magnitude'];
615: }
616: if (is_numeric($items[$i]['depth'])) {
617: if ($items[$i]['depth'] == 0) {
618: $depth = 'ごく浅い';
619: } else {
620: $depth = sprintf('約%dkm', $items[$i]['depth'] / 1000);
621: }
622: } else {
623: $depth = $items[$i]['depth'];
624: }
625: $points[$k + 1]['description'] .=<<< EOT
626: 発生日時:{$dt}<br />震源地:{$items[$i]['location']}<br />震源の位置:北緯 {$latitude}度,東経 {$longitude}度<br />震源の深さ:{$depth}<br />地震の規模:マグニチュード {$magnitude}<br />最大震度:{$items[$i]['maxintensity']}
627: EOT;
628: // 打ち切り条件
629: if ($j >= 26) break;
630: }
631:
632: return $j;
633: }
そこで、ユーザー関数 info2points を使って、震源1つに対して1つの要素が対応する配列 [$items] に対し、同じ緯度・経度に対して1つの要素が対応する配列 [$points] に情報を変換してやる。
こうすることで、同一震源にマップ・アイコンを1つだけ立てて、情報ウィンドウに複数回の地震情報を羅列するようにできる。
解説:地図描画について
解説:オーバーレイ表示(Leaflet選択時のみ)
オーバーレイ地図としては、地理院地図の 色別標高図、活断層図(都市圏活断層図)、治水地形分類図 更新版(2007~2020年)の3つを用意した。
pahooGeoCode.php
2030: // 地理院地図:色別標高図(オーバーレイ)
2031: let GSIELEV = new L.tileLayer(
2032: 'https://cyberjapandata.gsi.go.jp/xyz/relief/{z}/{x}/{y}.png',
2033: {
2034: attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>",
2035: opacity: 0.6,
2036: minZoom: 5,
2037: maxZoom: 15,
2038: name: 'GSIELEV'
2039: });
2040: // 地理院地図:活断層図(オーバーレイ)
2041: let GSIFAULT = new L.tileLayer(
2042: 'https://cyberjapandata.gsi.go.jp/xyz/afm/{z}/{x}/{y}.png',
2043: {
2044: attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>",
2045: opacity: 0.6,
2046: minZoom: 11,
2047: maxZoom: 16,
2048: name: 'GSIFAULT'
2049: });
2050: // 地理院地図:治水地形分類図 更新版(オーバーレイ)
2051: let GSIFLOOD = new L.tileLayer(
2052: 'https://cyberjapandata.gsi.go.jp/xyz/lcmfc2/{z}/{x}/{y}.png',
2053: {
2054: attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>",
2055: opacity: 0.6,
2056: minZoom: 11,
2057: maxZoom: 16,
2058: name: 'GSIFLOOD'
2059: });
2060:
2061: // baseMapsオブジェクトにオーバーレイ設定
2062: let overlayMaps = {
2063: "色別標高図" : GSIELEV,
2064: "活断層図" : GSIFAULT,
2065: "治水地形分類図" : GSIFLOOD,
2066: };
2067:
2068: // layersコントロールにbaseMapsオブジェクトを設定して地図に追加
2069: L.control.layers(baseMaps, overlayMaps).addTo(map);
2070: {$type}.addTo(map);
2071: {$addoverlay}
解説:表示とURLパラメータ
earthquake.php
940: // パラメータ
941: $res = '';
942: $number = getParam('number', FALSE, DEF_NUMBER);
943: $number = trim($number);
944: if (preg_match('/^[0-9]+$/i', $number) > 0) {
945: $errmsg = '';
946: if (($number < 1) || ($number > MAX_NUMBER)) {
947: $errmsg = sprintf('判定できる整数の範囲は,1から%dまでです', MAX_NUMBER);
948: }
949: } else {
950: $errmsg = '数値は正の整数(自然数)を指定してください';
951: }
952: $zoom = getParam('zoom', FALSE, DEF_ZOOM);
953: $zoom = ($zoom == '') ? DEF_ZOOM : $zoom;
954: $type = getParam('type', FALSE, DEF_TYPE);
955: $type = ($type == '') ? DEF_TYPE : $type;
956: $overlays = getParam('overlays', FALSE, DEF_OVERLAYS);
957: $overlays = ($overlays == '') ? DEF_OVERLAYS : $overlays;
earthquake.php?number=10のようにすることで、number で指定した値が求めたい震源の数になる。
また、
earthquake.php?number=10&zoom=8&type=GSISTD&overlays=GSIELEV,GSIFAULTのように指定すると、震源数が10、拡大率が8、ベースマップが地理院地図、オーバーレイは色別標高図と活断層図で表示する。
解説:SNS投稿機能
コンテンツを描画しているのはクライアントにあるブラウザ(レンダリングエンジン)であることから、クライアント側で画像を作成し、サーバ側で SNS の API をコールするという方針とした。
- ブラウザはサーバにコンテンツ描画をリクエストする。
- サーバはデータサイトからデータ取得する。(サーバキャッシュにデータがあればそれを利用する)
- サーバはコンテンツ描画スクリプトを生成する。
- サーバはブラウザへレスポンス(HTML文)を返す。
- ブラウザはコンテンツをレンダリングする。
- ブラウザはレンダリングしたコンテンツを画像データとしてサーバへアップロードする。
- サーバは SNS の API を使ってツイートする。
- サーバは SNS へメッセージと画像を送る。
- サーバはブラウザへレスポンス(HTML文)を返す。
解説:html2canvasライブラリ
earthquake.php
168: <script src="https://html2canvas.hertzen.com/dist/html2canvas.js"></script>
169: <script>
170: function mytweet() {
171: $('#tweet').val('1');
172: document.myform.submit();
173: }
画像化を実行するJavaScript関数は html2canvas である。
earthquake.php
909: <div id="{$target}" name="{$target}" style="width:{$width2}px;">
910: <p>
911: ⚠️最近の地震情報 {$dt}現在
912: </p>
913: {$html}
914: {$res}
915: </div>
レンダリングエンジンによって違うのかもしれないが、html2canvas ライブラリによって画像化される範囲が実際よりやや小さいため、あえてマップ領域より、幅を20ピクセル大きくした範囲を画像化範囲としている。
earthquake.php
347: /**
348: * HTMLオブジェクトの画像化
349: * @param なし
350: * @return string JavaScriptコード
351: */
352: function js_html2image() {
353: $target = TARGET;
354: $js = '';
355:
356: // Googleマップの場合
357: if (MAPSERVICE == 0) {
358: $js .=<<< EOT
359: google.maps.event.addListener(map, 'tilesloaded', function() {
360: var capture = document.querySelector('#{$target}');
361: html2canvas(capture, {useCORS: true, allowTaint:true}).then(canvas => {
362: var base64 = canvas.toDataURL('image/png'); // 画像化
363: $('#base64').val(base64);
364: });
365: });
366:
367: EOT;
368:
369: // Leafletの場合(ブラウザによってはうまく動作しない)
370: } else {
371: $js .=<<< EOT
372: HTMLCanvasElement.prototype.getContext = function(origFn) {
373: return function(type, attribs) {
374: attribs = attribs || {};
375: attribs.preserveDrawingBuffer = true;
376: return origFn.call(this, type, attribs);
377: };
378: } (HTMLCanvasElement.prototype.getContext);
379:
380: // HTML画像化イベント登録
381: function html2image() {
382: var capture = document.querySelector('#{$target}');
383: html2canvas(capture, {useCORS: true, allowTaint:true}).then(canvas => {
384: var base64 = canvas.toDataURL('image/png'); // 画像化
385: $('#base64').val(base64);
386: });
387: };
388:
389: // ズーム変更イベント
390: map.on('zoomend', function() {
391: html2image();
392: });
393:
394: // マップ移動イベント
395: map.on('moveend', function() {
396: html2image();
397: });
398:
399: // html2image()を発火させるために,ズームアウトして500msec後に元に戻す.
400: var zoom = map.getZoom();
401: map.setZoom(zoom - 1);
402: setTimeout(function() {
403: map.setZoom(zoom);
404: }, 500);
405:
406: EOT;
407: }
408:
409: return $js;
410: }
Leafletの場合、tilesloaded に相当するイベントがないため、ズーム変更完了イベント zoomend にフックするようにした。強制的にズームアウトし、500ミリ秒後に元ズーム値に戻す操作を行う。しかし、この方法だとブラウザによって、マップ画像が無い状態で画像化されてしまうことがあるようだ。もし対策方法をご存じの方がいたらお知らせいただきたい。
これらをJavaScriptとして生成するユーザー関数が js_html2image である。
準備:pahooTwitterAPI クラス
pahooTwitterAPI.php
11: // TwitterOAuth クラスをロードする.
12: $version = explode('.', phpversion());
13: if ($version[0] >= 8) {
14: require __DIR__ . '/vendor/autoload.php';
15: }
16: use Abraham\TwitterOAuth\TwitterOAuth;
17:
「Twitter API」を利用するために、API key、API key secret、Access Token、Access Token secret が必要で、入手方法は「Twitter API - WebAPIの登録方法」を参照されたい。
PHPのクラスについては「PHPでクラスを使ってテキストの読みやすさを調べる」を参照されたい。
OAuth認証 および Twitter API へのアクセスには、TwitterOAuth を利用する。導入方法は公式サイトにあるように composer を使うことを推奨している。
⚠ TwitterOAuth 8.1.0 は、2025年(令和7年)11月20日にリリースされた PHP 8.5 に対応している。
TwitterOAuth クラスをロードする。composer でインストールされたフォルダ "\vendor" を "pahooTwitterAPI.php" と同じフォルダへコピーすること。
pahooTwitterAPI.php
37: /**
38: * コンストラクタ
39: * @param なし
40: * @return なし
41: */
42: function __construct() {
43: if (isset($_ENV['PAHOO_TWTR_CONSUMER_KEY'])) {
44: $this->TWTR_CONSUMER_KEY = $_ENV['PAHOO_TWTR_CONSUMER_KEY'];
45: }
46: if (isset($_ENV['PAHOO_TWTR_CONSUMER_SECRET'])) {
47: $this->TWTR_CONSUMER_SECRET = $_ENV['PAHOO_TWTR_CONSUMER_SECRET'];
48: }
49: if (isset($_ENV['PAHOO_TWTR_ACCESS_KEY'])) {
50: $this->TWTR_ACCESS_KEY = $_ENV['PAHOO_TWTR_ACCESS_KEY'];
51: }
52: if (isset($_ENV['PAHOO_TWTR_ACCESS_SECRET'])) {
53: $this->TWTR_ACCESS_SECRET = $_ENV['PAHOO_TWTR_ACCESS_SECRET'];
54: }
55:
56: // プロパティを初期化する.
57: $this->responses = array();
58: $this->webapi = '';
59: $this->error = FALSE;
60: $this->errmsg = '';
61: $this->errcode = 0;
62:
63: $this->connection = new TwitterOAuth($this->TWTR_CONSUMER_KEY, $this->TWTR_CONSUMER_SECRET, $this->TWTR_ACCESS_KEY, $this->TWTR_ACCESS_SECRET);
64: // v2使用を宣言
65: $this->connection->setApiVersion('2');
66:
67: // PHP8.4で暗黙的にnullableを指定することが非推奨になったため
68: // TwitterOAuthが対応するまでの応急処置
69: error_reporting(E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED);
70: }
pahooTwitterAPI.php
30: // OAuth用パラメータ
31: // https://apps.twitter.com/
32: public $TWTR_CONSUMER_KEY = ''; // Cunsumer key
33: public $TWTR_CONSUMER_SECRET = ''; // Consumer secret
34: public $TWTR_ACCESS_KEY = ''; // Access Token (oauth_token)
35: public $TWTR_ACCESS_SECRET = ''; // Access Token Secret (oauth_token_secret)
36:
解説:メディア付き投稿(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)へ投稿する
earthquake.php
290: /**
291: * Twitter(現・X)投稿
292: * @param string $message 投稿文
293: * @param string $res 応答メッセージ格納用
294: * @return bool TRUE:成功/FALSE:失敗または未処理
295: */
296: function mediaTweet($message, &$res) {
297: if (! TWITTER) return FALSE;
298:
299: $ret = TRUE;
300: if (isset($_POST['base64']) && ($_POST['base64'] != '')) {
301: $base64 = preg_replace('/data\:image\/png\;base64\,/ui', '', $_POST['base64']);
302: $raws = array(base64_decode($base64));
303: $ptw = new pahooTwitterAPI();
304: $ptw->tweet_media_raw($message, $raws);
305: $errmsg = $ptw->errmsg;
306: $ret = ! $ptw->error;
307: $ptw = NULL;
308: if ($ret) {
309: $res = 'ツイートしました';
310: }
311: }
312: return $ret;
313: }
ブラウザからPOSTで受け取った画像データ $_POST['base64'&$x5D; はBASE64でデコードされており、冒頭に余計なヘッダ情報が付いているので、このヘッダ情報を除き( preg_replace )、バイナリデータにデコードする( base64_decode )。
続いて pahooTwitterAPI クラスを呼び出し、tweet_media_raw メソッドを使って、メッセージと画像を一気にツイートする。
準備:pahooBlueskyAPI クラス
pahooBlueskyAPI.php
19: class pahooBlueskyAPI {
20: public $pds; // PDSドメイン
21: public $webapi ; // 直前に呼び出したWebAPI URL
22: public $errmsg; // エラーメッセージ
23: public $accessJwt; // accessJwt
24: public $refreshJwt; // refreshJwt
25:
26: const INTERNAL_ENCODING = 'UTF-8'; // 内部エンコーディング
27: const MAX_MESSAGE_LEN = 300; // 投稿可能なメッセージ文字数
28: const URL_LEN = 23; // メッセージ中のURL文字数(相当)
29: const MAX_IMAGE_WIDTH = 1200; // 投稿可能な最大画像幅(ピクセル)
30: const MAX_IMAGE_HEIGHT = 675; // 投稿可能な最大画像高さ(ピクセル)
31: // これより大きいときは自動縮小する
32: // トークンを保存するファイル名
33: // 秘匿性を保つことができ、かつ、PHPプログラムから読み書き可能であること
34: const FILENAME_TOKEN = './.token';
35:
36: // -- 以下のデータは .env ファイルに記述可能
37: // Bluesky API アプリパスワード
38: // https://bsky.app/
39: public $BLUESKY_HANDLE = ''; // ハンドル名
40: public $BLUESKY_PASSWORD = ''; // アプリケーション・パスワード
「Bluesky API」を利用するために、ハンドル名、アプリケーション・パスワード が必要で、入手方法は「Bluesky API - WebAPIの登録方法」を参照されたい。
PHPのクラスについては「PHPでクラスを使ってテキストの読みやすさを調べる」を参照されたい。
解説:Blueskyへ投稿する
earthquake.php
315: /**
316: * Bluesky投稿
317: * @param string $message 投稿文
318: * @param string $res 応答メッセージ格納用
319: * @return bool TRUE:成功/FALSE:失敗または未処理
320: */
321: function mediaBluesky($message, &$res) {
322: if (! BLUESKY) return FALSE;
323:
324: $ret = TRUE;
325: if (isset($_POST['base64']) && ($_POST['base64'] != '')) {
326: // 地震発生地点画像を取得
327: $base64 = preg_replace('/data\:image\/png\;base64\,/ui', '', $_POST['base64']);
328: $raws = array(base64_decode($base64));
329: // インスタンス生成
330: $pbs = new pahooBlueskyAPI(BLUESKY_DOMAIN);
331: $res = $pbs->createSession();
332: // 投稿
333: $url = $pbs->post($message, FALSE, NULL, NULL, $raws);
334: // エラー処理
335: $errmsg = $pbs->geterror();
336: $ret = ! $pbs->iserror();
337: $res = $pbs->deleteSession();
338: // インスタンス解放
339: $pbs = NULL;
340: if ($ret) {
341: $res = "Blueskyに投稿しました -- <a href=\"{$url}\">{$url}</a>";
342: }
343: }
344: return $ret;
345: }
ブラウザからPOSTで受け取った画像データ $_POST['base64'] はBASE64でデコードされており、冒頭に余計なヘッダ情報が付いているので、このヘッダ情報を除き( preg_replace )、バイナリデータにデコードする( base64_decode )。
続いて pahooBlueskyAPI クラスを呼び出し、post メソッドを使って、メッセージと画像を一気に投稿する。
解説:SNSへ投稿する(メイン・プログラム)
earthquake.php
973: // Twitter(現・X)、Blueskyへ投稿する.
974: $dt = nowDT();
975: $message =<<< EOT
976: ⚠️最近の地震情報 {$dt}現在
977:
978: (ご参考)PHPで最近の地震情報を表示する https://www.pahoo.org/e-soul/webtech/php05/php05-17-01.shtm #地震
979:
980: EOT;
981: if (TWITTER && isButton('tweet')) {
982: mediaTweet($message, $res);
983: }
984: if (BLUESKY && isButton('bluesky')) {
985: mediaBluesky($message, $res);
986: }
987:
988: // 表示コンテンツを作成する.
活用例
参考サイト
- WebAPIの登録方法:ぱふぅ家のホームページ
- PHPで住所・ランドマークから最寄り駅を求める:ぱふぅ家のホームページ
- JavaScriptでHTML表示を画像保存する:ぱふぅ家のホームページ
- HTMLとCSSでさまざまなアイコンを表示する:ぱふぅ家のホームページ
- Twitter API - WebAPIの登録方法:ぱふぅ家のホームページ
- PHPでTwitterに画像付きメッセージ投稿:ぱふぅ家のホームページ
- Bluesky API - WebAPIの登録方法:ぱふぅ家のホームページ
- PHPでBlueskyに投稿する:ぱふぅ家のホームページ
- C++ で直近の地震情報を取得する:ぱふぅ家のホームページ
- 日本の直近の地震情報:みんなの知識 ちょっと便利帳

そこで今回は、気象庁防災情報XMLから、直近の地震情報を取得するPHPスクリプトを作成してみることにする。
余震が多いことが分かりやすいように、求めたい震源の数を任意に指定できるように改良した。表示マップの種類を変えたり、マップを含めてツイートすることができる。
また、Windowsアプリを「C++ で直近の地震情報を取得する」で公開している。あわせてご試用いただきたい。
(2026年1月12日)投稿URL表示機能を追加
(2025年8月24日).pahooEnv 導入
(2025年6月14日)GoogleMaps JavaScript APIの変更に対応した.