7.3 オンライン地図の利用

(1/1)
地図を見ている家族のイラスト
複数の WebAPI を組み合わせて新たなサービスを提供することをクラウド連携と呼ぶ。これまでの知識を総動員して、JavaScriptでクラウド連携プログラムを作ってみることにする。
指定した住所から最寄り駅を検索し、マップ上に表示することを目標にする。

目次

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

JavaScriptで最寄り駅を検索、マップに表示する

サンプル・プログラム

圧縮ファイルの内容
search_station.phpサンプル・プログラム本体。
pahooGeoCode.js住所・緯度・経度に関わるクラス pahooGeoCode。
使い方は「最寄り駅を求める」を参照。

要件定義

まず要件定義を明らかにしておこう。
  1. モダンブラウザおよびIEで動作すること。
  2. 無料で利用できること。
  3. マップを表示できること。
  4. 住所を入力し、対応する緯度・経度を検索できること。
  5. 緯度・経度の近くにある駅を検索できること。
  6. 検索した駅をマップ上に表示できること。
  7. 検索した駅を一覧表示できること。

使用するライブラリおよびWebAPI

マップ表示はGoogleマップが代表的であり、JavaScript向けのAPIも揃っており使い勝手がいい。しかし、Googleマップを含む Google Cloud Platform はAPIの利用量によって課金される。2021年9月現在、毎月200ドルまでは無料。個人で利用する分には無料枠で十分だろうが、今後無料枠の変更があるかもしれない。
そこで今回は、国土地理院の地理院地図や、オープンソースの世界地図作成プロジェクト「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として表示することにする。

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

JavaScriptで最寄り駅を検索、マップに表示する
マップ描画、ジオコードに関わるメソッド、および検索結果を格納するプロパティは、再利用を考え、クラス pahooGeoCode に分離する。

メインプログラムでは、まず draw_leaflet メソッドを使ってマップを描く。まだ情報が無いのでマーカーは表示しない。
検索ボタンが押下されたら、入力値(query)があれば、geocode メソッドを使って、対応する緯度・経度を検索する。geocodeメソッド は、引数 api の値に応じて、前述の3つの ジオコードWebAPI のいずれかを呼び出す。
これまで学んできたように、WebAPI 呼び出しは非同期で行われる。検索結果が返ってきたらプロパティpdataに代入し、カスタムイベント geocode を発生させる。
入力値(query)が無い、またはカスタムイベント geocode をキャッチしたら、search_station メソッドを使って、緯度・経度から最寄り駅を検索する。これも非同期で行われるから、検索結果が返ってきたらプロパティpdataに代入し、カスタムイベント station を発生させる。
カスタムイベント station をキャッチしたら、del_marker2leaflet で表示中のマーカーを全て削除し、あらたに marker2leaflet を使ってマップ上に駅の位置を表すマーカーを追加する。そして、一覧表示を行う。

YOLPコンテンツジオコーダAPI

YOLPコンテンツジオコーダAPI」は、入力パラメータはGET渡しで、出力結果はJSONなどで受け取る WebAPI である。今回使う入力パラメータと出力結果のデータ構造を以下に示す。
利用料は無料だが、Yahoo!JAPAN ID が必要となる。「各種WebAPIの登録方法 - Yahoo!JAPAN デベロッパーネットワーク」をご覧いただきたい。
取得した APIキー は次のように、pahooGeoCodeオブジェクト生成時に渡すこと。

0036: //グローバル変数:PGC
0037: let PGC = new pahooGeoCode('(取得したYahoo!APIキー)');

得られる緯度・経度は世界測地系(wgs84)であることに留意されたい。測地系については、「PHPの関数で複数の値を戻す - 測地系の違い」をご覧いただきたい。
WebAPIのURL
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として出力する際のコールバック関数名を入力するためのパラメータ。
応答データ構造(json) Feature Geometry Coordinates 経度,緯度 Type 図形種別 formatted_address 人間が読むことができる住所 address_component long_name 正式名称 short_name 略称 type 検索結果のタイプ geometry location lat 緯度 lng 経度

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 (querycategory) {
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: }

WebAPI の呼び出しと結果取得は「7.1 郵便番号→住所検索,Wikipedia検索」で説明したとおり。
非同期で結果を取得した後、サンプル・プログラムの流れに示したように、次に処理を渡さなければならない。そこで、結果を取得したらカスタムイベント geocode を発生させ、メインプログラム側で、このイベントをキャッチすることにした。
カスタムイベントを発生させるために、CustomEvent インターフェースが用意されている。
CustomEventコンストラクタ は第1引数にイベント名を、第2引数にイベント発生時に渡すオブジェクトを記述する。このオブジェクトは、detail を要素名とすると決められている。
インターフェースの用意ができたら、dispatchEvent メソッドでイベントを発生させる。

