PHPで住所・ランドマークから最寄り駅を求める

(1/1)
HeartRails Geo API」は、路線/駅名データ等の地理情報を無償のWebAPIとして提供している。
これと、Googleや地理院地図、オープンストリートマップの地図サービスをクラウド連携することで、住所やランドマークから最寄り駅を求めるPHPプログラムを作ってみることにする。
なお、Yahoo! JavaScriptマップは、2020年(令和2年)10月31日をもってサービスを終了しており利用できない。

(2025年8月24日).pahooEnv 導入
(2025年7月20日)マップ中央にマーカーを置くことができるようにした.
(2025年6月14日)GoogleMaps JavaScript APIの変更に対応した.

目次

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

PHPで住所・ランドマークから最寄り駅を求める
Googleマップ表示

サンプル・プログラム

圧縮ファイルの内容
stationsearch.phpサンプル・プログラム本体。
pahooGeoCode.php住所・緯度・経度に関わるクラス pahooGeoCode。
使い方は「PHPで住所・ランドマークから最寄り駅を求める」「PHPで住所・ランドマークから緯度・経度を求める」などを参照。include_path が通ったディレクトリに配置すること。
pahooInputData.phpデータ入力に関わる関数群。
使い方は「数値入力とバリデーション」「文字入力とバリデーション」などを参照。include_path が通ったディレクトリに配置すること。
stationsearch.php 更新履歴
バージョン 更新日 内容
4.7.0 2025/08/24 .pahooEnv導入
4.6.0 2025/07/19 マップ中心マーカー表示オプションを追加
4.5.0 2023/07/14 検索キーの最小・最大長の指定
4.4.0 2023/07/02 国土地理院ジオコーディングAPIを追加
4.3 2021/02/20 PHP8対応,Yahoo! JavaScriptマップ廃止
pahooGeoCode.php 更新履歴
バージョン 更新日 内容
6.8.0 2025/08/10 アクセスキーなどを ".env" に分離
6.7.1 2025/07/26 jsLine_Gmap() - bug-fix
6.7.0 2025/07/20 drawJSmap,drawGMap -- 引数 $markerLevel 追加
6.6.0 2025/07/19 drawJSmap,drawGMap,drawLeaflet -- マップ中心マーカー表示引数を追加
6.5.0 2025/06/14 GoogleMaps JavaScript APIの変更に対応
pahooInputData.php 更新履歴
バージョン 更新日 内容
2.0.1 2025/08/11 getParam() bug-fix
2.0.0 2025/08/11 pahooLoadEnv() 追加
1.9.0 2025/07/26 getParam() 引数に$trim追加
1.8.1 2025/03/15 validRegexPattern() debug
1.8.0 2024/11/12 validRegexPattern() 追加
サンプル・プログラムは2つの機能を持つ。
1つは、そのまま実行すると、検索キーワードを入力する画面を表示する機能――[検索]ボタンを押下することで、検索結果を表示する。
もう1つは、前述の変数を GET 渡しで呼び出すことで、検索結果のみを表示する機能である。たとえば「東京都千代田区皇居外苑1-1」の最寄り駅を検索する場合は、URL で 'stationsearch.php?query=%93%8C%8B%9E%93s%90%E7%91%E3%93c%8B%E6%8Dc%8B%8F%8AO%89%91%82P%81%7C%82P' と指定してやればよい。

サンプル・プログラムの流れ

PHPで住所・ランドマークから最寄り駅を求める

準備:PHP の https対応

クラウド連携や相手先サイトのデータを読み込むのに https通信を使うため、PHPに OpenSSLモジュールが組み込まれている必要がある。関数  phpinfo  を使って、下図のように表示されればOKだ。
OpenSSL - PHP
そうでない場合は、次の手順に従ってOpenSSLを有効化し、PHPを再起動させる必要がある。

Windowsでは、"php.ini" の下記の行を有効化する。
extension=php_openssl.dll
Linuxでは --with-openssl=/usr オプションを付けて再ビルドする。→OpenSSLインストール手順

これで準備は完了だ。

準備:pahooGeoCode クラス

