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

サンプル・プログラム
search_station.php | サンプル・プログラム本体。 |
pahooGeoCode.js | 住所・緯度・経度に関わるクラス pahooGeoCode。 使い方は「最寄り駅を求める」を参照。 |
要件定義
- モダンブラウザおよびIEで動作すること。
- 無料で利用できること。
- マップを表示できること。
- 住所を入力し、対応する緯度・経度を検索できること。
- 緯度・経度の近くにある駅を検索できること。
- 検索した駅をマップ上に表示できること。
- 検索した駅を一覧表示できること。
使用するライブラリおよびWebAPI
そこで今回は、国土地理院の地理院地図や、オープンソースの世界地図作成プロジェクト「OpenStreetMap」を利用できる無償のJavaScriptライブラリ「Leaflet」を利用することにする。

住所から緯度・経度を検索を検索することをジオコードと呼ぶが、これを実現する WebAPI は幾つかある。今回は、「YOLPコンテンツジオコーダAPI」、「HeartRails Geo API - キーワードによる住所検索 API」「OSM Nominatim Search API」の3つから選べるようにする。いずれも無料の WebAPI だ。

緯度・経度から駅を検索できる WebAPI として、「HeartRails Geo API - 最寄駅情報取得API」を使うことにする。これも無料の WebAPI だ。

検索結果の最寄り駅は複数あり、Leflet にマーカー表示する。
一覧は、HTMLのTABLEとして表示することにする。
サンプル・プログラムの流れ


