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オブジェクト生成時に渡すこと。

searchStation.html

  36: //グローバル変数:PGC
  37: 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 経度

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

WebAPI の呼び出しと結果取得は「7.1 郵便番号→住所検索,Wikipedia検索」で説明したとおり。
非同期で結果を取得した後、サンプル・プログラムの流れに示したように、次に処理を渡さなければならない。そこで、結果を取得したらカスタムイベント 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: }

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 郵便番号(ハイフンなし)

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

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 ライセンス

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

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 前の駅

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 = 0i < result.response.station.lengthi++) {
 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: }

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

コラム:Google Cloud Platform

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

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

参考サイト

(この項おわり)
header