「ポケモンGOみたいなゲーム作って〜」と言われたときのために、巨人(Google)の力をかりて、道路上にランダムにマーカーを設置する。
そろそろ、世間では「ポケモンGOみたいの作ってー作ってー」という無茶振りと、それに伴うエンジニアの悲鳴が聞こえてくる季節が近づいてきたかと思います。そこで、いつそんな無茶振りが来ても対応できるように、位置情報を用いたゲームを作る際の高いハードル「人が行ける場所にモンスターを配置する」という難問をグーグル様のお力を利用して、とりあえず形だけでもなんとか取り繕ってみたいと思います。
一応言っておきますと……あんまり、実用的ではないですよ。
ランダムに緯度経度を生成する
まず、手始めに日本が収まる範囲に限定してランダムに1000件の緯度経度を生成します。
turf.jsを使用するとランダムなポイントデータをgeojsonとして簡単に生成できます。
1 2 3 |
var points = turf.random('points', 1000, { bbox:[117.195432271875, 25.96808355647172,160.877072896875,46.460689156557244] }) |
生成したgeojsonをマーカーとして地図上に表示したのが以下となります。
ランダムに生成されてはいますが、海の上など不要な場所に大量にマーカーが配置されてしまいました。次のフェーズでは、日本列島以外に配置された不要なマーカーを除去します。
日本列島上の位置情報のみ残してほかを消す
不要な緯度経度データをそぎ落とし、 日本列島上に収まるデータだけを取り出してみます。
turf.jsには、ポイントデータが特定のポリゴン内に含まれているかどうかを判別するためのメソッドがあるので、大まかな日本列島のアウトラインをポリゴンとして生成し、そのポリゴン内に含まれているかどうかを条件としてフィルタリングを行います。
・アウトラインポリゴン
日本列島アウトライン | Github Gist
上記、geojsonを読み込み先ほど生成したマーカー群とのマッチングを行います。
1 2 3 4 |
var insidePoints = {type: "FeatureCollection", features: []} points.features.forEach(function(d){ if (turf.inside(d, geojson.features[0])) insidePoints.features.push(d) }) |
フィルタリング後の緯度経度を地図上にマーカーとして表示すると以下となります。
道路上の緯度経度のみを選ぶ
さて、ここからが本番です。
海上に配置されていたマーカーは綺麗さっぱり消すことができましたが、今のままでは、山の上だったり川の中だったりと、人の行けないような場所にもたくさんのマーカーが設置されてしまっています。
この中から、道路上に配置されたマーカーのみを残す、あるいは近くの道路までマーカーを移動するにはどうしたらよいでしょうか?
なかなかの難問ですが、Google様の力を利用することで一部解決できます。
キーとなるのは「ストリートビューAPI」
ストリートビューを表示する以外にも、非常に便利な機能が詰まったこのAPIを使って、ランダムに生成された緯度経度から道路上の緯度経度を取得します。
StreetViewServiceクラスが持つ、getPanoramaByLocationメソッドは、特定の緯度経度から指定した半径(下記では100m)以内にストリートビューに対応したポイントがないかを検索し、対応しているポイントがあればその緯度経度を返します。
このメソッドに、残りの緯度経度データを全て突っ込んで、手当たりしだいに検索リクエストを投げます。
getPanoramaByLocationのレスポンスに含まれる緯度経度は、ストリートビューに対応した「人が訪れることができる場所」として別途保存します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
var streetViewService = new google.maps.StreetViewService(); function searchStreetViewPoint(coordinates, callback){ var latlng = new google.maps.LatLng(coordinates[1], coordinates[0]); var reslut setTimeout(function(){ streetViewService.getPanoramaByLocation(latlng, 100, function(status){ if(status){ reslut = [status.location.latLng.lng() , status.location.latLng.lat()] }else{ reslut = false } callback(reslut) }) }, 100) } |
ちなみに、getPanoramaByLocationメソッドは非同期処理になるので、プロミスで包んで最後にまとめて取得しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
var PromiseArray = [] //ストリートビューへの問い合わせをプロミスで包む var pGen = (function(d){ return new Promise(function(resolve, reject){ var coordinates = d.geometry.coordinates var latlng = new google.maps.LatLng(coordinates[1], coordinates[0]) streetViewService.getPanoramaByLocation(latlng, 100, function(status){ if(status){ reslut = [status.location.latLng.lng() , status.location.latLng.lat()] }else{ reslut = false } resolve(reslut) }) }) }) //ポイント分の問い合わせを配列に保存 insidePoints.features.forEach(function(d){ PromiseArray.push(pGen(d)) }) //まとめて問い合わせ Promise.all(PromiseArray).then(function(data){ var filtertedPoint = {type: "FeatureCollection", features: []} data.filter(function(d){ return d }) .forEach(function(d){ filtertedPoint.features.push({ "type": "Feature", "properties": {}, "geometry": { "type": "Point", "coordinates": d } }) }) var onEachFeature = function(feature, layer) { layer.on('click', function (e) { map.setView(e.latlng, 16); }) } L.geoJson(filtertedPoint, {onEachFeature: onEachFeature}).addTo(map) }) |
上記の方法でフィルタリングした緯度経度を地図上に表示したのが以下。だいぶ数が減ってしまいましたが、表示されているマーカーは、すべて人が行ける場所(ほとんどが道路上)に配置されています。
マーカーにはクリックすると、その位置へズームするイベントがバインドされていますので、正しく配置されているか確認してみてください。
サンプルコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
d3.json("japan_outline.geojson", main) function main(geojson) { var map = initMap() var streetViewService = new google.maps.StreetViewService(); var points = turf.random('points', 1000, { bbox:[117.195432271875, 25.96808355647172,160.877072896875,46.460689156557244] }) var insidePoints = {type: "FeatureCollection", features: []} points.features.forEach(function(d){ if (turf.inside(d, geojson.features[0])) insidePoints.features.push(d) }) var PromiseArray = [] //ストリートビューへの問い合わせをプロミスで包む var pGen = (function(d){ return new Promise(function(resolve, reject){ var coordinates = d.geometry.coordinates var latlng = new google.maps.LatLng(coordinates[1], coordinates[0]) streetViewService.getPanoramaByLocation(latlng, 100, function(status){ if(status){ reslut = [status.location.latLng.lng() , status.location.latLng.lat()] }else{ reslut = false } resolve(reslut) }) }) }) //ポイント分の問い合わせを配列に保存 insidePoints.features.forEach(function(d){ PromiseArray.push(pGen(d)) }) //まとめて問い合わせ Promise.all(PromiseArray).then(function(data){ var filtertedPoint = {type: "FeatureCollection", features: []} data.filter(function(d){ return d }) .forEach(function(d){ filtertedPoint.features.push({ "type": "Feature", "properties": {}, "geometry": { "type": "Point", "coordinates": d } }) }) var onEachFeature = function(feature, layer) { layer.on('click', function (e) { map.setView(e.latlng, 16); }) } L.geoJson(filtertedPoint, {onEachFeature: onEachFeature}).addTo(map) }) //地図初期化 function initMap(){ var map = L.map('map').setView([36.322356, 139.013057], 4); var GmapsROA = new L.Google('ROADMAP'); //地図 var GmapsSAT = new L.Google('SATELLITE'); //航空写真 var GmapsHYB = new L.Google('HYBRID'); //航空写真&ラベル var GmapsTER = new L.Google('TERRAIN'); //地形地図 map.addLayer(GmapsROA); map.addControl(new L.Control.Layers({ 'Google Roadmap':GmapsROA, 'Google Satellite':GmapsSAT, 'Google Hybrid':GmapsHYB, 'Google Terrain':GmapsTER }, {})); return map } } |
更新されるたびにランダムにマーカーを設置します。
APIの制限数を越えると設置されなくなります。
example
まとめ
最初に生成する緯度経度の数を増やせば、最終的に取得できる数も増えるわけですが、その分Streetviewへのリクエストも増えるので注意してください。今回はAPIの無料制限(1日2500リクエスト)の範囲で収まるように少なめの数で生成しています。