メインプログラムでは、まず draw_leaflet メソッドを使ってマップを描く。まだ情報が無いのでマーカーは表示しない。
検索ボタンが押下されたら、入力値(query)があれば、geocode メソッドを使って、対応する緯度・経度を検索する。geocodeメソッド は、引数 api の値に応じて、前述の3つの ジオコードWebAPI のいずれかを呼び出す。
これまで学んできたように、WebAPI 呼び出しは非同期で行われる。検索結果が返ってきたらプロパティpdataに代入し、カスタムイベント geocode を発生させる。
入力値(query)が無い、またはカスタムイベント geocode をキャッチしたら、search_station メソッドを使って、緯度・経度から最寄り駅を検索する。これも非同期で行われるから、検索結果が返ってきたらプロパティpdataに代入し、カスタムイベント station を発生させる。
カスタムイベント station をキャッチしたら、del_marker2leaflet で表示中のマーカーを全て削除し、あらたに marker2leaflet を使ってマップ上に駅の位置を表すマーカーを追加する。そして、一覧表示を行う。
YOLPコンテンツジオコーダAPI
利用料は無料だが、Yahoo!JAPAN ID が必要となる。「各種WebAPIの登録方法 - Yahoo!JAPAN デベロッパーネットワーク」をご覧いただきたい。
取得した APIキー は次のように、pahooGeoCodeオブジェクト生成時に渡すこと。
searchStation.html
36: //グローバル変数:PGC
37: let PGC = new pahooGeoCode('(取得したYahoo!APIキー)');
URL |
---|
https://map.yahooapis.jp/geocode/cont/V1/contentsGeoCoder |
フィールド名 | 要否 | 内 容 |
---|---|---|
appid | 必須 | アプリケーションID |
query | 必須 | 住所やランドマーク |
ei | 任意 | 文字エンコード:UTF-8(デフォルト)/EUC-JP/SJISなど |
category | 任意 | 検索対象カテゴリ:address(デフォルト)/landmark/world |
results | 任意 | 表示件数:最大10(デフォルト) |
output | 任意 | 出力形式:json/xml |
callback | 任意 | JSONPとして出力する際のコールバック関数名を入力するためのパラメータ。 |
pahooGeoCode.js
74: /**
75: * 1:YOLPコンテンツジオコーダAPIを用いて緯度・経度を求める
76: * @param String query 検索キーワード:住所のみ(UTF-8)
77: * @param String category 検索対象カテゴリ
78: * address = 住所(省略時)
79: * landmark = ランドマーク
80: * world = 世界
81: * @return なし(→結果はgeocodeイベントにより取得)
82: */
83: pahooGeoCode.prototype.geocode_YOLP = function (query, category) {
84: //IE用デフォルト引数
85: if (typeof category === 'undefined') category = 'address';
86:
87: //空白除去
88: query = query.trim();
89: //入力文字のエスケープ
90: query = htmlspecialchars(query);
91:
92: //XMLHttpRequestオブジェクト生成
93: let request = new XMLHttpRequest();
94: this.clearError();
95:
96: //WebAPIリクエスト
97: let url = 'https://map.yahooapis.jp/geocode/cont/V1/contentsGeoCoder?appid=' + PGC.yahoo_api_application_id + '&query=' + encodeURI(query) + '&categoty=' + category + '&output=json&callback=__callback_geocode_YOLP';
98: console.log(url);
99:
100: //SOP回避
101: let target = document.createElement('script');
102: target.charset = 'utf-8';
103: target.src = url;
104: target.onerror = function() {
105: errmsg = 'map.yahooapis.jpに接続できません'
106: console.error(errmsg);
107: }
108: document.body.appendChild(target);
109:
110: //JSONP実行関数
111: target = document.createElement('script');
112: target.innerHTML = (
113: function __callback_geocode_YOLP(result) {
114: console.log(result);
115: //応答結果なし
116: if ((typeof result.Feature == 'undefined') || (result.Feature.length == 0) || (typeof result.Feature[0].Geometry.Coordinates == 'undefined')) {
117: errmsg = '検索キーワードが見つかりません'
118: PGC.clearData();
119: PGC.pdata.error = true;
120: PGC.pdata.errmsg = errmsg;
121: console.error(errmsg);
122: } else {
123: let arr = result.Feature[0].Geometry.Coordinates.split(',');
124: PGC.clearData();
125: PGC.pdata.latitude = arr[1];
126: PGC.pdata.longitude = arr[0];
127: }
128: //geocodeイベント
129: let event = new CustomEvent('geocode', {
130: detail: {
131: error: PGC.pdata.error,
132: errmsg: PGC.pdata.errmsg,
133: latitude: PGC.pdata.latitude,
134: longitude: PGC.pdata.longitude
135: }
136: });
137: document.dispatchEvent(event);
138: }
139: );
140: target.onerror = function() {
141: errmsg = 'WebAPIに接続できません'
142: this.clearData();
143: this.pdata.error = true;
144: this.pdata.errmsg = errmsg;
145: console.error(errmsg);
146: }
147: document.body.appendChild(target);
148: }
非同期で結果を取得した後、サンプル・プログラムの流れに示したように、次に処理を渡さなければならない。そこで、結果を取得したらカスタムイベント geocode を発生させ、メインプログラム側で、このイベントをキャッチすることにした。
カスタムイベントを発生させるために、CustomEvent インターフェースが用意されている。
CustomEventコンストラクタ は第1引数にイベント名を、第2引数にイベント発生時に渡すオブジェクトを記述する。このオブジェクトは、detail を要素名とすると決められている。
インターフェースの用意ができたら、dispatchEvent メソッドでイベントを発生させる。
pahooGeoCode.js
10: //IE用CustomEvent
11: if (document.documentMode) {
12: ! function () {
13: let prototype = CustomEvent.prototype
14: function CustomEvent(type, option) {
15: let eve = document.createEvent('Event')
16: option = option || {}
17: eve.initEvent(type, !!option.bubbles, !!option.cancelable)
18: return eve
19: }
20: CustomEvent.prototype = prototype
21: window.CustomEvent = CustomEvent
22: }();
23: }
「HeartRails Geo API」による緯度・経度変換
得られる緯度・経度は世界測地系(wgs84)であることに留意されたい。
URL |
---|
https://geoapi.heartrails.com/api/json?method=suggest |
フィールド名 | 要否 | 内 容 |
---|---|---|
method | 必須 | メソッド名:suggest(固定) |
keyword | 必須 | 検索キーワード(UTF-8でURLエンコード) |
matching | 必須 | prefix(前方一致)、like(部分一致)、suffix(後方一致)のいずれか |
jsonp | 任意 | JSONPとして出力する際のコールバック関数名を入力するためのパラメータ。 |
pahooGeoCode.js
150: /**
151: * 11:HeartRails Geo API - 住所検索APIを用いて緯度・経度を求める
152: * @param String query 検索キーワード:住所のみ(UTF-8)
153: * @param String matching 検索方式
154: * prefix = 前方一致
155: * like = 部分一致(省略時)
156: * suffix = 後方一致
157: * @return なし(→結果はgeocodeイベントにより取得)
158: */
159: pahooGeoCode.prototype.geocode_heartrailsgeo = function (query, matching) {
160: //IE用デフォルト引数
161: if (typeof matching === 'undefined') matching = 'like';
162:
163: //空白除去
164: query = query.trim();
165: //入力文字のエスケープ
166: query = htmlspecialchars(query);
167:
168: //XMLHttpRequestオブジェクト生成
169: let request = new XMLHttpRequest();
170: this.clearError();
171:
172: //WebAPIリクエスト
173: let url = 'https://geoapi.heartrails.com/api/json?method=suggest&jsonp=__callback_geocode_heartrailsgeo&matching=' + matching + '&keyword=' + encodeURI(query);
174: console.log(url);
175:
176: //SOP回避
177: let target = document.createElement('script');
178: target.charset = 'utf-8';
179: target.src = url;
180: target.onerror = function() {
181: errmsg = 'geoapi.heartrails.comに接続できません'
182: console.error(errmsg);
183: }
184: document.body.appendChild(target);
185:
186: //JSONP実行関数
187: target = document.createElement('script');
188: target.innerHTML = (
189: function __callback_geocode_heartrailsgeo(result) {
190: console.log(result);
191: //応答結果なし
192: if ((typeof result.response.location === 'undefined') || (typeof result.response.location[0].y === 'undefined') || (typeof result.response.location[0].x === 'undefined') || (result.response.location.length == 0)) {
193: errmsg = '検索キーワードが見つかりません'
194: PGC.clearData();
195: PGC.pdata.error = true;
196: PGC.pdata.errmsg = errmsg;
197: console.error(errmsg);
198: } else {
199: PGC.clearError();
200: PGC.pdata.latitude = result.response.location[0].y;
201: PGC.pdata.longitude = result.response.location[0].x;
202: }
203: //geocodeイベント
204: let event = new CustomEvent('geocode', {
205: detail: {
206: error: PGC.pdata.error,
207: errmsg: PGC.pdata.errmsg,
208: latitude: PGC.pdata.latitude,
209: longitude: PGC.pdata.longitude
210: }
211: });
212: document.dispatchEvent(event);
213: }
214: );
215: target.onerror = function() {
216: errmsg = 'WebAPIに接続できません'
217: this.clearData();
218: this.pdata.error = true;
219: this.pdata.errmsg = errmsg;
220: console.error(errmsg);
221: }
222: document.body.appendChild(target);
223: }
「OSM Nominatim Search API」による緯度・経度変換
得られる緯度・経度は世界測地系(wgs84)であることに留意されたい。
URL |
---|
https://nominatim.openstreetmap.org/search |
フィールド名 | 要否 | 内 容 |
---|---|---|
format | 任意 | 出力形式。html|xml|json|jsonv2。省略時はhtml |
q | 必須 | 住所やランドマーク(UTF-8) |
json_callback | 任意 | json の出力をラップするコールバック関数(JSONP) |
addressdetails | 任意 | 住所の要素への細分化を含むかどうか。0|1。省略時は0 |
pahooGeoCode.js
225: /**
226: * 12:OSM Nominatim Search API - 住所検索APIを用いて緯度・経度を求める
227: * @param String query 検索キーワード:住所のみ(UTF-8)
228: * @return なし(→結果はgeocodeイベントにより取得)
229: */
230: pahooGeoCode.prototype.geocode_nominatim = function (query) {
231: //空白除去
232: query = query.trim();
233: //入力文字のエスケープ
234: query = htmlspecialchars(query);
235:
236: //XMLHttpRequestオブジェクト生成
237: let request = new XMLHttpRequest();
238: this.clearError();
239:
240: //WebAPIリクエスト
241: let url = 'https://nominatim.openstreetmap.org/search?format=json&json_callback=__callback_geocode_nominatim&q=' + encodeURI(query);
242: console.log(url);
243:
244: //SOP回避
245: let target = document.createElement('script');
246: target.charset = 'utf-8';
247: target.src = url;
248: target.onerror = function() {
249: errmsg = 'nominatim.openstreetmap.orgに接続できません'
250: console.error(errmsg);
251: }
252: document.body.appendChild(target);
253:
254: //JSONP実行関数
255: target = document.createElement('script');
256: target.innerHTML = (
257: function __callback_geocode_nominatim(result) {
258: console.log(result);
259: //応答結果なし
260: if ((result.length == 0) || (typeof result[0].lat == 'undefined') || (typeof result[0].lon == 'undefined')) {
261: errmsg = '検索キーワードが見つかりません'
262: PGC.clearData();
263: PGC.pdata.error = true;
264: PGC.pdata.errmsg = errmsg;
265: console.error(errmsg);
266: } else {
267: PGC.pdata.error = false;
268: PGC.pdata.errmsg = '';
269: PGC.pdata.latitude = result[0].lat;
270: PGC.pdata.longitude = result[0].lon;
271: }
272: //geocodeイベント
273: let event = new CustomEvent('geocode', {
274: detail: {
275: error: PGC.pdata.error,
276: errmsg: PGC.pdata.errmsg,
277: latitude: PGC.pdata.latitude,
278: longitude: PGC.pdata.longitude
279: }
280: });
281: document.dispatchEvent(event);
282: }
283: );
284: target.onerror = function() {
285: errmsg = 'WebAPIに接続できません'
286: this.clearData();
287: this.pdata.error = true;
288: this.pdata.errmsg = errmsg;
289: console.error(errmsg);
290: }
291: document.body.appendChild(target);
292: }
HeartRails Geo API 最寄駅検索
URL |
---|
https://express.heartrails.com/api/json |
フィールド名 | 要否 | 内 容 |
---|---|---|
method | 必須 | メソッド名:getStation(固定) |
x | 必須 | 最寄り駅を取得したい場所の経度(世界測地系)。 |
y | 必須 | 最寄り駅を取得したい場所の緯度(世界測地系)。 |
searchStation.html
67: /**
68: * 指定した緯度・経度の近くにある駅を
69: * HeartRails Express API を利用する.
70: * @param Number latitude 緯度(世界測地系)
71: * @param Number longitude 経度(世界測地系)
72: * @return なし
73: */
74: function searchStation(latitude, longitude) {
75: //XMLHttpRequestオブジェクト生成
76: let request = new XMLHttpRequest();
77: PGC.clear_error();
78:
79: //WebAPIリクエスト
80: let url = 'https://express.heartrails.com/api/json?method=getStations&x=' + longitude + '&y=' + latitude + '&jsonp=__callback_searchStation';
81: console.log(url);
82:
83: //SOP回避
84: let target = document.createElement('script');
85: target.charset = 'utf-8';
86: target.src = url;
87: target.onerror = function() {
88: errmsg = 'express.heartrails.comに接続できません'
89: console.error(errmsg);
90: }
91: document.body.appendChild(target);
92:
93: //JSONP実行関数
94: target = document.createElement('script');
95: target.innerHTML = (
96: function __callback_searchStation(result) {
97: let items = Array();
98: console.log(result);
99: //応答結果なし
100: if ((typeof result.response.station == 'undefined')) {
101: errmsg = '検索キーワードが見つかりません'
102: PGC.clear_data();
103: PGC.pdata.error = true;
104: PGC.pdata.errmsg = errmsg;
105: console.error(errmsg);
106: } else {
107: for (let i = 0; i < result.response.station.length; i++) {
108: items[i] = result.response.station[i];
109: }
110: PGC.pdata.error = false;
111: PGC.pdata.errmsg = '';
112: PGC.pdata.items = items;
113: }
114: //geocodeイベント
115: let event = new CustomEvent('station', {
116: detail: {
117: error: PGC.pdata.error,
118: errmsg: PGC.pdata.errmsg,
119: latitude: PGC.pdata.latitude,
120: longitude: PGC.pdata.longitude
121: }
122: });
123: document.dispatchEvent(event);
124: }
125: );
126: target.onerror = function() {
127: errmsg = 'WebAPIに接続できません'
128: PGC.clear_data();
129: PGC.pdata.error = true;
130: PGC.pdata.errmsg = errmsg;
131: console.error(errmsg);
132: }
133: document.body.appendChild(target);
134: }
コラム:Google Cloud Platform

