aptpod Tech Blog

株式会社アプトポッドのテクノロジーブログです

VM2Mビジュアルパーツに Googleマップを3Dで表示してみた

aptpod Advent Calendar 2022 6日目の記事を担当する、Visual M2M グループの白金です。

弊社製品の Visual M2M Data Visualizer では、計測データを可視化するための様々なビジュアルパーツを提供しています。

その中の一つに、計測データに含まれる位置情報をもとにGoogleマップに現在位置を表示するビジュアルパーツが含まれています。

今回は、下図のように Googleマップのビジュアルパーツを3Dで表現する機能を試してみたので紹介したいと思います。

ビジュアルパーツで Googleマップを3Dで表示

はじめに

弊社製品の Visual M2M Data Visualizer は Google Chrome のWebブラウザで提供しており、Googleマップのビジュアルパーツは、Google Maps Platform から提供されている JavasScript API を使用しています。

このGoogleマップを3Dで表現するために WebGL Overlay View で構築する 3D マップ エクスペリエンス を参考に試してみました。

マップIDを準備する

Google Maps Platform では、Google Cloud Console で作成したマップのスタイルをマップIDに関連づけることができます。

JavaScript API で WebGL の機能を利用するためには、ベクター地図を有効にしたマップIDが必要になります。

設定方法については下記リンク先のページを参照ください。

developers.google.com

実装する

細かな実装方法は、各APIの説明については下記リンク先のステップ4〜8で説明が掲載されています。

developers.google.com

ここでは下記2点を表示するための実装を紹介します。

  1. 3Dモデルで現在位置の表示する
  2. GeoJSON を使用して軌跡を表示する

下記NPMパッケージを使用して、TypeScript、React で実装します。

npm i -S typescript react react-dom google-map-react three
npm i -D @types/google-map-react @types/google.maps

React の Component を実装します。

3DモデルのGLTFデータはこちらのサンプルデータを拝借しました。

import React, { memo, useEffect, useCallback, useRef, useState } from 'react'

import GoogleMapReact from 'google-map-react'
import * as THREE from 'three'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'

// WebGLOverlayView のサンプルで公開されているGLTFデータを参照します。
import PIN_GLTF from './pin.gltf'

// 当コンポーネントで指定する位置情報の型です。
type Coordinate = {
  lat: number
  lng: number
  heading: number
}

// 当コンポーネントのPropsの型です。
type Props = {
  /** Googleマップを表示するための Api Key */
  mapApiKey: string
  /** ベクター地図を有効にしたマップIDを指定します。 */
  mapId: string
  /** Googleマップの初期表示位置情報 */
  defaultCenter: GoogleMapReact.Coords
  /** Googleマップの初期表示ズーム値 */
  defaultZoom: number
  /** 3Dモデルの表示位置情報 */
  model3dCoordinate: Coordinate | undefined
  /** 軌跡の位置情報リスト */
  trajectoryCoordinates: Coordinate[]
}

const MODEL_3D_ALTITUDE = 80
const TRAJECTORY_STROKE_COLOR = '#a00'
const TRAJECTORY_STROKE_WEIGHT = 6

