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

目次
- サンプル・プログラムの実行例
- サンプル・プログラム
- プログラムの方針
- 気象庁防災情報XMLフォーマット
- VXSE53の構造
- 準備:pahooGeoCode クラス
- 準備:地図サービスの選択
- 準備:キャッシュ・システム
- 準備:各種定数
- 解説:気象庁防災情報XMLから最新の震源・震度に関する情報URLを取得
- 解説:地震情報の取り出し
- 解説:地震情報をマッピング情報に変換
- 解説:地図描画について
- 解説:オーバーレイ表示(Leaflet選択時のみ)
- 解説:表示とURLパラメータ
- 解説:ツイート機能
- 解説:html2canvasライブラリ
- 準備:pahooTwitterAPI クラス
- 解説:メディア付き投稿(RAWデータ)
- 解説:ツイート処理
- 活用例
- 参考サイト
サンプル・プログラム
earthquake.php | サンプル・プログラム本体。 |
pahooGeoCode.php | 住所・緯度・経度に関わるクラス pahooGeoCode。 使い方は「PHPで住所・ランドマークから最寄り駅を求める」などを参照。include_path が通ったディレクトリに配置すること。 |
pahooCache.php | キャッシュ処理に関わるクラス pahooCache。 キャッシュ処理に関わるクラスの使い方は「PHPで天気予報を求める」を参照。include_path が通ったディレクトリに配置すること。 |
pahooTwitterAPI.php | Twitter APIを利用するクラス pahooTwitterAPI。 使い方は「PHPでTwitterに投稿(ツイート)する」などを参照。include_path が通ったディレクトリに配置すること。 |
pahooBlueskyAPI.php | Bluesky APIに関わるクラス pahooBlueskyAPI。 使い方は「PHPでPHPでBlueskyに投稿する」などを参照。include_path が通ったディレクトリに配置すること。 |
バージョン | 更新日 | 内容 |
---|---|---|
5.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 | 色別標高図+活断層図をオーバーレイ表示 |
バージョン | 更新日 | 内容 |
---|---|---|
6.5.0 | 2025/06/14 | GoogleMaps JavaScript APIの変更に対応 |
6.4.0 | 2025/03/01 | makeYOLP_GeoSelectCategory()--引数$flagWorld追加 |
6.3.3 | 2024/09/14 | $this->NOMINATIM_EMAIL 追加 |
6.3.2 | 2024/02/14 | getStaticMap() -- bug-fix |
6.3.1 | 2023/07/09 | bug-fix |
バージョン | 更新日 | 内容 |
---|---|---|
1.1.1 | 2023/02/11 | コメント追記 |
1.1 | 2021/04/08 | simplexml_load()メソッド追加 |
1.0 | 2021/04/02 | 初版 |
バージョン | 更新日 | 内容 |
---|---|---|
5.5.1 | 2024/11/23 | __construct() -- PHP8.4における応急処置 |
5.5.0 | 2024/06/21 | TwitterOAuth 7.0.0 対応 |
5.4.0 | 2024/05/18 | twitter.com → x.com 変更対応 |
5.3.0 | 2023/08/15 | tweet3() -- メディアのシャフル機能 |
5.2.1 | 2023/07/22 | bug-fix |
バージョン | 更新日 | 内容 |
---|---|---|
2.1.0 | 2025/03/20 | getUserPosts() 追加 |
2.0.1 | 2025/01/24 | getPostThread() -- 認証必要のエンドポイントに変更 |
2.0.0 | 2025/01/24 | トークンを保持するよう改良 |
1.9.0 | 2025/01/16 | getEmbedPosts() 追加 |
1.8.1 | 2024/12/11 | getOGPInformation()--リダイレクト対応 |
プログラムの方針
正規表現を使って、このページから
- 発生日時分
- 震源地
- 震源の位置
- 震源の深さ
- 規模
- 最大震度
気象庁防災情報XMLフォーマット
https://www.data.jma.go.jp/developer/xml/data/yyyymmddhhmmss_番号_電文コード_連番.xml震源・震度に関する情報は、長期フィードの地震火山の中にある電文コード VXSE53 に入っている。そこで、フィードから配信日時 yyyymmddhhmmss が最も大きく、VXSE53 を含むURLを取り出せば、それが目指す情報XMLとなる。
VXSE53の構造
ここから必要な情報を取り出す。
準備:pahooGeoCode クラス
pahooGeoCode.php
37: class pahooGeoCode {
38: var $items; // 検索結果格納用
39: var $error; // エラー・フラグ
40: var $errmsg; // エラー・メッセージ
41: var $hits; // 検索ヒット件数
42: var $webapi; // 直前に呼び出したWebAPI URL
43:
44: // Google Cloud Platform APIキー
45: // https://cloud.google.com/maps-platform/
46: // ※Google Maps APIを利用しないのなら登録不要
47: var $GOOGLE_API_KEY_1 = '**************************'; // HTTPリファラ用
48: var $GOOGLE_API_KEY_2 = '**************************'; // IP制限用
49: var $GOOGLE_MAP_ID = '*************************'; // GoogleMaps ID
50:
51: // Yahoo! JAPAN Webサービス アプリケーションID
52: // https://e.developer.yahoo.co.jp/register
53: // ※Yahoo! JAPAN Webサービスを利用しないのなら登録不要
54: var $YAHOO_APPLICATION_ID = '*****************************';
クラスについては「PHPでクラスを使ってテキストの読みやすさを調べる」を参照されたい。

地図としてGoogleマップを利用するのであれば、Google Cloud Platform APIキー が必要で、その入手方法は「Google Cloud Platform - WebAPIの登録方法」を参照されたい。
準備:地図サービスの選択
earthquake.php
47: // 地図描画サービスの選択
48: // 0:Google
49: // 2:地理院地図・OSM
50: define('MAPSERVICE', 2);

準備:キャッシュ・システム
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/');
キャッシュ保持時間、キャッシュ・ディレクトリともに、自サイトの環境に応じて変更してほしい。天気予報系プログラムと別のキャッシュ・ディレクトリにした方が、お互いのキャッシュ保持時間の干渉を受けなくなる。
配布ファイルは、新しい地震情報が入ってくることを考え、フィードの方を短く、地震情報の方は長くキャッシュ保持時間を設定してある。
準備:各種定数など
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) ===============================================================

