渋谷あたりをドローンで飛行してみる

## MapboxとCesiumを使ってドローンを飛行させてみます

MapboxのMVTはデータが新しく3Dビルディングのカバー率がかなり優秀です
Cesiumからは現在使用できないようです

Cesiumはデータの専門的な可視化を目的としているようで、データが正確です
OpenStreetMapのTilesを使用できます!

Styleは[こちら]()を使っています

![mapbox.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117164/88ae5f2a-0878-4d1d-b52e-aa18d4cd6103.gif)

![cesium.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117164/a25a7d38-c3f1-4214-a6fe-c6e4b7ef4f1f.gif)

* Mapbox
```javascript
import config from './config.js'
import { GUI } from 'https://threejs.org/examples/jsm/libs/lil-gui.module.min.js'
import Stats from 'https://threejs.org/examples/jsm/libs/stats.module.js'

let initialHeading = 0

mapboxgl.accessToken = config.accessToken

const BUILDING_LAYER_ID = '3d-buildings'
const MIN_ZOOM = 12

function createBuildingLayer() {
  return {
    id: BUILDING_LAYER_ID,
    source: 'composite',
    'source-layer': 'building',
    filter: ['==', 'extrude', 'true'],
    type: 'fill-extrusion',
    minzoom: MIN_ZOOM,
    paint: {
      'fill-extrusion-color': ['case', ['boolean', ['feature-state', 'select'], false], 'red', ['boolean', ['feature-state', 'hover'], false], 'lightblue', 'mediumaquamarine'],
      // use an 'interpolate' expression to add a smooth transition effect to the buildings as the user zooms in
      'fill-extrusion-height': ['interpolate', ['linear'], ['zoom'], MIN_ZOOM, 0, MIN_ZOOM + 0.05, ['get', 'height']],
      'fill-extrusion-base': ['interpolate', ['linear'], ['zoom'], MIN_ZOOM, 0, MIN_ZOOM + 0.05, ['get', 'min_height']],
      'fill-extrusion-opacity': 0.8,
    },
  }
}

function createModelLayer(callback) {
  return {
    id: '3d-model',
    type: 'custom',
    renderingMode: '3d',
    onAdd: (map, gl) =>
      tb.loadObj(
        {
          type: 'glb',
          obj: './CesiumDrone.glb',
          units: 'meters',
          scale: 1,
          rotation: { x: 90, y: 0, z: 0 },
          anchor: 'top',
        },
        callback,
      ),
    render: (gl, matrix) => tb.update(),
  }
}

let guiParameter = {
  buildings: true,
  acceleration: 5,
  inertia: 10,
}

let mixer = null
let clock = new THREE.Clock()

let stats = null
let gui = null
let render = false

export async function renderMap(keys, coords) {
  if (render) return
  render = true

  const { longitude, latitude } = coords
  const container = document.querySelector('.navigation-map')

  const mapContainer = document.createElement('div')
  container.appendChild(mapContainer)
  mapContainer.id = 'map'

  stats = new Stats()
  mapContainer.appendChild(stats.dom)
  stats.dom.style.position = 'absolute'

  const guiContainer = document.createElement('div')
  container.appendChild(guiContainer)
  gui = new GUI({ container: guiContainer, width: '100%' })

  const map = new mapboxgl.Map({
    container: mapContainer.id,
    style: `mapbox://styles/${config.username}/${config.styleId}`,
    zoom: 19,
    center: [longitude, latitude],
    pitch: 65,
    bearing: initialHeading,
    antialias: true, // create the gl context with MSAA antialiasing, so custom layers are antialiased
    // attributionControl: false,
  })
  new mapboxgl.Marker().setLngLat([longitude, latitude]).addTo(map)

  window.tb = new Threebox(map, map.getCanvas().getContext('webgl'), {
    // realSunlight: true,
    // enableSelectingObjects: true,
    // enableDraggingObjects: true,
    enableRotatingObjects: true,
    // enableTooltips: true,
  })
  // tb.setSunlight(new Date(2020, 6, 19, 23), map.getCenter())

  let drone
  const modelLoadedCallback = (model) => {
    drone = model.setCoords([longitude, latitude, 10])
    drone.setRotation({ x: 0, y: 0, z: -initialHeading }) //turn it to the initial street way
    drone.castShadow = true
    drone.selected = true
    drone.addEventListener(
      'ObjectChanged',
      (e) => {
        if (guiParameter.buildings) {
          let point = map.project(e.detail.object.coordinates) //here's the object already modified
          let features = map.queryRenderedFeatures(point, { layers: [BUILDING_LAYER_ID] })
          if (features.length > 0) {
            const { source, sourceLayer, id } = features[0]
            map.setFeatureState({ source, sourceLayer, id }, { select: true })
          }
        }
      },
      false,
    )

    if (drone.animations && drone.animations.length > 0) {
      mixer = new THREE.AnimationMixer(drone)
      drone.animations.forEach((clip, index) => {
        const action = mixer.clipAction(clip)
        action.play()
      })
    }

    tb.add(drone)
    tb.scene.add(new THREE.AmbientLight(0xffffff, 0.5))

    gui
      .add(guiParameter, 'buildings')
      .name('buildings')
      .onChange(() => {
        if (guiParameter.buildings) {
          if (!map.getLayer(BUILDING_LAYER_ID)) {
            map.addLayer(createBuildingLayer())
          }
        } else {
          if (map.getLayer(BUILDING_LAYER_ID)) {
            map.removeLayer(BUILDING_LAYER_ID)
          }
        }
        tb.map.repaint = true
      })
    gui.add(guiParameter, 'acceleration', 1, 16).step(0.5)
    gui.add(guiParameter, 'inertia', 1, 32).step(0.5)
    gui.close()

    animate()
  }

  map.on('style.load', () => {
    map.addLayer(createModelLayer(modelLoadedCallback))
    if (guiParameter.buildings) {
      if (!map.getLayer(BUILDING_LAYER_ID)) {
        map.addLayer(createBuildingLayer())
      }
    }
    map.getCanvas().focus()
  })

  map.once('load', () => {
    map.setPadding({ top: 180 })
  })

  const delta = 0.01
  let velocity = 0.0

  function toDegree(rad) {
    return (rad / Math.PI) * 180
  }

  function toRadian(deg) {
    return (deg * Math.PI) / 180
  }

  function animate() {
    requestAnimationFrame(animate)

    if (mixer) mixer.update(clock.getDelta())
    stats.update()

    let { inertia, acceleration } = guiParameter

    const newSpeed = calculateSpeed(keys, inertia, acceleration, velocity)

    if (newSpeed !== 0.0) {
      velocity += (newSpeed - velocity) * acceleration * delta
      drone.set({ worldTranslate: new THREE.Vector3(0, -velocity, 0) })
    }

    const options = {
      center: drone.coordinates,
      bearing: map.getBearing(),
      easing: (t) => t * (2 - t),
    }

    const deg = 1
    const rad = toRadian(deg)
    const zAxis = new THREE.Vector3(0, 0, 1)

    if (keys.a || keys.d) {
      const rotation = rad * (keys.d ? -1 : 1)
      drone.set({ quaternion: [zAxis, drone.rotation.z + rotation] })
      options.bearing = -toDegree(drone.rotation.z)
    }

    map.jumpTo(options)
  }

  function calculateSpeed(keys, inertia, acceleration, velocity) {
    let speed = 0.0

    if (!(keys.w || keys.s)) {
      if (velocity > 0) {
        speed = -inertia * delta
      } else if (velocity < 0) {
        speed = inertia * delta
      }
      if (velocity > -0.0008 && velocity < 0.0008) {
        speed = velocity = 0.0
      }
    }

    if (keys.w) {
      speed = acceleration * delta
    } else if (keys.s) {
      speed = -acceleration * delta
    }

    return speed
  }
}
```
* Cesium
```javascript
import config from './config.js'
import { GUI } from 'https://threejs.org/examples/jsm/libs/lil-gui.module.min.js'
import Stats from 'https://threejs.org/examples/jsm/libs/stats.module.js'