pahooGeoCode.php

  40: class pahooGeoCode {
  41:     public $items;      // 検索結果格納用
  42:     public $error;      // エラー・フラグ
  43:     public $errmsg;     // エラー・メッセージ
  44:     public $hits;       // 検索ヒット件数
  45:     public $webapi// 直前に呼び出したWebAPI URL
  46: 
  47:     // -- 以下のデータは .env ファイルに記述可能
  48:     // Google Cloud Platform APIキー
  49:     // https://cloud.google.com/maps-platform/
  50:     // ※Google Maps APIを利用しないのなら登録不要
  51:     public $GOOGLE_API_KEY_1 = '';      // HTTPリファラ用
  52:     public $GOOGLE_API_KEY_2 = '';      // IP制限用
  53:     public $GOOGLE_MAP_ID    = '';      // GoogleMaps ID
  54: 
  55:     // Yahoo! JAPAN Webサービス アプリケーションID
  56:     // https://e.developer.yahoo.co.jp/register
  57:     // ※Yahoo! JAPAN Webサービスを利用しないのなら登録不要
  58:     public $YAHOO_APPLICATION_ID = '';
  59: 
  60:     // OSM Nominatim Search API利用時に知らせるメールアドレス
  61:     // https://wiki.openstreetmap.org/wiki/JA:Nominatim#.E6.A4.9C.E7.B4.A2
  62:     // ※OSM Nominatim Search APIを利用しないのなら登録不要
  63:     public $NOMINATIM_EMAIL = '';
  64: 
  65:     // IP2Location.io APIキー
  66:     // https://www.ip2location.io/
  67:     // ※IP2Location.ioを利用しないのなら登録不要
  68:     public $IP2LOCATION_API_KEY = '';

GoogleマップやLeafletなどによる地図描画や住所検索を行うためのクラスが pahooGeoCode である。同梱のクラス・ファイル "pahooGeoCode.php" は include_path が通ったディレクトリに配置してほしい。他のプログラムでも pahooGeoCodeクラス を利用するが、常に最新のクラス・ファイルを1つ配置すればよい。

地図や住所検索として Google を利用するのであれば Google Cloud Platform APIキーマップID が必要で、その入手方法は「Google Cloud Platform - WebAPIの登録方法」を、Yahoo!JAPAN を利用するのであれば Yahoo! JAPAN Webサービス アプリケーションIDが必要で、その入手方法は「Yahoo!JAPAN デベロッパーネットワーク - WebAPIの登録方法」を、IP2Location.ioを利用するのであれば「PHPでIPアドレスやホスト名から住所を求める」を、それぞれ参照されたい。

PHPのクラスについては「PHPでクラスを使ってテキストの読みやすさを調べる」を参照されたい。

準備:pahooInputData 関数群

PHPのバージョンや入力データのバリデーションなど、汎用的に使う関数群を収めたファイル "pahooInputData.php" が同梱されているが、include_path が通ったディレクトリに配置してほしい。他のプログラムでも "pahooInputData.php" を利用するが、常に最新のファイルを1つ配置すればよい。

また、各種クラウドサービスに登録したときに取得するアカウント情報、アプリケーションパスワードなどを登録した .pahooEnv ファイルから読み込む関数 pahooLoadEnv を備えている。こちらについては、「各種クラウド連携サービス(WebAPI)の登録方法」をご覧いただきたい。

準備:各種定数など

stationsearch.php

  56: // 各種定数(START) ===========================================================
  57: 
  58: // 地図描画サービスの選択
  59: //    0:Google
  60: //    2:地理院地図・OSM
  61: define('MAPSERVICE', 2);
  62: 
  63: // 住所検索サービスの選択
  64: //    0:Google
  65: //    1:Yahoo!ジオコーダAPI
  66: //   11:HeartRails Geo API
  67: //   12:OSM Nominatim Search API
  68: //   13:国土地理院ジオコーディングAPI
  69: define('GEOSERVICE', 1);
  70: 
  71: // 逆ジオコーディングサービスの選択
  72: //    0:Google
  73: //    1:Yahoo!JAPAN
  74: //   11:HeartRails Geo API
  75: //   21:簡易ジオコーディングサービス
  76: define('REVGEOSERVICE', 1);
  77: 
  78: // マップの表示サイズ(単位:ピクセル)
  79: define('MAP_WIDTH',  600);
  80: define('MAP_HEIGHT', 400);
  81: // マップID
  82: define('MAPID', 'map_id');
  83: // マップ中心マーカーのURL;表示したくなければNULLにする
  84: define('CENTER_MARKER', 'https://maps.google.com/mapfiles/ms/micons/ltblu-pushpin.png');
  85: 
  86: // 初期値
  87: define('DEF_LATITUDE',  35.7);          // 緯度
  88: define('DEF_LONGITUDE', 139.7);         // 経度
  89: define('DEF_TYPE',      'roadmap');     // マップタイプ
  90: define('DEF_ZOOM',      13);            // ズーム
  91: define('DEF_CATEGORY', 'address');      // カテゴリ
  92: 
  93: // 検索キーの最小文字長
  94: define('QUERY_MIN_LEN', 3);
  95: 
  96: // 検索キーの最大文字長
  97: define('QUERY_MAX_LEN', 99);
  98: 
  99: // 各種定数(END) ===============================================================

各種パラメータは定数を defineしている。とくに記載のないものは、適宜変更してかまわない。

表示する地図は、Googleマップ地理院地図・オープンストリートマップ(OSM)から選べる。あらかじめ、定数 MAPSERVIC に値を設定すること。
住所検索サービスは、GoogleYahoo!JAPANHeartRails Geo APIから選べる。あらかじめ、定数 GEOSERVICE に値を設定すること。
逆ジオコーディングサービスは、GoogleYahoo!JAPANHeartRails Geo API簡易ジオコーディングサービスから選べる。あらかじめ、定数 REVGEOSERVICE に値を設定すること。
PHPで住所・ランドマークから最寄り駅を求める
地理院地図(淡色)
PHPで住所・ランドマークから最寄り駅を求める
OpenStreetMap

準備:その他の初期値

stationsearch.php

  78: // マップの表示サイズ(単位:ピクセル)
  79: define('MAP_WIDTH',  600);
  80: define('MAP_HEIGHT', 400);
  81: // マップID
  82: define('MAPID', 'map_id');
  83: // マップ中心マーカーのURL;表示したくなければNULLにする
  84: define('CENTER_MARKER', 'https://maps.google.com/mapfiles/ms/micons/ltblu-pushpin.png');
  85: 
  86: // 初期値
  87: define('DEF_LATITUDE',  35.7);          // 緯度
  88: define('DEF_LONGITUDE', 139.7);         // 経度
  89: define('DEF_TYPE',      'roadmap');     // マップタイプ
  90: define('DEF_ZOOM',      13);            // ズーム
  91: define('DEF_CATEGORY', 'address');      // カテゴリ
  92: 
  93: // 検索キーの最小文字長
  94: define('QUERY_MIN_LEN', 3);
  95: 
  96: // 検索キーの最大文字長
  97: define('QUERY_MAX_LEN', 99);

その他の初期値は、とくに断り書きがない場合に限り自由に変更することができる。

HeartRails Geo API 最寄駅検索

HeartRails Geo API」は、2015年(平成27年)9月現在、以下のAPIを無償公開している。
これらの API は、入力パラメータ(IN)は GET 渡しで、出力結果(OUT)は XML で戻るという形である。
  • エリア情報取得 API
  • 都道府県情報取得 API
  • 市区町村情報取得 API
  • 町域情報取得 API
  • 最寄駅情報取得 API
  • 郵便番号による住所検索 API
  • 緯度経度による住所検索 API
  • キーワードによる住所検索 API
  • 「エリア名」 「市区町村名」 「町域名」 の連結コンボボックス
  • 「都道府県名」 「市区町村名」 「町域名」 の連結コンボボックス
  • 「郵便番号」 による住所検索フォーム
WebAPIのURL
URL
https://express.heartrails.com/api/xml

入力パラメータ
フィールド名 要否 内  容
method 必須 メソッド名:getStation(固定)
x 必須 最寄り駅を取得したい場所の経度(世界測地系)。
y 必須 最寄り駅を取得したい場所の緯度(世界測地系)。
応答データ(xml) response station name 駅名 line 路線名 distance 検索地点からの距離 x 経度 y 緯度 prefecture 都道府県 postal 郵便番号 next 次の駅 prev 前の駅

解説:HeartRails Geo APIの呼び出し

stationsearch.php

 228: /**
 229:  * HeartRails Express のURLを取得する
 230:  * @param   double $lat 緯度(世界測地系)
 231:  * @param   double $lng 経度(世界測地系)
 232:  * @return  string URL
 233: */
 234: function getURL_Heartrails($lat, $lng) {
 235:     $res = "http://express.heartrails.com/api/xml?method=getStations&x={$lng}&y={$lat}";
 236: 
 237:     return $res;
 238: }

stationsearch.php

 240: /**
 241:  * HeartRails Express API から必要な情報を配列に格納する
 242:  * @param   double $latitude  緯度(世界測地系)
 243:  * @param   double $longitude 経度(世界測地系)
 244:  * @return  array(ヒットした施設数, メッセージ, APIのURL)
 245:  * @return  int ヒット数
 246: */
 247: function getResults_Heartrails($latitude, $longitude, &$items) {
 248: // 受信データの要素名
 249: $tbl = array(
 250:     'name',         // 最寄駅名
 251:     'prev',         // 前の駅名 (始発駅の場合は null)
 252:     'next',         // 次の駅名 (終着駅の場合は null)
 253:     'x',            // 最寄駅の経度 (世界測地系)
 254:     'y',            // 最寄駅の緯度 (世界測地系)
 255:     'distance', // 指定の場所から最寄駅までの距離 (精度は 10 m)
 256:     'postal',       // 最寄駅の郵便番号 
 257:     'prefecture',   // 最寄駅の存在する都道府県名
 258:     'line'          // 最寄駅の存在する路線名
 259: );
 260: 
 261:     $url = $this->getURL_Heartrails($latitude, $longitude); // リクエストURL
 262:     $cnt = 1;
 263: 
 264: // PHP4用; DOM XML利用
 265:     if (! $this->isphp5over()) {
 266:         if (($dom = $this->read_xml($url)) == NULL) {
 267:             return array(FALSE, 'WebAPIのトラブルです.', FALSE);
 268:         }
 269:         $resultset = $dom->get_elements_by_tagname('response');
 270:         $results = $resultset[0]->get_elements_by_tagname('station');
 271:         // 検索結果取りだし
 272:         foreach ($results as $element) {
 273:             foreach ($tbl as $name) {
 274:                 $node = $element->get_elements_by_tagname($name);
 275:                 if ($node !NULL) {
 276:                     $items[$cnt][$name] = (string)$node[0]->get_content();
 277:                 }
 278:             }
 279:             $items[$cnt]['id']        = $this->num2alpha($cnt);
 280:             $items[$cnt]['title']     = $items[$cnt]['name'];
 281:             $items[$cnt]['longitude'] = $items[$cnt]['x'];
 282:             $items[$cnt]['latitude']  = $items[$cnt]['y'];
 283:             $items[$cnt]['description']  =<<< EOT
 284: {$items[$cnt]['name']}&nbsp;({$items[$cnt]['line']}){$items[$cnt]['distance']}
 285: EOT;
 286:             $cnt++;
 287:         }
 288: 
 289: // PHP5用; SimpleXML利用
 290:     } else {
 291:         $response = simplexml_load_file($url);
 292:         // レスポンス・チェック
 293:         if (isset($response->station) == FALSE) {
 294:             return array(FALSE, 'WebAPIのトラブルです.', FALSE);
 295:         }
 296:         // 検索結果取りだし
 297:         foreach ($response->station as $element) {
 298:             foreach ($tbl as $name) {
 299:                 if (isset($element->$name)) {
 300:                     $items[$cnt][$name] = (string)$element->$name;
 301:                 }
 302:             }
 303:             $items[$cnt]['id']        = $this->num2alpha($cnt);
 304:             $items[$cnt]['title']     = $items[$cnt]['name'];
 305:             $items[$cnt]['longitude'] = $items[$cnt]['x'];
 306:             $items[$cnt]['latitude']  = $items[$cnt]['y'];
 307:             $items[$cnt]['description']  =<<< EOT
 308: {$items[$cnt]['name']}&nbsp;({$items[$cnt]['line']}){$items[$cnt]['distance']}
 309: EOT;
 310:             $cnt++;
 311:         }
 312:     }
 313: 
 314:     return array($cnt - 1, '', $url);
 315: }

HeartRails Geo 最寄駅情報取得 APIの使い方は、前回と同じである。応答メッセージの処理は、PHP4では DOM XML を、PHP5以降では SimpleXML を使っている。

Googleマップ描画

Google Maps JavaScript API は、JavaScriptを使ってGoogleマップを動的に描画するためのWebAPIである。

pahooGeoCode.php

 888: /**
 889:  * Googleマップを描く
 890:  * @param   string $id        マップID
 891:  * @param   float  $latitude  中心座標:緯度(世界測地系)
 892:  * @param   float  $longitude 中心座標:経度(世界測地系)
 893:  * @param   string $type      マップタイプ:HYBRID/ROADMAP/SATELLITE/TERRAIN
 894:  * @param   int    $zoom      拡大率
 895:  * @param   string $call      イベント発生時にコールする関数(省略可)
 896:  * @param   array  $items     地点情報(省略可能)
 897:  *                  string title       タイトル
 898:  *                  string description 情報ウィンドウに表示する内容(HTML文)
 899:  *                  float latitude    緯度
 900:  *                  float longitude   経度
 901:  *                  string icon        アイコンURL
 902:  * @param   string $call2     追加スクリプト(省略可)
 903:  * @param   int    $max_width 情報ウィンドウの最大幅(省略時:200)
 904:  * @param   array  $offset    アイコンから情報ウィンドウのオフセット位置(省略時:0,0)
 905:  * @param   string $centerMarker マップ中心マーカーURL(省略可能)
 906:  * @param   string $markerLevel  マーカーの種類(0:Maker, 1:AdvancedMarker;省略可能))
 907:  * @return  string Googleマップのコード
 908: */
 909: function drawGMap($id, $latitude, $longitude, $type, $zoom, $call=NULL, $items=NULL, $call2=NULL, $max_width=200, $offset=NULL, $centerMarker=NULL, $markerLevel=1) {
 910:     $key = $this->GOOGLE_API_KEY_1;
 911:     $call = ($call !NULL? $call . '()' : '';
 912:     if (! is_array($offset)) {
 913:         $offset = array(0, 0);
 914:     }
 915: 
 916:     // マップ中心マーカー
 917:     $jsCenterMarker = '';
 918:     if ($centerMarker !== NULL) {
 919:         $size = getimagesize($centerMarker);
 920:         $jsCenterMarker .=<<< EOT
 921: const markerCenter = new google.maps.marker.AdvancedMarkerElement({
 922:     position: { lat: {$latitude}, lng: {$longitude} },
 923:     map: map,
 924:     content: (() => {
 925:         const icon = document.createElement('img');
 926:         icon.src = '{$centerMarker}';
 927:         icon.style.width  = '{$size[0]}px';
 928:         icon.style.height = '{$size[1]}px';
 929:         icon.style.transform = 'translate(0%, 0%)';
 930:         return icon;
 931:     })(),
 932:     zIndex: 10
 933: });
 934: 
 935: EOT;
 936:     }
 937: 
 938:     $mapId  = $this->GOOGLE_MAP_ID;
 939:     $code =<<< EOT
 940: <script src="https://maps.googleapis.com/maps/api/js?key={$key}&loading=async&libraries=marker&callback=initMap&region=JP" async defer loading="async"></script>
 941: <script>
 942: function initMap() {
 943:     let map = new google.maps.Map(document.getElementById('{$id}'), {
 944:         center: { lat: {$latitude}, lng: {$longitude} },
 945:         mapId: '{$mapId}',
 946:         zoom: {$zoom},
 947:         mapTypeId: google.maps.MapTypeId.{$type},
 948:         mapTypeControl: true,
 949:         scaleControl: true
 950:     });
 951: 
 952:     map.addListener('dragend', getPointData);
 953:     map.addListener('zoom_changed', getPointData);
 954:     map.addListener('maptypeid_changed', getPointData);
 955: 
 956:     // イベント発生時の地図情報を取得・格納
 957:     function getPointData() {
 958:         const point = map.getCenter();
 959:         // 経度
 960:         if (document.getElementById("longitude") != null) {
 961:             document.getElementById("longitude").value = point.lng();
 962:         }
 963:         // 緯度
 964:         if (document.getElementById("latitude") != null) {
 965:             document.getElementById("latitude").value = point.lat();
 966:         }
 967:         // ズーム
 968:         if (document.getElementById("zoom") != null) {
 969:             document.getElementById("zoom").value = map.getZoom();
 970:         }
 971:         // 地図タイプ
 972:         if (document.getElementById("type") != null) {
 973:             const type_g = map.getMapTypeId();
 974:             const types = {"roadmap":"地図", "satellite":"航空写真", "hybrid":"ハイブリッド", "terrain":"地形図" };
 975:             for (key in types) {
 976:                 if (key == type_g) {
 977:                     document.getElementById("type").value = key;
 978:                     break;
 979:                 }
 980:             }
 981:         }
 982:         {$call}
 983:     }
 984:     {$jsCenterMarker}
 985: 
 986: EOT;
 987:     // 地点情報
 988:     if ($items !NULL) {
 989:         // AdvancedMarker
 990:         if ($markerLevel > 0) {
 991:             foreach ($items as $i=>$item) {
 992:                 if ($i > 999)   break;      // 最大999箇所まで
 993:                 $mark = (string)sprintf('%03d', $i);
 994:                 // アイコン
 995:                 $mark2 = ($i <26? $this->num2alpha($i: 'Z';
 996:                 $icon = isset($item['icon']) ? $item['icon': 
 997:                         "https://www.google.com/mapfiles/marker{$mark2}.png";
 998:                 list($icon_width, $icon_height) = getimagesize($icon);
 999:                 if (isset($item['label']) && ($item['label'!'')) {
1000:                     $size1 = $item['label_size'* mb_strlen($item['label']);
1001:                     $size2 = (int)($size1 / 2);
1002:                     $ss =<<< EOT
1003:     const icon = document.createElement('div');
1004:     icon.innerHTML = `
1005:         <div style="position: relative; text-align: center;">
1006:         <img src="https://www.pahoo.org/common/space.gif" style="width:{$size1}px; height:{$size1}px;">
1007:         <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']};">
1008:         {$item['label']}
1009:     </div>
1010:   </div>
1011: `;
1012: 
1013: EOT;
1014:                 } else  {
1015:                     $ss =<<< EOT
1016:     const icon = document.createElement('img');
1017:     icon.src = '{$icon}';
1018:     icon.style.width  = '{$icon_width}px';
1019:     icon.style.height = '{$icon_height}px';
1020:     icon.style.transform = 'translate(0%, 0%)';
1021: 
1022: EOT;
1023:                 }
1024:                 $code .=<<< EOT
1025: const marker_{$mark} = new google.maps.marker.AdvancedMarkerElement({
1026:     position: { lat: {$item['latitude']}, lng: {$item['longitude']} },
1027:     map: map,
1028:     content: (() => {
1029: {$ss}
1030:         return icon;
1031:     })(),
1032:     title: '{$item['title']}',
1033:     zIndex: 100,
1034: });
1035: 
1036: EOT;
1037:                 if (isset($item['description'])) {
1038:                     $code .=<<< EOT
1039: const infowindow_{$mark} = new google.maps.InfoWindow({
1040:     content: '{$item['description']}',
1041:     maxWidth: {$max_width},
1042:     pixelOffset: new google.maps.Size({$offset[0]}, {$offset[1]})
1043: });
1044: marker_{$mark}.addListener('gmp-click', function() {
1045:     infowindow_{$mark}.open(map, marker_{$mark});
1046: });
1047: 
1048: EOT;
1049:                 }
1050:             }
1051: 
1052:         // Marker
1053:         } else {
1054:             foreach ($items as $i=>$item) {
1055:                 if ($i > 999)   break;      // 最大999箇所まで
1056:                 $mark = (string)sprintf('%03d', $i);
1057:                 // アイコン
1058:                 $mark2 = ($i <26? $this->num2alpha($i: 'Z';
1059:                 $icon = isset($item['icon']) ? $item['icon':
1060:                         "https://www.google.com/mapfiles/marker{$mark2}.png";
1061:                 list($icon_width, $icon_height) = getimagesize($icon);
1062:                 if (isset($item['label']) && ($item['label'!'')) {
1063:                     $ss =<<< EOT
1064:     icon: {
1065:         url: 'https://www.pahoo.org/common/space.gif'
1066:     },
1067:     label: {
1068:         text: '{$item['label']}',
1069:         color: '{$item['label_color']}',
1070:         fontSize: '{$item['label_size']}px',
1071:         fontWeight: '{$item['label_weight']}'
1072:     }
1073: 
1074: EOT;
1075:                 } else {
1076:                     $ss =<<< EOT
1077:     icon: {
1078:         url: '{$icon}',
1079:         size: new google.maps.Size({$icon_width}, {$icon_height}),
1080:         origin: new google.maps.Point(0, 0),
1081:         anchor: new google.maps.Point({$icon_width} / 2, {$icon_height})
1082:     }
1083: 
1084: EOT;
1085:                 }
1086:                 $code .=<<< EOT
1087: const marker_{$mark} = new google.maps.Marker({
1088:     position: new google.maps.LatLng({$item['latitude']}, {$item['longitude']}),
1089:     map: map,
1090:     {$ss},
1091:     title: '{$item['title']}',
1092:     zIndex: 100
1093: });
1094: 
1095: EOT;
1096:                 if (isset($item['description'])) {
1097:                     $code .=<<< EOT
1098: const infowindow_{$mark} = new google.maps.InfoWindow({
1099:     content: '{$item['description']}',
1100:     maxWidth: {$max_width},
1101:     pixelOffset: new google.maps.Size({$offset[0]}, {$offset[1]})
1102: });
1103: marker_{$mark}.addListener('click', function() {
1104:     infowindow_{$mark}.open(map, marker_{$mark});
1105: });
1106: 
1107: EOT;
1108:                 }
1109:             }
1110:         }
1111:     }
1112: 
1113:     // 追加関数
1114:     if ($call2 !NULL) {
1115:         $code .=<<< EOT
1116: {$call2}
1117: 
1118: EOT;
1119:     }
1120:     $code .=<<< EOT
1121: }
1122: 
1123: </script>
1124: 
1125: EOT;
1126: 
1127:     return $code;
1128: }

メソッド drawGMap は、Googleマップを描画するためのJavaScriptを生成する。
まずソースとして、"https://maps.googleapis.com/maps/api/js" を読み込む。このとき、下表に示すパラメータを渡してやる必要がある。
GoogleMaps JavaScript API パラメータ
パラメータ 意味
key Google Cloud Platform APIキー  
loading async 非同期呼び出しを行う。
libraries marker 高度なマーカーを利用する。
callback initMap コールバック関数名
region JP 日本の地図を表示する。
また、非同期呼び出しのために sync defer loading="async" を指定する。
引数 $centerMarker の値が NULL でなければ、マップ中央に $centerMarker で指定したマーカーを配置する。このプログラムでは、「準備:その他の初期値」にある定数 CENTER_MARKER にマーカー画像の URL を用意している。

JavaScript のコールバック関数 initMap の中で、google.maps.Map インスタンスを生成する。このとき、後述する新しいマーカーを使うために、mapId要素にマップIDをセットする。

マップのドラッグ、ズーム変更、マップタイプ変更のイベントを拾って(addListner)、getPointDate() 関数を呼び出す。
このユーザー関数内で、緯度・経度、ズーム値、マップタイプを、IDで示されるオブジェクトに格納する。hidden属性のテキストボックスに格納することを想定している。
これにより、ページ切替が起きても地図の状態を保持できる。
また、本メソッドに JavaScriptを引数 $call として渡してやれば、getPointDate() 関数内で追加で呼び出す。

引数 $items を渡してやれば、地図上にマーキングする。
$items は2次元配列で、1次元目は地点番号(1以上)、2次元目は情報の種類である。たとえば $items[3]['description'] には、地点番号3の情報ウィンドウに表示する内容HTMLを代入する。
$items には、マッピングするマーカーURLも指定可能である。指定しない場合は、Googleマップの標準的なアルファベットマーカーによってマーキングする。

GoogleMapsの新しいマーカー(AdvancedMarkerElement)では、マーカーを DOMオブジェクトとして扱うことができる。
$items[3]['label'] が存在するときには、そこに代入された文字をマーカーにする。すなわち、空白画像(https://www.pahoo.org/common/space.gif)を背景として、div要素を使って文字を配置したオブジェクトを JavaScriptの変数 icon に格納する。
一方、$items[3]['label'] が存在しないときは、img要素を使って、指定されたURLにあるマーカー画像を JavaScriptの変数 icon に格納する。
そして、google.maps.marker.AdvancedMarkerElement のインスタンスを生成する際に、content要素として、いずれかを代入する。ここで、いずれも JavaScriptの文となっているため、そのまま content要素にセットすることができない。そこで、無名関数を用いて、iconの値を returnさせて content要素にセットするようにした。

地理院地図・OSM描画

国土地理院 は、基本測量結果や電子国土基本図、白地図、航空写真などを、各縮尺に応じて、256×256ドットの分割画像である地理院タイルとして、URLを指定して直接参照できるようにしている。

ここでは、このタイル画像を繋げて、Googleマップのようなユーザー・インターフェースを提供する無償のJavaScriptライブラリ Leaflet を利用することにする。

あわせて、自由に利用できる世界地図作成プロジェクト「OpenStreetMap」(OSM)が提供する地図タイルも利用できるようにした。

pahooGeoCode.php

1865: /**
1866:  * Leafletによるマップ描画
1867:  * @param   string $id        マップID
1868:  * @param   float  $latitude  中心座標:緯度(世界測地系)
1869:  * @param   float  $longitude 中心座標:経度(世界測地系)
1870:  * @param   string $type      マップタイプ:GSISTD/GSIPALE/GSIBLANK/GSIPHOTO/OSM
1871:  * @param   int    $zoom      拡大率
1872:  * @param   string $call      イベント発生時にコールする関数(省略可)
1873:  * @param   array  $items     地点情報(省略可能)
1874:  *                  string description 情報ウィンドウに表示する内容(HTML文)
1875:  *                  float  latitude    緯度
1876:  *                  float  longitude   経度
1877:  *                  string icon        アイコンURL
1878:  * @param   string $call2     追加スクリプト(省略可)
1879:  * @param   int    $max_width 情報ウィンドウの最大幅(省略時:200)
1880:  * @param   array  $offset    アイコンから情報ウィンドウのオフセット位置(省略時:NULL)
1881:  * @param   array  $overlays  オーバーレイ:GSIELEV/GSIFAULT/GSIFLOOD
1882:  * @param   string $centerMarker マップ中心マーカーURL(省略可能)
1883:  * @return  string Leafletマップのコード
1884: */
1885: function drawLeaflet($id, $latitude, $longitude, $type, $zoom, $call=NULL, $items=NULL, $call2=NULL, $max_width=200, $offset=NULL, $overlays=NULL, $centerMarker=NULL) {
1886: 
1887:     if (! is_array($offset)) {
1888:         $offset = array(0, 0);
1889:     }
1890:     // デフォルト・オーバーレイ
1891:     $addoverlay = '';
1892:     if ($overlays !NULL) {
1893:         foreach ($overlays as $overlay) {
1894:             $addoverlay .=<<< EOT
1895:     {$overlay}.addTo(map);
1896: 
1897: EOT;
1898:         }
1899:     }
1900: 
1901:     // マップ中心マーカー
1902:     $jsCenterMarker = '';
1903:     if ($centerMarker !== NULL) {
1904:         $size = getimagesize($centerMarker);
1905:         $ofstx = (int)($size[0] / 2);
1906:         $ofsty = (int)($size[1] / 2);
1907:         $jsCenterMarker .=<<< EOT
1908: let iconCenter = new L.icon({
1909:     iconUrl: '{$centerMarker}',
1910:     iconAnchor: [$ofstx, $ofsty]
1911: });
1912: let markerCenter = new L.Marker([{$latitude}, {$longitude}], {icon: iconCenter}).addTo(map);
1913: 
1914: EOT;
1915:     }
1916: 
1917:     // マップ描画コード
1918:     $code =<<< EOT
1919: <link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" />
1920: <script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
1921: <script>
1922: window.onload = function() {
1923:     let map = L.map('{$id}',{zoomControl:false});
1924:     map.setView([{$latitude}, {$longitude}], {$zoom});
1925:     L.control.scale({
1926:         maxWidth: 200,
1927:         position: 'bottomright',
1928:         imperial: false
1929:     }).addTo(map);
1930:     L.control.zoom({position:'topleft'}).addTo(map);
1931: 
1932:     // 地理院地図:標準地図
1933:     let GSISTD = new L.tileLayer(
1934:         'https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png',
1935:         {
1936:             attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>",
1937:             minZoom: 0,
1938:             maxZoom: 18,
1939:             name: 'GSISTD'
1940:         });
1941:     // 地理院地図:淡色地図
1942:     let GSIPALE = new L.tileLayer(
1943:         'https://cyberjapandata.gsi.go.jp/xyz/pale/{z}/{x}/{y}.png',
1944:         {
1945:             attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>",
1946:             minZoom: 2,
1947:             maxZoom: 18,
1948:             name: 'GSIPALE'
1949:         });
1950:     // 地理院地図:白地図
1951:     let GSIBLANK = new L.tileLayer(
1952:         'https://cyberjapandata.gsi.go.jp/xyz/blank/{z}/{x}/{y}.png',
1953:         {
1954:             attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>",
1955:             minZoom: 5,
1956:             maxZoom: 14,
1957:             name: 'GSIBLANK'
1958:         });
1959:     // 地理院地図:写真
1960:     let GSIPHOTO = new L.tileLayer(
1961:         'https://cyberjapandata.gsi.go.jp/xyz/seamlessphoto/{z}/{x}/{y}.jpg',
1962:         {
1963:             attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>",
1964:             minZoom: 2,
1965:             maxZoom: 18,
1966:             name: 'GSIPHOTO'
1967:         });
1968:     // OpenStreetMap
1969:     let OSM = new L.tileLayer(
1970:         'https://tile.openstreetmap.jp/{z}/{x}/{y}.png',
1971:         {
1972:             attribution: "© <a href='https://osm.org/copyright' target='_blank'>OpenStreetMap</a> contributors",
1973:             minZoom: 0,
1974:             maxZoom: 18,
1975:             name: 'OSM'
1976:         });
1977: 
1978:     // baseMapsオブジェクトにタイル設定
1979:     let baseMaps = {
1980:         "地理院地図" : GSISTD,
1981:         "淡色地図"   : GSIPALE,
1982:         "白地図"     : GSIBLANK,
1983:         "写真地図"   : GSIPHOTO,
1984:         "オープンストリートマップ" : OSM
1985:     };
1986: 
1987:     // 地理院地図:色別標高図(オーバーレイ)
1988:     let GSIELEV = new L.tileLayer(
1989:         'https://cyberjapandata.gsi.go.jp/xyz/relief/{z}/{x}/{y}.png',
1990:         {
1991:             attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>",
1992:             opacity: 0.6,
1993:             minZoom: 5,
1994:             maxZoom: 15,
1995:             name: 'GSIELEV'
1996:         });
1997:     // 地理院地図:活断層図(オーバーレイ)
1998:     let GSIFAULT = new L.tileLayer(
1999:         'https://cyberjapandata.gsi.go.jp/xyz/afm/{z}/{x}/{y}.png',
2000:         {
2001:             attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>",
2002:             opacity: 0.6,
2003:             minZoom: 11,
2004:             maxZoom: 16,
2005:             name: 'GSIFAULT'
2006:         });
2007:     // 地理院地図:治水地形分類図 更新版(オーバーレイ)
2008:     let GSIFLOOD = new L.tileLayer(
2009:         'https://cyberjapandata.gsi.go.jp/xyz/lcmfc2/{z}/{x}/{y}.png',
2010:         {
2011:             attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>",
2012:             opacity: 0.6,
2013:             minZoom: 11,
2014:             maxZoom: 16,
2015:             name: 'GSIFLOOD'
2016:         });
2017: 
2018:     // baseMapsオブジェクトにオーバーレイ設定
2019:     let overlayMaps = {
2020:         "色別標高図"     : GSIELEV,
2021:         "活断層図"       : GSIFAULT,
2022:         "治水地形分類図" : GSIFLOOD,
2023:     };
2024: 
2025:     // layersコントロールにbaseMapsオブジェクトを設定して地図に追加
2026:     L.control.layers(baseMaps, overlayMaps).addTo(map);
2027:     {$type}.addTo(map);
2028: {$addoverlay}
2029: 
2030:     // イベント追加
2031:     map.on('viewreset', getPointData);
2032:     map.on('zoomend', getPointData);
2033:     map.on('baselayerchange', getPointData);
2034:     map.on('overlayadd', getPointData);
2035:     map.on('overlayremove', getPointData);
2036:     map.on('move', getPointData);
2037: 
2038:     // イベント発生時の地図情報を取得・格納
2039:     function getPointData() {
2040:         let pos = map.getCenter();
2041:         // 経度
2042:         if (document.getElementById('longitude') != null) {
2043:             document.getElementById('longitude').value = pos.lng;
2044:         }
2045:         // 緯度
2046:         if (document.getElementById('latitude') != null) {
2047:             document.getElementById('latitude').value = pos.lat;
2048:         }
2049:         // ズーム
2050:         if (document.getElementById('zoom') != null) {
2051:             document.getElementById('zoom').value = map.getZoom();
2052:         }
2053:         // タイプ
2054:         if (document.getElementById('type') != null) {
2055:             for (let k in baseMaps) {
2056:                 if (map.hasLayer(baseMaps[k])) {
2057:                     document.getElementById('type').value = baseMaps[k].options.name;
2058:                 }
2059:             }
2060:         }
2061:         // オーバーレイ
2062:         if (document.getElementById('overlays') != null) {
2063:             let str = '';
2064:             let cnt = 0;
2065:             for (let k in overlayMaps) {
2066:                 if (map.hasLayer(overlayMaps[k])) {
2067:                     if (cnt > 0)    str += ',';
2068:                     str += overlayMaps[k].options.name;
2069:                     cnt++;
2070:                 }
2071:             }
2072:             document.getElementById('overlays').value = str;
2073:         }
2074:         {$call}
2075:     }
2076:     {$jsCenterMarker}
2077: 
2078: EOT;
2079:     // 地点情報
2080:     if ($items !NULL) {
2081:         foreach ($items as $i=>$item) {
2082:             if ($i > 999)   break;      // 最大999箇所まで
2083:             $mark = (string)sprintf('%03d', $i);
2084:             // アイコン
2085:             $mark2 = ($i <26? $this->num2alpha($i: 'Z';
2086:             $icon = isset($item['icon']) ? $item['icon': 
2087:                         "https://www.google.com/mapfiles/marker{$mark2}.png";
2088:             list($icon_width, $icon_height) = getimagesize($icon);
2089:             $offx = round($icon_width / 2);
2090:             $offy = $icon_height;
2091:             $info  = isset($item['description']) ? "marker_{$mark}.bindPopup('{$item['description']}', {maxWidth: {$max_width}, offset: [{$offset[0]}, {$offset[1]}] });" : '';
2092: 
2093:             // アイコン・ラベル
2094:             if (isset($item['label']) && ($item['label'!'')) {
2095:                 $ss =<<< EOT
2096:     let icon_{$mark} = new L.divIcon({
2097:         html: '<span style="color:{$item['label_color']}; font-size:{$item['label_size']}px; font-weight:{$item['label_weight']}; white-space:nowrap;">{$item['label']}</span>',
2098:         iconSize: [0, 0],
2099:         iconAnchor: [{$item['label_size']}, {$item['label_size']}],
2100:     });
2101: 
2102: EOT;
2103:             // 通常アイコン
2104:             } else {
2105:                 $ss =<<< EOT
2106:     let icon_{$mark} = new L.icon({
2107:         iconUrl: '{$icon}',
2108:         iconAnchor: [{$offx}, {$offy}]
2109:     });
2110: EOT;
2111:             }
2112:             $code .=<<< EOT
2113:     {$ss}
2114:     let marker_{$mark} = new L.Marker([{$item['latitude']}, {$item['longitude']}], {icon: icon_{$mark}}).addTo(map);
2115:     {$info}
2116: 
2117: EOT;
2118:         }
2119:     }
2120:     // 追加関数
2121:     if ($call2 !NULL) {
2122:         $code .=<<< EOT
2123: {$call2}
2124: 
2125: EOT;
2126:     }
2127:     $code .=<<< EOT
2128: }
2129: </script>
2130: 
2131: EOT;
2132: 
2133:     return $code;
2134: }

メソッド drawLeaflet は、地理院地図とOSMを描画するためのJavaScriptを生成する。
まずソースとして、UNPKG から Leaflet 本体とスタイルシートを読み込む。

Leaflet のレイヤを切り替えることで、以下の地図が切り替わるようにしてある。
  1. 地理院地図:標準地図
  2. 地理院地図:淡色地図
  3. 地理院地図:白地図
  4. 地理院地図:写真
  5. OpenStreetMap
この他、タイル形式の地図コンテンツがあれば、自由に追加できる。じつはGoogleマップもタイル形式で呼び出すことができるのだが、APIコールしないとGoogleの規約違反となるため、実装してはいけない。

マップのドラッグ、ズーム変更、レイヤ変更のイベントを拾って(on)、getPointDate() 関数を呼び出す。
このユーザー関数内で、緯度・経度、ズーム値を、IDで示されるオブジェクトに格納する。hidden属性のテキストボックスに格納することを想定している。
これにより、ページ切替が起きても地図の状態を保持できる。
また、本メソッドに JavaScriptを引数 $call として渡してやれば、getPointDate() 関数内で追加で呼び出す。

引数 $items を渡してやれば、地図上にマーキングする。
$items は2次元配列で、1次元目は地点番号(1以上)、2次元目は情報の種類である。たとえば $items[3]['description'] には、地点番号3の情報ウィンドウに表示する内容HTMLを代入する。
$items には、マッピングするアイコンURLも指定可能である。指定しない場合は、Googleマップの標準的なアルファベットアイコンによってマーキングする。

動的マップ描画

pahooGeoCode.php

2507: /**
2508:  * 地図サービスを利用してJavaScriptマップを描く
2509:  * @param   string $id        マップID
2510:  * @param   float  $latitude  中心座標:緯度(世界測地系)
2511:  * @param   float  $longitude 中心座標:経度(世界測地系)
2512:  * @param   string $type      マップタイプ
2513:  *                              Googleの場合 HYBRID/ROADMAP/SATELLITE/TERRAIN
2514:  *                              Yahoo!JAPANの場合 NORMAL/PHOTO/B1/OSM
2515:  * @param   int    $zoom      拡大率
2516:  * @param   string $call      イベント発生時にコールする関数(省略可)
2517:  * @param   array  $items     地点情報(省略可能)
2518:  *                  string title        タイトル(Yahoo!では無効)
2519:  *                  string description  情報ウィンドウに表示する内容(HTML文)
2520:  *                  float  latitude     緯度
2521:  *                  float  longitude    経度
2522:  *                  string icon         アイコンURL
2523:  *                  string label        アイコン・ラベル(省略可能)
2524:  *                  string label_size   アイコン・ラベルのサイズ(省略可能)
2525:  *                  string label_weight アイコン・ラベルの太さ(省略可能)
2526:  *                  string label_color  アイコン・ラベルの色(省略可能)
2527:  * @param   string $api   0:Google Maps JavaScript(省略時)
2528:  *                        2:地理院地図・OSM(Leaflet使用)
2529:  * @param   string $call2 追加スクリプト(省略可)
2530:  * @param   int    $max_width 情報ウィンドウの最大幅(省略時:200)
2531:  * @param   array  $offset    アイコンから情報ウィンドウのオフセット位置(省略時:0,0)
2532:  * @param   array  $overlays  Leaflet用オーバーレイ
2533:  * @param   string $centerMarker マップ中心マーカーURL(省略可能)
2534:  * @param   string $markerLevel  GoogleMaps用マーカーの種類(0:Maker, 1:AdvancedMarker;省略可能)
2535:  * @return  string JavaScriptマップのコード
2536: */
2537: function drawJSmap($id, $latitude, $longitude, $type, $zoom, $call=NULL, $items=NULL, $api=0, $call2=NULL, $max_width=200, $offset=NULL, $overlays=NULL, $centerMarker=NULL, $markerLevel=1) {
2538:     // マップタイプの読み替え
2539:     static $tbl1 = array('HYBRID'=>'PHOTO', 'ROADMAP'=>'NORMAL', 'SATELLITE'=>'PHOTO', 'TERRAIN'=>'PHOTO');
2540:     static $tbl2 = array('NORMAL'=>'ROADMAP', 'PHOTO'=>'SATELLITE', 'B1'=>'ROADMAP', 'OSM'=>'ROADMAP');
2541:     static $tbl3 = array('HYBRID'=>'GSISTD', 'ROADMAP'=>'OSM', 'SATELLITE'=>'GSIPHOTO', 'TERRAIN'=>'GSIPHOTO');
2542:     $type = strtoupper($type);
2543: 
2544:     switch ($api) {
2545:     // Google Maps JavaScript
2546:     case 0;
2547:         $type = isset($tbl2[$type]) ? $tbl2[$type: $type;
2548:         $js = $this->drawGMap($id, $latitude, $longitude, $type, $zoom, $call, $items, $call2, $max_width, $offset, $centerMarker, $markerLevel);
2549:         break;
2550:     // 地理院地図・OSM(Leaflet使用)
2551:     case 2:
2552:         $type = isset($tbl3[$type]) ? $tbl3[$type: $type;
2553:         $js = $this->drawLeaflet($id, $latitude, $longitude, $type, $zoom, $call, $items, $call2, $max_width, $offset, $overlays, $centerMarker);
2554:         break;
2555:     }
2556: 
2557:     return $js;
2558: }

メソッド drawJSmap は、引数 $api の値によって、Googleマップ描画メソッド drawGMap、地理院地図・OSM描画メソッド drawLeaflet のいずれかを呼び出す。
マップタイプについては、相互に読み替えができるようにしてある。

その他の WebAPI

pahooGeoCode::searchPoint3 が呼び出すWebAPIについては、「PHPで住所・ランドマークから緯度・経度を求める」を参照のこと。
pahooGeoCode::getAddress3 が呼び出すWebAPIについては、「PHPで緯度・経度から住所を求める」を参照のこと。

活用例

みんなの知識 ちょっと便利帳」では、「地図・住所から「最寄り駅」をグーグルマップで探す」で本プログラムを利用し、検索しやすいページを提供している。ありがとうございます。

参考サイト

(この項おわり)
header