PHPで最近の地震情報を表示する

(1/1)
東日本大震災の余震と見られる地震が収まらない。
そこで今回は、気象庁防災情報XMLから、直近の地震情報を取得するPHPスクリプトを作成してみることにする。
余震が多いことが分かりやすいように、求めたい震源の数を任意に指定できるように改良した。表示マップの種類を変えたり、マップを含めてツイートすることができる。
また、Windowsアプリを「C++ で直近の地震情報を取得する」で公開している。あわせてご試用いただきたい。

(2024年12月8日)Bluesky投稿機能を追加
(2024年6月22日)TwitterOAuth 7.0.0 対応,Twitter(現・X)ボタンを "X" に変更

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

PHPで直近の地震情報を表示する
オープンストリートマップ+色別標高図+活断層図

目次

サンプル・プログラム

圧縮ファイルの内容
earthquake.phpサンプル・プログラム本体。
pahooGeoCode.php住所・緯度・経度に関わるクラス pahooGeoCode。
使い方は「PHPで住所・ランドマークから最寄り駅を求める」などを参照。include_path が通ったディレクトリに配置すること。
pahooCache.phpキャッシュ処理に関わるクラス pahooCache。
キャッシュ処理に関わるクラスの使い方は「PHPで天気予報を求める」を参照。include_path が通ったディレクトリに配置すること。
pahooTwitterAPI.phpTwitter APIを利用するクラス pahooTwitterAPI。
使い方は「PHPでTwitterに投稿(ツイート)する」などを参照。include_path が通ったディレクトリに配置すること。
pahooBlueskyAPI.phpBluesky APIに関わるクラス pahooBlueskyAPI。
使い方は「PHPでPHPでBlueskyに投稿する」などを参照。include_path が通ったディレクトリに配置すること。
earthquake.php 更新履歴
バージョン 更新日 内容
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()発火プロセス見直し
5.4 2022/03/19 気象庁防災情報XMLのhttps化に対応
5.3 2021/12/03 色別標高図+活断層図をオーバーレイ表示
pahooGeoCode.php 更新履歴
バージョン 更新日 内容
6.5.0 2025/06/14 GoogleMaps JavaScript APIの変更に対応
6.4.0 2025/03/01 makeYOLP_GeoSelectCategory()--引数$flagWorld追加
6.3.3 2024/09/14 $this->NOMINATIM_EMAIL 追加
6.3.2 2024/02/14 getStaticMap() -- bug-fix
6.3.1 2023/07/09 bug-fix
pahooCache.php 更新履歴
バージョン 更新日 内容
1.1.1 2023/02/11 コメント追記
1.1 2021/04/08 simplexml_load()メソッド追加
1.0 2021/04/02 初版
pahooTwitterAPI.php 更新履歴
バージョン 更新日 内容
5.5.1 2024/11/23 __construct() -- PHP8.4における応急処置
5.5.0 2024/06/21 TwitterOAuth 7.0.0 対応
5.4.0 2024/05/18 twitter.com → x.com 変更対応
5.3.0 2023/08/15 tweet3() -- メディアのシャフル機能
5.2.1 2023/07/22 bug-fix
pahooBlueskyAPI.php 更新履歴
バージョン 更新日 内容
2.1.0 2025/03/20 getUserPosts() 追加
2.0.1 2025/01/24 getPostThread() -- 認証必要のエンドポイントに変更
2.0.0 2025/01/24 トークンを保持するよう改良
1.9.0 2025/01/16 getEmbedPosts() 追加
1.8.1 2024/12/11 getOGPInformation()--リダイレクト対応

プログラムの方針

