JavaScriptで「ぐるなび」を使って喫茶店を探す

(1/1)
JavaScript ES6 がIE以外のブラウザで利用できるようになった。このバージョンではクラス宣言もできる。
そこで今回は、「PHPで『ぐるなび』を使って喫茶店を探す」で紹介したPHPサーバサイド・プラグラムを、JavaScriptを使ってクライアント・サイドに移植してみる。PHP環境が無くてもブラウザさえあれば、Googleマップ上で喫茶店を検索できるようになる。

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

JavaScriptで「ぐるなび」を使って喫茶店を探す

サンプル・プログラムのダウンロード

圧縮ファイルの内容
searchCafe.htmlサンプル・プログラム本体。
pahooGeoCode.js住所・緯度・経度に関わるクラス pahooGeoCode。

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

JavaScriptで「ぐるなび」を使って喫茶店を探す
プログラムの流れは、冒頭で jQuery とクラス pahooGeoCode を読み込み、各種初期化を行う。
続いて、3つのボタン押下によって、各々 exec, map, clear のアクションを発生する。
execは、「JavaScriptでWebAPIを利用して住所から緯度・経度を求める」と同じで、キーワードから緯度・経度を求める。
mapは、今回のメインプログラムで、詳細は後述する。
clearは、初期化してからmapと同じ処理を行う。

解説:クラスファイル

住所やランドマークから緯度・経度を求める処理は、「JavaScriptでWebAPIを利用して住所から緯度・経度を求める」で作成したクラス pahooGeoCode を利用する。
クラスファイル "pahooGeoCode.js" の配置、各種WebAPI用のアプリケーションIDの設定方法については、「準備、クラスについて」を参照してほしい。

