目次
サンプル・プログラムの実行例
サンプル・プログラム
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オブジェクト生成時に渡すこと。
0036: //グローバル変数:PGC
0037: 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として出力する際のコールバック関数名を入力するためのパラメータ。 |
0074: /**
0075: * 1:YOLPコンテンツジオコーダAPIを用いて緯度・経度を求める
0076: * @param String query 検索キーワード:住所のみ(UTF-8)
0077: * @param String category検索対象カテゴリ
0078: * address = 住所(省略時)
0079: * landmark = ランドマーク
0080: * world = 世界
0081: * @return なし(→結果はgeocodeイベントにより取得)
0082: */
0083: pahooGeoCode.prototype.geocode_YOLP = function (query, category) {
0084: //IE用デフォルト引数
0085: if (typeof category === 'undefined') category = 'address';
0086:
0087: //空白除去
0088: query = query.trim();
0089: //入力文字のエスケープ
0090: query = htmlspecialchars(query);
0091:
0092: //XMLHttpRequestオブジェクト生成
0093: let request = new XMLHttpRequest();
0094: this.clear_error();
0095:
0096: //WebAPIリクエスト
0097: 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';
0098: console.log(url);
0099:
0100: //SOP回避
0101: let target = document.createElement('script');
0102: target.charset = 'utf-8';
0103: target.src = url;
0104: target.onerror = function() {
0105: errmsg = 'map.yahooapis.jpに接続できません'
0106: console.error(errmsg);
0107: }
0108: document.body.appendChild(target);
0109:
0110: //JSONP実行関数
0111: target = document.createElement('script');
0112: target.innerHTML = (
0113: function __callback_geocode_YOLP(result) {
0114: console.log(result);
0115: //応答結果なし
0116: if ((typeof result.Feature == 'undefined') || (result.Feature.length == 0) || (typeof result.Feature[0].Geometry.Coordinates == 'undefined')) {
0117: errmsg = '検索キーワードが見つかりません'
0118: PGC.clear_data();
0119: PGC.pdata.error = true;
0120: PGC.pdata.errmsg = errmsg;
0121: console.error(errmsg);
0122: } else {
0123: let arr = result.Feature[0].Geometry.Coordinates.split(',');
0124: PGC.clear_data();
0125: PGC.pdata.latitude = arr[1];
0126: PGC.pdata.longitude = arr[0];
0127: }
0128: //geocodeイベント
0129: let event = new CustomEvent('geocode', {
0130: detail: {
0131: error: PGC.pdata.error,
0132: errmsg: PGC.pdata.errmsg,
0133: latitude: PGC.pdata.latitude,
0134: longitude: PGC.pdata.longitude
0135: }
0136: });
0137: document.dispatchEvent(event);
0138: }
0139: );
0140: target.onerror = function() {
0141: errmsg = 'WebAPIに接続できません'
0142: this.clear_data();
0143: this.pdata.error = true;
0144: this.pdata.errmsg = errmsg;
0145: console.error(errmsg);
0146: }
0147: document.body.appendChild(target);
0148: }
非同期で結果を取得した後、サンプル・プログラムの流れに示したように、次に処理を渡さなければならない。そこで、結果を取得したらカスタムイベント geocode を発生させ、メインプログラム側で、このイベントをキャッチすることにした。
カスタムイベントを発生させるために、CustomEvent インターフェースが用意されている。
CustomEventコンストラクタ は第1引数にイベント名を、第2引数にイベント発生時に渡すオブジェクトを記述する。このオブジェクトは、detail を要素名とすると決められている。
インターフェースの用意ができたら、dispatchEvent メソッドでイベントを発生させる。
0010: //IE用CustomEvent
0011: if (document.documentMode) {
0012: ! function () {
0013: let prototype = CustomEvent.prototype
0014: function CustomEvent(type, option) {
0015: let eve = document.createEvent('Event')
0016: option = option || {}
0017: eve.initEvent(type, !!option.bubbles, !!option.cancelable)
0018: return eve
0019: }
0020: CustomEvent.prototype = prototype
0021: window.CustomEvent = CustomEvent
0022: }();
0023: }
「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として出力する際のコールバック関数名を入力するためのパラメータ。 |
0150: /**
0151: * 11:HeartRails Geo API - 住所検索APIを用いて緯度・経度を求める
0152: * @param String query 検索キーワード:住所のみ(UTF-8)
0153: * @param String matching検索方式
0154: * prefix = 前方一致
0155: * like = 部分一致(省略時)
0156: * suffix = 後方一致
0157: * @return なし(→結果はgeocodeイベントにより取得)
0158: */
0159: pahooGeoCode.prototype.geocode_heartrailsgeo = function (query, matching) {
0160: //IE用デフォルト引数
0161: if (typeof matching === 'undefined') matching = 'like';
0162:
0163: //空白除去
0164: query = query.trim();
0165: //入力文字のエスケープ
0166: query = htmlspecialchars(query);
0167:
0168: //XMLHttpRequestオブジェクト生成
0169: let request = new XMLHttpRequest();
0170: this.clear_error();
0171:
0172: //WebAPIリクエスト
0173: let url = 'https://geoapi.heartrails.com/api/json?method=suggest&jsonp=__callback_geocode_heartrailsgeo&matching=' + matching + '&keyword=' + encodeURI(query);
0174: console.log(url);
0175:
0176: //SOP回避
0177: let target = document.createElement('script');
0178: target.charset = 'utf-8';
0179: target.src = url;
0180: target.onerror = function() {
0181: errmsg = 'geoapi.heartrails.comに接続できません'
0182: console.error(errmsg);
0183: }
0184: document.body.appendChild(target);
0185:
0186: //JSONP実行関数
0187: target = document.createElement('script');
0188: target.innerHTML = (
0189: function __callback_geocode_heartrailsgeo(result) {
0190: console.log(result);
0191: //応答結果なし
0192: 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)) {
0193: errmsg = '検索キーワードが見つかりません'
0194: PGC.clear_data();
0195: PGC.pdata.error = true;
0196: PGC.pdata.errmsg = errmsg;
0197: console.error(errmsg);
0198: } else {
0199: PGC.clear_error();
0200: PGC.pdata.latitude = result.response.location[0].y;
0201: PGC.pdata.longitude = result.response.location[0].x;
0202: }
0203: //geocodeイベント
0204: let event = new CustomEvent('geocode', {
0205: detail: {
0206: error: PGC.pdata.error,
0207: errmsg: PGC.pdata.errmsg,
0208: latitude: PGC.pdata.latitude,
0209: longitude: PGC.pdata.longitude
0210: }
0211: });
0212: document.dispatchEvent(event);
0213: }
0214: );
0215: target.onerror = function() {
0216: errmsg = 'WebAPIに接続できません'
0217: this.clear_data();
0218: this.pdata.error = true;
0219: this.pdata.errmsg = errmsg;
0220: console.error(errmsg);
0221: }
0222: document.body.appendChild(target);
0223: }
「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 |
0225: /**
0226: * 12:OSM Nominatim Search API - 住所検索APIを用いて緯度・経度を求める
0227: * @param String query検索キーワード:住所のみ(UTF-8)
0228: * @return なし(→結果はgeocodeイベントにより取得)
0229: */
0230: pahooGeoCode.prototype.geocode_nominatim = function (query) {
0231: //空白除去
0232: query = query.trim();
0233: //入力文字のエスケープ
0234: query = htmlspecialchars(query);
0235:
0236: //XMLHttpRequestオブジェクト生成
0237: let request = new XMLHttpRequest();
0238: this.clear_error();
0239:
0240: //WebAPIリクエスト
0241: let url = 'https://nominatim.openstreetmap.org/search?format=json&json_callback=__callback_geocode_nominatim&q=' + encodeURI(query);
0242: console.log(url);
0243:
0244: //SOP回避
0245: let target = document.createElement('script');
0246: target.charset = 'utf-8';
0247: target.src = url;
0248: target.onerror = function() {
0249: errmsg = 'nominatim.openstreetmap.orgに接続できません'
0250: console.error(errmsg);
0251: }
0252: document.body.appendChild(target);
0253:
0254: //JSONP実行関数
0255: target = document.createElement('script');
0256: target.innerHTML = (
0257: function __callback_geocode_nominatim(result) {
0258: console.log(result);
0259: //応答結果なし
0260: if ((result.length == 0) || (typeof result[0].lat == 'undefined') || (typeof result[0].lon == 'undefined')) {
0261: errmsg = '検索キーワードが見つかりません'
0262: PGC.clear_data();
0263: PGC.pdata.error = true;
0264: PGC.pdata.errmsg = errmsg;
0265: console.error(errmsg);
0266: } else {
0267: PGC.pdata.error = false;
0268: PGC.pdata.errmsg = '';
0269: PGC.pdata.latitude = result[0].lat;
0270: PGC.pdata.longitude = result[0].lon;
0271: }
0272: //geocodeイベント
0273: let event = new CustomEvent('geocode', {
0274: detail: {
0275: error: PGC.pdata.error,
0276: errmsg: PGC.pdata.errmsg,
0277: latitude: PGC.pdata.latitude,
0278: longitude: PGC.pdata.longitude
0279: }
0280: });
0281: document.dispatchEvent(event);
0282: }
0283: );
0284: target.onerror = function() {
0285: errmsg = 'WebAPIに接続できません'
0286: this.clear_data();
0287: this.pdata.error = true;
0288: this.pdata.errmsg = errmsg;
0289: console.error(errmsg);
0290: }
0291: document.body.appendChild(target);
0292: }
HeartRails Geo API 最寄駅検索
URL |
---|
https://express.heartrails.com/api/json |
フィールド名 | 要否 | 内 容 |
---|---|---|
method | 必須 | メソッド名:getStation(固定) |
x | 必須 | 最寄り駅を取得したい場所の経度(世界測地系)。 |
y | 必須 | 最寄り駅を取得したい場所の緯度(世界測地系)。 |
0067: /**
0068: * HeartRails Express APIから必要な情報を取得する
0069: * @param Number latitude 緯度(世界測地系)
0070: * @param Number longitude 経度(世界測地系)
0071: * @return なし
0072: */
0073: function search_station(latitude, longitude) {
0074: //XMLHttpRequestオブジェクト生成
0075: let request = new XMLHttpRequest();
0076: PGC.clear_error();
0077:
0078: //WebAPIリクエスト
0079: let url = 'https://express.heartrails.com/api/json?method=getStations&x=' + longitude + '&y=' + latitude + '&jsonp=__callback_search_station';
0080: console.log(url);
0081:
0082: //SOP回避
0083: let target = document.createElement('script');
0084: target.charset = 'utf-8';
0085: target.src = url;
0086: target.onerror = function() {
0087: errmsg = 'express.heartrails.comに接続できません'
0088: console.error(errmsg);
0089: }
0090: document.body.appendChild(target);
0091:
0092: //JSONP実行関数
0093: target = document.createElement('script');
0094: target.innerHTML = (
0095: function __callback_search_station(result) {
0096: let items = Array();
0097: console.log(result);
0098: //応答結果なし
0099: if ((typeof result.response.station == 'undefined')) {
0100: errmsg = '検索キーワードが見つかりません'
0101: PGC.clear_data();
0102: PGC.pdata.error = true;
0103: PGC.pdata.errmsg = errmsg;
0104: console.error(errmsg);
0105: } else {
0106: for (let i = 0; i < result.response.station.length; i++) {
0107: items[i] = result.response.station[i];
0108: }
0109: PGC.pdata.error = false;
0110: PGC.pdata.errmsg = '';
0111: PGC.pdata.items = items;
0112: }
0113: //geocodeイベント
0114: let event = new CustomEvent('station', {
0115: detail: {
0116: error: PGC.pdata.error,
0117: errmsg: PGC.pdata.errmsg,
0118: latitude: PGC.pdata.latitude,
0119: longitude: PGC.pdata.longitude
0120: }
0121: });
0122: document.dispatchEvent(event);
0123: }
0124: );
0125: target.onerror = function() {
0126: errmsg = 'WebAPIに接続できません'
0127: PGC.clear_data();
0128: PGC.pdata.error = true;
0129: PGC.pdata.errmsg = errmsg;
0130: console.error(errmsg);
0131: }
0132: document.body.appendChild(target);
0133: }
コラム:Google Cloud Platform
測地系に揺らぎがあるなどの問題は徐々に改善されたが、途中、何度か WebAPI の仕様が変更になり、その都度対応しなければならなかった。
2018年6月に有料化し、同時に国内地図がゼンリンでなくなった。この影響は大きなものだったが、現時点でも無料枠内で利用を続けている。
さらにアクセス数が増えたら、今回利用している OpenStreetMap か 地理院地図 に乗り換えることになるだろう。
なお、今回利用した Leflet からGoogleマップを利用することもできる。もちろん課金される。マップ右上の地図を選ぶメニューにGoogleマップが追加され、切り替えることができるようになる。
そのやり方は、「C++で直近の地震情報を取得する - マップを生成する」で紹介している。興味のある方はGoogleマップを利用できるよう改造してみてほしい。
参考サイト
- Leaflet
- YOLPコンテンツジオコーダAPI
- HeartRails Geo API
- OSM Nominatim Search API
- 各種WebAPIの登録方法:ぱふぅ家のホームページ
- PHPで住所・ランドマークから緯度・経度を求める:ぱふぅ家のホームページ
- PHPで住所・ランドマークから最寄り駅を求める:ぱふぅ家のホームページ
- C++ で最寄駅を検索:ぱふぅ家のホームページ
指定した住所から最寄り駅を検索し、マップ上に表示することを目標にする。