export const GoogleMaps3dSample: React.FC<Props> = memo((props) => {
  const {
    mapApiKey,
    mapId,
    defaultCenter,
    defaultZoom,
    model3dCoordinate,
    trajectoryCoordinates,
  } = props

  // ロードした Google Maps Api のインスタンスを格納します。
  const [mapApi, setMapApi] = useState<{
    map: google.maps.Map
    maps: typeof google.maps
  } | null>(null)

  // WebGLOverlayView で3Dモデルの表示位置を参照するための変数にコピーします。
  const refModel3dCoordinate = useRef<Coordinate | undefined>()
  useEffect(() => {
    refModel3dCoordinate.current = model3dCoordinate
  }, [model3dCoordinate])

  // Googleマップに表示する軌跡のスタイルを定義します。
  useEffect(() => {
    mapApi?.map.data.setStyle(() => {
      return {
        strokeColor: TRAJECTORY_STROKE_COLOR,
        strokeWeight: TRAJECTORY_STROKE_WEIGHT,
      }
    })
  }, [mapApi])

  // 軌跡のデータをGeoJSONフォーマットに変換してGoogleマップに表示します。
  useEffect(() => {
    if (!mapApi) {
      return () => {}
    }

    const geoJSON = {
      type: 'FeatureCollection',
      features: [
        {
          type: 'Feature',
          properties: {},
          geometry: {
            type: 'MultiLineString',
            coordinates: [
              trajectoryCoordinates.map(({ lat, lng }) => [lng, lat]),
            ],
          },
        },
      ],
    }

    const features = mapApi.map.data.addGeoJson(geoJSON)

    return () => {
      features.forEach((feature: any) => {
        mapApi.map.data.remove(feature)
      })
    }
  }, [mapApi, trajectoryCoordinates])

  // WebGLOverLayView を使用してGoogle Mapに3Dモデルを表示します。
  useEffect(() => {
    if (!mapApi) {
      return
    }

    const webGLOverlayView = new mapApi.maps.WebGLOverlayView()

    let scene: THREE.Scene
    let camera: THREE.PerspectiveCamera
    let renderer: THREE.WebGLRenderer
    const model3dGroup: THREE.Group = new THREE.Group()

    webGLOverlayView.onAdd = () => {
      // Scene、Camera の情報をセットアップします。
      scene = new THREE.Scene()
      camera = new THREE.PerspectiveCamera()
      const ambientLight = new THREE.AmbientLight(0xffffff, 0.75)
      scene.add(ambientLight)
      const directionalLight = new THREE.DirectionalLight(0xffffff, 0.25)
      directionalLight.position.set(0.5, -1, 0.5)
      scene.add(directionalLight)

      // 3Dモデル(GLTF)をロードします。
      const loader = new GLTFLoader()
      const source = PIN_GLTF
      loader.load(source, (gltf) => {
        // ロードしたGLTFのスケール、姿勢角の表示調整
        gltf.scene.scale.set(25, 25, 25)
        gltf.scene.rotation.x = (180 * Math.PI) / 180
        model3dGroup.add(gltf.scene)
        scene.add(model3dGroup)
      })
    }

    webGLOverlayView.onContextRestored = ({ gl }) => {
      // renderer を作成します。
      renderer = new THREE.WebGLRenderer({
        canvas: gl.canvas,
        context: gl,
        ...gl.getContextAttributes(),
      })

      renderer.autoClear = false
    }

    webGLOverlayView.onDraw = ({ transformer }) => {
      // 3Dモデルを表示する位置情報を作成します。
      const latLngAltitudeLiteral = {
        lat: refModel3dCoordinate.current?.lat ?? 0,
        lng: refModel3dCoordinate.current?.lng ?? 0,
        altitude: MODEL_3D_ALTITUDE,
      }

      // 3Dモデルの位置情報が無効な場合は非表示にします。
      model3dGroup.visible = Boolean(refModel3dCoordinate.current)

      // 参照する3DモデルのZ軸の回転角度を設定します。
      // heading と回転方向が逆のため、反転しています。
      model3dGroup.rotation.z =
        (-1 * (refModel3dCoordinate.current?.heading ?? 0) * Math.PI) / 180

      // 3Dモデルの表示情報を renderer に反映します。
      const matrix = transformer.fromLatLngAltitude(latLngAltitudeLiteral)
      camera.projectionMatrix = new THREE.Matrix4().fromArray(matrix)
      webGLOverlayView.requestRedraw()
      renderer.render(scene, camera)
      renderer.resetState()
    }

    webGLOverlayView.setMap(mapApi.map)
  }, [mapApi])

  // MouseDown のイベントハンドラを無効にします。
  // Data Visualizer 本体でドラッグイベントを使用するためです。
  const onMouseDownEventCancel = useCallback(
    (evt: React.MouseEvent<HTMLElement>) => {
      evt.preventDefault()
    },
    [],
  )

  return (
    <div
      role="button"
      tabIndex={0}
      style={{ width: '100%', height: '100%' }}
      onMouseDown={onMouseDownEventCancel}
    >
      <GoogleMapReact
        bootstrapURLKeys={{
          key: mapApiKey,
          // ベクター地図を有効にしたマップIDを有効にするため、version に beta を指定します。
          version: 'beta',
        }}
        options={{
          mapId,
          tilt: 60,
          heading: 0,
        }}
        defaultCenter={defaultCenter}
        defaultZoom={defaultZoom}
        onGoogleApiLoaded={setMapApi}
      />
    </div>
  )
})