解説:Googleマップ描画

 280: /**
 281:  * Googleマップを描く
 282:  * @param   String  id        マップID
 283:  * @param   Float   latitude  中心座標:緯度(世界測地系)
 284:  * @param   Float   longitude 中心座標:経度(世界測地系)
 285:  * @param   String  type      マップタイプ:HYBRID/ROADMAP/SATELLITE/TERRAIN
 286:  * @param   Integer zoom      拡大率
 287:  * @param   String  call      イベント発生時にコールする関数(省略可)
 288:  * @param   Array   items[]   地点情報(省略可能)
 289:  *                  'title'       タイトル
 290:  *                  'description' 情報ウィンドウに表示する内容
 291:  *                  'latitude'    緯度
 292:  *                  'longitude'   経度
 293:  *                  'icon'        アイコンURL
 294:  * @return  String Googleマップのコード
 295: */
 296: drawGMap(id, latitude, longitude, type, zoom, items=null) {
 297:     var map = new google.maps.Map(document.getElementById(id), {
 298:         center: new google.maps.LatLng(parseFloat(latitude), parseFloat(longitude)),
 299:         zoom: parseInt(zoom),
 300:         mapTypeId: type,
 301:         mapTypeControl: true,
 302:         scaleControl: true
 303:     });
 304:     map.addListener('dragend', getPointData);
 305:     map.addListener('zoom_changed', getPointData);
 306:     map.addListener('maptypeid_changed', getPointData);
 307: 
 308:     //イベント発生時の地図情報を取得・格納
 309:     function getPointData() {
 310:         var point = map.getCenter();
 311:         //経度
 312:         if (document.getElementById('longitude'!null) {
 313:             document.getElementById('longitude').value = point.lng();
 314:         }
 315:         //緯度
 316:         if (document.getElementById('latitude'!null) {
 317:             document.getElementById('latitude').value = point.lat();
 318:         }
 319:         //ズーム
 320:         if (document.getElementById('zoom'!null) {
 321:             document.getElementById('zoom').value = map.getZoom();
 322:         }
 323:         //地図タイプ
 324:         if (document.getElementById('type'!null) {
 325:             var type_g = map.getMapTypeId();
 326:             const types = {'roadmap':'地図', 'satellite':'航空写真', 'hybrid':'ハイブリッド', 'terrain':'地形図'};
 327:             for (var key in types) {
 328:                 if (key == type_g) {
 329:                     document.getElementById('type').value = key;
 330:                     break;
 331:                 }
 332:             }
 333:         }
 334:     }
 335: 
 336:     if (items !null) {
 337:         var n = items.length;
 338:         items.forEach(function(item, i) {
 339:             if (i <26) {           //'Z'を超えたらスキップ
 340:                 //マーカー
 341:                 var mark = String.fromCodePoint('A'.charCodeAt(0+ i);
 342:                 var icon_url = (typeof(item.icon) == 'undefined'? 'https://www.google.com/mapfiles/marker' + mark + '.png' : item.icon;
 343:                 var marker = new google.maps.Marker({
 344:                     position: new google.maps.LatLng(item.latitude, item.longitude),
 345:                     icon: icon_url,
 346:                     map: map,
 347:                     title: item.title,
 348:                     zIndex: 100
 349:                 });
 350:                 var infowindow = new google.maps.InfoWindow({
 351:                     content: item.description,
 352:                     maxWidth: 200
 353:                 });
 354:                 google.maps.event.addListener(marker, 'click', function() {
 355:                     infowindow.open(map, marker);
 356:                 });
 357:             }
 358:         });
 359:     }
 360: }

クラス pahooGeoCode に、Googleマップを描画するメソッド drawGMap を追加した。これもPHPプログラムを、ほぼそのまま移植している。

PHPプログラムではマーカーを静的に発生させていたが、JavaScript版ではメソッドの後半で、引数 items にforEachループをかけ、動的にマーカーを発生させている。forループを使って実装すると、new でマーカー・オブジェクトを追加生成する処理がうまくいかないので、あえてforEachループを使っている。

「ぐるなびWebサービス:レストラン検索API」による店舗探索

ぐるなびWebサービス:レストラン検索API」は、入力パラメータ(IN)は GET 渡しで、出力結果(OUT)は JSON で戻すという形である。今回使う入力パラメータと出力結果のデータ構造の一部を以下に記す。
WebAPI
URL
https://api.gnavi.co.jp/RestSearchAPI/v3/

入力パラメータ
フィールド名 要否 内  容
keyid 必須 ぐるなびより提供されたアクセスキー
input_coordinates_mode 任意 入力パラメータに含まれる緯度/経度の測地系を指定
1:日本測地系
2:世界測地系(デフォルト)
latitude 任意 検索地点の緯度(小数表記)
longitude 任意 検索地点の経度(小数表記)
coordinates_mode 任意 レスポンスに含まれる緯度/経度の測地系を指定
1:日本測地系
2:世界測地系(デフォルト)
range 任意 緯度/経度からの検索範囲(半径)
1:300m、2:500m(デフォルト)、3:1000m、4:2000m、5:3000m
freeword 任意 検索ワードをUTF-8でURLエンコードすること「,」区切りで複数ワードが検索可能(10個まで)
出力パラメータ (JSON)
フィールド名 内  容
@attributes api_versionAPIのバージョン
total_hit_count該当件数
hit_per_page表示件数
page_offset表示ページ
total_hit_count表示ページ
rest[] id店舗ID
update_date情報更新日時
name店舗名
name_kana店舗名称(カタカナ)
latitude緯度(小数表記)
longitude経度(小数表記)
categoryフリーワードカテゴリー
urlPCサイトURL
url_mobile携帯サイトURL
address住所
tel電話番号
opentime営業時間
holiday休業日

アプリIDの入手

  31:     //ぐるなびWebサービス アクセスキー:http://api.gnavi.co.jp/api/ で発行
  32:     GNAVI_ACCESSKEY = '*************************************';

ぐるなびWebサービス」を利用するには、利用申請を行い、アカウントを発行してもらい、アクセスキーを取得する必要がある。アカウントは無料である。

サンプル・プログラムをダウンロードしたら、定数 GNAVI_ACCESSKEY に自分のアクセスキーを記述する。

解説:ぐるなびWebサービス

 179: /**
 180:  * ぐるなびWebサービスのURLを取得する
 181:  * @param   Float  latitude  緯度(世界測地系)
 182:  * @param   Float  longitude 経度(世界測地系)
 183:  * @param   Float  distance  範囲(メートル)
 184:  * @param   String freeword  フリーワード検索(カンマ区切り)
 185:  * @return  String URL 電源情報APIのURL
 186: */
 187: function getURL_RestSearchAPI(latitude, longitude, distance, freeword) {
 188:     var range_tbl = [0, 300, 500, 1000, 2000, 3000];
 189: 
 190:     var keyid = GNAVI_ACCESSKEY;
 191:     range = range_tbl.length;
 192:     for (var key = 1key < rangekey++) {
 193:         if (distance <range_tbl[key]) {
 194:             range = key;
 195:             break;
 196:         }
 197:     }
 198:     freeword = encodeURI(freeword);
 199: 
 200:     url = 'https://api.gnavi.co.jp/RestSearchAPI/v3/?keyid=' + keyid + '&input_coordinates_mode=2&coordinates_mode=2&latitude=' + latitude + '&longitude=' + longitude + '&range=' + range + '&freeword=' + freeword;
 201:     return url;
 202: }
 203: 
 204: /**
 205:  * レストラン検索APIを利用して指定座標の近くにある喫茶店を検索
 206:  * @param   Array  items[]   情報を格納する配列
 207:  * @param   Object pp{error:エラーフラグ,errmsg:エラーメッセージ,
 208:  *                    hits:ヒット件数,webapi:WebAPIのURL}
 209:  * @return  なし
 210: */
 211: function searchCafe(items, pp) {
 212:     var latitude  = parseFloat($('#latitude').val());
 213:     var longitude = parseFloat($('#longitude').val());
 214:     var distance  = parseInt($('#distance').val());
 215:     items = [];
 216:     var url = getURL_RestSearchAPI(latitude, longitude, distance, '喫茶店');
 217:     var n = 0;
 218: 
 219:     //API呼び出し
 220:     var req = new XMLHttpRequest();
 221:     req.onreadystatechange = function() {
 222:         searchCafe_callback(req, items, pp);
 223:     };
 224:     req.onreadystatechange = function() {
 225:         if (req.readyState == 4) {
 226:             if (req.status == 200) {
 227:                 searchCafe_callback(req.responseText, items, pp);
 228:                 pp.error  = false;
 229:                 pp.errmsg = '';
 230:             } else {
 231:                 searchCafe_callback(req.responseText, items, pp);
 232:             }
 233:         }
 234:     };
 235:     try {
 236:         req.open('GET', url, true);
 237:         req.send(null);
 238:     } catch(e) {
 239:         pp.error  = true;
 240:         pp.errmsg = 'エラー:レストラン検索APIが呼び出せない.';
 241:         pp.hits   = 0;
 242:         searchCafe_callback(null, items, pp);
 243:     }
 244: }
 245: 
 246: /**
 247:  * レストラン検索APIのコールバック関数
 248:  * @param   String results APIのリターン値
 249:  * @param   Array  items[]   情報を格納する配列
 250:  * @param   Object pp{error:エラーフラグ,errmsg:エラーメッセージ,
 251:  *                    hits:ヒット件数,webapi:WebAPIのURL}
 252:  * @return  なし
 253: */
 254: function searchCafe_callback(results, items, pp) {
 255:     if (results !null) {
 256:         var json = JSON.parse(results);
 257:         var n = 1;
 258:         //情報取得
 259:         if (json.total_hit_count > 0) {
 260:             json.rest.forEach(function(val) {
 261:                 var obj = new Object();
 262:                 obj.id        = String.fromCodePoint('A'.charCodeAt(0+ n - 1);
 263:                 obj.title     = val.name;
 264:                 obj.url       = val.url;
 265:                 obj.category  = val.category;
 266:                 obj.phone     = val.tel;
 267:                 obj.opentime  = val.opentime.replace(/\n/ui, '<br />');
 268:                 obj.holiday   = val.holiday.replace(/\n/ui, '<br />');
 269:                 obj.address   = val.address.replace(/\n/ui, '<br />');
 270:                 obj.address   = obj.address.replace(/[0-9\-]+ /ui, '');
 271:                 obj.latitude  = parseFloat(val.latitude);
 272:                 obj.longitude = parseFloat(val.longitude);
 273:                 obj.description   = `
 274: <a href="${obj.url}" target="_blank">${obj.title}</a><br />電話:${obj.phone}<br />住所:${obj.address}<br />営業時間:${obj.opentime}<br />定休日:${obj.holiday}
 275: `;
 276:                 items.push(obj);
 277:                 n++;
 278:             });
 279:             pp.error  = false;
 280:             pp.errmsg = '';
 281:             pp.hits   = n;
 282: 
 283:         //エラー
 284:         } else if (typeof json.error !'undefined') {
 285:             pp.error  = true;
 286:             pp.errmsg = 'エラー:' + json.error[0].message;
 287:             pp.hits   = 0;
 288:         }
 289:     }
 290:     //Googleマップ描画
 291:     var zoom = distance2zoom($('#distance').val());
 292:     PGC.drawGMap(GMAPID, $('#latitude').val(), $('#longitude').val(), $('#type').val(), zoom, items);
 293: 
 294:     //一覧表作成
 295:     $('#results').html(makeTable(items));
 296: 
 297:     //エラーメッセージ
 298:     $('#errmsg').html(PGC.pp.errmsg);
 299: }
 300: 
 301: /**
 302:  * レストラン検索APIを利用して指定座標の近くにある喫茶店を検索し
 303:  * Googleマップにマッピングし、一覧表を表示する

ぐるなびWebサービス:レストラン検索API」を XMLHttpRequest によって呼び出し、店舗情報をオブジェクト配列 items に格納するのが、ユーザー関数 searchCafe である。
JavaScriptでWebAPIを利用して住所から緯度・経度を求める」と同様、非同期処理になるため、APIから戻ってくるJSONの処理はコールバック関数 searchCafe_callback で行う。

関数 searchCafe_callback では、WebAPIの応答を JSON.parse を使って解釈し、オブジェクト配列 items に格納してゆく。
Googleマップに描画する際に必要になるので、要素 description には、店舗名、電話番号、住所、営業時間、定休日とリンク先URLをハイパーリンクする。

最後に、Googleマップへの描画、一覧表の作成、エラーメッセージの表示を行い、コールバックを完了する。

参考サイト

(この項おわり)
header