気象庁の地震情報(https://www.jma.go.jp/jp/quake/)では、直近の地震情報を表示している。
正規表現を使って、このページから
  1. 発生日時分
  2. 震源地
  3. 震源の位置
  4. 震源の深さ
  5. 規模
  6. 最大震度
の6つを取り出していたが、2021年(令和3年)2月24日のサイト・リニューアルにより、スクレイピングによる取り出しが難しくなった。代わりに、気象庁防災情報XMLフォーマットから直近の地震情報を取り出し、これまで同様、震源の位置をマップ上にプロットすることにする。

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

気象庁防災情報XMLフォーマットは、気象や地震、火山に関する情報を随時流している。
まず、下表の6つの Atomフィードにアクセスし、必要なXML情報のURLを求める。
フィードの構造(xml) feed title タイトル subtitle サブタイトル updated 配信日時 id ID rights 著作権表記 entry title XML情報タイトル(1) id XML情報のURL(1) updated 更新日時(1) author name 作者(1) content 内容説明(1) entry title XML情報タイトル(2) id XML情報のURL(2) updated 更新日時(2) author name 作者(2) content 内容説明(2)
XML情報のURLの命名規則は以下の通り。
https://www.data.jma.go.jp/developer/xml/data/yyyymmddhhmmss_番号_電文コード_連番.xml
震源・震度に関する情報は、長期フィードの地震火山の中にある電文コード VXSE53 に入っている。そこで、フィードから配信日時 yyyymmddhhmmss が最も大きく、VXSE53 を含むURLを取り出せば、それが目指す情報XMLとなる。

VXSE53の構造

震源・震度に関する情報XML VXSE53 の構造は下記の通りである。
ここから必要な情報を取り出す。
VXSE53の構造(xml) Report Control Title 震源・震度に関する情報 DateTime 作成日時 Status ステータス EditorialOffice 編集者 PublishingOffice 配信者 Head Title 震源・震度情報 ReportDateTime 報告日時 TargetDateTime 発生日時 EventID イベントID InfoType 情報の種類 Serial 連番 InfoKind 情報の種類 InfoKindVersion 情報バージョン Headline Text 説明文 Body Earthquake OriginTime 発生日時 ArrivalTime 到達日時 Hypocenter Area Name 震源域 Code 震央地名 Intensity Observation CodeDefine MaxInt 最大震度 Pref Name 都道府県名 Code 都道府県コード MaxInt 最大震度 Area Name 地域名 Code 地域コード MaxInt 最大震度 City Name 市区町村名 Code 市区町村コード MaxInt 最大震度 IntensityStation Name 区長村名(1) Code 区長村コード(1) Int 震度(2) IntensityStation Name 区長村名(2) Code 区長村コード(2) Int 震度(2)

準備:pahooGeoCode クラス

pahooGeoCode.php

  37: class pahooGeoCode {
  38:     var $items;     // 検索結果格納用
  39:     var $error;     // エラー・フラグ
  40:     var $errmsg;    // エラー・メッセージ
  41:     var $hits;      // 検索ヒット件数
  42:     var $webapi;    // 直前に呼び出したWebAPI URL
  43: 
  44:     // Google Cloud Platform APIキー
  45:     // https://cloud.google.com/maps-platform/
  46:     // ※Google Maps APIを利用しないのなら登録不要
  47:     var $GOOGLE_API_KEY_1 = '**************************';   // HTTPリファラ用
  48:     var $GOOGLE_API_KEY_2 = '**************************';   // IP制限用
  49:     var $GOOGLE_MAP_ID = '*************************';       // GoogleMaps ID
  50: 
  51:     // Yahoo! JAPAN Webサービス アプリケーションID
  52:     // https://e.developer.yahoo.co.jp/register
  53:     // ※Yahoo! JAPAN Webサービスを利用しないのなら登録不要
  54:     var $YAHOO_APPLICATION_ID = '*****************************';

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

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

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

earthquake.php

  47: // 地図描画サービスの選択
  48: //    0:Google
  49: //    2:地理院地図・OSM
  50: define('MAPSERVICE', 2);

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

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

earthquake.php

  71: // キャッシュ保持時間(分) 0:キャッシュしない
  72: // 気象庁へのアクセス負荷軽減のため,適切な時間を設定してください.
  73: define('LIFE_CACHE_FEED',     5);   // 高頻度 - 地震火山フィードに対して
  74: define('LIFE_CACHE_FEED_L', 120);   // 長期 - 地震火山フィードに対して
  75: define('LIFE_CACHE_DATA',   720);   // 地震情報に対して
  76: 
  77: // キャッシュ・ディレクトリ
  78: // 書き込み可能で,外部からアクセスされないディレクトリを指定してください.
  79: // 天気予報系のプログラムとは別のディレクトリを指定してください.
  80: define('DIR_CACHE_FEED',   './pcache1/');
  81: define('DIR_CACHE_FEED_L', './pcache2/');
  82: define('DIR_CACHE_DATA',   './pcache3/');

気象庁サイトへ負荷をかけないよう、キャッシュ・クラス pahooCache を導入した。使用方法については、「PHPで天気予報を求める - キャッシュ・システム」を参照いただきたい。
キャッシュ保持時間、キャッシュ・ディレクトリともに、自サイトの環境に応じて変更してほしい。天気予報系プログラムと別のキャッシュ・ディレクトリにした方が、お互いのキャッシュ保持時間の干渉を受けなくなる。
配布ファイルは、新しい地震情報が入ってくることを考え、フィードの方を短く、地震情報の方は長くキャッシュ保持時間を設定してある。

準備:各種定数など

earthquake.php

  37: // 各種定数(START) ===========================================================
  38: // Twitter(現・X)投稿ボタン  TRUE:有効,FALSE:無効
  39: define('TWITTER', FALSE);
  40: 
  41: // Bluesky投稿ボタン  TRUE:有効,FALSE:無効
  42: define('BLUESKY', FALSE);
  43: 
  44: // 画像化したいオブジェクト
  45: define('TARGET', 'target');
  46: 
  47: // 地図描画サービスの選択
  48: //    0:Google
  49: //    2:地理院地図・OSM
  50: define('MAPSERVICE', 2);
  51: 
  52: // マップの表示サイズ(単位:ピクセル)
  53: define('MAP_WIDTH',  600);
  54: define('MAP_HEIGHT', 400);
  55: // マップID
  56: define('MAPID', 'map_id');
  57: // 初期値
  58: define('DEF_ZOOM',      6);                 // ズーム
  59: define('DEF_TYPE', 'ROADMAP');              // マップタイプ
  60: define('DEF_OVERLAYS', 'GSIELEV,GSIFAULT'); // オーバーレイ(Leaflet使用時のみ)
  61: define('INFO_WIDTH', (MAP_WIDTH * 0.75));   // 情報ウィンドウの最大幅
  62: define('INFO_OFFSET_X', +20);           // 情報ウィンドウのオフセット位置(X)
  63: define('INFO_OFFSET_Y', -20);           // 情報ウィンドウのオフセット位置(Y)
  64: 
  65: // Spinner - jQuery UI を使用するかどうか
  66: define('USESPINNER', TRUE);
  67: 
  68: define('DEF_NUMBER', 1);    // 求めたい震源の数(初期値)
  69: define('MAX_NUMBER', 50);   // 求めたい震源の数(最大)
  70: 
  71: // キャッシュ保持時間(分) 0:キャッシュしない
  72: // 気象庁へのアクセス負荷軽減のため,適切な時間を設定してください.
  73: define('LIFE_CACHE_FEED',     5);   // 高頻度 - 地震火山フィードに対して
  74: define('LIFE_CACHE_FEED_L', 120);   // 長期 - 地震火山フィードに対して
  75: define('LIFE_CACHE_DATA',   720);   // 地震情報に対して
  76: 
  77: // キャッシュ・ディレクトリ
  78: // 書き込み可能で,外部からアクセスされないディレクトリを指定してください.
  79: // 天気予報系のプログラムとは別のディレクトリを指定してください.
  80: define('DIR_CACHE_FEED',   './pcache1/');
  81: define('DIR_CACHE_FEED_L', './pcache2/');
  82: define('DIR_CACHE_DATA',   './pcache3/');
  83: 
  84: // 気象庁防災情報XML:高頻度フィード - 地震火山【変更不可】
  85: define('FEED', 'https://www.data.jma.go.jp/developer/xml/feed/eqvol.xml');
  86: 
  87: // 気象庁防災情報XML:長期フィード - 地震火山【変更不可】
  88: define('FEED_L', 'https://www.data.jma.go.jp/developer/xml/feed/eqvol_l.xml');
  89: 
  90: // 住所・緯度・経度に関わるクラス:include_pathが通ったディレクトリに配置
  91: require_once('pahooGeoCode.php');
  92: 
  93: // キャッシュ処理に関わるクラス:include_pathが通ったディレクトリに配置
  94: require_once('pahooCache.php');
  95: 
  96: // Twitterクラス:include_pathが通ったディレクトリに配置
  97: if (TWITTER) {
  98:     require_once('pahooTwitterAPI.php');
  99: }
 100: 
 101: // BlueskyAPIクラス:include_pathが通ったディレクトリに配置
 102: if (BLUESKY) {
 103:     require_once('pahooBlueskyAPI.php');
 104:     define('BLUESKY_DOMAIN', 'bsky.social');    // あなたのドメインを記入
 105: }
 106: // 各種定数(END) ===============================================================

表示に関わる各種パラメータは定数を defineしている。【変更不可】の記載のないものは、適宜変更してかまわない。

出力結果を 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

 433: /**
 434:  * 気象庁防災情報XMLから震源・震度に関する情報URLを取得
 435:  * @param   int    $mode    0:最新1件のみ取得,1:すべての情報URL取得
 436:  * @param   array  $urls    URL格納配列
 437:  * @param   string $errmsg  エラーメッセージ格納用
 438:  * @return  bool TRUE:取得成功/FALSE:取得失敗
 439: */
 440: function jma_getLastEarthquakeURLs($mode, &$urls, &$errmsg) {
 441:     // URLパターン
 442:     $vxse53 = '/https?\:\/\/www\.data\.jma\.go\.jp\/developer\/xml\/data\/([0-9\_]+)VXSE53\_[0-9]+\.xml/ui';
 443: 
 444:     // 随時フィードの解析
 445:     $cnt = 0;
 446:     $pcc = new pahooCache(LIFE_CACHE_FEED, DIR_CACHE_FEED);
 447:     $xml = $pcc->simplexml_load(FEED);
 448:     // レスポンス・チェック
 449:     if ($pcc->iserror() || !isset($xml->entry)) {
 450:         $errmsg = '気象庁防災情報XMLにアクセスできません';
 451:         return FALSE;
 452:     }
 453:     foreach ($xml->entry as $node) {
 454:         // URLを取得
 455:         if (preg_match($vxse53, $node->id, $arr> 0) {
 456:             $urls[$cnt] = $arr[0];
 457:             $cnt++;
 458:         }
 459:     }
 460:     $pcc = NULL;
 461: 
 462:     // 長期フィードの解析
 463:     $pcc = new pahooCache(LIFE_CACHE_FEED_L, DIR_CACHE_FEED_L);
 464:     $xml = $pcc->simplexml_load(FEED_L);
 465:     // レスポンス・チェック
 466:     if ($pcc->iserror() || !isset($xml->entry)) {
 467:         $errmsg = '気象庁防災情報XMLにアクセスできません';
 468:         return FALSE;
 469:     }
 470:     foreach ($xml->entry as $node) {
 471:         // URLを取得
 472:         if (preg_match($vxse53, $node->id, $arr> 0) {
 473:             if (array_search($arr[0], $urls) === FALSE) {
 474:                 $urls[$cnt] = $arr[0];
 475:                 $cnt++;
 476:             }
 477:         }
 478:     }
 479:     $pcc = NULL;
 480: 
 481:     // エラー・チェック
 482:     if ($cnt == 0) {
 483:         $errmsg = '直近の地震情報はありません';
 484:         return FALSE;
 485:     }
 486: 
 487:     // URLを日時の新しい順にソート
 488:     rsort($urls);
 489: 
 490:     return TRUE;
 491: }

前述のフィードから、最新の震源・震度に関する情報URLを取得するユーザー関数が jma_getLastEarthquakeURLs である。
VXSE53 を含むURLを配列 $urls に格納していき、最後に配列 $urls を大きい順にソートすることで、ひづけんの新しい順にソートしたことになる。

解説:地震情報の取り出し

earthquake.php

 493: /**
 494:  * 地震情報取得(気象庁防災情報XMLから)
 495:  * @param   object $pgc     pahooGeoCodeオブジェクト
 496:  * @param   array  $items   地震情報を格納する配列
 497:  * @param   string $urls    情報XMLのURLを格納する配列
 498:  * @param   string $errmsg  エラーメッセージ格納用
 499:  * @param   int    $count   取得件数(省略時=1)
 500:  * @return  bool TRUE:取得成功/FALSE:失敗
 501: */
 502: function get_earthquake($pgc, &$items, &$urls, &$errmsg, $count=1) {
 503:     // 名前空間
 504:     define('JMX_EB', 'http://xml.kishou.go.jp/jmaxml1/elementBasis1/');
 505:     // マッチングパターン
 506:     $pat1 = '/([0-9]+)\-([0-9]+)\-([0-9]+)T([0-9]+)\:([0-9]+)/ui';      // 年月日時分
 507:     $pat2 = '/([\+\-][0-9\.]+)([\+\-][0-9\.]+)([\-\+\/])([0-9]*)/ui';   // 緯度・経度・深さ
 508: 
 509:     // オブジェクト生成
 510:     $pcc = new pahooCache(LIFE_CACHE_DATA, DIR_CACHE_DATA);
 511: 
 512:     // 最新の震源・震度に関する情報URLを取得
 513:     $urls = array();
 514:     $mode = ($count == 1? 0 : 1;
 515:     jma_getLastEarthquakeURLs($mode, $urls, $errmsg);
 516:     if ($errmsg !'')      return FALSE;
 517: 
 518:     foreach ($urls as $key=>$vxse53) {
 519:         // 取得件数が上限だったらループ脱出
 520:         if ($key >$count)     break;
 521: 
 522:         // 震源情報の取得
 523:         $xml = $pcc->simplexml_load($vxse53);
 524:         // レスポンス・チェック
 525:         if ($pcc->iserror() || !isset($xml->Body->Earthquake)) {
 526:             $errmsg = '気象庁防災情報XMLから最新の震源・震度情報を取得できません';
 527:             return FALSE;
 528:         }
 529: 
 530:         // 地震発生日時の取得
 531:         if ((preg_match($pat1, (string)$xml->Body->Earthquake->OriginTime, $arr> 0&& isset($arr[5])) {
 532:             $items[$key]['year']    = (int)$arr[1];
 533:             $items[$key]['month']   = (int)$arr[2];
 534:             $items[$key]['day']     = (int)$arr[3];
 535:             $items[$key]['hour']    = (int)$arr[4];
 536:             $items[$key]['minuite'] = (int)$arr[5];
 537:         } else {
 538:             $errmsg = '気象庁防災情報XMLから最新の震源・震度情報を取得できません';
 539:             return FALSE;
 540:         }
 541: 
 542:         // 震源地の取得
 543:         if (isset($xml->Body->Earthquake->Hypocenter->Area->Name)) {
 544:             $items[$key]['location'] = $xml->Body->Earthquake->Hypocenter->Area->Name;
 545:         } else {
 546:             $items[$key]['location'] = '不明';
 547:         }
 548:         if ($items[$key]['location'] == '') {
 549:             $items[$key]['location'] = '不明';
 550:         }
 551:         $node = $xml->Body->Earthquake->Hypocenter->Area->children(JMX_EB);
 552:         if (preg_match($pat2, (string)$node->Coordinate, $arr)) {
 553:             if (isset($arr[1]) && isset($arr[2])) {
 554:                 list($items[$key]['longitude'], $items[$key]['latitude']) = $pgc->tokyo_wgs84((float)$arr[2], (float)$arr[1]);
 555:             } else {
 556:                 $items[$key]['latitude'] = $items[$key]['longitude'] = '不明';
 557:             }
 558:             if (isset($arr[3]) && ($arr[3] == '/')) {
 559:                 $items[$key]['depth']  = '不明';
 560:             } else if (isset($arr[4])) {
 561:                 $items[$key]['depth']  = (float)$arr[4];
 562:             } else {
 563:                 $items[$key]['depth']  = '不明';
 564:             }
 565:         }
 566: 
 567:         // マグニチュードの取得
 568:         $node = $xml->Body->Earthquake->children(JMX_EB);
 569:         if (isset($node->Magnitude)) {
 570:             $items[$key]['magnitude']  = (float)$node->Magnitude;
 571:         } else {
 572:             $items[$key]['magnitude']  = '不明';
 573:         }
 574: 
 575:         // 震度を取得
 576:         if (isset($xml->Body->Intensity->Observation->MaxInt)) {
 577:             $items[$key]['maxintensity'] = (int)$xml->Body->Intensity->Observation->MaxInt;
 578:         } else {
 579:             $items[$key]['maxintensity'] = '不明';
 580:         }
 581:     }
 582: 
 583:     // オブジェクト解法
 584:     $pcc = NULL;
 585: 
 586:     return TRUE;
 587: }

震源・震度に関する情報XML VXSE53 から、地震発生年月日、震源の緯度・経度・深さ、地震の規模を表すマグニチュード、最大震度を取り出し、配列 $items に格納する。

pahooGeoCode.php

 588: /**
 589:  * 日本測地系を世界測地系に変換する
 590:  * @param   float $long 経度(日本測地系)
 591:  * @param   float $lat  緯度(日本測地系)
 592:  * @return  float array(経度,緯度)(世界測地系)
 593: */
 594: function tokyo_wgs84($long, $lat) {
 595:     $glong = $long - $lat * 0.000046038 - $long * 0.000083043 + 0.010040;
 596:     $glat  = $lat  - $lat * 0.00010695  + $long * 0.000017464 + 0.0046017;
 597:     return array($glong, $glat);
 598: }

ここで、緯度・経度は日本測地系であるため、あとでマップ描画しやすいように、pahooGeoCodeクラスのメソッド tokyo_wgs84 を使って世界測地系に変換しておく。

解説:地震情報をマッピング情報に変換

earthquake.php

 600: /**
 601:  * 地震情報をマッピング情報に変換
 602:  * @param   array  $items   地震情報を格納した配列
 603:  * @param   array  $points  マッピング情報を格納する配列
 604:  * @return  int 変換したマッピング情報の件数
 605: */
 606: function info2points(&$items, &$points) {
 607:     $i = 0;
 608:     $j = 0;
 609:     foreach ($items as $i=>$item) {
 610:         // 同じマッピング位置があるかどうか
 611:         $flag = FALSE;
 612:         for ($k = 0$k < $j$k++) {
 613:             if (($items[$i]['latitude'] == $points[$k + 1]['latitude']) && ($items[$i]['longitude'] == $points[$k + 1]['longitude'])) {
 614:                 $flag = TRUE;
 615:                 $points[$k + 1]['description'.'<br /><hr />';
 616:                 break;
 617:             }
 618:         }
 619:         // 新規のマッピング位置
 620:         if (! $flag) {
 621:             $k = $j;
 622:             $points[$k + 1]['latitude']    = $items[$i]['latitude'];
 623:             $points[$k + 1]['longitude']   = $items[$i]['longitude'];
 624:             $points[$k + 1]['title'] = '';
 625:             $points[$k + 1]['description'] = '';
 626:             $j++;
 627:         }
 628:         $items[$i]['id'] = num2alpha($k + 1);
 629:         $dt = makeDateTime($items[$i]);
 630:         $latitude  = sprintf('%.1f', $items[$i]['latitude']);
 631:         $longitude = sprintf('%.1f', $items[$i]['longitude']);
 632:         if (is_numeric($items[$i]['magnitude'])) {
 633:             $magnitude = sprintf('%.1f', $items[$i]['magnitude']);
 634:         } else {
 635:             $magnitude = $items[$i]['magnitude'];
 636:         }
 637:         if (is_numeric($items[$i]['depth'])) {
 638:             if ($items[$i]['depth'] == 0) {
 639:                 $depth = 'ごく浅い';
 640:             } else {
 641:                 $depth = sprintf('約%dkm', $items[$i]['depth'] / 1000);
 642:             }
 643:         } else {
 644:             $depth = $items[$i]['depth'];
 645:         }
 646:         $points[$k + 1]['description'.=<<< EOT
 647: 発生日時:{$dt}<br />震源地:{$items[$i]['location']}<br />震源の位置:北緯 {$latitude}度,東経 {$longitude}度<br />震源の深さ:{$depth}<br />地震の規模:マグニチュード {$magnitude}<br />最大震度:{$items[$i]['maxintensity']}
 648: EOT;
 649:         // 打ち切り条件
 650:         if ($j >26)   break;
 651:     }
 652: 
 653:     return $j;
 654: }

同じ震源で複数回地震が起きることがある。
そこで、ユーザー関数 info2points を使って、震源1つに対して1つの要素が対応する配列 [$items] に対し、同じ緯度・経度に対して1つの要素が対応する配列 [$points] に情報を変換してやる。
こうすることで、同一震源にマップ・アイコンを1つだけ立てて、情報ウィンドウに複数回の地震情報を羅列するようにできる。

解説:地図描画について

マップの描画については、「PHPで住所・ランドマークから最寄り駅を求める」をご覧いただきたい。

解説:オーバーレイ表示(Leaflet選択時のみ)

マップで Leaflet を選択したとき、オーバーレイ地図を表示できるようにした。
オーバーレイ地図としては、地理院地図の 色別標高図活断層図(都市圏活断層図)治水地形分類図 更新版(2007~2020年)の3つを用意した。

pahooGeoCode.php

   1: <?php
   2: /** pahooGeoCode.php
   3:  * 住所・緯度・経度に関わるクラス
   4:  *
   5:  * @copyright   (c)studio pahoo
   6:  * @author      パパぱふぅ
   7:  * @動作環境    PHP 5/7/8
   8:  * @参考URL     https://www.pahoo.org/e-soul/webtech/php06/php06-05-01.shtm
   9:  *              https://www.pahoo.org/e-soul/webtech/php06/php06-08-01.shtm
  10:  *              https://www.pahoo.org/e-soul/webtech/php06/php06-09-01.shtm
  11:  *              https://www.pahoo.org/e-soul/webtech/php06/php06-19-01.shtm
  12:  *              https://www.pahoo.org/e-soul/webtech/php06/php06-33-01.shtm
  13:  *
  14:  * [利用するライブラリ]
  15:  *  PEAR::XML_Unserializer
  16:  *  JavaScript::Leaflet
  17:  *
  18:  * [利用するWebAPI]
  19:  *  GoogleMaps JavaScript API
  20:  *  GoogleMaps Geocoding API
  21:  *  ジオどすII API【廃止】
  22:  *  簡易逆ジオコーディングサービス
  23:  *      出典: 農研機構 (https://aginfo.cgk.affrc.go.jp/)
  24:  *  YOLP 標高API
  25:  *  YOLP 気象情報API
  26:  *  YOLPコンテンツジオコーダAPI
  27:  *  Yahoo! JavaScriptマップ【廃止】
  28:  *  HeartRails Geo API
  29:  *  OSM Nominatim
  30:  *
  31:  * [その他,利用する外部リソース]
  32:  *  地理院地図タイル
  33:  *  OpenStreetMapタイル
  34: */
  35: 
  36: // pahooGeoCodeクラス =======================================================
  37: class pahooGeoCode {
  38:     var $items;     // 検索結果格納用
  39:     var $error;     // エラー・フラグ
  40:     var $errmsg;    // エラー・メッセージ
  41:     var $hits;      // 検索ヒット件数
  42:     var $webapi;    // 直前に呼び出したWebAPI URL
  43: 
  44:     // Google Cloud Platform APIキー
  45:     // https://cloud.google.com/maps-platform/
  46:     // ※Google Maps APIを利用しないのなら登録不要
  47:     var $GOOGLE_API_KEY_1 = '**************************';   // HTTPリファラ用
  48:     var $GOOGLE_API_KEY_2 = '**************************';   // IP制限用
  49:     var $GOOGLE_MAP_ID = '*************************';       // GoogleMaps ID
  50: 
  51:     // Yahoo! JAPAN Webサービス アプリケーションID
  52:     // https://e.developer.yahoo.co.jp/register
  53:     // ※Yahoo! JAPAN Webサービスを利用しないのなら登録不要
  54:     var $YAHOO_APPLICATION_ID = '*****************************';
  55: 
  56:     // OSM Nominatim Search API利用時に知らせるメールアドレス
  57:     // https://wiki.openstreetmap.org/wiki/JA:Nominatim#.E6.A4.9C.E7.B4.A2
  58:     var $NOMINATIM_EMAIL = '*****************************';
  59: 
  60:     // ジオどすII APIキー【廃止】
  61:     // http://geodosu.com/user/register
  62:     var $APIKEY_GEOTOS = '*****************';
  63: 
  64:     // IP2Location.io APIキー
  65:     // https://www.ip2location.io/
  66:     // ※IP2Location.ioを利用しないのなら登録不要
  67:     var $IP2LOCATION_API_KEY = '*****************';
  68: 
  69: /**
  70:  * コンストラクタ
  71:  * @param   なし
  72:  * @return  なし
  73: */
  74: function __construct() {
  75:     $this->error  = FALSE;
  76:     $this->errmsg = '';
  77:     $this->hits = 0;
  78:     $this->webapi = '';
  79: }
  80: 
  81: /**
  82:  * デストラクタ
  83:  * @return  なし
  84: */
  85: function __destruct() {
  86:     unset($this->items);
  87: }
  88: 
  89: /**
  90:  * エラー状況
  91:  * @return  bool TRUE:異常/FALSE:正常
  92: */
  93: function iserror() {
  94:     return $this->error;
  95: }
  96: 
  97: /**
  98:  * エラーメッセージ取得
  99:  * @param   なし
 100:  * @return  string 現在発生しているエラーメッセージ
 101: */
 102: function geterror() {
 103:     return $this->errmsg;
 104: }
 105: 
 106: /**
 107:  * PHP5以上かどうか検査する
 108:  * @return  bool TRUE:PHP5以上/FALSE:PHP5未満
 109: */
 110: function isphp5over() {
 111:     $version = explode('.', phpversion());
 112: 
 113:     return $version[0>5 ? TRUE : FALSE;
 114: }
 115: 
 116: /**
 117:  * PHP7以上かどうか検査する
 118:  * @return  bool TRUE:PHP5以上/FALSE:PHP5未満
 119: */
 120: function isphp7over() {
 121:     $version = explode('.', phpversion());
 122: 
 123:     return $version[0>7 ? TRUE : FALSE;
 124: }
 125: 
 126: /**
 127:  * 無効な証明書サイトからXML取得できるようにする
 128:  * @param   なし
 129:  * @return  なし
 130: */
 131: function unknown_certificate() {
 132:     $context = array(
 133:         'ssl' => array(
 134:             'verify_peer' => FALSE,
 135:             'verify_peer_name' => FALSE,
 136:         )
 137:     );
 138:     libxml_set_streams_context(stream_context_create($context));
 139: }
 140: 
 141: // GoogleMaps API Geocoding =================================================
 142: /**
 143:  * 指定XMLファイルを読み込んでDOMを返す
 144:  * @param   string $xml XMLファイル名
 145:  * @return  object DOMオブジェクト/NULL 失敗
 146: */
 147: function read_xml($xml) {
 148:     if ($this->isphp5over())    return NULL;
 149:     if (($fp = fopen($xml, 'r')) == FALSE)  return NULL;
 150: 
 151:     // いったん変数に読み込む
 152:     $str = fgets($fp);
 153:     $str = preg_replace('/UTF-8/', 'utf-8', $str);
 154: 
 155:     while (! feof($fp)) {
 156:         $str = $str . fgets($fp);
 157:     }
 158:     fclose($fp);
 159: 
 160:     // DOMを返す
 161:     $dom = domxml_open_mem($str);
 162:     if ($dom == NULL) {
 163:         echo "\n>Error while parsing the document - " . $xml . "\n";
 164:         exit(1);
 165:     }
 166: 
 167:     return $dom;
 168: }
 169: 
 170: /**
 171:  * GoogleMaps API Geocodingのformatted_addressから国名、郵便番号を除く
 172:  * @param   string $formatted_address 国名、郵便番号付き住所
 173:  * @return  string 住所のみ
 174: */
 175: function trimAddress($formatted_address) {
 176:     $pat1 = '/〒[0-9\-]+\s(.+)$/ui';
 177:     $pat2 = '/.+、(.+)$/ui';
 178: 
 179:     if (preg_match($pat1, $formatted_address, $arr> 0) {
 180:         $res = $arr[1];
 181:     } else if (preg_match($pat2, $formatted_address, $arr> 0) {
 182:         $res = $arr[1];
 183:     } else {
 184:         $res = $formatted_address;
 185:     }
 186: 
 187:     return $res;
 188: }
 189: 
 190: /**
 191:  * 指定した検索キーワードからGoogleMaps API Geocoding(V3) のURLを取得する.
 192:  * @param   string $query 検索キーワード(UTF-8)
 193:  * @return  string URL URL
 194: */
 195: function getURL_GeoCodeAPI_V3($query) {
 196:     $key = $this->GOOGLE_API_KEY_2;
 197:     return "https://maps.googleapis.com/maps/api/geocode/xml?key={$key}&language=ja&region=JP&address=" . urlencode($query);
 198: }
 199: 
 200: /**
 201:  * 指定した検索キーワードの緯度・経度を求める.
 202:  * クラウドサービスとしてGoogle Geocoding API(V3) を利用する.
 203:  * 検索結果結果は複数になることがあり,配列$itemsに格納する.
 204:  * 参考サイト https://www.pahoo.org/e-soul/webtech/php06/php06-08-01.shtm
 205:  * @param   string $query 検索キーワード
 206:  * @param   array  $items 情報を格納する配列
 207:  * @return  int ヒットした施設数/(-1):API呼び出し失敗
 208: */
 209: function getPointsV3_all($query, &$items) {
 210:     $url = $this->getURL_GeoCodeAPI_V3($query); // リクエストURL
 211:     $this->webapi = $url;
 212:     $n = 1;
 213: 
 214: // PEAR::XML_Unserializer
 215:     if (class_exists('XML_Unserializer')) {
 216:         $xml = new XML_Unserializer();
 217:         $xml_data = file_get_contents($url);
 218:         $xml->unserialize($xml_data);
 219:         $arr = $xml->getUnserializedData();
 220:         // レスポンス・チェック
 221:         if (preg_match('/ok/i', $arr['status']) == 0)   return (-1);
 222:         // 位置情報
 223:         if (isset($arr['result']['geometry'])) {
 224:             $items[$n]['latitude']  = $arr['result']['geometry']['location']['lat'];
 225:             $items[$n]['longitude'] = $arr['result']['geometry']['location']['lng'];
 226:             $items[$n]['address']   = $this->trimAddress($arr['result']['formatted_address']);
 227:             $n++;
 228:         } else {
 229:             foreach ($arr['result'as $val) {
 230:                 $items[$n]['latitude']  = $val['geometry']['location']['lat'];
 231:                 $items[$n]['longitude'] = $val['geometry']['location']['lng'];
 232:                 $items[$n]['address']   = $this->trimAddress($val['formatted_address']);
 233:                 $n++;
 234:             }
 235:         }
 236:         $xml = NULL;
 237: 
 238: // PHP4用; DOM XML利用
 239:     } else if ($this->isphp5over() == FALSE) {
 240:         if (($dom = $this->read_xml($url)) == NULLreturn FALSE;
 241:         $gr = $dom->get_elements_by_tagname('GeocodeResponse');
 242:         // レスポンス・チェック
 243:         $res  = $gr[0]->get_elements_by_tagname('status');
 244:         if (preg_match("/ok/i", $res[0]->get_content()) == 0)   return 0;
 245:         // 位置情報
 246:         $res = $gr[0]->get_elements_by_tagname('result');
 247:         foreach ($res as $val) {
 248:             $geo = $val->get_elements_by_tagname('geometry');
 249:             $loc = $geo[0]->get_elements_by_tagname('location');
 250:             $lat = $loc[0]->get_elements_by_tagname('lat');
 251:             $items[$n]['latitude'] = (double)$lat[0]->get_content();
 252:             $lng = $loc[0]->get_elements_by_tagname('lng');
 253:             $items[$n]['longitude'] = (double)$lng[0]->get_content();
 254:             $addr = $val->get_elements_by_tagname('formatted_address');
 255:             $items[$n]['address'] = $this->trimAddress((string)$addr[0]->get_content());
 256:             $n++;
 257:         }
 258: // PHP5用; SimpleXML利用
 259:     } else {
 260:         $this->unknown_certificate();
 261:         $res = simplexml_load_file($url);
 262:         // レスポンス・チェック
 263:         if (preg_match("/ok/i", $res->status) == 0)     return 0;
 264:         foreach ($res->result as $element) {
 265:             $items[$n]['latitude']  = (double)$element->geometry->location->lat;
 266:             $items[$n]['longitude'] = (double)$element->geometry->location->lng;
 267:             $items[$n]['address']   = $this->trimAddress((string)$element->formatted_address);
 268:             $n++;
 269:         }
 270:     }
 271:     return ($n - 1);
 272: }
 273: 
 274: /**
 275:  * Google Geocoding API V3 のリクエストURLを返す
 276:  * @param   string $query 検索キーワード(UTF-8)
 277:  * @return  string URL
 278: */
 279: function geturl($query) {
 280:     return $this->getURL_GeoCodeAPI_V3($query);
 281: }
 282: 
 283: /**
 284:  * 緯度経度文字列を分解する
 285:  * @param   string $str 緯度経度文字列
 286:  * @return  float  array(経度,緯度)
 287: */
 288: function parse_geo($str) {
 289:     static $pat1 = '/E(\d+\.?\d*)N(\d+\.?\d*)/i';
 290:     static $pat2 = '/E(\d+)\.(\d+)\.(\d+)\.(\d+)N(\d+)\.(\d+)\.(\d+)\.(\d+)/i';
 291: 
 292:     if (preg_match($pat1, $str, $regs> 0) {
 293:         $longitude = $regs[1];
 294:         $latitude  = $regs[2];
 295:     } else if (preg_match($pat2, $str, $regs> 0) {
 296:         $longitude = $regs[1+ $regs[2] / 60 + $regs[3] / 3600 + $regs[4] / 36000;
 297:         $latitude  = $regs[5+ $regs[6] / 60 + $regs[7] / 3600 + $regs[8] / 36000;
 298:     }
 299: 
 300:     return array($latitude, $longitude);
 301: }
 302: 
 303: /**
 304:  * Google Geocoding API V3 を用いて住所・駅名の緯度・経度を検索
 305:  * @param   string $query 検索キーワード(UTF-8)
 306:  * @return  int ヒットした地点数
 307: */
 308: function searchPoint($query) {
 309:     unset($this->items);
 310:     $this->items = array();
 311: 
 312:     // 緯度・経度表記
 313:     $pat = '/E(\d+)\.(\d+)\.(\d+)\.(\d+)N(\d+)\.(\d+)\.(\d+)\.(\d+)/i';
 314:     if (preg_match($pat, $query> 0) {
 315:         list($this->items[1]['latitude'], $this->items[1]['longitude']) =
 316:             $this->parse_geo($query);
 317:         $this->items[1]['address'] = '';
 318:         $this->hits = 1;
 319: 
 320:     // Google Geocoding API使用
 321:     } else {
 322:         $n = $this->getPointsV3_all($query, $this->items);      // 検索実行
 323:         if ($n == FALSE) {
 324:             $this->error  = TRUE;
 325:             $this->errmsg = 'Google Geocoding APIにトラブル発生';
 326:             $this->hits = 0;
 327:         } else if ($n == 0) {
 328:             $this->error  = TRUE;
 329:             $this->errmsg = '検索結果がない';
 330:             $this->hits = 0;
 331:         } else {
 332:             $this->hits = $n;
 333:         }
 334:     }
 335: 
 336:     return $this->hits;
 337: }
 338: 
 339: /**
 340:  * 検索結果(緯度・経度)を取得
 341:  * @param   int $id 取得したい地点番号
 342:  * @return  array(緯度,経度,住所):世界測地系
 343: */
 344: function getPoint($id) {
 345:     if ($id <0 || $id > $this->hits) {
 346:         $this->error  = TRUE;
 347:         $this->errmsg = '不正な地点番号';
 348:         $latitude  = FALSE;
 349:         $longitude = FALSE;
 350:         $address   = FALSE;
 351:     } else {
 352:         $this->error  = FALSE;
 353:         $this->errmsg = '';
 354:         $latitude  = $this->items[$id]['latitude'];
 355:         $longitude = $this->items[$id]['longitude'];
 356:         $address   = $this->items[$id]['address'];
 357:     }
 358: 
 359:     return array($latitude, $longitude, $address);
 360: }
 361: 
 362: /**
 363:  * Google Geocoding API を用いて緯度・経度から住所を求める
 364:  * @param   float $latitude  緯度(世界測地系,10進数の度表記)
 365:  * @param   float $longitude 経度(世界測地系,10進数の度表記)
 366:  * @return  array ['address']   フォーマット済み住所
 367:  *                ['$$$$']      都道府県など(サービスによって添字が変わる)
 368:  *          FALSE=エラー
 369: */
 370: function getGoogleAddress($latitude, $longitude) {
 371:     $key = $this->GOOGLE_API_KEY_2;
 372:     $url = "https://maps.googleapis.com/maps/api/geocode/xml?key={$key}&latlng={$latitude},{$longitude}&language=ja&region=JP";
 373:     $this->webapi = $url;
 374: 
 375:     $res = array();
 376:     $this->unknown_certificate();
 377:     $xml = simplexml_load_file($url);
 378:     // レスポンス・チェック
 379:     if (preg_match("/ok/i", $xml->status) == 0)     return FALSE;
 380:     foreach ($xml->result as $element) {
 381:         if ($element->type == 'street_address') {
 382:             $res['address'] = $this->trimAddress((string)$element->formatted_address);
 383:             // 有効な住所部品を格納
 384:             foreach ($element->address_component as $elem2) {
 385:                 $str = '';
 386:                 $flag = FALSE;
 387:                 foreach ($elem2->type as $val) {
 388:                     $type = (string)$val;
 389:                     if (preg_match('/_level_[0-9]+/i', $type> 0) {
 390:                         $str = $type;
 391:                     } else if ($type == 'postal_code') {
 392:                         $flag = TRUE;
 393:                         $str = $type;
 394:                     } else if ($type == 'political') {
 395:                         $flag = TRUE;
 396:                     } else if ($str == '') {
 397:                         $str = $type;
 398:                     }
 399:                 }
 400:                 if ($flag && ($str !'')) {
 401:                     $res[$str] = (string)$elem2->long_name;
 402:                 }
 403:             }
 404:             break;
 405:         }
 406:     }
 407: 
 408:     return $res;
 409: }
 410: 
 411: // ジオどすII API ===========================================================
 412: /**
 413:  * ジオどすII・京都通り名ジオコーダAPI のURLを取得する
 414:  * @param   string $query 検索キーワード(UTF-8)
 415:  * @return  string URL URL
 416: */
 417: function getURL_GeoDos_V2($query) {
 418:     return 'http://api.geodosu.com/v2/geodosu2?apikey=' . $this->APIKEY_GEOTOS . '&address=' . urlencode($query);
 419: }
 420: 
 421: /**
 422:  * ジオどすII・京都通り名ジオコーダAPI を用いて住所の緯度・経度を求める
 423:  * @param   string $query 検索キーワード
 424:  * @param   array  $items 情報を格納する配列
 425:  * @return  int ヒットした施設数
 426: */
 427: function getPoints_GeoDos_V2($query, &$items) {
 428:     $url = $this->getURL_GeoDos_V2($query); // リクエストURL
 429:     $this->webapi = $url;
 430:     $n = 1;
 431: 
 432: // PEAR::XML_Unserializer
 433:     if (class_exists('XML_Unserializer')) {
 434:         $xml = new XML_Unserializer();
 435:         $xml_data = file_get_contents($url);
 436:         $xml->unserialize($xml_data);
 437:         $arr = $xml->getUnserializedData();
 438:         // 位置情報
 439:         $items[$n]['latitude']  = (double)$arr['result']['coordinates']['point']['lat'];
 440:         $items[$n]['longitude'] = (double)$arr['result']['coordinates']['point']['lon'];
 441:         $items[$n]['address']   = (string)$arr['result']['addresses']['pre_processed'];
 442:         $xml = NULL;
 443: 
 444: // PHP4用; DOM XML利用
 445:     } else if ($this->isphp5over() == FALSE) {
 446:         if (($dom = $this->read_xml($url)) == NULLreturn FALSE;
 447:         $result = $dom->get_elements_by_tagname('result');
 448:         // レスポンス・チェック
 449:         $co = $result[0]->get_elements_by_tagname('coordinates');
 450:         // 位置情報
 451:         $po = $co[0]->get_elements_by_tagname('point');
 452:         $lat = $po[0]->get_elements_by_tagname('lat');
 453:         $items[$n]['latitude'] = (double)$lat[0]->get_content();
 454:         $lng = $po[0]->get_elements_by_tagname('lon');
 455:         $items[$n]['longitude'] = (double)$lng[0]->get_content();
 456:         $addr = $result[0]->get_elements_by_tagname('addresses');
 457:         $pre  = $addr[0]->get_elements_by_tagname('pre_processed');
 458:         $items[$n]['address'] = (string)$pre[0]->get_content();
 459: // PHP5用; SimpleXML利用
 460:     } else {
 461:         $this->unknown_certificate();
 462:         $res = simplexml_load_file($url);
 463:         // レスポンス・チェック
 464:         $items[$n]['latitude']  = (double)$res->result->coordinates->point->lat;
 465:         $items[$n]['longitude'] = (double)$res->result->coordinates->point->lon;
 466:         $items[$n]['address']   = (string)$res->result->addresses->pre_processed;
 467:     }
 468: 
 469:     return $n;
 470: }
 471: 
 472: /**
 473:  * Google Geocoding API(V3)+ジオどすII API を用いて
 474:  * 住所・駅名の緯度・経度を検索
 475:  *
 476:  * @param   string $query 検索キーワード(UTF-8)
 477:  * @return  array(int ヒットした地点数,string WebAPI)
 478: */
 479: function searchPoint2($query) {
 480:     static $pat1 = '/E(\d+\.?\d*)N(\d+\.?\d*)/i';
 481:     static $pat2 = '/E(\d+)\.(\d+)\.(\d+)\.(\d+)N(\d+)\.(\d+)\.(\d+)\.(\d+)/i';
 482: 
 483:     unset($this->items);
 484:     $this->items = array();
 485:     $this->hits = 0;
 486: 
 487:     // 緯度・経度表記(1)
 488:     if (preg_match($pat1, $query> 0) {
 489:         list($this->items[1]['latitude'], $this->items[1]['longitude']) =
 490:             $this->parse_geo($query);
 491:         $this->items[1]['address'] = '';
 492:         $this->hits = 1;
 493: 
 494:     // 緯度・経度表記(2)
 495:     } else if (preg_match($pat2, $query> 0) {
 496:         list($this->items[1]['latitude'], $this->items[1]['longitude']) =
 497:             $this->parse_geo($query);
 498:         $this->items[1]['address'] = '';
 499:         $this->hits = 1;
 500:     }
 501:     // Google Geocoding API使用
 502:     if ($this->hits == 0) {
 503:         $n = $this->getPointsV3_all($query, $this->items);
 504:         if ($n == FALSE) {
 505:             $this->error  = TRUE;
 506:             $this->errmsg = 'Google Geocoding APIにトラブル発生';
 507:             $this->hits = 0;
 508:         } else if ($n == 0) {
 509:             $this->error  = TRUE;
 510:             $this->errmsg = '検索結果がない';
 511:             $this->hits = 0;
 512:         } else {
 513:             $this->hits = $n;
 514:         }
 515:     }
 516: 
 517:     return array($this->hits, $this->webapi);
 518: }
 519: 
 520: // 簡易逆ジオコーディングサービス ============================================
 521: /**
 522:  * 簡易逆ジオコーディングサービスのWebAPI URLを取得する
 523:  * @param   float $latitude  緯度(世界測地系,10進数の度表記)
 524:  * @param   float $longitude 経度(世界測地系,10進数の度表記)
 525:  * @return  string URL URL
 526: */
 527: function getURLrgeocode($latitude, $longitude) {
 528:     return "https://aginfo.cgk.affrc.go.jp/ws/rgeocode.php?v=2&lat={$latitude}&lon={$longitude}";
 529: }
 530: 
 531: /**
 532:  * 簡易逆ジオコーディングサービスを用いて緯度・経度から住所を求める
 533:  * @param   float $latitude  緯度(世界測地系,10進数の度表記)
 534:  * @param   float $longitude 経度(世界測地系,10進数の度表記)
 535:  * @return  array (都道府県名,市町村名,町丁目,番地)/FALSE=エラー
 536: */
 537: function getAddress($latitude, $longitude) {
 538:     // APIコール
 539:     $url = $this->getURLrgeocode($latitude, $longitude);
 540:     $this->webapi = $url;
 541: 
 542: // PHP4用; DOM XML利用
 543:     if ($this->isphp5over() == FALSE) {
 544:         if (($dom = read_xml($url)) == NULL)    return FALSE;
 545:         $rgeocode = $dom->get_elements_by_tagname('rgeocode');
 546:         // 住所取得
 547:         if (($result = $rgeocode[0]->get_elements_by_tagname('result')) == NULL)    return FALSE;
 548:         if (($pref = $result[0]->get_elements_by_tagname('prefecture')) == NULL)    return FALSE;
 549:         if (($pref2 = $pref[0]->get_elements_by_tagname('pname')) == NULL)          return FALSE;
 550:         $prefecture = $pref2[0]->get_content();
 551:         $muni = $result[0]->get_elements_by_tagname('municipality');
 552:         $municipality = '';
 553:         if ($muni !NULL) {
 554:             $muni2 = $muni[0]->get_elements_by_tagname('mname');
 555:             if ($muni2 !NULL)     $municipality = $muni2[0]->get_content();
 556:         }
 557:         $section = '';
 558:         $homenumber = '';
 559:         $loc = $result[0]->get_elements_by_tagname('local');
 560:         if ($loc !NULL) {
 561:             $loc2 = $loc[0]->get_elements_by_tagname('section');
 562:             if ($loc2 !NULL)  $section = $loc2[0]->get_content();
 563:             $loc2 = $loc[0]->get_elements_by_tagname('homenumber');
 564:             if ($loc2 !NULL)  $homenumber = $loc2[0]->get_content();
 565:         }
 566: 
 567: // PHP5用; SimpleXML利用
 568:     } else {
 569:         $this->unknown_certificate();
 570:         $rgeocode = simplexml_load_file($url);
 571:         // レスポンス・チェック
 572:         if (! isset($rgeocode->result)) {
 573:             $this->error = TRUE;
 574:             $this->errmsg = '簡易ジオコーディングサービス' . (isset($rgeocode->error? (' ' . $rgeocode->error: 'にトラブル発生');
 575:             return FALSE;
 576:         }
 577:         // 住所取得
 578:         $prefecture = $rgeocode->result->prefecture->pname;
 579:         $municipality = $rgeocode->result->municipality->mname;
 580:         $section = $rgeocode->result->local->section;
 581:         $homenumber = $rgeocode->result->local->homenumber;
 582:     }
 583: 
 584:     return array($prefecture, $municipality, $section, $homenumber);
 585: }
 586: 
 587: // 座標計算 =================================================================
 588: /**
 589:  * 日本測地系を世界測地系に変換する
 590:  * @param   float $long 経度(日本測地系)
 591:  * @param   float $lat  緯度(日本測地系)
 592:  * @return  float array(経度,緯度)(世界測地系)
 593: */
 594: function tokyo_wgs84($long, $lat) {
 595:     $glong = $long - $lat * 0.000046038 - $long * 0.000083043 + 0.010040;
 596:     $glat  = $lat  - $lat * 0.00010695  + $long * 0.000017464 + 0.0046017;
 597:     return array($glong, $glat);
 598: }
 599: 
 600: /**
 601:  * 世界測地系を日本測地系に変換する
 602:  * @param   float $long 経度(世界測地系)
 603:  * @param   float $lat  緯度(世界測地系)
 604:  * @return  float array(経度,緯度)(日本測地系)
 605: */
 606: function wgs84_tokyo($long, $lat) {
 607:     $glong = $long + $lat * 0.000046047 + $long * 0.000083049 - 0.010041;
 608:     $glat  = $lat  + $lat * 0.00010696  - $long * 0.000017467 - 0.0046020;
 609:     return array($glong, $glat);
 610: }
 611: 
 612: /**
 613:  * 2地点間の直線距離を求める(Hubenyの簡易式による;日本測地系!)
 614:  * @param   float $long_a, $lati_a  A地点の経度,緯度(世界測地系)
 615:  * @param   float $long_b, $lati_b  B地点の経度,緯度(世界測地系)
 616:  * @return  float 直線距離(メートル)
 617: */
 618: function distance($long_a, $lati_a,  $long_b, $lati_b) {
 619:     // 西経の補正
 620:     if ($long_a < 0$long_a +360;
 621:     if ($long_b < 0$long_b +360;
 622: 
 623:     // ラジアンに変換
 624:     $long_a = deg2rad($long_a);
 625:     $lati_a = deg2rad($lati_a);
 626:     $long_b = deg2rad($long_b);
 627:     $lati_b = deg2rad($lati_b);
 628: 
 629:     $latave = ($lati_a + $lati_b) / 2;
 630:     $latidiff = $lati_a - $lati_b;
 631:     $longdiff = $long_a - $long_b;
 632: 
 633:     // 子午線曲率半径
 634:     $meridian = 6335439 / sqrt(pow(1 - 0.006694 * sin($latave* sin($latave), 3));
 635:     // 卯酉線曲率半径
 636:     $primevertical = 6378137 / sqrt(1 - 0.006694 * sin($latave* sin($latave));
 637: 
 638:     // Hubenyの簡易式
 639:     $x = $meridian * $latidiff;
 640:     $y = $primevertical * cos($latave* $longdiff;
 641: 
 642:     return sqrt($x * $x + $y * $y);
 643: }
 644: 
 645: /**
 646:  * ある地点から指定距離離れた地点の緯度・経度を求める
 647:  * @param   float $longitude 経度(世界測地系)
 648:  * @param   float $latitude  緯度(世界測地系)
 649:  * @param   float $y         北への距離(メートル;南ならマイナス)
 650:  * @param   float $x         東への距離(メートル;西ならマイナス)
 651:  * @return  float array(緯度,経度)
 652: */
 653: function getPointDistance($longitude, $latitude, $y, $x) {
 654:     $rad = 6378137;         // 地球の半径(メートル)
 655: 
 656:     $lat = ($y / $rad + $latitude * (pi() / 180)) * (180 / pi());
 657:     $lng = ($x / ($rad * cos($latitude * (pi() / 180))) + $longitude * (pi() / 180)) * (180 / pi());
 658: 
 659:     return array($lat, $lng);
 660: }
 661: 
 662: /**
 663:  * ある地点から方位角と距離を指定した地点の緯度・経度を求める
 664:  * @param   float $longitude 経度(世界測地系)
 665:  * @param   float $latitude  緯度(世界測地系)
 666:  * @param   float $angle     方位角(度)
 667:  * @param   float $distance  距離(メートル)
 668:  * @return  float array(緯度,経度)
 669: */
 670: function getPointAngle($longitude, $latitude, $angle, $distance) {
 671:     $rad = 6378137.0;           // 地球の半径(メートル)
 672:     $e2  = 6.69437999019758E-03;
 673: 
 674:     $wt  = sqrt(1.0 - $e2 * pow(sin($latitude * pi() / 180.0), 2));
 675:     $mt  = $rad * (1.0 - $e2) / pow($wt, 3);
 676:     $dit = $distance * cos($angle * pi() / 180.0) / $mt;
 677:     $i   = $latitude * pi() / 180.0 + $dit / 2;
 678:     $w   = sqrt(1.0 - $e2 * pow(sin($i), 2));
 679: 
 680:     // 緯度
 681:     $m   = $rad * (1 - $e2) / pow($w, 3);
 682:     $di  = $distance * cos($angle * pi() / 180) / $m;
 683:     $lat = $latitude + $di * 180 / pi();
 684: 
 685:     // 経度
 686:     $n   = $rad / $w;
 687:     $dk  = $distance * sin($angle * pi() / 180) / ($n * cos($i));
 688:     $lng = $longitude + $dk * 180 / pi();
 689: 
 690:     return array($lat, $lng);
 691: }
 692: 
 693: /**
 694:  * 2地点間の大圏航路距離を求める
 695:  * @param   float $long_a, $lati_a  A地点の経度,緯度(世界測地系)
 696:  * @param   float $long_b, $lati_b  B地点の経度,緯度(世界測地系)
 697:  * @return  float 大圏航路距離
 698:  *
 699: */
 700: function greatCircleDistance($long_a, $lati_a, $long_b, $lati_b) {
 701:     $lati_a = deg2rad($lati_a);
 702:     $long_a = deg2rad($long_a);
 703:     $lati_b = deg2rad($lati_b);
 704:     $long_b = deg2rad($long_b);
 705: 
 706:     // 距離の計算
 707:     $ll = abs($long_b - $long_a);
 708:     $distance = 6371.0 * acos(sin($lati_a* sin($lati_b+ cos($lati_a* cos($lati_b* cos($ll));
 709: 
 710:     return $distance;
 711: }
 712: 
 713: /**
 714:  * 2地点間の大圏航路軌跡を求める
 715:  * @param   float $long_a, $lati_a  A地点の経度,緯度(世界測地系)
 716:  * @param   float $long_b, $lati_b  B地点の経度,緯度(世界測地系)
 717:  * @param   array  $points  軌跡の座標を格納
 718:  *                      [$n]['longitude'] 軌跡の経度(世界測地系)
 719:  *                      [$n]['latitude']  軌跡の緯度(世界測地系)
 720:  * @return  int 座標の数
 721:  *
 722: */
 723: function greatCircleSailing($long_a, $lati_a, $long_b, $lati_b, &$points) {
 724:     $lati_a = deg2rad($lati_a);
 725:     $long_a = deg2rad($long_a);
 726:     $lati_b = deg2rad($lati_b);
 727:     $long_b = deg2rad($long_b);
 728: 
 729:     $l1 = ($long_a >0? $long_a : 2 * pi() + $long_a;
 730:     $l2 = ($long_b >0? $long_b : 2 * pi() + $long_b;
 731:     $dd = $l2 - $l1;
 732:     $tt = 0.01;         // 経度方向の増分
 733:     if ($dd < 0) {
 734:         $dd = abs($dd);
 735:         $tt = -$tt;
 736:     } else if ($dd > pi()) {
 737:         list($lati_a, $lati_b) = array($lati_b, $lati_a);
 738:         list($long_a, $long_b) = array($long_b, $long_a);
 739:         $dd = 2 * pi() - $dd;
 740:     }
 741:     $st = 0.0;
 742:     $cnt = 0;
 743: 
 744:     // 軌跡の計算
 745:     $latitude  = $lati_a;
 746:     $longitude = $long_a;
 747:     while ($st < $dd) {
 748:         if ($latitude  >0.5 * pi())   $latitude -pi();
 749:         if ($longitude >1.0 * pi())   $longitude = $longitude - pi();
 750:         $points[$cnt]['latitude']  = rad2deg($latitude);
 751:         $points[$cnt]['longitude'] = rad2deg($longitude);
 752:         $longitude +$tt;
 753:         if ($longitude >pi())     $longitude = $longitude - 2 * pi();
 754:         $latitude = (sin($lati_a* sin($long_b - $longitude)) / (cos($lati_a* sin($long_b - $long_a)) + (sin($lati_b* sin($long_a - $longitude)) / (cos($lati_b* sin($long_a - $long_b));
 755:         $latitude = atan($latitude);
 756:         if (sin($lati_a) / sin($lati_b< 0$latitude +pi();
 757: 
 758:         $st +abs($tt);
 759:         $cnt++;
 760:     }
 761:     $points[$cnt]['latitude']  = rad2deg($lati_b);
 762:     $points[$cnt]['longitude'] = rad2deg($long_b);
 763:     $cnt++;
 764: 
 765:     return $cnt;
 766: }
 767: 
 768: /**
 769:  * 2地点間の等角航路軌跡を求める
 770:  * @param   float $long_a, $lati_a  A地点の経度,緯度(世界測地系)
 771:  * @param   float $long_b, $lati_b  B地点の経度,緯度(世界測地系)
 772:  * @param   array  $points  軌跡の座標を格納
 773:  *                      [$n]['longitude'] 軌跡の経度(世界測地系)
 774:  *                      [$n]['latitude']  軌跡の緯度(世界測地系)
 775:  * @return  float $distance 2地点間の等角航路距離を格納
 776:  * @return  int 座標の数
 777:  *
 778: */
 779: function rhumbLine($long_a, $lati_a, $long_b, $lati_b, &$points, &$distance) {
 780:     $lati_a = deg2rad($lati_a);
 781:     $long_a = deg2rad($long_a);
 782:     $lati_b = deg2rad($lati_b);
 783:     $long_b = deg2rad($long_b);
 784:     $n = 200;
 785: 
 786:     // 経度方向の増分
 787:     $l1 = ($long_a >0? $long_a : 2 * pi() + $long_a;
 788:     $l2 = ($long_b >0? $long_b : 2 * pi() + $long_b;
 789:     $d1 = $l2 - $l1;
 790:     $t1 = 0.01;
 791:     $n = abs($d1) / $t1;
 792:     if ($d1 < 0) {
 793:         $d1 = 2 * pi() + $d1;
 794:         $t1 = -$t1;
 795:     } else if ($d1 > pi()) {
 796:         list($lati_a, $lati_b) = array($lati_b, $lati_a);
 797:         list($long_a, $long_b) = array($long_b, $long_a);
 798:         $d1 = 2 * pi() - $d1;
 799:         $n = abs($d1) / $t1;
 800:     }
 801: 
 802:     // 緯度方向の増分
 803:     $l1 = $lati_a;
 804:     $l2 = $lati_b;
 805:     $d2 = $l2 - $l1;
 806:     if ($d2 >pi()) {
 807:         $d2 -pi();
 808:         $t2 = - $d2 / $n;
 809:     } else {
 810:         $t2 = $d2 / $n;             // 緯度方向の増分
 811:     }
 812: 
 813:     // 軌跡の計算
 814:     $latitude  = $lati_a;
 815:     $longitude = $long_a;
 816:     $distance  = 0.0;
 817:     for ($cnt = 0$cnt < $n$cnt++) {
 818:         if ($latitude  >0.5 * pi())   $latitude -pi();
 819:         if ($longitude >1.0 * pi())   $longitude = $longitude - 2 * pi();
 820:         $points[$cnt]['latitude']  = rad2deg($latitude);
 821:         $points[$cnt]['longitude'] = rad2deg($longitude);
 822:         $longitude +$t1;
 823:         $latitude  +$t2;
 824:         if ($cnt >1) {
 825:             $distance +$this->greatCircleDistance($points[$cnt - 1]['longitude'], $points[$cnt - 1]['latitude'], $points[$cnt]['longitude'], $points[$cnt]['latitude']);
 826:         }
 827:     }
 828:     $points[$cnt]['latitude']  = rad2deg($lati_b);
 829:     $points[$cnt]['longitude'] = rad2deg($long_b);
 830:     $distance +$this->greatCircleDistance($points[$cnt - 1]['longitude'], $points[$cnt - 1]['latitude'], $points[$cnt]['longitude'], $points[$cnt]['latitude']);
 831:     $cnt++;
 832: 
 833:     return $cnt;
 834: }
 835: 
 836: /**
 837:  * キロメートル→マイル変換
 838:  * @param   float $km キロメートル
 839:  * @return  float マイル
 840:  *
 841: */
 842: function km2mi($km) {
 843:     return $km / 1.609344;
 844: }
 845: 
 846: /**
 847:  * キロメートル→マイル変換
 848:  * @param   float $km キロメートル
 849:  * @return  float 海里
 850:  *
 851: */
 852: function km2nm($km) {
 853:     return $km / 1.852;
 854: }
 855: 
 856: 
 857: // Googleマップ描画 ========================================================
 858: /**
 859:  * 数値に対応するアルファベットを返す
 860:  * @param   int $i 値
 861:  * @return  string アルファベット
 862: */
 863: function num2alpha($i) {
 864:     return chr(64 + $i);
 865: }
 866: 
 867: /**
 868:  * Googleマップを描く
 869:  * @param   string $id        マップID
 870:  * @param   float  $latitude  中心座標:緯度(世界測地系)
 871:  * @param   float  $longitude 中心座標:経度(世界測地系)
 872:  * @param   string $type      マップタイプ:HYBRID/ROADMAP/SATELLITE/TERRAIN
 873:  * @param   int    $zoom      拡大率
 874:  * @param   string $call      イベント発生時にコールする関数(省略可)
 875:  * @param   array  $items     地点情報(省略可能)
 876:  *                  string title       タイトル
 877:  *                  string description 情報ウィンドウに表示する内容(HTML文)
 878:  *                  float latitude    緯度
 879:  *                  float longitude   経度
 880:  *                  string icon        アイコンURL
 881:  * @param   string $call2     追加スクリプト(省略可)
 882:  * @param   int    $max_width 情報ウィンドウの最大幅(省略時:200)
 883:  * @param   array  $offset    アイコンから情報ウィンドウのオフセット位置(省略時:0,0)
 884:  * @return  string Googleマップのコード
 885: */
 886: function drawGMap($id, $latitude, $longitude, $type, $zoom, $call=NULL, $items=NULL, $call2=NULL, $max_width=200, $offset=NULL) {
 887:     $key = $this->GOOGLE_API_KEY_1;
 888:     $call = ($call !NULL? $call . '()' : '';
 889:     if (! is_array($offset)) {
 890:         $offset = array(0, 0);
 891:     }
 892: 
 893:     $mapId  = $this->GOOGLE_MAP_ID;
 894:     $code =<<< EOT
 895: <script src="https://maps.googleapis.com/maps/api/js?key={$key}&loading=async&libraries=marker&callback=initMap&region=JP" async defer loading="async"></script>
 896: <script>
 897: function initMap() {
 898:     var map = new google.maps.Map(document.getElementById('{$id}'), {
 899:         center: { lat: {$latitude}, lng: {$longitude} },
 900:         mapId: '{$mapId}',
 901:         zoom: {$zoom},
 902:         mapTypeId: google.maps.MapTypeId.{$type},
 903:         mapTypeControl: true,
 904:         scaleControl: true
 905:     });
 906: 
 907:     map.addListener('dragend', getPointData);
 908:     map.addListener('zoom_changed', getPointData);
 909:     map.addListener('maptypeid_changed', getPointData);
 910: 
 911:     // イベント発生時の地図情報を取得・格納
 912:     function getPointData() {
 913:         var point = map.getCenter();
 914:         // 経度
 915:         if (document.getElementById("longitude") != null) {
 916:             document.getElementById("longitude").value = point.lng();
 917:         }
 918:         // 緯度
 919:         if (document.getElementById("latitude") != null) {
 920:             document.getElementById("latitude").value = point.lat();
 921:         }
 922:         // ズーム
 923:         if (document.getElementById("zoom") != null) {
 924:             document.getElementById("zoom").value = map.getZoom();
 925:         }
 926:         // 地図タイプ
 927:         if (document.getElementById("type") != null) {
 928:             var type_g = map.getMapTypeId();
 929:             var types = {"roadmap":"地図", "satellite":"航空写真", "hybrid":"ハイブリッド", "terrain":"地形図" };
 930:             for (key in types) {
 931:                 if (key == type_g) {
 932:                     document.getElementById("type").value = key;
 933:                     break;
 934:                 }
 935:             }
 936:         }
 937:         {$call}
 938:     }
 939: 
 940: EOT;
 941:     // 地点情報
 942:     if ($items !NULL) {
 943:         foreach ($items as $i=>$item) {
 944:             if ($i > 999)   break;      // 最大999箇所まで
 945:             $mark = (string)sprintf('%03d', $i);
 946:             // アイコン
 947:             $mark2 = ($i <26? $this->num2alpha($i: 'Z';
 948:             $icon = isset($item['icon']) ? $item['icon': 
 949:                         "https://www.google.com/mapfiles/marker{$mark2}.png";
 950:             list($icon_width, $icon_height) = getimagesize($icon);
 951:             if (isset($item['label']) && ($item['label'!'')) {
 952:                 $size1 = $item['label_size'* mb_strlen($item['label']);
 953:                 $size2 = (int)($size1 / 2);
 954:                 $ss =<<< EOT
 955:     const icon = document.createElement('div');
 956:     icon.innerHTML = `
 957:         <div style="position: relative; text-align: center;">
 958:         <img src="https://www.pahoo.org/common/space.gif" style="width:{$size1}px; height:{$size1}px;">
 959:         <div style="position: absolute; top:{$size2}px; left:0px; width:100%; font-size:{$item['label_size']}px; color:{$item['label_color']}; font-weight:{$item['label_weight']};">
 960:         {$item['label']}
 961:     </div>
 962:   </div>
 963: `;
 964: 
 965: EOT;
 966:             } else {
 967:                 $ss =<<< EOT
 968:     const icon = document.createElement('img');
 969:     icon.src = '{$icon}';
 970:     icon.style.width  = '{$icon_width}px';
 971:     icon.style.height = '{$icon_height}px';
 972:     icon.style.transform = 'translate(0%, 0%)';
 973: 
 974: EOT;
 975:             }
 976:             $code .=<<< EOT
 977: const marker_{$mark} = new google.maps.marker.AdvancedMarkerElement({
 978:     position: { lat: {$item['latitude']}, lng: {$item['longitude']} },
 979:     map: map,
 980:     content: (() => {
 981: {$ss}
 982:         return icon;
 983:     })(),
 984:     title: '{$item['title']}',
 985:     zIndex: 100
 986: });
 987: 
 988: EOT;
 989:             if (isset($item['description'])) {
 990:                 $code .=<<< EOT
 991: var infowindow_{$mark} = new google.maps.InfoWindow({
 992:     content: '{$item['description']}',
 993:     maxWidth: {$max_width},
 994:     pixelOffset: new google.maps.Size({$offset[0]}, {$offset[1]})
 995: });
 996: marker_{$mark}.addListener('gmp-click', function() {
 997:     infowindow_{$mark}.open(map, marker_{$mark});
 998: });
 999: 
1000: EOT;
1001:             }
1002:         }
1003:     }
1004:     // 追加関数
1005:     if ($call2 !NULL) {
1006:         $code .=<<< EOT
1007: {$call2}
1008: 
1009: EOT;
1010:     }
1011:     $code .=<<< EOT
1012: }
1013: </script>
1014: 
1015: EOT;
1016: 
1017:     return $code;
1018: }
1019: 
1020: /**
1021:  * GoogleMaps staticmap の画像URLを求める
1022:  * @param   array  $items 情報配列
1023:  * @return  string 画像URL
1024: */
1025: function getStaticMap($items) {
1026:     $key = $this->GOOGLE_API_KEY_1;
1027: 
1028:     return "https://maps.googleapis.com/maps/api/staticmap?center={$items['latitude']},{$items['longitude']}&markers=color:red%7Clabel:A%7C{$items['latitude']},{$items['longitude']}&zoom={$items['zoom']}&size={$items['width']}x{$items['height']}&key={$key}";
1029: }
1030: 
1031: /**
1032:  * 直線描画スクリプト:Googleマップ用
1033:  * @param   array  $points  直線の座標配列
1034:  *                      [$n]['longitude'] 経度(世界測地系)
1035:  *                      [$n]['latitude']  緯度(世界測地系)
1036:  * @param   string $color   描画色(省略時=#FF0000)
1037:  * @param   float  $opacity 透明度(省略時=1)
1038:  * @param   int    $weight  線の太さ(省略時=1)
1039:  * @return  string JavaScript
1040: */
1041: function jsLine_Gmap($points, $color='#FF0000', $opacity=1.0, $weight=1) {
1042:     $ss = '';
1043:     $cnt = 0;
1044:     foreach ($points as $pt) {
1045:         if ($cnt > 0)   $ss .",\n";
1046: //      $ss .= "\t\tnew google.maps.LatLng({$pt['latitude']}, {$pt['longitude']})";
1047:         $ss ."\t\t{ lat: {$pt['latitude']}, lng: {$pt['longitude']} }";
1048:         $cnt++;
1049:     }
1050: 
1051:     $js =<<< EOT
1052:     var pt = [
1053: {$ss}
1054:     ];
1055:     var lines = new google.maps.Polyline({
1056:         map: map,
1057:         path: pt,
1058:         strokeColor: '{$color}',
1059:         strokeOpacity: {$opacity},
1060:         strokeWeight: {$weight}
1061:     });
1062:     lines.setMap(map);
1063: 
1064: EOT;
1065:     return $js;
1066: }
1067: 
1068: /**
1069:  * 円描画スクリプト:Googleマップ用
1070:  * @param   float  $longitude 中心の経度(世界測地系)
1071:  * @param   float  $latitude  中心の緯度(世界測地系)
1072:  * @param   float  $radius    半径(メートル)
1073:  * @param   string $color   描画色(省略時=#FF0000)
1074:  * @param   float  $opacity 透明度(省略時=1)
1075:  * @param   int    $weight  線の太さ(省略時=1)
1076:  * @return  string JavaScript
1077: */
1078: function jsCircle_Gmap($longitude, $latitude, $radius, $color='#FF0000', $opacity='1.0', $weight=1) {
1079:     $js =<<< EOT
1080:     new google.maps.Circle({
1081:         map: map,
1082:         center: {lat: {$latitude}, lng: {$longitude} },
1083:         radius: {$radius},
1084:         strokeColor:  "{$color}",
1085:         strokeOpacity: {$opacity},
1086:         strokeWeight:  {$weight},
1087:         fillColor:    "{$color}",
1088:         fillOpacity:   0.0
1089:     });
1090: 
1091: EOT;
1092:     return $js;
1093: }
1094: 
1095: /**
1096:  * ラベル表示用スクリプト作成:Googleマップ
1097:  * @param   float  $latitude, $longitude ラベル表示座標
1098:  * @param   string $label  ラベル
1099:  * @param   int    $size   フォントサイズ(pt)(省略時=14)
1100:  * @param   string $color  フォントカラー(省略時='#000000')
1101:  * @param   string $weight 太さ(省略時="normal")
1102:  * @return  string 描画用スクリプト
1103: */
1104: function jsLabel_gmap($latitude, $longitude, $label, $size=14, $color='#000000', $weight="normal") {
1105:     $size1 = $size * mb_strlen($label);
1106:     $size2 = (int)($size / 2);
1107:     $js =<<< EOT
1108: new google.maps.marker.AdvancedMarkerElement({
1109:     map: map,
1110:     position: {lat: {$latitude}, lng: {$longitude} },
1111:     content: (() => {
1112:         const icon = document.createElement('div');
1113:         icon.innerHTML = `
1114:             <div style="position: relative; text-align: center;">
1115:             <img src="https://www.pahoo.org/common/space.gif" style="width:{$size1}px; height:{$size}px;">
1116:             <div style="position: absolute; top:{$size2}px; left:0px; width:100%; font-size:{$size}px; color:{$color}; font-weight:{$weight};">
1117:             {$label}
1118:             </div>
1119:           </div>
1120:         `;
1121:         return icon;
1122:     })()
1123: });
1124: 
1125: EOT;
1126:     return $js;
1127: }
1128: 
1129: // YOLP 標高API =============================================================
1130: /**
1131:  * YOLP 標高API のURLを取得する
1132:  * @param   float $latitude  緯度(世界測地系)
1133:  * @param   float $longitude 経度(世界測地系)
1134:  * @return  string URL YOLP 標高API のURL
1135: */
1136: function getURL_YOLP_altitude($latitude, $longitude) {
1137:     $appID = $this->YAHOO_APPLICATION_ID;
1138: 
1139:     $url = "https://map.yahooapis.jp/alt/V1/getAltitude?appid={$appID}&coordinates={$longitude},{$latitude}&output=xml";
1140: 
1141:     return $url;
1142: }
1143: 
1144: /**
1145:  * 「YOLP 標高API」を利用して標高を求める
1146:  * @param   float $latitude  緯度(世界測地系)
1147:  * @param   float $longitude 経度(世界測地系)
1148:  * @return  float 標高(メートル)/FALSE
1149: */
1150: function getAltitude($latitude, $longitude) {
1151:     $url = $this->getURL_YOLP_altitude($latitude, $longitude);
1152:     $this->webapi = $url;
1153: 
1154: // PHP4用; DOM XML利用
1155:     if ($this->isphp5over() == FALSE) {
1156:         if (($dom = $this->read_xml($url)) == NULLreturn FALSE;
1157:         $pagingInfo = $dom->get_elements_by_tagname('YDF');
1158:         // レスポンス・チェック
1159:         $rc = $pagingInfo[0]->get_elements_by_tagname('recordCount');
1160:         $rc = (int)$rc[0]->get_content();
1161:         if ($rc <0)    return FALSE;
1162:         // 検索結果取りだし
1163:         $hotels = $dom->get_elements_by_tagname('hotels');
1164:         $hotel  = $hotels[0]->get_elements_by_tagname('hotel');
1165:         $cnt = 1;
1166:         foreach ($hotel as $val) {
1167:             foreach ($RakutenItems as $name) {
1168:                 $node = $val->get_elements_by_tagname('hotelBasicInfo');
1169:                 $node = $node[0]->get_elements_by_tagname($name);
1170:                 if ($node !NULL) {
1171:                     $items[$cnt][$name] = (string)$node[0]->get_content();
1172:                 }
1173:             }
1174:             $cnt++;
1175:         }
1176: 
1177: // PHP5用; SimpleXML利用
1178:     } else {
1179:         $this->unknown_certificate();
1180:         $xml = simplexml_load_file($url);
1181:         // レスポンス・チェック
1182:         if ($xml == NULL)   return FALSE;
1183:         if ($xml->ResultInfo->Count <0)    return FALSE;
1184:         // 検索結果取りだし
1185:         $alt = (double)$xml->Feature[0]->Property->Altitude;
1186:     }
1187: 
1188:     return $alt;
1189: }
1190: 
1191: /**
1192:  * 気圧を求める
1193:  * @param   float $altitude 標高(メートル)
1194:  * @return  float 気圧(ヘクトパスカル)
1195: */
1196: function getPressure($altitude) {
1197:     return pow(10, (log10(1013.25- ($altitude / 18410)));
1198: }
1199: 
1200: // YOLP 気象情報API ==========================================================
1201: /**
1202:  * 「YOLP 気象情報API」のリクエストURLを取得する
1203:  * @param   float $latitude  緯度(世界測地系)
1204:  * @param   float $longitude 経度(世界測地系)
1205:  * @return  string URL YOLP 標高API のURL
1206: */
1207: function getURL_YOLP_precipitation($latitude, $longitude) {
1208:     $appID = $this->YAHOO_APPLICATION_ID;
1209: 
1210:     $url = "https://map.yahooapis.jp/weather/V1/place?appid={$appID}&coordinates={$longitude},{$latitude}&output=xml&past=2&interval=10";
1211: 
1212:     return $url;
1213: }
1214: 
1215: /**
1216:  * 「YOLP 気象情報API」を利用して降水量を求める
1217:  * @param   array  $items     降水量を格納する配列
1218:  * @param   float $latitude  緯度(世界測地系)
1219:  * @param   float $longitude 経度(世界測地系)
1220:  * @return  bool TRUE/FALSE
1221: */
1222: function getPrecipitation(&$items, $latitude, $longitude) {
1223:     $url = $this->getURL_YOLP_precipitation($latitude, $longitude);
1224:     $this->webapi = $url;
1225: 
1226: // PHP4用; DOM XML利用
1227:     if ($this->isphp5over() == FALSE) {
1228:         if (($dom = $this->read_xml($url)) == NULLreturn FALSE;
1229:         // レスポンス・チェック
1230:         $node = $dom->get_elements_by_tagname('ResultInfo');
1231:         $node = $node[0]->get_elements_by_tagname('Count');
1232:         $rc = (int)$node[0]->get_content();
1233:         if ($rc <0)    return FALSE;
1234:         // 検索結果取りだし
1235:         $node = $dom->get_elements_by_tagname('Feature');
1236:         $node = $node[0]->get_elements_by_tagname('Property');
1237:         $node = $node[0]->get_elements_by_tagname('WeatherList');
1238:         $node = $node[0]->get_elements_by_tagname('Weather');
1239:         foreach ($node as $val) {
1240:             $n2 = $val->get_elements_by_tagname('Date');
1241:             $dt = (string)$n2[0]->get_content();
1242:             $n2 = $val->get_elements_by_tagname('Rainfall');
1243:             $items[$dt]['Rainfall'] = (double)$n2[0]->get_content();
1244:             $n2 = $val->get_elements_by_tagname('Type');
1245:             $items[$dt]['Type'] = (string)$n2[0]->get_content();
1246:         }
1247: 
1248: // PHP5用; SimpleXML利用
1249:     } else {
1250:         $this->unknown_certificate();
1251:         $xml = simplexml_load_file($url);
1252:         // レスポンス・チェック
1253:         if ($xml->ResultInfo->Count <0)    return FALSE;
1254:         // 検索結果取りだし
1255:         foreach ($xml->Feature->Property->WeatherList->Weather as $weather) {
1256:             $dt = (string)$weather->Date;
1257:             $items[$dt]['Rainfall'] = (double)$weather->Rainfall;
1258:             $items[$dt]['Type'] = (string)$weather->Type;
1259:         }
1260:     }
1261: 
1262:     return TRUE;
1263: }
1264: 
1265: // YOLPコンテンツジオコーダAPI ===============================================
1266: /**
1267:  * 指定した検索キーワードからYOLPコンテンツジオコーダAPIのURLを取得する.
1268:  * @param   string $query 検索キーワード(UTF-8)
1269:  * @param   string $category 検索対象カテゴリ
1270:  *                           address  = 住所(省略時)
1271:  *                           landmark = ランドマーク
1272:  *                           world    = 世界
1273:  * @return  string URL リクエストURL
1274: */
1275: function getURL_YOLP_GeoCoder($query, $category='address') {
1276:     $appid = $this->YAHOO_APPLICATION_ID;
1277:     return "https://map.yahooapis.jp/geocode/cont/V1/contentsGeoCoder?appid={$appid}&el=UTF-8&output=xml&category={$category}&query=" . urlencode($query);
1278: }
1279: 
1280: /**
1281:  * 指定した検索キーワードの緯度・経度を求める.
1282:  * クラウドサービスとしてYOLPコンテンツジオコーダAPIを利用する.
1283:  * $categoryに検索対象カテゴリをセットする(省略時は'address').
1284:  * 検索結果結果は複数になることがあり,配列$itemsに格納する.
1285:  * 参考サイト https://www.pahoo.org/e-soul/webtech/php06/php06-08-01.shtm
1286:  * @param   string $query 検索キーワード
1287:  * @param   array  $items 情報を格納する配列
1288:  * @param   string $category 検索対象カテゴリ
1289:  *                           address  = 住所(省略時)
1290:  *                           landmark = ランドマーク
1291:  *                           world    = 世界
1292:  * @return  int ヒットした施設数/(-1):API呼び出し失敗
1293: */
1294: function getPointsYOLP_all($query, &$items, $category='address') {
1295:     $url = $this->getURL_YOLP_GeoCoder($query, $category);  // リクエストURL
1296:     $this->webapi = $url;
1297:     $n = 1;
1298: 
1299:     $this->unknown_certificate();
1300:     $res = simplexml_load_file($url);
1301:     // レスポンス・チェック
1302:     if (!isset($res->ResultInfo))   return (-1);
1303:     if (isset($res->ResultInfo->Total&& ((int)$res->ResultInfo->Total <0))   return 0;       // v.5.73追加
1304: 
1305:     foreach ($res->Feature as $element) {
1306:         if (preg_match('/([\-0-9\.]+)\,([\-0-9\.]+)/i', $element->Geometry->Coordinates, $arr> 0) {
1307:             if (isset($arr[1]) && isset($arr[2])) {
1308:                 $items[$n]['latitude']  = (double)$arr[2];
1309:                 $items[$n]['longitude'] = (double)$arr[1];
1310:                 $items[$n]['address']   = (string)$element->Property->Address;
1311:                 $n++;
1312:             }
1313:         }
1314:     }
1315: 
1316:     return ($n - 1);
1317: }
1318: 
1319: /**
1320:  * YOLPコンテンツジオコーダAPI のカテゴリ選択ラジオボタンの生成
1321:  * @param   string $name      HTML name
1322:  * @param   string $default   デフォルト値(省略時は空文字)
1323:  * @param   string $flagWorld 世界を選択できるようにする(省略時はTRUE)
1324:  * @return  string HTML
1325: */
1326: function makeYOLP_GeoSelectCategory($name, $default='', $flagWorld=TRUE) {
1327:     // デフォルト値設定
1328:     if (isset($this->YOLP_GeoCategory[$default])) {
1329:         foreach ($this->YOLP_GeoCategory as $key=>$item) {
1330:             $item[$key]['checked'] = '';
1331:         }
1332:         $this->YOLP_GeoCategory[$default]['checked'] = 'checked';
1333:     }
1334:     // 選択ラジオボタンの生成
1335:     $html = '';
1336:     $i = 1;
1337:     foreach ($this->YOLP_GeoCategory as $key=>$val) {
1338:         if (! $flagWorld && ($key == 'world'))      continue;
1339:         $html ."<input type=\"radio\" name=\"{$name}\" value=\"{$key}\" {$val['checked']} />{$val['title']} ";
1340:         $i++;
1341:     }
1342:     return $html;
1343: }
1344: 
1345: // カテゴリ
1346: var $YOLP_GeoCategory = array(
1347:     // 値                    タイトル               ラジオボタンcheked
1348:     'address'   => array('title'=>'住所',         'checked'=>'checked'),
1349:     'landmark'  => array('title'=>'ランドマーク', 'checked'=>''),
1350:     'world'     => array('title'=>'世界',         'checked'=>'')
1351: );
1352: 
1353: /**
1354:  * checkedされているカテゴリを検索する
1355:  * @return  string 選択された関数名/FALSE=checkedされている処理がない
1356: */
1357: function getYOLP_GeoSelectCategory() {
1358:     foreach ($this->YOLP_GeoCategory as $key=>$val) {
1359:         if ($val['checked'] == 'checked')   return $key;
1360:     }
1361: 
1362:     return FALSE;
1363: }
1364: 
1365: /**
1366:  * カテゴリをchekedする
1367:  * @param   string $val カテゴリ値
1368:  * @return  bool TRUE/FALSE
1369: */
1370: function setYOLP_GeoSelectCategory($val) {
1371:     $old = $this->getYOLP_GeoSelectCategory();
1372:     if ($val !FALSE)  $this->YOLP_GeoCategory[$old]['checked'] = '';
1373:     $this->YOLP_GeoCategory[$val]['checked'] = 'checked';
1374: 
1375:     return TRUE;
1376: }
1377: 
1378: // Yahoo!リバースジオコーダAPI ==============================================
1379: /**
1380:  * Yahoo!リバースジオコーダAPIを用いて緯度・経度から住所を求める
1381:  * @param   float $latitude  緯度(世界測地系,10進数の度表記)
1382:  * @param   float $longitude 経度(世界測地系,10進数の度表記)
1383:  * @return  array ['address']   フォーマット済み住所
1384:  *                ['city']      市区町村
1385:  *                ['oaza']      大字
1386:  *                ['aza']       字
1387:  *                ['detail1']   街区
1388:  *          FALSE=エラー
1389: */
1390: function getYOLP_Address($latitude, $longitude) {
1391:     $appid = $this->YAHOO_APPLICATION_ID;
1392:     $url = "https://map.yahooapis.jp/geoapi/V1/reverseGeoCoder?appid={$appid}&lat={$latitude}&lon={$longitude}&datum=wgs&output=xml";
1393:     $this->webapi = $url;
1394: 
1395:     $res = array();
1396:     $this->unknown_certificate();
1397:     $xml = simplexml_load_file($url);
1398:     // レスポンス・チェック
1399:     if (isset($xml->Error)) {
1400:         $this->error = TRUE;
1401:         $this->errmsg = 'Yahoo!リバースジオコーダAPI - ' . $xml->Message;
1402:         $res = FALSE;
1403:     } else {
1404:         $res['address'] = (string)$xml->Feature->Property->Address;
1405:         foreach ($xml->Feature->Property->AddressElement as $val) {
1406:             $res[(string)$val->Level] = (string)$val->Name;
1407:         }
1408:     }
1409: 
1410:     return $res;
1411: }
1412: 
1413: // Yahoo! JavaScriptマップ描画 ===============================================
1414: // 2020年10月31日サービス終了
1415: /**
1416:  * Yahoo! JavaScriptマップを描く
1417:  * @param   string $id        マップID
1418:  * @param   float $latitude  中心座標:緯度(世界測地系)
1419:  * @param   float $longitude 中心座標:経度(世界測地系)
1420:  * @param   string $type      マップタイプ:NORMAL/PHOTO/B1/OSM
1421:  *                  ※注意:マップタイプ変更イベントをキャッチアップできない
1422:  * @param   int    $zoom      拡大率
1423:  * @param   string $call      イベント発生時にコールする関数(省略可)
1424:  * @param   array  $items     地点情報(省略可能)
1425:  *                  string description 情報ウィンドウに表示する内容(HTML文)
1426:  *                  float latitude    緯度
1427:  *                  float longitude   経度
1428:  *                  string icon        アイコンURL
1429:  * @param   string $call2     追加スクリプト(省略可)
1430:  * @return  string Yahoo! JavaScriptマップのコード
1431: */
1432: function drawYOLPmap($id, $latitude, $longitude, $type, $zoom, $call=NULL, $items=NULL, $call2=NULL) {
1433:     $appid = $this->YAHOO_APPLICATION_ID;
1434: 
1435:     $code =<<< EOT
1436: <script src="https://map.yahooapis.jp/js/V1/jsapi?appid={$appid}"></script>
1437: <script>
1438: window.onload = function() {
1439:     let ymap = new Y.Map("{$id}", {
1440:         configure : {
1441:             doubleClickZoom : true,
1442:             scrollWheelZoom : true,
1443:             singleClickPan : true,
1444:             dragging : true
1445:         }
1446:     });
1447:     let control1 = new Y.LayerSetControl();
1448:     ymap.addControl(control1);
1449:     let control2 = new Y.ZoomControl();
1450:     ymap.addControl(control2);
1451:     ymap.drawMap(new Y.LatLng($latitude, $longitude), $zoom, Y.LayerSetId.{$type});
1452: 
1453:     ymap.bind('moveend',  getPointData);
1454:     ymap.bind('zoomend',  getPointData);
1455: 
1456:     // イベント発生時の地図情報を取得・格納
1457:     function getPointData() {
1458:         let point = ymap.getCenter();
1459:         // 経度
1460:         if (document.getElementById("longitude") != null) {
1461:             document.getElementById("longitude").value = point.lng();
1462:         }
1463:         // 緯度
1464:         if (document.getElementById("latitude") != null) {
1465:             document.getElementById("latitude").value = point.lat();
1466:         }
1467:         // ズーム
1468:         if (document.getElementById("zoom") != null) {
1469:             document.getElementById("zoom").value = ymap.getZoom();
1470:         }
1471:         {$call}
1472:     }
1473: 
1474: 
1475: EOT;
1476:     // 地点情報
1477:     if ($items !NULL) {
1478:         foreach ($items as $i=>$item) {
1479:             if ($i > 26)    break;      // 'Z'を超えたらスキップ
1480:             // アイコン
1481:             $mark2 = ($i <26? $this->num2alpha($i: 'Z';
1482:             $icon = isset($item['icon']) ? $item['icon': 
1483:                         "https://www.google.com/mapfiles/marker{$mark2}.png";
1484:             $info  = isset($item['description']) ? "marker_{$mark}.bindInfoWindow('{$item['description']}');" : '';
1485: 
1486:             $code .=<<< EOT
1487:     let icon_{$mark} = new Y.Icon('{$icon}');
1488:     let marker_{$mark} = new Y.Marker(new Y.LatLng({$item['latitude']}, {$item['longitude']}), {icon: icon_{$mark}});
1489:     {$info}
1490:     ymap.addFeature(marker_{$mark});
1491: 
1492: EOT;
1493:         }
1494:     }
1495:     // 追加関数
1496:     if ($call2 !NULL) {
1497:         $code .=<<< EOT
1498: {$call2}
1499: 
1500: EOT;
1501:     }
1502:     $code .=<<< EOT
1503: }
1504: </script>
1505: 
1506: EOT;
1507: 
1508:     return $code;
1509: }
1510: 
1511: // HeartRails Geo API ========================================================
1512: /**
1513:  * 指定した検索キーワードの緯度・経度を求める.
1514:  * クラウドサービスとしてHeartRails Geo APIの住所検索APIを利用する.
1515:  * 検索方式は$matchingにセットする(省略時は'like').
1516:  * 検索結果結果は複数になることがあり,配列$itemsに格納する.
1517:  * 参考サイト https://www.pahoo.org/e-soul/webtech/php06/php06-08-01.shtm
1518:  * @param   string $query 検索キーワード:住所のみ(UTF-8)
1519:  * @param   array  $items 情報を格納する配列
1520:  * @param   string $matching 検索方式
1521:  *                             prefix = 前方一致
1522:  *                             like   = 部分一致(省略時)
1523:  *                             suffix = 後方一致
1524:  * @return  int ヒットした地点数/(-1):API呼び出し失敗
1525: */
1526: function getPointsHeartRailsGeo_all($query, &$items, $matching='like') {
1527:     // リクエストURL
1528:     $url = "https://geoapi.heartrails.com/api/xml?method=suggest&matching={$matching}&keyword=" . urlencode($query);
1529:     $this->webapi = $url;
1530:     $n = 1;
1531: 
1532:     $this->unknown_certificate();
1533:     $res = simplexml_load_file($url);
1534:     // レスポンス・チェック
1535:     if (isset($res->error)) {
1536:         $this->error = TRUE;
1537:         $this->errmsg = (string)$res->error;
1538:         $n = 0;
1539:     } else if (! isset($res->location)) {
1540:         $this->error = TRUE;
1541:         $this->errmsg = 'Not found';
1542:         $n = 1;
1543:     } else {
1544:         foreach ($res->location as $element) {
1545:             $items[$n]['latitude']   = (double)$element->y;
1546:             $items[$n]['longitude']  = (double)$element->x;
1547:             $items[$n]['prefecture'] = (string)$element->prefecture;
1548:             $items[$n]['city']       = (string)$element->city;
1549:             $items[$n]['city_kana']  = (string)$element->{'city-kana'};
1550:             $items[$n]['town']       = (string)$element->town;
1551:             $items[$n]['town_kana']  = (string)$element->{'town-kana'};
1552:             $items[$n]['postal']     = (string)$element->postal;
1553:             $items[$n]['address']    = (string)$element->prefecture . (string)$element->city . (string)$element->town;
1554:             $n++;
1555:         }
1556:     }
1557: 
1558:     return ($n - 1);
1559: }
1560: 
1561: /**
1562:  * HeartRails Geo API - 緯度経度による住所検索APIを用いて
1563:  * 緯度・経度から住所を求める
1564:  * @param   float $latitude  緯度(世界測地系,10進数の度表記)
1565:  * @param   float $longitude 経度(世界測地系,10進数の度表記)
1566:  * @return  array ['address']       フォーマット済み住所
1567:  *                ['prefecture']    都道府県名
1568:  *                ['city']          市区町村名
1569:  *                ['town']          町域名
1570:  *                ['postal']        郵便番号
1571:  *          FALSE=エラー
1572: */
1573: function getHeartRailsGeo_Address($latitude, $longitude) {
1574:     // リクエストURL
1575:     $url = "https://geoapi.heartrails.com/api/xml?method=searchByGeoLocation&y={$latitude}&x={$longitude}";
1576:     $this->webapi = $url;
1577: 
1578:     $res = array();
1579:     $this->unknown_certificate();
1580:     $xml = simplexml_load_file($url);
1581:     // レスポンス・チェック
1582:     if (isset($xml->error)) {
1583:         $this->error = TRUE;
1584:         $this->errmsg = 'HeartRails Geo API - ' . (string)$xml->error;
1585:         $res = FALSE;
1586:     } else if (! isset($xml->location)) {
1587:         $this->error = TRUE;
1588:         $this->errmsg = 'HeartRails Geo APIにトラブル発生';
1589:         $res = FALSE;
1590:     } else {
1591:         foreach ($xml->location as $element) {
1592:             $res['prefecture'] = (string)$element->prefecture;
1593:             $res['city']       = (string)$element->city;
1594:             $res['city_kana']  = (string)$element->{'city-kana'};
1595:             $res['town']       = (string)$element->town;
1596:             $res['town_kana']  = (string)$element->{'town-kana'};
1597:             $res['postal']     = (string)$element->postal;
1598:             $res['address'] = $res['prefecture'. $res['city'. $res['town'];
1599:             break;
1600:         }
1601:     }
1602: 
1603:     return $res;
1604: }
1605: 
1606: /**
1607:  * HeartRails Geo API - 都道府県情報取得API
1608:  * @param   array  $items 情報を格納する配列
1609:  * @return  int ヒットした件数
1610: */
1611: function getPrefectures_HeartRailsGeo(&$items) {
1612:     // リクエストURL
1613:     $url = 'https://geoapi.heartrails.com/api/xml?method=getPrefectures';
1614:     $this->webapi = $url;
1615:     $n = 1;
1616: 
1617:     $this->unknown_certificate();
1618:     $res = simplexml_load_file($url);
1619:     // レスポンス・チェック
1620:     if (isset($res->error)) {
1621:         $this->error = TRUE;
1622:         $this->errmsg = (string)$res->error;
1623:         $n = FALSE;
1624:     } else if (! isset($res->prefecture)) {
1625:         $this->error = TRUE;
1626:         $this->errmsg = 'Not found';
1627:         $n = 0;
1628:     } else {
1629:         foreach ($res->prefecture as $val) {
1630:             $items[$n] = (string)$val;
1631:             $n++;
1632:         }
1633:     }
1634: 
1635:     return ($n - 1);
1636: }
1637: 
1638: /**
1639:  * HeartRails Geo API - 市区町村情報取得API
1640:  * @param   string $prefecture 都道府県名
1641:  * @param   array  $items      情報を格納する配列
1642:  * @return  int ヒットした件数
1643: */
1644: function getCities_HeartRailsGeo($prefecture, &$items) {
1645:     // リクエストURL
1646:     $url = "https://geoapi.heartrails.com/api/xml?method=getCities&prefecture={$prefecture}";
1647:     $this->webapi = $url;
1648:     $n = 1;
1649: 
1650:     $this->unknown_certificate();
1651:     $res = simplexml_load_file($url);
1652:     // レスポンス・チェック
1653:     if (isset($res->error)) {
1654:         $this->error = TRUE;
1655:         $this->errmsg = (string)$res->error;
1656:         $n = FALSE;
1657:     } else if (! isset($res->location)) {
1658:         $this->error = TRUE;
1659:         $this->errmsg = 'Not found';
1660:         $n = 0;
1661:     } else {
1662:         foreach ($res->location as $element) {
1663:             $items[$n]['city']      = (string)$element->city;
1664:             $items[$n]['city_kana'] = (string)$element->{'city-kana'};
1665:             $n++;
1666:         }
1667:     }
1668: 
1669:     return ($n - 1);
1670: }
1671: 
1672: /**
1673:  * HeartRails Geo API - 町域情報取得API
1674:  * @param   string $prefecture 都道府県名
1675:  * @param   string $city       市区町村名
1676:  * @param   array  $items      情報を格納する配列
1677:  * @return  int ヒットした件数
1678: */
1679: function getTowns_HeartRailsGeo($prefecture, $city, &$items) {
1680:     // リクエストURL
1681:     $url = "https://geoapi.heartrails.com/api/xml?method=getTowns&prefecture={$prefecture}&city={$city}";
1682:     $this->webapi = $url;
1683:     $n = 1;
1684: 
1685:     $this->unknown_certificate();
1686:     $res = simplexml_load_file($url);
1687:     // レスポンス・チェック
1688:     if (isset($res->error)) {
1689:         $this->error = TRUE;
1690:         $this->errmsg = (string)$res->error;
1691:         $n = FALSE;
1692:     } else if (! isset($res->location)) {
1693:         $this->error = TRUE;
1694:         $this->errmsg = 'Not found';
1695:         $n = 0;
1696:     } else {
1697:         foreach ($res->location as $element) {
1698:             $items[$n]['city']      = (string)$element->city;
1699:             $items[$n]['city_kana'] = (string)$element->{'city-kana'};
1700:             $items[$n]['town']      = (string)$element->town;
1701:             $items[$n]['town_kana'] = (string)$element->{'town-kana'};
1702:             $items[$n]['postal']    = (string)$element->postal;
1703:             $n++;
1704:         }
1705:     }
1706: 
1707:     return ($n - 1);
1708: }
1709: 
1710: /**
1711:  * HeartRails Geo API - 郵便番号による住所検索API
1712:  * @param   string $postal 郵便番号(7桁の半角数字)
1713:  * @param   array  $items  情報を格納する配列
1714:  * @return  int ヒットした地点数
1715: */
1716: function searchByPostal_HeartRailsGeo($postal, &$items) {
1717:     // リクエストURL
1718:     $url = "https://geoapi.heartrails.com/api/xml?method=searchByPostal&postal={$postal}";
1719:     $this->webapi = $url;
1720:     $n = 1;
1721: 
1722:     $this->unknown_certificate();
1723:     $res = simplexml_load_file($url);
1724:     // レスポンス・チェック
1725:     if (isset($res->error)) {
1726:         $this->error = TRUE;
1727:         $this->errmsg = (string)$res->error;
1728:         $n = FALSE;
1729:     } else if (! isset($res->location)) {
1730:         $this->error = TRUE;
1731:         $this->errmsg = 'Not found';
1732:         $n = 0;
1733:     } else {
1734:         foreach ($res->location as $element) {
1735:             $items[$n]['postal']     = $postal;
1736:             $items[$n]['latitude']   = (double)$element->y;
1737:             $items[$n]['longitude']  = (double)$element->x;
1738:             $items[$n]['prefecture'] = (string)$element->prefecture;
1739:             $items[$n]['city']       = (string)$element->city;
1740:             $items[$n]['city_kana']  = (string)$element->{'city-kana'};
1741:             $items[$n]['town']       = (string)$element->town;
1742:             $items[$n]['town_kana']  = (string)$element->{'town-kana'};
1743:             $items[$n]['address']    = (string)$element->prefecture . (string)$element->city . (string)$element->town;
1744:             $n++;
1745:         }
1746:     }
1747: 
1748:     return ($n - 1);
1749: }
1750: 
1751: // OSM Nominatim Search API ==================================================
1752: /**
1753:  * 指定した検索キーワードの緯度・経度を求める.
1754:  * クラウドサービスとしてOSM Nominatim Search API住所検索APIを利用する.
1755:  * 検索結果結果は複数になることがあり,配列$itemsに格納する.
1756:  * 参考サイト https://www.pahoo.org/e-soul/webtech/php06/php06-08-01.shtm
1757:  * @param   string $query 検索キーワード:住所のみ(UTF-8)
1758:  * @param   array  $items 情報を格納する配列
1759:  * @return  int ヒットした地点数/(-1):API呼び出し失敗
1760: */
1761: function getPointsNominatim_all($query, &$items) {
1762:     // リクエストURL
1763:     $url = 'https://nominatim.openstreetmap.org/search?format=json&q=' . urlencode($query);
1764:     $this->webapi = $url;
1765:     $n = 1;
1766: 
1767:     // User-Agent偽装
1768:     $header = array(
1769:         'Content-Type: application/x-www-form-urlencoded',
1770:         'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 ' . $this->NOMINATIM_EMAIL,
1771:     );
1772:     $stream = stream_context_create(array(
1773:         'http' => array(
1774:             'method' => 'GET',
1775:             'header' => implode("\r\n", $header),
1776:             'ignore_errors'=>TRUE
1777:         )
1778:     ));
1779:     $json = file_get_contents($url, FALSE, $stream);
1780: 
1781:     // レスポンス・チェック
1782:     if ($json == FALSE) {
1783:         $this->error = TRUE;
1784:         $this->errmsg = '';
1785:         $n = 0;
1786:     } else {
1787:         if ($this->isphp7over()) {
1788:             $res = @json_decode($json, FALSE, 512, JSON_BIGINT_AS_STRING);
1789:         } else {
1790:             $res = @json_decode($json, FALSE, 512);
1791:         }
1792:         // エラー・チェック
1793:         if ($res == FALSE) {
1794:             $this->error = TRUE;
1795:             $this->errmsg = $res;
1796:             return (-1);
1797:         }
1798:         foreach ($res as $element) {
1799:             $items[$n]['latitude']   = (double)$element->lat;
1800:             $items[$n]['longitude']  = (double)$element->lon;
1801:             $items[$n]['address']    = (string)$element->display_name;
1802:             $n++;
1803:         }
1804:     }
1805:     if ($n == 1) {
1806:         $this->error = TRUE;
1807:         $this->errmsg = 'Not found';
1808:     }
1809: 
1810:     return ($n - 1);
1811: }
1812: 
1813: // 国土地理院ジオコーディングAPI =============================================
1814: /**
1815:  * 指定した検索キーワードの緯度・経度を求める.
1816:  * クラウドサービスとして国土地理院ジオコーディングAPIを利用する.
1817:  * 検索方式は$matchingにセットする(省略時は'like').
1818:  * 検索結果結果は複数になることがあり,配列$itemsに格納する.
1819:  * 参考サイト https://www.pahoo.org/e-soul/webtech/php06/php06-08-01.shtm
1820:  * @param   string $query 検索キーワード:住所のみ(UTF-8)
1821:  * @param   array  $items 情報を格納する配列
1822:  * @return  int ヒットした地点数/(-1):API呼び出し失敗
1823: */
1824: function getPointsGSI($query, &$items) {
1825:     // リクエストURL
1826:     $url = "https://msearch.gsi.go.jp/address-search/AddressSearch?q=" . urlencode($query);
1827:     $this->webapi = $url;
1828:     $n = 1;
1829: 
1830:     $this->unknown_certificate();
1831:     $json = file_get_contents($url);
1832:     // レスポンス・チェック
1833:     if ($json == FALSE) {
1834:         $this->error = TRUE;
1835:         $this->errmsg = '国土地理院ジオコーディングAPIが応答しません';
1836:         $n = 0;
1837:     } else {
1838:         $arr = json_decode($json);
1839:         if (count($arr) == 0) {
1840:             $this->error = TRUE;
1841:             $this->errmsg = '住所が見つかりません';
1842:             $n = 0;
1843:         } else {
1844:             foreach ($arr as $element) {
1845:                 $items[$n]['latitude']  = (double)$element->geometry->coordinates[1];
1846:                 $items[$n]['longitude'] = (double)$element->geometry->coordinates[0];
1847:                 $items[$n]['address']   = (string)$element->properties->title;
1848:                 $items[$n]['postal']    = (string)$element->properties->addressCode;
1849:                 $n++;
1850:             }
1851:         }
1852:     }
1853: 
1854:     return ($n - 1);
1855: }
1856: 
1857: // Leafletによるマップ描画 ===================================================
1858: /**
1859:  * Leafletによるマップ描画
1860:  * @param   string $id        マップID
1861:  * @param   float  $latitude  中心座標:緯度(世界測地系)
1862:  * @param   float  $longitude 中心座標:経度(世界測地系)
1863:  * @param   string $type      マップタイプ:GSISTD/GSIPALE/GSIBLANK/GSIPHOTO/OSM
1864:  * @param   int    $zoom      拡大率
1865:  * @param   string $call      イベント発生時にコールする関数(省略可)
1866:  * @param   array  $items     地点情報(省略可能)
1867:  *                  string description 情報ウィンドウに表示する内容(HTML文)
1868:  *                  float  latitude    緯度
1869:  *                  float  longitude   経度
1870:  *                  string icon        アイコンURL
1871:  * @param   string $call2     追加スクリプト(省略可)
1872:  * @param   int    $max_width 情報ウィンドウの最大幅(省略時:200)
1873:  * @param   array  $offset    アイコンから情報ウィンドウのオフセット位置(省略時:NULL)
1874:  * @param   array  $overlays  オーバーレイ:GSIELEV/GSIFAULT/GSIFLOOD
1875:  * @return  string Leafletマップのコード
1876: */
1877: function drawLeaflet($id, $latitude, $longitude, $type, $zoom, $call=NULL, $items=NULL, $call2=NULL, $max_width=200, $offset=NULL, $overlays=NULL) {
1878: 
1879:     if (! is_array($offset)) {
1880:         $offset = array(0, 0);
1881:     }
1882:     // デフォルト・オーバーレイ
1883:     $addoverlay = '';
1884:     if ($overlays !NULL) {
1885:         foreach ($overlays as $overlay) {
1886:             $addoverlay .=<<< EOT
1887:     {$overlay}.addTo(map);
1888: 
1889: EOT;
1890:         }
1891:     }
1892: 
1893:     $code =<<< EOT
1894: <link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" />
1895: <script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
1896: <script>
1897: window.onload = function() {
1898:     let map = L.map('{$id}',{zoomControl:false});
1899:     map.setView([{$latitude}, {$longitude}], {$zoom});
1900:     L.control.scale({
1901:         maxWidth: 200,
1902:         position: 'bottomright',
1903:         imperial: false
1904:     }).addTo(map);
1905:     L.control.zoom({position:'topleft'}).addTo(map);
1906: 
1907:     // 地理院地図:標準地図
1908:     let GSISTD = new L.tileLayer(
1909:         'https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png',
1910:         {
1911:             attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>",
1912:             minZoom: 0,
1913:             maxZoom: 18,
1914:             name: 'GSISTD'
1915:         });
1916:     // 地理院地図:淡色地図
1917:     let GSIPALE = new L.tileLayer(
1918:         'https://cyberjapandata.gsi.go.jp/xyz/pale/{z}/{x}/{y}.png',
1919:         {
1920:             attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>",
1921:             minZoom: 2,
1922:             maxZoom: 18,
1923:             name: 'GSIPALE'
1924:         });
1925:     // 地理院地図:白地図
1926:     let GSIBLANK = new L.tileLayer(
1927:         'https://cyberjapandata.gsi.go.jp/xyz/blank/{z}/{x}/{y}.png',
1928:         {
1929:             attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>",
1930:             minZoom: 5,
1931:             maxZoom: 14,
1932:             name: 'GSIBLANK'
1933:         });
1934:     // 地理院地図:写真
1935:     let GSIPHOTO = new L.tileLayer(
1936:         'https://cyberjapandata.gsi.go.jp/xyz/seamlessphoto/{z}/{x}/{y}.jpg',
1937:         {
1938:             attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>",
1939:             minZoom: 2,
1940:             maxZoom: 18,
1941:             name: 'GSIPHOTO'
1942:         });
1943:     // OpenStreetMap
1944:     let OSM = new L.tileLayer(
1945:         'https://tile.openstreetmap.jp/{z}/{x}/{y}.png',
1946:         {
1947:             attribution: "© <a href='https://osm.org/copyright' target='_blank'>OpenStreetMap</a> contributors",
1948:             minZoom: 0,
1949:             maxZoom: 18,
1950:             name: 'OSM'
1951:         });
1952: 
1953:     // baseMapsオブジェクトにタイル設定
1954:     let baseMaps = {
1955:         "地理院地図" : GSISTD,
1956:         "淡色地図"   : GSIPALE,
1957:         "白地図"     : GSIBLANK,
1958:         "写真地図"   : GSIPHOTO,
1959:         "オープンストリートマップ" : OSM
1960:     };
1961: 
1962:     // 地理院地図:色別標高図(オーバーレイ)
1963:     let GSIELEV = new L.tileLayer(
1964:         'https://cyberjapandata.gsi.go.jp/xyz/relief/{z}/{x}/{y}.png',
1965:         {
1966:             attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>",
1967:             opacity: 0.6,
1968:             minZoom: 5,
1969:             maxZoom: 15,
1970:             name: 'GSIELEV'
1971:         });
1972:     // 地理院地図:活断層図(オーバーレイ)
1973:     let GSIFAULT = new L.tileLayer(
1974:         'https://cyberjapandata.gsi.go.jp/xyz/afm/{z}/{x}/{y}.png',
1975:         {
1976:             attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>",
1977:             opacity: 0.6,
1978:             minZoom: 11,
1979:             maxZoom: 16,
1980:             name: 'GSIFAULT'
1981:         });
1982:     // 地理院地図:治水地形分類図 更新版(オーバーレイ)
1983:     let GSIFLOOD = new L.tileLayer(
1984:         'https://cyberjapandata.gsi.go.jp/xyz/lcmfc2/{z}/{x}/{y}.png',
1985:         {
1986:             attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>",
1987:             opacity: 0.6,
1988:             minZoom: 11,
1989:             maxZoom: 16,
1990:             name: 'GSIFLOOD'
1991:         });
1992: 
1993:     // baseMapsオブジェクトにオーバーレイ設定
1994:     let overlayMaps = {
1995:         "色別標高図"     : GSIELEV,
1996:         "活断層図"       : GSIFAULT,
1997:         "治水地形分類図" : GSIFLOOD,
1998:     };
1999: 
2000:     // layersコントロールにbaseMapsオブジェクトを設定して地図に追加
2001:     L.control.layers(baseMaps, overlayMaps).addTo(map);
2002:     {$type}.addTo(map);
2003: {$addoverlay}

PHPで直近の地震情報を表示する
オーバーレイ選択
オーバレイは複数指定することが可能で、マップの右上のアイコンをクリックして、チェックボックスで指定する。

解説:表示とURLパラメータ

earthquake.php

 940: // パラメータ
 941: $number = getParam('number', FALSE, DEF_NUMBER);
 942: $number = trim($number);
 943: if (preg_match('/^[0-9]+$/i', $number> 0) {
 944:     $errmsg = '';
 945:     if (($number < 1|| ($number > MAX_NUMBER)) {
 946:         $errmsg = sprintf('判定できる整数の範囲は,1から%dまでです', MAX_NUMBER);
 947:     }
 948: else {
 949:     $errmsg = '数値は正の整数(自然数)を指定してください';
 950: }
 951: $zoom     = getParam('zoom',     FALSE, DEF_ZOOM);
 952: $zoom     = ($zoom == ''? DEF_ZOOM : $zoom;
 953: $type     = getParam('type',     FALSE, DEF_TYPE);
 954: $type     = ($type == ''? DEF_TYPE : $type;
 955: $overlays = getParam('overlays', FALSE, DEF_OVERLAYS);
 956: $overlays = ($overlays == ''? DEF_OVERLAYS : $overlays;

URLパラメータを使って
earthquake.php?number=10
のようにすることで、number で指定した値が求めたい震源の数になる。
また、
earthquake.php?number=10&zoom=8&type=GSISTD&overlays=GSIELEV,GSIFAULT
のように指定すると、震源数が10、拡大率が8、ベースマップが地理院地図、オーバーレイは色別標高図と活断層図で表示する。

解説:SNS投稿機能

PHPによるSNS投稿機能の概念図
PHPによるSNS投稿機能の概念図
表示したコンテンツを画像として、メッセージと一緒にボタン1つでSNSに投稿する機能を追加した。流れは次の通りである。
コンテンツを描画しているのはクライアントにあるブラウザ(レンダリングエンジン)であることから、クライアント側で画像を作成し、サーバ側で SNS の API をコールするという方針とした。
  1. ブラウザはサーバにコンテンツ描画をリクエストする。
  2. サーバはデータサイトからデータ取得する。(サーバキャッシュにデータがあればそれを利用する)
  3. サーバはコンテンツ描画スクリプトを生成する。
  4. サーバはブラウザへレスポンス(HTML文)を返す。
  5. ブラウザはコンテンツをレンダリングする。
  6. ブラウザはレンダリングしたコンテンツを画像データとしてサーバへアップロードする。
  7. サーバは SNS の API を使ってツイートする。
  8. サーバは SNS へメッセージと画像を送る。
  9. サーバはブラウザへレスポンス(HTML文)を返す。
ブラウザでレンダリングしたグラフを画像に変換する処理は、JavaScriptの html2canvas ライブラリを利用した。このライブラリの使い方は後述する。投稿するSNSのうち、Twitter(現・X) については、「PHPでTwitterに投稿(ツイート)する」で作成した pahooTwitterAPI クラスを利用する。Bluesky については、「PHPでBlueskyに投稿する」で作成した pahooBlueskyAPI クラスを利用する。

解説:html2canvasライブラリ

earthquake.php

 152: <script src="https://html2canvas.hertzen.com/dist/html2canvas.js"></script>
 153: <script>
 154: function mytweet() {
 155:     $('#tweet').val('1');
 156:     document.myform.submit();
 157: }

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

earthquake.php

 910: <div id="{$target}" name="{$target}" style="width:{$width2}px;">
 911: <p>
 912: ⚠️最近の地震情報 {$dt}現在
 913: </p>
 914: {$html}
 915: </div>

画像化するオブジェクトは、<div id="{$target}"> で指定する範囲である。
レンダリングエンジンによって違うのかもしれないが、html2canvas ライブラリによって画像化される範囲が実際よりやや小さいため、あえてマップ領域より、幅を20ピクセル大きくした範囲を画像化範囲としている。

earthquake.php

 368: /**
 369:  * HTMLオブジェクトの画像化
 370:  * @param   なし
 371:  * @return  string JavaScriptコード
 372: */
 373: function js_html2image() {
 374:     $target = TARGET;
 375:     $js = '';
 376: 
 377:     // Googleマップの場合
 378:     if (MAPSERVICE == 0) {
 379:         $js .=<<< EOT
 380: google.maps.event.addListener(map, 'tilesloaded', function() {
 381:     var capture = document.querySelector('#{$target}');
 382:     html2canvas(capture, {useCORS: true, allowTaint:true}).then(canvas => {
 383:         var base64 = canvas.toDataURL('image/png');     // 画像化
 384:         $('#base64').val(base64);
 385:     });
 386: });
 387: 
 388: EOT;
 389: 
 390:     // Leafletの場合(ブラウザによってはうまく動作しない)
 391:     } else {
 392:         $js .=<<< EOT
 393: HTMLCanvasElement.prototype.getContext = function(origFn) {
 394:     return function(type, attribs) {
 395:         attribs = attribs || {};
 396:         attribs.preserveDrawingBuffer = true;
 397:         return origFn.call(this, type, attribs);
 398:     };
 399: } (HTMLCanvasElement.prototype.getContext);
 400: 
 401: // HTML画像化イベント登録
 402: function html2image() {
 403:     var capture = document.querySelector('#{$target}');
 404:     html2canvas(capture, {useCORS: true, allowTaint:true}).then(canvas => {
 405:         var base64 = canvas.toDataURL('image/png');     // 画像化
 406:         $('#base64').val(base64);
 407:     });
 408: };
 409: 
 410: // ズーム変更イベント
 411: map.on('zoomend', function() {
 412:     html2image();
 413: });
 414: 
 415: // マップ移動イベント
 416: map.on('moveend', function() {
 417:     html2image();
 418: });
 419: 
 420: // html2image()を発火させるために,ズームアウトして500msec後に元に戻す.
 421: var zoom = map.getZoom();
 422: map.setZoom(zoom - 1);
 423: setTimeout(function() {
 424:     map.setZoom(zoom);
 425: }, 500);
 426: 
 427: EOT;
 428:     }
 429: 
 430:     return $js;
 431: }

html2canvas を呼び出すタイミングだが、Googleマップの場合は tilesloadedイベントにフックする。
Leafletの場合、tilesloaded に相当するイベントがないため、ズーム変更完了イベント zoomend にフックするようにした。強制的にズームアウトし、500ミリ秒後に元ズーム値に戻す操作を行う。しかし、この方法だとブラウザによって、マップ画像が無い状態で画像化されてしまうことがあるようだ。もし対策方法をご存じの方がいたらお知らせいただきたい。
これらを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)

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

解説:メディア付き投稿(RAWデータ)

pahooTwitterAPI.php

 587: /**
 588:  * バイナリデータを使ったメディア付きメッセージをツイートする.
 589:  * Tweetet API v2 を使用する.
 590:  * @param   string $message 投稿メッセージ(UTF-8限定)
 591:  * @param   array  $items   メディアデータ(バイナリデータ配列)
 592:  * @return  bool TRUE:リクエスト成功/FALSE:失敗
 593: */
 594: function tweet_media_raw($message, $items) {
 595:     //メディアのアップロード
 596:     $media_ids = array();
 597:     $cnt = 0;
 598:     //Tweetet API v1.1 を使用する(v2にメディアアップロードが未実装のため)
 599:     $this->connection->setApiVersion('1.1'); 
 600:     foreach ($items as $data) {
 601:         $tmpname = $this->saveTempFile($data);
 602: //      $media = $this->connection->upload('media/upload', ['media' => $tmpname]);
 603:         $media = $this->connection->upload('media/upload', ['media' => $tmpname], ['chunkedUpload' => true]);   //twitteroauth 7.0.0 対応
 604:         unlink($tmpname);
 605:         if (! isset($media->media_id_string))   break;      //処理失敗
 606:         $media_ids[] = (string)$media->media_id_string;
 607:         $cnt++;
 608:         if ($cnt > 3)   break;      //最大4つまで
 609:     }
 610: 
 611:     //メディア付きツイート(Tweetet API v2 を使用する)
 612:     $this->connection->setApiVersion('2'); 
 613:     $option = [
 614:         'text' => $message,
 615:         'media' => [
 616:             'media_ids' => $media_ids
 617:         ]
 618:     ];
 619: //  $status = $this->connection->post('tweets', $option, TRUE);
 620:     $status = $this->connection->post('tweets', $option, ['jsonPayload' => true]);  //twitteroauth 7.0.0 対応
 621:     $this->webapi = 'https://api.twitter.com/2/tweets';
 622: 
 623:     //処理に成功した.
 624:     if ($this->isSuccess()) {
 625:         $this->responses = $status->data;
 626:         $this->errcode   = NULL;
 627:         $this->errmsg    = '';
 628:         $this->error     = FALSE;
 629:         $res = TRUE;
 630:     //処理に失敗した.
 631:     } else {
 632:         if ($this->isAuthError() == FALSE) {
 633:             $this->errmsg = $status->detail;
 634:             $this->error  = TRUE;
 635:         }
 636:         $res = FALSE;
 637:     }
 638:     return $res;
 639: }

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

解説:Twitter(現・X)へ投稿する

earthquake.php

 316: /**
 317:  * Twitter(現・X)投稿
 318:  * @param   string $message 投稿文
 319:  * @param   string $res     応答メッセージ格納用
 320:  * @return  bool TRUE:成功/FALSE:失敗または未処理
 321: */
 322: function mediaTweet($message, &$res) {
 323:     if (! TWITTER)  return FALSE;
 324: 
 325:     $ret = TRUE;
 326:     if (isset($_POST['base64']) && ($_POST['base64'!'')) {
 327:         $base64 = preg_replace('/data\:image\/png\;base64\,/ui', '', $_POST['base64']);
 328:         $raws = array(base64_decode($base64));
 329:         $ptw = new pahooTwitterAPI();
 330:         $ptw->tweet_media_raw($message, $raws);
 331:         $errmsg = $ptw->errmsg;
 332:         $ret = ! $ptw->error;
 333:         $ptw = NULL;
 334:         if ($ret) {
 335:             $res = 'ツイートしました';
 336:         }
 337:     }
 338:     return $ret;
 339: }

メッセージと画像をツイート処理するのはサーバ側のPHPユーザー関数 mediaTweet で処理する。
ブラウザから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:     var $refreshJwt;    // refreshJwt
  21: 
  22:     const INTERNAL_ENCODING = 'UTF-8';  // 内部エンコーディング
  23:     const MAX_MESSAGE_LEN = 300;        // 投稿可能なメッセージ文字数
  24:     const URL_LEN = 23;                 // メッセージ中のURL文字数(相当)
  25:     const MAX_IMAGE_WIDTH  = 1200;      // 投稿可能な最大画像幅(ピクセル)
  26:     const MAX_IMAGE_HEIGHT = 630;       // 投稿可能な最大画像高さ(ピクセル)
  27:                                         // これより大きいときは自動縮小する
  28:     // トークンを保存するファイル名
  29:     // 秘匿性を保つことができ、かつ、PHPプログラムから読み書き可能であること
  30:     const FILENAME_TOKEN = './.token';
  31: 
  32:     // Bluesky API アプリパスワード
  33:     // https://bsky.app/
  34:     var $BLUESKY_HANDLE   = '***************';      // ハンドル名
  35:     var $BLUESKY_PASSWORD = '***************';      // アプリケーション・パスワード

Bluesky API」を利用するために、ハンドル名アプリケーション・パスワード が必要で、入手方法は「Bluesky API - WebAPIの登録方法」を参照されたい。

解説:Blueskyへ投稿する

earthquake.php

 341: /**
 342:  * Bluesky投稿
 343:  * @param   string $message 投稿文
 344:  * @param   string $res     応答メッセージ格納用
 345:  * @return  bool TRUE:成功/FALSE:失敗または未処理
 346: */
 347: function mediaBluesky($message, &$res) {
 348:     if (! BLUESKY)  return FALSE;
 349: 
 350:     $ret = TRUE;
 351:     if (isset($_POST['base64']) && ($_POST['base64'!'')) {
 352:         $base64 = preg_replace('/data\:image\/png\;base64\,/ui', '', $_POST['base64']);
 353:         $raws = array(base64_decode($base64));
 354:         $pbs = new pahooBlueskyAPI(BLUESKY_DOMAIN);
 355:         $res = $pbs->createSession();
 356:         $pbs->post($message, FALSE, NULL, NULL, $raws);
 357:         $errmsg = $pbs->geterror();
 358:         $ret = ! $pbs->iserror();
 359:         $res = $pbs->deleteSession();
 360:         $pbs = NULL;
 361:         if ($ret) {
 362:             $res = 'Blueskyに投稿しました';
 363:         }
 364:     }
 365:     return $ret;
 366: }

メッセージと画像を Blueskyに投稿するのはサーバ側のPHPユーザー関数 mediaBluesky である。
ブラウザからPOSTで受け取った画像データ $_POST['base64'] はBASE64でデコードされており、冒頭に余計なヘッダ情報が付いているので、このヘッダ情報を除き( preg_replace )、バイナリデータにデコードする( base64_decode )。
続いて pahooBlueskyAPI クラスを呼び出し、post メソッドを使って、メッセージと画像を一気に投稿する。

解説:SNSへ投稿する(メイン・プログラム)

earthquake.php

 972: // Twitter(現・X)、Blueskyへ投稿する.
 973: $dt = nowDT();
 974: $message =<<< EOT
 975: ⚠️最近の地震情報 {$dt}現在
 976: 
 977: (ご参考)PHPで最近の地震情報を表示する https://www.pahoo.org/e-soul/webtech/php05/php05-17-01.shtm #地震
 978: 
 979: EOT;
 980: if (TWITTER && isButton('tweet')) {
 981:     mediaTweet($message, $res);
 982: }
 983: if (BLUESKY && isButton('bluesky')) {
 984:     mediaBluesky($message, $res);
 985: }
 986: 
 987: // 表示コンテンツを作成する.

メイン・プログラムでは、mediaTweetmediaBluesky を呼び出し、メッセージと画像をSNSに投稿する。メッセージの内容は自由に変更していただいて構わない。

活用例

みんなの知識 ちょっと便利帳」では、「日本の直近の地震情報」で本プログラムを利用し、見やすいページを提供している。ありがとうございます。

参考サイト

(この項おわり)
header