上記ソースコードで実装した React Component を呼び出します。

mapId は、事前に準備したベクター地図を有効にしたマップIDを指定します。

mapApiKey は Google Maps Platform で作成したAPIキーを指定します。APIキーの作成手順についてはこちらをご確認ください。

<GoogleMaps3dSample
  mapApiKey="xxxxxx"
  mapId="xxxxxx"
  defaultCenter={{
    lat: 35.68783052263802,
    lng: 139.71728196798034,
  }}
  defaultZoom={19}
  model3dCoordinate={{
    lat: 35.68781633,
    lng: 139.7180315,
    heading: 80,
  }}
  trajectoryCoordinates={[
    { lat: 35.68778533, lng: 139.71701067, heading: 79 },
    { lat: 35.68778533, lng: 139.71782767, heading: 79 },
    { lat: 35.68778533, lng: 139.71782767, heading: 80 },
    { lat: 35.68778817, lng: 139.71784633, heading: 80 },
    { lat: 35.68778817, lng: 139.71784633, heading: 80 },
    { lat: 35.687791, lng: 139.71786467, heading: 79 },
    { lat: 35.687791, lng: 139.71786467, heading: 79 },
    { lat: 35.68779417, lng: 139.717883, heading: 79 },
    { lat: 35.68779417, lng: 139.717883, heading: 79 },
    { lat: 35.68779767, lng: 139.71790067, heading: 79 },
    { lat: 35.68779767, lng: 139.71790067, heading: 79 },
    { lat: 35.687801, lng: 139.71791883, heading: 79 },
    { lat: 35.687801, lng: 139.71791883, heading: 79 },
    { lat: 35.68780417, lng: 139.7179375, heading: 79 },
    { lat: 35.68780417, lng: 139.7179375, heading: 79 },
    { lat: 35.687807, lng: 139.71795633, heading: 79 },
    { lat: 35.687807, lng: 139.71795633, heading: 79 },
    { lat: 35.68780933, lng: 139.717975, heading: 79 },
    { lat: 35.68780933, lng: 139.717975, heading: 79 },
    { lat: 35.68781283, lng: 139.71799333, heading: 79 },
    { lat: 35.68781283, lng: 139.71799333, heading: 79 },
    { lat: 35.687815, lng: 139.7180125, heading: 79 },
    { lat: 35.687815, lng: 139.7180125, heading: 79 },
    { lat: 35.68781633, lng: 139.7180315, heading: 80 },
  ]}
/>

以上で下図のようにGoogleマップ上に3Dモデル、及び軌跡を表示することできました。

実行結果01

実行結果02

Data Visualizer の計測データを使用して可視化する

次に、Visual M2M Data Visualizer のビジュアルパーツに Googleマップ3Dのビジュアルパーツを表示してみました。

位置情報を含む走行データは、弊社製品の Visual Parts SDK を使用して Visual M2M Data Visualizer から取得し、再生時間に沿って可視化しています。

また、他のビジュアルパーツとの比較、位置情報を確認するため、Open Street Map、緯度、軽度の値を表示するビジュアルパーツも表示しました。

youtu.be

Visual Parts SDK を含む弊社製品に関するお問い合わせは下記リンク先までお願いします。

www.aptpod.co.jp

おわりに

新しい機能を試して実現できた瞬間は、いつになってもテンションが上りますね。

今回は道路に接している移動体の可視化となりましたが、今後は飛行している移動体の可視化、軌跡も表現してみたいと思います。