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

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

(2026年1月11日) PHP8.5対応:double→float
(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.8.0 2026/01/11 PHP8.5対応:double→float
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を追加
pahooGeoCode.php 更新履歴
バージョン 更新日 内容
6.9.1 2025/11/25 PHP8.5対応:double→float
6.9.0 2025/09/21 jsPolygon, jsPolygon_Gmap, jsPolygon_Leaflet, loadGeoJSON, getPrefBorderList を追加
6.8.0 2025/08/10 アクセスキーなどを ".pahooEnd" に分離
6.7.1 2025/07/26 jsLine_Gmap() - bug-fix
6.7.0 2025/07/20 drawJSmap,drawGMap -- 引数 $markerLevel 追加
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

  41: class pahooGeoCode {
  42:     public $items;      // 検索結果格納用
  43:     public $error;      // エラー・フラグ
  44:     public $errmsg;     // エラー・メッセージ
  45:     public $hits;       // 検索ヒット件数
  46:     public $webapi; // 直前に呼び出したWebAPI URL
  47: 
  48:     // 都道府県境界線データ
  49:     // SimpleMaps.com is a product of Pareto Software, LLC. © 2010-2025
  50:     // https://simplemaps.com/gis/country/jp
  51:     // ※各自の環境に合わせて設定すること
  52:     public $GeoJsonJP = __DIR__ . '/jp.json';
  53: 
  54:     // -- 以下のデータは .env ファイルに記述可能
  55:     // Google Cloud Platform APIキー
  56:     // https://cloud.google.com/maps-platform/
  57:     // ※Google Maps APIを利用しないのなら登録不要
  58:     public $GOOGLE_API_KEY_1 = '';      // HTTPリファラ用
  59:     public $GOOGLE_API_KEY_2 = '';      // IP制限用
  60:     public $GOOGLE_MAP_ID    = '';      // GoogleMaps ID
  61: 
  62:     // Yahoo! JAPAN Webサービス アプリケーションID
  63:     // https://e.developer.yahoo.co.jp/register
  64:     // ※Yahoo! JAPAN Webサービスを利用しないのなら登録不要
  65:     public $YAHOO_APPLICATION_ID = '';
  66: 
  67:     // OSM Nominatim Search API利用時に知らせるメールアドレス
  68:     // https://wiki.openstreetmap.org/wiki/JA:Nominatim#.E6.A4.9C.E7.B4.A2
  69:     // ※OSM Nominatim Search APIを利用しないのなら登録不要
  70:     public $NOMINATIM_EMAIL = '';
  71: 
  72:     // IP2Location.io APIキー
  73:     // https://www.ip2location.io/
  74:     // ※IP2Location.ioを利用しないのなら登録不要
  75:     public $IP2LOCATION_API_KEY = '';

Googleマップや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   float $lat 緯度(世界測地系)
 231:  * @param   float $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   float $latitude  緯度(世界測地系)
 243:  * @param   float $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

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

メソッド 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

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

メソッド 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

2585: /**
2586:  * 地図サービスを利用してJavaScriptマップを描く
2587:  * @param   string $id        マップID
2588:  * @param   float  $latitude  中心座標:緯度(世界測地系)
2589:  * @param   float  $longitude 中心座標:経度(世界測地系)
2590:  * @param   string $type      マップタイプ
2591:  *                              Googleの場合 HYBRID/ROADMAP/SATELLITE/TERRAIN
2592:  *                              Yahoo!JAPANの場合 NORMAL/PHOTO/B1/OSM
2593:  * @param   int    $zoom      拡大率
2594:  * @param   string $call      イベント発生時にコールする関数(省略可)
2595:  * @param   array  $items     地点情報(省略可能)
2596:  *                  string title        タイトル(Yahoo!では無効)
2597:  *                  string description  情報ウィンドウに表示する内容(HTML文)
2598:  *                  float  latitude     緯度
2599:  *                  float  longitude    経度
2600:  *                  string icon         アイコンURL
2601:  *                  string label        アイコン・ラベル(省略可能)
2602:  *                  string label_size   アイコン・ラベルのサイズ(省略可能)
2603:  *                  string label_weight アイコン・ラベルの太さ(省略可能)
2604:  *                  string label_color  アイコン・ラベルの色(省略可能)
2605:  * @param   string $api   0:Google Maps JavaScript(省略時)
2606:  *                        2:地理院地図・OSM(Leaflet使用)
2607:  * @param   string $call2 追加スクリプト(省略可)
2608:  * @param   int    $max_width 情報ウィンドウの最大幅(省略時:200)
2609:  * @param   array  $offset    アイコンから情報ウィンドウのオフセット位置(省略時:0,0)
2610:  * @param   array  $overlays  Leaflet用オーバーレイ
2611:  * @param   string $centerMarker マップ中心マーカーURL(省略可能)
2612:  * @param   string $markerLevel  GoogleMaps用マーカーの種類(0:Maker, 1:AdvancedMarker;省略可能)
2613:  * @return  string JavaScriptマップのコード
2614: */
2615: 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) {
2616:     // マップタイプの読み替え
2617:     static $tbl1 = array('HYBRID'=>'PHOTO', 'ROADMAP'=>'NORMAL', 'SATELLITE'=>'PHOTO', 'TERRAIN'=>'PHOTO');
2618:     static $tbl2 = array('NORMAL'=>'ROADMAP', 'PHOTO'=>'SATELLITE', 'B1'=>'ROADMAP', 'OSM'=>'ROADMAP');
2619:     static $tbl3 = array('HYBRID'=>'GSISTD', 'ROADMAP'=>'OSM', 'SATELLITE'=>'GSIPHOTO', 'TERRAIN'=>'GSIPHOTO');
2620:     $type = strtoupper($type);
2621: 
2622:     switch ($api) {
2623:     // Google Maps JavaScript
2624:     case 0:
2625:         $type = isset($tbl2[$type]) ? $tbl2[$type: $type;
2626:         $js = $this->drawGMap($id, $latitude, $longitude, $type, $zoom, $call, $items, $call2, $max_width, $offset, $centerMarker, $markerLevel);
2627:         break;
2628:     // 地理院地図・OSM(Leaflet使用)
2629:     case 2:
2630:         $type = isset($tbl3[$type]) ? $tbl3[$type: $type;
2631:         $js = $this->drawLeaflet($id, $latitude, $longitude, $type, $zoom, $call, $items, $call2, $max_width, $offset, $overlays, $centerMarker);
2632:         break;
2633:     }
2634: 
2635:     return $js;
2636: }

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

その他の WebAPI

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

活用例

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

参考サイト

(この項おわり)
header