地図に色々書いてみる
やりたいこと
皆さんこんにちは。i-Vinci TFZ部所属の藤田と申します。
会社内では元自衛官エンジニアとして主に力仕事と専守防衛を担当しています。
今回は、地図上に3Dでオブジェクトを描画すること、に挑戦してみたいと思います。
きっかけ
身近なMapサービスであるGoogle Mapを使って何か3Dで描画できると面白いかなっと、ふと思いました。
(実はお客さんが地図を使ったサービスを展開していて、現状2Dで色々と描画しているけどいずれ3Dで何か書きたくなるよね だって人間だもの ふじを と思ったのはここだけの話)
調査
Google Map APIに3D描画用のAPIとかないか調べたところ、、、
「調査の結果、、、何も得られませんでしたぁ!!!(某調査兵団風)」
Google Mapの機能として、主要都市周辺の建物などを3D描画する機能はあるものの、3Dを描画するためのAPIは提供されていないようです。
実装方針
調査の結果、3D描画のAPIは存在しないことが分かりました。しかし、直線や円などをMap上に描画するAPIは提供されていることが分かりました。
描画したい図形の頂点の緯度・経度を指定することにより、Map上に図形を描画することが出来ます。
また、通常地図は真上から見た鳥観図形式のアングルでブラウザ上に表示されますが、MapOptionとして定義されているtilt(画角とでもいうのでしょうか)を変更することにより、0°もしくは45°のどちらかのアングルを設定できるようです。
以上のことを踏まえて以下の2つの実装方針を考えました。
実装方針1(数学的にまじめな方法)
自分の絵心にほれぼれしますね。
少し補足させていただきます。補足に当たり以下のように用語を定義します。
viewPoint: 「め」と書いてある点
vertex: オブジェクトの各頂点
shadow: viewPointとvertexを通る直線と地面の交点
focusPoint: Map表示時に焦点を合わせている点
T: viewPointの高さ ≒ Mapのzoomレベルに応じた高さ
t: vertexの高さ
W: viewPointからshadowまでの水平距離(厳密には曲面である地表に沿った線の長さ。今回は短距離のため直線とみなす)
w: vertex & shadowからshadowまでの水平距離(厳密には曲面である地表に沿った線の長さ。今回は短距離のため直線とみなす)
横から見た位置関係を見てみましょう。
wを求めることにより、既知のfocusPointからのshadowまでの距離を計算し、その値を用いてshadowの緯度・経度を求めることが出来ます。
T : t = W : w
T : t = (T + Δw + w) : w
w = t(T + Δw) / (T - t)
ここから色々と何か計算することによって、shadowの緯度・経度が求まる気がします。
また、上から見た位置関係を見ると、
shadowとなる点については、viewPointとfocusPointを結ぶ線からそれぞれ∠a、∠bでvertex & shadow 1, 2から距離tの点の緯度・経度を求めるとよさそうです。
……でも、これ絶対大変ですね……
数学的な考慮がさらに必要になりそうです。
実装方針2(ざっくりとそれっぽい方法)
今回の期待値としてですが、実装方針1をまじめにやり精度を追い求めるほどのことは期待していません。なんとなくそれっぽく3Dで描ければ良い、といった感じ。
国土交通省のこのサイトの地図の計測機能をいろいろいじってみたところ、大体100mくらい南北、もしくは東西に距離を取ると、どちらも経緯度において5秒程度の差がありました。
その値を参考にして、例えば20mの高さのオブジェクトの上面のある頂点は、1秒ほど緯度を足して地図上に描画する、といった感じでやってみましょう。
準備
今回の実装では、Google Maps PlatformのMaps JavaScript APIを使用しますが、APIを使用するに当たりGCPアカウントと作成したプロジェクトに紐づくAPI Keyが必要になるようです。Get an API Key
Google Maps PlatformにはいくつかAPIが提供されていますが、有料のものも多く存在します。いうまでもありませんが、作成したAPI Keyの取り扱いには注意しましょう。Restrict API Key
また、実装に当たりnode.jsを使用しています。(version 12.18.0)
実装 - まずは平面から
i-Vinciの本社ビルの近くにi-Vinciの文字を「平面的」に書いてみましょう。
次にコードと使用したpolygonデータ(geojsonを使用していますが、nodeで読み込ませる関係上jsonとして定義しています。本質的には変わりないです。)
import geoData from './data/gsi20201101220423432.json';
let map: google.maps.Map;
function initMap(): void {
map = new google.maps.Map(document.getElementById("map") as HTMLElement, {
zoom: 17,
center: { lat: 35.693173, lng: 139.761801 },
});
map.data.addGeoJson(geoData);
map.data.setStyle({
fillColor: "orange",
fillOpacity: 0.75
});
}
export { initMap };
使用したpolygonデータ
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"_color": "#000000",
"_opacity": 0.5,
"_weight": 3,
"_fillColor": "#ff9900",
"_fillOpacity": 0.75
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
139.756533,
35.695138
],
[
139.756522,
35.694859
],
[
139.756898,
35.694833
],
[
139.756898,
35.695121
],
[
139.756533,
35.695138
]
]
]
}
},
{
"type": "Feature",
"properties": {
"_color": "#000000",
"_opacity": 0.5,
"_weight": 3,
"_fillColor": "#ff9900",
"_fillOpacity": 0.75
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
139.7665,
35.693883
],
[
139.766564,
35.692646
],
[
139.767283,
35.692681
],
[
139.766876,
35.69384
],
[
139.7665,
35.693883
]
]
]
}
},
{
"type": "Feature",
"properties": {
"_color": "#000000",
"_opacity": 0.5,
"_weight": 3,
"_fillColor": "#ff9900",
"_fillOpacity": 0.75
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
139.756533,
35.694519
],
[
139.756544,
35.693029
],
[
139.756908,
35.693003
],
[
139.756844,
35.694476
],
[
139.756533,
35.694519
]
]
]
}
},
{
"type": "Feature",
"properties": {
"_color": "#000000",
"_opacity": 0.5,
"_weight": 3,
"_fillColor": "#ff9900",
"_fillOpacity": 0.75
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
139.758378,
35.695704
],
[
139.759247,
35.692925
],
[
139.759891,
35.692925
],
[
139.761286,
35.696105
],
[
139.760513,
35.696079
],
[
139.759719,
35.693535
],
[
139.758786,
35.695922
],
[
139.758378,
35.695704
]
]
]
}
},
{
"type": "Feature",
"properties": {
"_color": "#000000",
"_opacity": 0.5,
"_weight": 3,
"_fillColor": "#ff9900",
"_fillOpacity": 0.75
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
139.757445,
35.694136
],
[
139.757445,
35.693761
],
[
139.758335,
35.693753
],
[
139.758282,
35.694084
],
[
139.757445,
35.694136
]
]
]
}
},
{
"type": "Feature",
"properties": {
"_color": "#000000",
"_opacity": 0.5,
"_weight": 3,
"_fillColor": "#ff9900",
"_fillOpacity": 0.75
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
139.761479,
35.694354
],
[
139.761425,
35.694136
],
[
139.761801,
35.69411
],
[
139.761844,
35.694345
],
[
139.761479,
35.694354
]
]
]
}
},
{
"type": "Feature",
"properties": {
"_color": "#000000",
"_opacity": 0.5,
"_weight": 3,
"_fillColor": "#ff9900",
"_fillOpacity": 0.75
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
139.761254,
35.693831
],
[
139.761028,
35.693012
],
[
139.761339,
35.693012
],
[
139.761608,
35.693831
],
[
139.761254,
35.693831
]
]
]
}
},
{
"type": "Feature",
"properties": {
"_color": "#000000",
"_opacity": 0.5,
"_weight": 3,
"_fillColor": "#ff9900",
"_fillOpacity": 0.75
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
139.762831,
35.694215
],
[
139.76252,
35.692977
],
[
139.763024,
35.692942
],
[
139.763153,
35.693753
],
[
139.763646,
35.693709
],
[
139.763603,
35.692977
],
[
139.763957,
35.692968
],
[
139.764065,
35.694093
],
[
139.76326,
35.694136
],
[
139.763153,
35.693901
],
[
139.76312,
35.69418
],
[
139.762831,
35.694215
]
]
]
}
},
{
"type": "Feature",
"properties": {
"_color": "#000000",
"_opacity": 0.5,
"_weight": 3,
"_fillColor": "#ff9900",
"_fillOpacity": 0.75
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
139.766039,
35.69404
],
[
139.764676,
35.694084
],
[
139.764515,
35.693003
],
[
139.765931,
35.692794
],
[
139.765953,
35.69316
],
[
139.765019,
35.693282
],
[
139.764998,
35.693779
],
[
139.765888,
35.6937
],
[
139.766039,
35.69404
]
]
]
}
},
{
"type": "Feature",
"properties": {
"_color": "#000000",
"_opacity": 0.5,
"_weight": 3,
"_fillColor": "#ff9900",
"_fillOpacity": 0.75
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
139.766361,
35.694702
],
[
139.766436,
35.694249
],
[
139.767058,
35.69431
],
[
139.766972,
35.694746
],
[
139.766361,
35.694702
]
]
]
}
}
]
}
非常に少ないコードでMap上にi-Vinciの文字を描画することが出来ました。
因みに今回使用したgeojsonのデータは、国土交通省が公開しているサイトを利用することで非常に簡単に作成することが出来ます。(別の記事で簡単に紹介したいと思います)国土交通省 国土情報ウェブマッピングシステム
実装 - 3D
では次は3Dでの描画に挑戦してみましょう。
今回表示するi-VinciオブジェクトのMap上のオブジェクトとしての高さは、40mとします。実装方針2で、緯度・経度において100mの差は5秒程度の差になることが分かっていますので、40m = 2秒ほどずらしてあげるといい感じに見えるのではないでしょうか。
まずは実行結果です。
初期表示がこちらです。最初の2D表示の時からZoomレベルとMapタイプを変更しています。
どうでしょうか?3Dに見えますでしょうか?
神田の地にi-Vinciの3D文字を刻んでやりました(笑)
では、使用したjsのコードとPolygonデータを見てみましょう。
import lowerGeoData from './data/gsi20201101220423432.json';
import upperGeoData from './data/upper_layer.json';
let map: google.maps.Map;
function initMap(): void {
map = new google.maps.Map(document.getElementById("map") as HTMLElement, {
zoom: 17.5,
center: { lat: 35.693173, lng: 139.761801 },
});
map.setMapTypeId(google.maps.MapTypeId.SATELLITE);
map.setTilt(45);
// オブジェクト(i-Vinciの立体文字列)の底面と上面のPolygonをoverlayに追加
map.data.addGeoJson(lowerGeoData);
map.data.addGeoJson(upperGeoData);
map.data.setStyle({
fillColor: "orange",
fillOpacity: 0.75
});
// オブジェクト(i-Vinciの立体文字列)の側面のpolylineを追加
for (let i = 0; i < lowerGeoData.features.length; i++){
for (let j = 0; j < lowerGeoData.features[i].geometry.coordinates[0].length; j++){
let lp = lowerGeoData.features[i].geometry.coordinates[0][j];
let up = upperGeoData.features[i].geometry.coordinates[0][j];
const line = new google.maps.Polyline({
path: [
{lat: lp[1], lng: lp[0]},
{lat: up[1], lng: up[0]}
],
map: map
});
}
}
}
export { initMap };
Polygonデータはオブジェクトの底面と上面のデータで2つに分けました。
オブジェクト底面のデータ
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"_color": "#000000",
"_opacity": 0.5,
"_weight": 3,
"_fillColor": "#ff9900",
"_fillOpacity": 0.75
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
139.756533,
35.695138
],
[
139.756522,
35.694859
],
[
139.756898,
35.694833
],
[
139.756898,
35.695121
],
[
139.756533,
35.695138
]
]
]
}
},
{
"type": "Feature",
"properties": {
"_color": "#000000",
"_opacity": 0.5,
"_weight": 3,
"_fillColor": "#ff9900",
"_fillOpacity": 0.75
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
139.7665,
35.693883
],
[
139.766564,
35.692646
],
[
139.767283,
35.692681
],
[
139.766876,
35.69384
],
[
139.7665,
35.693883
]
]
]
}
},
{
"type": "Feature",
"properties": {
"_color": "#000000",
"_opacity": 0.5,
"_weight": 3,
"_fillColor": "#ff9900",
"_fillOpacity": 0.75
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
139.756533,
35.694519
],
[
139.756544,
35.693029
],
[
139.756908,
35.693003
],
[
139.756844,
35.694476
],
[
139.756533,
35.694519
]
]
]
}
},
{
"type": "Feature",
"properties": {
"_color": "#000000",
"_opacity": 0.5,
"_weight": 3,
"_fillColor": "#ff9900",
"_fillOpacity": 0.75
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
139.758378,
35.695704
],
[
139.759247,
35.692925
],
[
139.759891,
35.692925
],
[
139.761286,
35.696105
],
[
139.760513,
35.696079
],
[
139.759719,
35.693535
],
[
139.758786,
35.695922
],
[
139.758378,
35.695704
]
]
]
}
},
{
"type": "Feature",
"properties": {
"_color": "#000000",
"_opacity": 0.5,
"_weight": 3,
"_fillColor": "#ff9900",
"_fillOpacity": 0.75
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
139.757445,
35.694136
],
[
139.757445,
35.693761
],
[
139.758335,
35.693753
],
[
139.758282,
35.694084
],
[
139.757445,
35.694136
]
]
]
}
},
{
"type": "Feature",
"properties": {
"_color": "#000000",
"_opacity": 0.5,
"_weight": 3,
"_fillColor": "#ff9900",
"_fillOpacity": 0.75
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
139.761479,
35.694354
],
[
139.761425,
35.694136
],
[
139.761801,
35.69411
],
[
139.761844,
35.694345
],
[
139.761479,
35.694354
]
]
]
}
},
{
"type": "Feature",
"properties": {
"_color": "#000000",
"_opacity": 0.5,
"_weight": 3,
"_fillColor": "#ff9900",
"_fillOpacity": 0.75
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
139.761254,
35.693831
],
[
139.761028,
35.693012
],
[
139.761339,
35.693012
],
[
139.761608,
35.693831
],
[
139.761254,
35.693831
]
]
]
}
},
{
"type": "Feature",
"properties": {
"_color": "#000000",
"_opacity": 0.5,
"_weight": 3,
"_fillColor": "#ff9900",
"_fillOpacity": 0.75
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
139.762831,
35.694215
],
[
139.76252,
35.692977
],
[
139.763024,
35.692942
],
[
139.763153,
35.693753
],
[
139.763646,
35.693709
],
[
139.763603,
35.692977
],
[
139.763957,
35.692968
],
[
139.764065,
35.694093
],
[
139.76326,
35.694136
],
[
139.763153,
35.693901
],
[
139.76312,
35.69418
],
[
139.762831,
35.694215
]
]
]
}
},
{
"type": "Feature",
"properties": {
"_color": "#000000",
"_opacity": 0.5,
"_weight": 3,
"_fillColor": "#ff9900",
"_fillOpacity": 0.75
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
139.766039,
35.69404
],
[
139.764676,
35.694084
],
[
139.764515,
35.693003
],
[
139.765931,
35.692794
],
[
139.765953,
35.69316
],
[
139.765019,
35.693282
],
[
139.764998,
35.693779
],
[
139.765888,
35.6937
],
[
139.766039,
35.69404
]
]
]
}
},
{
"type": "Feature",
"properties": {
"_color": "#000000",
"_opacity": 0.5,
"_weight": 3,
"_fillColor": "#ff9900",
"_fillOpacity": 0.75
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
139.766361,
35.694702
],
[
139.766436,
35.694249
],
[
139.767058,
35.69431
],
[
139.766972,
35.694746
],
[
139.766361,
35.694702
]
]
]
}
}
]
}
オブジェクト上面のデータ
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"_color": "#000000",
"_opacity": 0.5,
"_weight": 3,
"_fillColor": "#ff9900",
"_fillOpacity": 0.75
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
139.756533,
35.695338
],
[
139.756522,
35.695059
],
[
139.756898,
35.695033
],
[
139.756898,
35.695321
],
[
139.756533,
35.695338
]
]
]
}
},
{
"type": "Feature",
"properties": {
"_color": "#000000",
"_opacity": 0.5,
"_weight": 3,
"_fillColor": "#ff9900",
"_fillOpacity": 0.75
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
139.7665,
35.694083
],
[
139.766564,
35.692846
],
[
139.767283,
35.692881
],
[
139.766876,
35.69404
],
[
139.7665,
35.694083
]
]
]
}
},
{
"type": "Feature",
"properties": {
"_color": "#000000",
"_opacity": 0.5,
"_weight": 3,
"_fillColor": "#ff9900",
"_fillOpacity": 0.75
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
139.756533,
35.694719
],
[
139.756544,
35.693229
],
[
139.756908,
35.693203
],
[
139.756844,
35.694676
],
[
139.756533,
35.694719
]
]
]
}
},
{
"type": "Feature",
"properties": {
"_color": "#000000",
"_opacity": 0.5,
"_weight": 3,
"_fillColor": "#ff9900",
"_fillOpacity": 0.75
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
139.758378,
35.695904
],
[
139.759247,
35.693125
],
[
139.759891,
35.693125
],
[
139.761286,
35.696305
],
[
139.760513,
35.696279
],
[
139.759719,
35.693735
],
[
139.758786,
35.696122
],
[
139.758378,
35.695904
]
]
]
}
},
{
"type": "Feature",
"properties": {
"_color": "#000000",
"_opacity": 0.5,
"_weight": 3,
"_fillColor": "#ff9900",
"_fillOpacity": 0.75
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
139.757445,
35.694336
],
[
139.757445,
35.693961
],
[
139.758335,
35.693953
],
[
139.758282,
35.694284
],
[
139.757445,
35.694336
]
]
]
}
},
{
"type": "Feature",
"properties": {
"_color": "#000000",
"_opacity": 0.5,
"_weight": 3,
"_fillColor": "#ff9900",
"_fillOpacity": 0.75
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
139.761479,
35.694554
],
[
139.761425,
35.694336
],
[
139.761801,
35.69431
],
[
139.761844,
35.694545
],
[
139.761479,
35.694554
]
]
]
}
},
{
"type": "Feature",
"properties": {
"_color": "#000000",
"_opacity": 0.5,
"_weight": 3,
"_fillColor": "#ff9900",
"_fillOpacity": 0.75
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
139.761254,
35.694031
],
[
139.761028,
35.693212
],
[
139.761339,
35.693212
],
[
139.761608,
35.694031
],
[
139.761254,
35.694031
]
]
]
}
},
{
"type": "Feature",
"properties": {
"_color": "#000000",
"_opacity": 0.5,
"_weight": 3,
"_fillColor": "#ff9900",
"_fillOpacity": 0.75
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
139.762831,
35.694415
],
[
139.76252,
35.693177
],
[
139.763024,
35.693142
],
[
139.763153,
35.693953
],
[
139.763646,
35.693909
],
[
139.763603,
35.693177
],
[
139.763957,
35.693168
],
[
139.764065,
35.694293
],
[
139.76326,
35.694336
],
[
139.763153,
35.694101
],
[
139.76312,
35.69438
],
[
139.762831,
35.694415
]
]
]
}
},
{
"type": "Feature",
"properties": {
"_color": "#000000",
"_opacity": 0.5,
"_weight": 3,
"_fillColor": "#ff9900",
"_fillOpacity": 0.75
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
139.766039,
35.69424
],
[
139.764676,
35.694284
],
[
139.764515,
35.693203
],
[
139.765931,
35.692994
],
[
139.765953,
35.69336
],
[
139.765019,
35.693482
],
[
139.764998,
35.693979
],
[
139.765888,
35.6939
],
[
139.766039,
35.69424
]
]
]
}
},
{
"type": "Feature",
"properties": {
"_color": "#000000",
"_opacity": 0.5,
"_weight": 3,
"_fillColor": "#ff9900",
"_fillOpacity": 0.75
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
139.766361,
35.694902
],
[
139.766436,
35.694449
],
[
139.767058,
35.69441
],
[
139.766972,
35.694946
],
[
139.766361,
35.694902
]
]
]
}
}
]
}
オブジェクトの側面については、底面と上面のデータから側面の辺を形成する点を指定し、Polylineとして描画しています。そのためMap上ではオブジェクトの側面は色なしの透明な面として描画しています。
まとめと今後の課題
Google Maps PlatformのMaps JavaScript APIを使用することで、無事、3D(っぽい)オブジェクトをMap上に描画することが、非常に簡単に出来ました。
今後の課題としては以下のような点があります。
1. 立体としての描画が正確でない
2. 視点(viewPoint)を水平方向や上下方向に移動した場合に、立体の見え方が変化しない≒shadowとなる点が移動しない
3. ZoomOutした場合に、立体の見え方が変化しない≒shadowとなる点が移動しない
上記3点を解決することで、さらに立体に見えるオブジェクトをMap上に描画することが出来るようになるはずです。
今後の課題として取り組みたいと思います。
今回作成したコードはこちら