0010: //IE用CustomEvent
0011: if (document.documentMode) {
0012:     ! function () {
0013:         let prototype = CustomEvent.prototype
0014:         function CustomEvent(typeoption) {
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: }

IEには CustomEventインターフェース が無いため、別途用意した。

「HeartRails Geo API」による緯度・経度変換

HeartRails Geo API - キーワードによる住所検索 API」は、入力パラメータはGET渡しで、出力結果はJSONなどで戻すという形である。今回使う入力パラメータと出力結果のデータ構造を以下に示す。
得られる緯度・経度は世界測地系(wgs84)であることに留意されたい。
WebAPIのURL
URL
https://geoapi.heartrails.com/api/json?method=suggest

入力パラメータ
フィールド名 要否 内  容
method 必須 メソッド名:suggest(固定)
keyword 必須 検索キーワード(UTF-8でURLエンコード)
matching 必須 prefix(前方一致)、like(部分一致)、suffix(後方一致)のいずれか
jsonp 任意 JSONPとして出力する際のコールバック関数名を入力するためのパラメータ。
応答データ構造(json) response location city 市区町村名 city-kana 市区町村名よみ(平仮名) town 町域名 town-kana 町域名よみ(平仮名) x 経度(世界測値系) y 緯度(世界測値系) prefecture 都道府県名 postal 郵便番号(ハイフンなし)

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 (querymatching) {
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: }

WebAPI の呼び出しと、検索結果(JSONデータ)の処理(イベント)は、これまでと同様である。

「OSM Nominatim Search API」による緯度・経度変換

OSM Nominatim Search API」は、誰でも自由に地図を使えるよう、みんなでオープンデータの地理情報を作る「OpenStreetMap」(OSM)プロジェクトに含まれるWebAPIで、入力パラメータはGET渡しで、出力結果はJSONなどで戻すという形である。今回使う入力パラメータと出力結果のデータ構造を以下に示す。
得られる緯度・経度は世界測地系(wgs84)であることに留意されたい。
WebAPIのURL
URL
https://nominatim.openstreetmap.org/search

入力パラメータ
フィールド名 要否 内  容
format 任意 出力形式。html|xml|json|jsonv2。省略時はhtml
q 必須 住所やランドマーク(UTF-8)
json_callback 任意 json の出力をラップするコールバック関数(JSONP)
addressdetails 任意 住所の要素への細分化を含むかどうか。0|1。省略時は0
応答データ構造(json) place_id ID lon 経度(世界測値系) lat 緯度(世界測値系) display_name 住所,郵便番号など license ライセンス

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: }

WebAPI の呼び出しと、検索結果(JSONデータ)の処理(イベント)は、これまでと同様である。

HeartRails Geo API 最寄駅検索

HeartRails Geo API - 最寄駅情報取得 API」は、入力パラメータはGET渡しで、出力結果はJSONなどで戻すという形である。今回使う入力パラメータと出力結果のデータ構造を以下に示す。
WebAPIのURL
URL
https://express.heartrails.com/api/json

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

0067: /**
0068:  * HeartRails Express APIから必要な情報を取得する
0069:  * @param   Number latitude  緯度(世界測地系)
0070:  * @param   Number longitude 経度(世界測地系)
0071:  * @return  なし
0072: */
0073: function search_station(latitudelongitude) {
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.lengthi++) {
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: }

WebAPI の呼び出しと、検索結果(JSONデータ)の処理(イベント)は、これまでと同様である。

コラム:Google Cloud Platform

Googleマップ
本文で触れた Google Cloud Platform は、Googleマップ以外にも様々な WebAPI を提供している。とくにジオコードは、住所だけでなく、駅やランドマークから緯度・経度を検索することができる強力なもので、世界中を検索対象としている。その分、マップを表示するより課金設定が高い。
ぱふぅ家のホームページでは、Googleマップが登場してすぐに利用を始めた。当時は無料だった。
測地系に揺らぎがあるなどの問題は徐々に改善されたが、途中、何度か WebAPI の仕様が変更になり、その都度対応しなければならなかった。
2018年6月に有料化し、同時に国内地図がゼンリンでなくなった。この影響は大きなものだったが、現時点でも無料枠内で利用を続けている。
さらにアクセス数が増えたら、今回利用している OpenStreetMap地理院地図 に乗り換えることになるだろう。

なお、今回利用した Leflet からGoogleマップを利用することもできる。もちろん課金される。マップ右上の地図を選ぶメニューにGoogleマップが追加され、切り替えることができるようになる。
そのやり方は、「C++で直近の地震情報を取得する - マップを生成する」で紹介している。興味のある方はGoogleマップを利用できるよう改造してみてほしい。

参考サイト

(この項おわり)
header