let guiParameter = {
  buildings: true,
  acceleration: 50,
  inertia: 3,
}

let stats = null
let gui = null
let render = false

export async function renderMap(keys, coords) {
  if (render) return
  render = true

  const { longitude, latitude } = coords
  const container = document.querySelector('.navigation-map')

  const mapContainer = document.createElement('div')
  container.appendChild(mapContainer)
  mapContainer.id = 'map'

  stats = new Stats()
  mapContainer.appendChild(stats.dom)
  stats.dom.style.position = 'absolute'

  const guiContainer = document.createElement('div')
  container.appendChild(guiContainer)
  gui = new GUI({ container: guiContainer, width: '100%' })

  Cesium.Ion.defaultAccessToken = config.defaultAccessToken

  const viewer = new Cesium.Viewer(mapContainer.id, {
    animation: false,
    timeline: false,
    fullscreenButton: false,
    // creditContainer: document.createElement('div'),

    geocoder: false,
    homeButton: false,
    sceneModePicker: false,
    baseLayerPicker: false,
    navigationHelpButton: false,

    shouldAnimate: true,

    terrain: Cesium.Terrain.fromWorldTerrain(),
  })

  // Imagery -----------------
  viewer.imageryLayers.addImageryProvider(
    new Cesium.MapboxStyleImageryProvider({
      styleId: config.styleId,
      username: config.username,
      accessToken: config.accessToken,
      // tilesize: 256,
      // scaleFactor: true,
      // maximumLevel: 20,
    }),
  )

  // 3DBuildings -----------------
  const osmBuildings = await Cesium.createOsmBuildingsAsync()
  osmBuildings.style = new Cesium.Cesium3DTileStyle({
    color: "color('mediumaquamarine', 0.8)",
  })
  viewer.scene.primitives.add(osmBuildings)

  // HomePoint -----------------
  const homePosition = Cesium.Cartesian3.fromDegrees(longitude, latitude, 95)
  viewer.entities.add({
    position: homePosition,
    point: { pixelSize: 10, color: Cesium.Color.RED },
  })

  // Drone -----------------
  const dronePosition = Cesium.Cartesian3.fromDegrees(longitude, latitude, 100)
  // const heading = Cesium.Math.toRadians(-initialHeading)
  const heading = Cesium.Math.toRadians(0)
  const pitch = 0
  const roll = 0
  const hpr = new Cesium.HeadingPitchRoll(heading, pitch, roll)
  const droneOrientation = Cesium.Transforms.headingPitchRollQuaternion(dronePosition, hpr)
  const drone = viewer.entities.add({
    name: 'CesiumDrone',
    position: dronePosition,
    orientation: droneOrientation,
    model: { uri: './CesiumDrone.glb' },
  })

  // Camera(tracked) -----------------
  // viewer.trackedEntity = drone
  // viewer.flyTo(drone, {
  //   duration: 2,
  //   offset: new Cesium.HeadingPitchRange(Cesium.Math.toRadians(90 - initialHeading), Cesium.Math.toRadians(-30), 100),
  // })

  // Camera -----------------
  const cameraPosition = Cesium.Cartesian3.fromDegrees(longitude, latitude, 100)
  viewer.camera.setView({ destination: cameraPosition })

  const delta = 0.01
  let velocity = 0.0

  // Animation -----------------
  function animate() {
    stats.update()
    if (!drone || !Cesium.defined(drone.position)) return

    const dronePosition = drone.position.getValue(viewer.clock.currentTime)
    const center = Cesium.Cartesian3.clone(dronePosition)

    let { inertia, acceleration } = guiParameter

    const newSpeed = calculateSpeed(keys, inertia, acceleration, velocity)

    velocity += (newSpeed - velocity) * acceleration * delta

    let orientation = drone.orientation.getValue(viewer.clock.currentTime)

    if (keys.w || keys.s) {
      const position = drone.position.getValue(viewer.clock.currentTime)
      const forward = new Cesium.Cartesian3(1, 0, 0)
      const rotationMatrix = Cesium.Matrix3.fromQuaternion(orientation)
      const rotatedForward = Cesium.Matrix3.multiplyByVector(rotationMatrix, forward, new Cesium.Cartesian3())
      if (keys.w) {
        const movement = Cesium.Cartesian3.multiplyByScalar(rotatedForward, velocity, new Cesium.Cartesian3())
        drone.position = Cesium.Cartesian3.add(position, movement, new Cesium.Cartesian3())
      } else if (keys.s) {
        const movement = Cesium.Cartesian3.multiplyByScalar(rotatedForward, -velocity, new Cesium.Cartesian3())
        drone.position = Cesium.Cartesian3.subtract(position, movement, new Cesium.Cartesian3())
      }
    }

    if (keys.a || keys.d) {
      const turnSpeed = Cesium.Math.toRadians(0.5)
      let newHeading = null
      if (keys.a) {
        newHeading = Cesium.Quaternion.fromAxisAngle(Cesium.Cartesian3.UNIT_Z, turnSpeed, new Cesium.Quaternion())
      } else if (keys.d) {
        newHeading = Cesium.Quaternion.fromAxisAngle(Cesium.Cartesian3.UNIT_Z, -turnSpeed, new Cesium.Quaternion())
      }
      let q = Cesium.Quaternion.multiply(orientation, newHeading, new Cesium.Quaternion())
      drone.orientation = new Cesium.ConstantProperty(q)
    }

    viewer.trackedEntity = drone

    const droneOrientation = drone.orientation.getValue(viewer.clock.currentTime)
    const hpr = Cesium.HeadingPitchRoll.fromQuaternion(droneOrientation)
    let h = Cesium.Math.toRadians(-38.0 + Cesium.Math.toDegrees(hpr.heading))
    let p = Cesium.Math.toRadians(-20.0)
    let r = 100.0
    viewer.camera.lookAt(center, new Cesium.HeadingPitchRange(h, p, r))

    requestAnimationFrame(animate)
  }

  function calculateSpeed(keys, inertia, acceleration, velocity) {
    let speed = 0.0

    if (!(keys.w || keys.s)) {
      if (velocity > 0) {
        speed = -inertia * delta
      } else if (velocity < 0) {
        speed = inertia * delta
      }
      if (velocity > -0.0008 && velocity < 0.0008) {
        speed = velocity = 0.0
      }
    }

    if (keys.w) {
      speed = acceleration * delta
    } else if (keys.s) {
      speed = -acceleration * delta
    }

    return speed
  }

  function toggleBuildings() {
    if (guiParameter.buildings) {
    } else {
    }
  }

  gui.add(guiParameter, 'buildings').name('buildings').onChange(toggleBuildings)
  gui.add(guiParameter, 'acceleration', 1, 10).step(0.5)
  gui.add(guiParameter, 'inertia', 1, 5).step(0.5)
  gui.close()
  animate()
}

```