🤯

Google Photorealistic 3D Tiles で地名ラベルを重ねて表示したかった

2023/05/14に公開3

はじめに

いま巷で話題のGoogle Photorealistic 3D Tiles、皆様もう試されましたか?Mapboxの記事でGoogleの機能を試してみるというのもどうなのか?という疑問もありましたが、このビッグウェーブに乗らない訳にはいかないので触ってみました。

ここでは、3Dの上に地名・道路の名前・POIが表示されたら便利だなぁ、思って悪戦苦闘した過程を記事にしました。

ラベルを重ねる方法の検討

今回はdeck.glを使ってラベルを重ねる方法を検討しました。deck.glを使うとレイヤーを重ねて表示できるので、Photorealistic 3D Tilesの上に既存のラベルのレイヤーを重ねればよいのでは?という単純な発想です。ここでは3通りの方法について検討しました。

  • ベースマップにMapbox GL JSを使用し、road-labelレイヤーの下にPhotorealistic 3D TilesをInterleavedで挿入
  • ベースマップにGoogle Mapsを使用し、Photorealistic 3D TilesをInterleavedで挿入
  • ベースマップを使用せず、Photorealistic 3D Tilesのレイヤーの上にGeoJsonLayerでラベルデータを重ねる

deck.glとは

deck.glは地図にまつわる様々なレイヤーが定義されたライブラリで、WebGLが使用されています。deck.gl自体に地図を描画する機能はなく、外部から何らかの形で取り込む必要があります。この外部から取り込む地図をベースマップと呼び、Mapbox GL JSやGoogle Mapsがベースマップとして使用可能です。

また、ベースマップに対してレイヤーを作用させる方法としてOverlaidとInterleavedの二種類があります。Overlaidはベースマップの上にdeck.glのレイヤーを覆いかぶせる描画方法です。そのため、deck.glのレイヤーが広い面積を塗りつぶすようなケースではベースマップが隠れます。それに対し、Interlevedはベースマップのレイヤーの間にdeck.glのレイヤーを挟み込めます。これにより、広い面積を塗りつぶしつつ、道路や建物は表示するという表現が可能になります。このあたりの挙動については以下の記事もご参照ください。

deck.glでPhotorealistic 3D Tilesを表示する

こちらの記事を参考にdeck.glでPhotorealistic 3D Tilesを表示する方法を試しました。具体的にはTile3DLayerを使用します。

まず、以下のdeck.glを読み込みます。

<script src="https://unpkg.com/deck.gl@^8.9.0/dist.min.js"></script>

CSSを設定します。

<style>
  body { margin: 0; padding: 0; }
  #map { position: absolute; top: 0; bottom: 0; width: 100%; }
</style>

次に地図を表示する場所を作ります。

<div id="map"></div>

JavaScriptのコードは以下のようになります。ポイントはTile3DLayerdataですが、このURLはGoogleのドキュメントに記載されています。APIキーの渡し方もクエリパラメータではなくx-goog-api-keyヘッダを使用します。これはタイルデータを取得するために順次APIアクセスが発生するため、その際に使用するキーを明示する必要があるためです。loadOptionsの使い方はこちらの例をご参照ください。

また、事前にMap Tiles APIをEnableしておく必要があるのでご注意ください。

const deckgl = new deck.DeckGL({
  container: 'map',

  initialViewState: {
    latitude: 35.6736926988029,
    longitude: 139.7586677640881,
    zoom: 16,
    pitch: 50
  },
  controller: true,

  layers: [
    new deck.Tile3DLayer({
        id: 'tile-3d-layer',
        data: 'https://tile.googleapis.com/v1/3dtiles/root.json',
        loadOptions: {
            fetch: { headers: { 'X-GOOG-API-KEY': YOUR_GOOGLE_API_KEY }}
        },
    }),
  ]
});

結果は以下のとおりです。きれいに表示されて感動ですね!ちなみにShiftキーを押下しながらドラッグするとTilt(Pitch)が変えられます。

それぞれの方法の検討

それでは早速3通りの方法について見ていきましょう!

ベースマップがMapbox GL JS

deck.glでベースマップをMapbox GL JSにする方法は、こちらのサンプルをご覧いただくとわかりやすいです。Interleavedで使用する際には以下のようにinterleaved: trueを指定し、さらにレイヤーでbeforeIdを指定します。あとは//ここの部分に先程と同じコードを書けば動きます。

export const overlay = new MapboxOverlay({
  interleaved: true,
  layers: [
    new Tile3DLayer({
      beforeId: 'road-label',
      //ここ
    }),
  ]
});

・・・が、Mapbox GL JS上でMapboxのタイルセット(またはスタイル)と組み合わせて使用することはGoogle Mapsの利用規約の以下の条文に抵触すると考えられます。

(e) No Use With Non-Google Maps. To avoid quality issues and/or brand confusion, Customer will not use the Google Maps Core Services with or near a non-Google Map in a Customer Application. For example, Customer will not (i) display or use Places content on a non-Google map, (ii) display Street View imagery and non-Google maps on the same screen, or (iii) link a Google Map to non-Google Maps content or a non-Google map.

ということで、Photorealistic 3D Tilesの上にラベルを表示する事はできたのですが、これはボツとします。結果の画像の掲載も控えさせていただきます。

ベースマップがGoogle Maps

こちらは利用規約に抵触しないので、早速試してみましょう!まず、以下のように地図の表示場所とライブラリを読み込みを行います。