出力結果を 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: }
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: }
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: }
解説:地震情報をマッピング情報に変換
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つだけ立てて、情報ウィンドウに複数回の地震情報を羅列するようにできる。
解説:地図描画について
解説:オーバーレイ表示(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®ion=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)) == NULL) return 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®ion=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)) == NULL) return 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®ion=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)) == NULL) return 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)) == NULL) return 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}

解説:表示と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;
earthquake.php?number=10のようにすることで、number で指定した値が求めたい震源の数になる。
また、
earthquake.php?number=10&zoom=8&type=GSISTD&overlays=GSIELEV,GSIFAULTのように指定すると、震源数が10、拡大率が8、ベースマップが地理院地図、オーバーレイは色別標高図と活断層図で表示する。
解説:SNS投稿機能

コンテンツを描画しているのはクライアントにあるブラウザ(レンダリングエンジン)であることから、クライアント側で画像を作成し、サーバ側で SNS の API をコールするという方針とした。
- ブラウザはサーバにコンテンツ描画をリクエストする。
- サーバはデータサイトからデータ取得する。(サーバキャッシュにデータがあればそれを利用する)
- サーバはコンテンツ描画スクリプトを生成する。
- サーバはブラウザへレスポンス(HTML文)を返す。
- ブラウザはコンテンツをレンダリングする。
- ブラウザはレンダリングしたコンテンツを画像データとしてサーバへアップロードする。
- サーバは SNS の API を使ってツイートする。
- サーバは SNS へメッセージと画像を送る。
- サーバはブラウザへレスポンス(HTML文)を返す。
解説:html2canvasライブラリ
earthquake.php
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: }
画像化を実行するJavaScript関数は html2canvas である。
earthquake.php
910: <div id="{$target}" name="{$target}" style="width:{$width2}px;">
911: <p>
912: ⚠️最近の地震情報 {$dt}現在
913: </p>
914: {$html}
915: </div>
レンダリングエンジンによって違うのかもしれないが、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: }
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)
解説:メディア付き投稿(RAWデータ)
pahooTwitterAPI.php
587: /**
588: * バイナリデータを使ったメディア付きメッセージをツイートする.
589: * Tweetet API v2 を使用する.
590: * @param string $message 投稿メッセージ(UTF-8限定)
591: * @param array $items メディアデータ(バイナリデータ配列)
592: * @return bool TRUE:リクエスト成功/FALSE:失敗
593: */
594: function tweet_media_raw($message, $items) {
595: //メディアのアップロード
596: $media_ids = array();
597: $cnt = 0;
598: //Tweetet API v1.1 を使用する(v2にメディアアップロードが未実装のため)
599: $this->connection->setApiVersion('1.1');
600: foreach ($items as $data) {
601: $tmpname = $this->saveTempFile($data);
602: // $media = $this->connection->upload('media/upload', ['media' => $tmpname]);
603: $media = $this->connection->upload('media/upload', ['media' => $tmpname], ['chunkedUpload' => true]); //twitteroauth 7.0.0 対応
604: unlink($tmpname);
605: if (! isset($media->media_id_string)) break; //処理失敗
606: $media_ids[] = (string)$media->media_id_string;
607: $cnt++;
608: if ($cnt > 3) break; //最大4つまで
609: }
610:
611: //メディア付きツイート(Tweetet API v2 を使用する)
612: $this->connection->setApiVersion('2');
613: $option = [
614: 'text' => $message,
615: 'media' => [
616: 'media_ids' => $media_ids
617: ]
618: ];
619: // $status = $this->connection->post('tweets', $option, TRUE);
620: $status = $this->connection->post('tweets', $option, ['jsonPayload' => true]); //twitteroauth 7.0.0 対応
621: $this->webapi = 'https://api.twitter.com/2/tweets';
622:
623: //処理に成功した.
624: if ($this->isSuccess()) {
625: $this->responses = $status->data;
626: $this->errcode = NULL;
627: $this->errmsg = '';
628: $this->error = FALSE;
629: $res = TRUE;
630: //処理に失敗した.
631: } else {
632: if ($this->isAuthError() == FALSE) {
633: $this->errmsg = $status->detail;
634: $this->error = TRUE;
635: }
636: $res = FALSE;
637: }
638: return $res;
639: }
解説:Twitter(現・X)へ投稿する
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: }
ブラウザから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へ投稿する
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: }
ブラウザから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: // 表示コンテンツを作成する.
活用例
参考サイト
- WebAPIの登録方法:ぱふぅ家のホームページ
- PHPで住所・ランドマークから最寄り駅を求める:ぱふぅ家のホームページ
- PHPで緯度・経度から住所を求める:ぱふぅ家のホームページ
- JavaScriptでHTML表示を画像保存する:ぱふぅ家のホームページ
- HTMLとCSSでさまざまなアイコンを表示する:ぱふぅ家のホームページ
- Twitter API - WebAPIの登録方法:ぱふぅ家のホームページ
- PHPでTwitterに画像付きメッセージ投稿:ぱふぅ家のホームページ
- Bluesky API - WebAPIの登録方法:ぱふぅ家のホームページ
- PHPでBlueskyに投稿する:ぱふぅ家のホームページ
- C++ で直近の地震情報を取得する:ぱふぅ家のホームページ
- 日本の直近の地震情報:みんなの知識 ちょっと便利帳
そこで今回は、気象庁防災情報XMLから、直近の地震情報を取得するPHPスクリプトを作成してみることにする。
余震が多いことが分かりやすいように、求めたい震源の数を任意に指定できるように改良した。表示マップの種類を変えたり、マップを含めてツイートすることができる。
また、Windowsアプリを「C++ で直近の地震情報を取得する」で公開している。あわせてご試用いただきたい。
(2024年12月8日)Bluesky投稿機能を追加
(2024年6月22日)TwitterOAuth 7.0.0 対応,Twitter(現・X)ボタンを "X" に変更