測地系に揺らぎがあるなどの問題は徐々に改善されたが、途中、何度か WebAPI の仕様が変更になり、その都度対応しなければならなかった。
2018年6月に有料化し、同時に国内地図がゼンリンでなくなった。この影響は大きなものだったが、現時点でも無料枠内で利用を続けている。
さらにアクセス数が増えたら、今回利用している OpenStreetMap か 地理院地図 に乗り換えることになるだろう。

なお、今回利用した Leaflet からGoogleマップを利用することもできる。もちろん課金される。マップ右上の地図を選ぶメニューにGoogleマップが追加され、切り替えることができるようになる。
そのやり方は、「C++で直近の地震情報を取得する - マップを生成する」で紹介している。興味のある方はGoogleマップを利用できるよう改造してみてほしい。
参考サイト
- Leaflet
- YOLPコンテンツジオコーダAPI
- HeartRails Geo API
- OSM Nominatim Search API
- 各種WebAPIの登録方法:ぱふぅ家のホームページ
- PHPで住所・ランドマークから緯度・経度を求める:ぱふぅ家のホームページ
- PHPで住所・ランドマークから最寄り駅を求める:ぱふぅ家のホームページ
- C++ で最寄駅を検索:ぱふぅ家のホームページ
指定した住所から最寄り駅を検索し、マップ上に表示することを目標にする。