<div id="map"></div>
<script src="https://unpkg.com/deck.gl@^8.9.0/dist.min.js"></script>
<script async src="https://maps.googleapis.com/maps/api/js?key=AIzaSyCfX7L7RJvQa31tA7sL-dvoXAsNBT3786A&callback=initMap"></script>

CSSを設定します。

<style>
  body { margin: 0; padding: 0; }
  #map { position: absolute; top: 0; bottom: 0; width: 100%; }
</style>

JavaScriptのコードは以下のようになります。

function initMap() {
  const map = new google.maps.Map(document.getElementById("map"), {
    center: { lat: 35.67369269880291, lng: 139.7586677640881 },
    zoom: 17,
    tilt: 50,
    mapId: YOUR_MAP_ID
  });

  const deckOverlay = new deck.GoogleMapsOverlay({
    layers: [
    new deck.Tile3DLayer({
        id: 'tile-3d-layer',
        data: 'https://tile.googleapis.com/v1/3dtiles/root.json',
        loadOptions: {
            fetch: { headers: { 'X-GOOG-API-KEY': YOUR_GOOGLE_API_KEY }}
        },
    })
    ]
  });
  
  deckOverlay.setMap(map);
}

window.initMap = initMap;

mapIdはクラウドカスタマイズ機能で作成したベクターマップのMap IDを指定してください。これはGoogle MapsでInterleavedを使用するにはベクターマップを使用する必要があるためです。詳しくは以下の記事をご参照ください。

結果は以下のとおりです。ちゃんとラベルが重なって表示されていますね!ただ、ズームやスクロールをするとベースマップのビルや道路が一瞬チラチラと見えてしまいます。

GeoJSONを重ねる

こちらの記事にあるように、GeoJsonLayerを重ねて表示することができます。

However, since the launch of version 8.9, deck.gl now seamlessly aligns terrain, making it effortless to overlay other data into the Tile3DLayer. It’s as simple as this:

GeoJSONで表示するためには、実際の地図上のデータをどうにかして入手する必要があります。ここではその部分は少し忘れて、簡単なGeoJSONを表示することを試してみます。

まず、geojson.ioで以下のようなポリゴン1個、ポイント1個からなるGeoJSONデータを作成しました。

GeoJSON

HTML, CSSは「deck.glでPhotorealistic 3D Tilesを表示する」と同じです。JavaScriptは以下のとおりです。

const data = {
  type: "FeatureCollection",
  features: [
    {
      type: "Feature",
      properties: {},
      geometry: {
        coordinates: [
          [
            [139.75929236950446, 35.674510172038055],
            [139.75779857768703, 35.67307710722632],
            [139.76000369894115, 35.67225655046701],
            [139.7606154422578, 35.673134892595115],
            [139.75930659609452, 35.67349316095135],
            [139.75929236950446, 35.674510172038055]
          ]
        ],
        type: "Polygon"
      }
    },
    {
      type: "Feature",
      properties: { name: "LABEL" },
      geometry: {
        coordinates: [139.75826805511542, 35.67475286595004],
        type: "Point"
      }
    }
  ]
};

const deckgl = new deck.DeckGL({
  container: "map",

  initialViewState: {
    latitude: 35.6736926988029,
    longitude: 139.7586677640881,
    zoom: 16,
    pitch: 50
  },
  controller: true,

  layers: [
    new deck.Tile3DLayer({
      id: "tile-3d-layer",
      data: "https://tile.googleapis.com/v1/3dtiles/root.json",
      loadOptions: {
        fetch: {
          headers: {
            "X-GOOG-API-KEY": YOUR_GOOGLE_API_KEY
          }
        }
      },
      operation: "terrain+draw"
    }),

    new deck.GeoJsonLayer({
      id: "geojson-layer",
      data,
      getText: (f) => {
        return f.properties.name;
      },
      getPointRadius: 10,
      getTextColor: [255, 0, 0],
      getFillColor: [0, 255, 0],
      pointType: "circle+text",
      extensions: [new deck._TerrainExtension()]
    })
  ]
});

結果は以下のとおりです。Photorealistic 3D Tilesで描画されたビルをterrain(標高データ)とみなし、GeoJSONをその表面にテクスチャのように貼り付ける挙動をするようです。ポリゴンは貼り付けられますが、ポイントは隠れてしまうようです。残念。

まとめ

deck.glで地名ラベルを表示するにはベースマップをGoogle Mapsにするのが簡単なようです。他にもいいやり方があったら、ぜひ教えてください!

GitHubで編集を提案
マップボックス・ジャパン合同会社

Discussion

mnrmnr

素晴らしい記事ありがとうございます。
coordinatesのところを100にしてみたらラベル見えました。

{
  type: "Feature",
  properties: { name: "LABEL" },
  geometry: {
    coordinates: [139.75826805511542, 35.67475286595004,100],
    type: "Point"
  }
}
OttyLabOttyLab

mnrさん

貴重な情報ありがとうございます!なるほど、標高を入れることでPhotorealistic 3D Tilesで隠されてしまっていた部分が見えるようになるんですね。角度にもよりますが、標高を1m程度に設定しても概ね表示されたので、これを使って実験してみたいと思います。

OttyLabOttyLab

なるほど、Pitchが0より大きいとPointが40m程度下に潜るんですね。

この数値は東京湾のジオイド高に近いので、Pitchが設定されているときにはPointの標高が楕円体高のように扱われているように見えます。しかしながら、私自身deck.glで標高がどのように扱われているのか存じ上げないので詳細はわかりません。

初期Pitchを0でPointを描画し、後でユーザーが手動でPitchを変える分には問題なさそうです。