Demonstrates how to animate a graphic's position and rotation and follow it using a OrbitGeoElementCameraController. Also shows how to combine a SceneView and MapView in an MVC application with property binding.
      
  
    
Use case
Visualize movement through a 3D landscape.
How to use the sample
Animation Controls (Top Left Corner):
- Select a mission -- selects a location with a route for the plane to fly
 - Play/Pause -- toggles playing and stopping the animation
 - Fixed/Follow -- toggles the camera's free cam mode and follow
 - Mission progress -- shows how far along the route the plane is. Slide to change keyframe in animation
 
Camera Controls (Top Right Corner):
- Camare zoom -- distance between camera and plane
 - Camera angle -- viewing angle between camera and plane
 - Flight speed -- controls speed of animation
 
2D Map Controls (Bottom Left Corner):
- Plus and Minus -- controls distance of 2D view from ground level
 
How it works
- Create a 
GraphicsOverlayand add it to theSceneView. - Create a 
ModelSceneSymbolobject. - Create a 
Graphicobject and set its geometry to aPoint. - Set the 
ModelSceneSymbolobject to the graphic. - Add heading, pitch, and roll attributes to the graphic. Get the attributes from the graphic with 
Graphic::attributes. - Create a 
SimpleRendererobject and set its expression properties. - Add graphic and a renderer to the graphics overlay.
 - Create a 
OrbitGeoElementCameraControllerwhich is set to target the graphic. - Assign the camera controller to the 
SceneView. - Update the graphic's location, heading, pitch, and roll.
 
Relevant API
- Camera
 - GlobeCameraController
 - Graphic
 - GraphicsOverlay
 - ModelSceneSymbol
 - OrbitGeoElementCameraController
 - Renderer
 - Scene
 - SceneProperties
 - SceneView
 - SurfacePlacement
 
Offline Data
Read more about how to set up the sample's offline data here.
| Link | Local Location | 
|---|---|
| Model Marker Symbol Data | <userhome>/ArcGIS/Runtime/Data/3D/Bristol/Collada/Bristol.dae | 
| GrandCanyon.csv mission data | <userhome>/ArcGIS/Runtime/Data/3D/Missions/GrandCanyon.csv | 
| Hawaii.csv mission data | <userhome>/ArcGIS/Runtime/Data/3D/Missions/Hawaii.csv | 
| Pyrenees.csv mission data | <userhome>/ArcGIS/Runtime/Data/3D/Missions/Pyrenees.csv | 
| Snowdon.csv mission data | <userhome>/ArcGIS/Runtime/Data/3D/Missions/Snowdon.csv | 
Tags
animation, camera, heading, pitch, roll, rotation, visualize
Sample Code
// [WriteFile Name=Animate3DSymbols, Category=Scenes]
// [Legal]
// Copyright 2016 Esri.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// [Legal]
#ifdef PCH_BUILD
#include "pch.hpp"
#endif // PCH_BUILD
// sample headers
#include "Animate3DSymbols.h"
#include "MissionData.h"
// ArcGIS Maps SDK headers
#include "ArcGISTiledElevationSource.h"
#include "AttributeListModel.h"
#include "Camera.h"
#include "DistanceCompositeSceneSymbol.h"
#include "ElevationSourceListModel.h"
#include "GlobeCameraController.h"
#include "Graphic.h"
#include "GraphicListModel.h"
#include "GraphicsOverlay.h"
#include "GraphicsOverlayListModel.h"
#include "LayerSceneProperties.h"
#include "Map.h"
#include "MapQuickView.h"
#include "MapTypes.h"
#include "ModelSceneSymbol.h"
#include "OrbitGeoElementCameraController.h"
#include "PointCollection.h"
#include "Polyline.h"
#include "PolylineBuilder.h"
#include "RendererSceneProperties.h"
#include "Scene.h"
#include "SceneQuickView.h"
#include "SceneViewTypes.h"
#include "SimpleLineSymbol.h"
#include "SimpleMarkerSceneSymbol.h"
#include "SimpleMarkerSymbol.h"
#include "SimpleRenderer.h"
#include "SpatialReference.h"
#include "Surface.h"
#include "SymbolTypes.h"
#include "Viewpoint.h"
// Qt headers
#include <QFileInfo>
#include <QFuture>
#include <QStandardPaths>
#include <QStringListModel>
#include <QtCore/qglobal.h>
using namespace Esri::ArcGISRuntime;
// helper method to get cross platform data path
namespace
{
  QString defaultDataPath()
  {
    QString dataPath;
  #ifdef Q_OS_IOS
    dataPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
  #else
    dataPath = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
  #endif
    return dataPath;
  }
} // namespace
using namespace Esri::ArcGISRuntime;
const QString Animate3DSymbols::HEADING = QStringLiteral("heading");
const QString Animate3DSymbols::ROLL = QStringLiteral("roll");
const QString Animate3DSymbols::PITCH = QStringLiteral("pitch");
const QString Animate3DSymbols::ANGLE = QStringLiteral("angle");
Animate3DSymbols::Animate3DSymbols(QQuickItem* parent /* = nullptr */):
  QQuickItem(parent),
  m_dataPath(defaultDataPath() + "/ArcGIS/Runtime/Data/3D"),
  m_missionsModel(new QStringListModel({QStringLiteral("Grand Canyon"),
                                        QStringLiteral("Hawaii"),
                                        QStringLiteral("Pyrenees"),
                                        QStringLiteral("Snowdon")},
                                       this)),
  m_missionData(new MissionData())
{
}
Animate3DSymbols::~Animate3DSymbols() = default;
void Animate3DSymbols::init()
{
  qmlRegisterType<SceneQuickView>("Esri.Samples", 1, 0, "SceneView");
  qmlRegisterType<MapQuickView>("Esri.Samples", 1, 0, "MapView");
  qmlRegisterType<Animate3DSymbols>("Esri.Samples", 1, 0, "Animate3DSymbolsSample");
  qmlRegisterUncreatableType<QAbstractListModel>("Esri.Samples", 1, 0, "AbstractListModel", "AbstractListModel is uncreateable");
}
void Animate3DSymbols::componentComplete()
{
  QQuickItem::componentComplete();
  // find QML SceneView component
  m_sceneView = findChild<SceneQuickView*>("sceneView");
  // create a new scene instance
  Scene* scene = new Scene(BasemapStyle::ArcGISImageryStandard, this);
  // set scene on the scene view
  m_sceneView->setArcGISScene(scene);
  // for use when not in following mode
  m_globeController = new GlobeCameraController(this);
  // create a new elevation source
  ArcGISTiledElevationSource* elevationSource = new ArcGISTiledElevationSource(
    QUrl("https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer"), this);
  // add the elevation source to the scene to display elevation
  scene->baseSurface()->elevationSources()->append(elevationSource);
  // create a new graphics overlay and add it to the sceneview
  GraphicsOverlay* sceneOverlay = new GraphicsOverlay(this);
  sceneOverlay->setSceneProperties(LayerSceneProperties(SurfacePlacement::Absolute));
  m_sceneView->graphicsOverlays()->append(sceneOverlay);
  SimpleRenderer* renderer3D = new SimpleRenderer(this);
  RendererSceneProperties renderProperties = renderer3D->sceneProperties();
  renderProperties.setHeadingExpression(QString("[%1]").arg(HEADING));
  renderProperties.setPitchExpression(QString("[%1]").arg(PITCH));
  renderProperties.setRollExpression(QString("[%1]").arg(ROLL));
  renderer3D->setSceneProperties(renderProperties);
  sceneOverlay->setRenderer(renderer3D);
  // find QML MapView component
  m_mapView = findChild<MapQuickView*>("mapView");
  m_mapView->setAttributionTextVisible(false);
  // set up mini map
  Map* map = new Map(BasemapStyle::ArcGISImageryStandard, this);
  m_mapView->setMap(map);
  // create a graphics overlay for the mini map
  GraphicsOverlay* mapOverlay = new GraphicsOverlay(this);
  m_mapView->graphicsOverlays()->append(mapOverlay);
  // set up route graphic
  createRoute2d(mapOverlay);
  // set up overlay 2D graphic
  createModel2d(mapOverlay);
}
void Animate3DSymbols::setMissionFrame(int newFrame)
{
  if (!m_missionData||
     newFrame < 0 ||
     m_frame == newFrame)
    return;
  m_frame = newFrame;
  emit missionFrameChanged();
}
void Animate3DSymbols::animate()
{
  if (!m_missionData)
    return;
  if (missionFrame() < missionSize())
  {
    // get the data for this stage in the mission
    const MissionData::DataPoint& dp = m_missionData->dataAt(missionFrame());
    // move 3D graphic to the new position
    m_graphic3d->setGeometry(dp.m_pos);
    // update attribute expressions to immediately update rotation
    m_graphic3d->attributes()->replaceAttribute(HEADING, dp.m_heading);
    m_graphic3d->attributes()->replaceAttribute(PITCH, dp.m_pitch);
    m_graphic3d->attributes()->replaceAttribute(ROLL, dp.m_roll);
    // move 2D graphic to the new position
    m_graphic2d->setGeometry(dp.m_pos);
    m_symbol2d->setAngle(dp.m_heading);
  }
  // increment the frame count
  emit nextFrameRequested();
}
void Animate3DSymbols::changeMission(const QString &missionNameStr)
{
  setMissionFrame(0);
  // read the mission data from the samples .csv files
  QString formattedname = missionNameStr;
  m_missionData->parse(m_dataPath + "/Missions/" + formattedname.remove(" ") + ".csv");
  // if the mission was loaded successfully, move to the start position
  if (missionReady() && m_routeGraphic)
  {
    // create a polyline representing the route for the mission
    PolylineBuilder* routeBldr = new PolylineBuilder(SpatialReference::wgs84(), this);
    for (int i = 0; i < missionSize(); ++i)
    {
      const MissionData::DataPoint& dp = m_missionData->dataAt(i);
      routeBldr->addPoint(dp.m_pos);
    }
    // set the polyline as a graphic on the mapView
    m_routeGraphic->setGeometry(routeBldr->toGeometry());
    m_mapView->setViewpointAndWait(Viewpoint(m_routeGraphic->geometry()));
    createGraphic3D();
  }
  emit missionReadyChanged();
  emit missionSizeChanged();
}
QAbstractListModel *Animate3DSymbols::missionsModel()
{
  return m_missionsModel;
}
void Animate3DSymbols::setZoom(double zoomDist)
{
  if (m_followingController)
  {
    m_followingController->setCameraDistance(zoomDist);
    emit zoomChanged();
  }
}
void Animate3DSymbols::setAngle(double angle)
{
  if (m_followingController)
  {
    m_followingController->setCameraPitchOffset(angle);
    emit angleChanged();
  }
}
void Animate3DSymbols::createModel2d(GraphicsOverlay* mapOverlay)
{
  if (m_symbol2d || m_graphic2d)
    return;
  // get the mission data for the frame
  const MissionData::DataPoint& dp = m_missionData->dataAt(missionFrame());
  // create a blue triangle symbol to represent the plane on the mini map
  m_symbol2d = new SimpleMarkerSymbol(SimpleMarkerSymbolStyle::Triangle, Qt::blue, 10, this);
  m_symbol2d->setAngle(dp.m_heading);
  // create a graphic with the symbol
  m_graphic2d = new Graphic(dp.m_pos, m_symbol2d, this);
  mapOverlay->graphics()->append(m_graphic2d);
}
void Animate3DSymbols::createRoute2d(GraphicsOverlay* mapOverlay)
{
  // Create a 2d graphic of a solid red line to represen the route of the mission
  SimpleLineSymbol* routeSymbol = new SimpleLineSymbol(SimpleLineSymbolStyle::Solid, Qt::red, 1, this);
  m_routeGraphic = new Graphic(this);
  m_routeGraphic->setSymbol(routeSymbol);
  mapOverlay->graphics()->append(m_routeGraphic);
}
void Animate3DSymbols::createGraphic3D()
{
  if (!missionReady())
    return;
  // create the ModelSceneSymbol to be animated in the 3d view
  if (!m_model3d)
    m_model3d = new ModelSceneSymbol(QUrl(m_dataPath + "/Bristol/Collada/Bristol.dae"), 10.0f, this);
  // get the mission data for the frame
  const MissionData::DataPoint& dp = m_missionData->dataAt(missionFrame());
  if (!m_graphic3d)
  {
    // create a graphic using the model symbol
    m_graphic3d = new Graphic(dp.m_pos, m_model3d, this);
    m_graphic3d->attributes()->insertAttribute(HEADING, dp.m_heading);
    m_graphic3d->attributes()->insertAttribute(PITCH, dp.m_pitch);
    m_graphic3d->attributes()->insertAttribute(ROLL, dp.m_roll);
    // add the graphic to the graphics overlay
    m_sceneView->graphicsOverlays()->at(0)->graphics()->append(m_graphic3d);
    // create the camera controller to follow the graphic
    m_followingController = new OrbitGeoElementCameraController(m_graphic3d, 500, this);
    m_sceneView->setCameraController(m_followingController);
  }
  else
  {
    // update existing graphic's geometry and attributes
    m_graphic3d->setGeometry(dp.m_pos);
    m_graphic3d->attributes()->replaceAttribute(HEADING, dp.m_heading);
    m_graphic3d->attributes()->replaceAttribute(PITCH, dp.m_pitch);
    m_graphic3d->attributes()->replaceAttribute(ROLL, dp.m_roll);
  }
}
void Animate3DSymbols::setFollowing(bool following)
{
  if (following)
    m_sceneView->setCameraController(m_followingController);
  else
    m_sceneView->setCameraController(m_globeController);
}
void Animate3DSymbols::zoomMapIn()
{
  if (!m_mapView || !m_routeGraphic)
    return;
  // zoom the map view in, focusing on the position of the 2d graphic for the airplane
  m_mapView->setViewpointAsync(Viewpoint((Point)m_routeGraphic->geometry(), m_mapView->mapScale() / m_mapZoomFactor));
}
void Animate3DSymbols::zoomMapOut()
{
  if (!m_mapView || !m_routeGraphic)
    return;
  // zoom the map view out, focusing on the position of the 2d graphic for the airplane
  m_mapView->setViewpointAsync(Viewpoint((Point)m_routeGraphic->geometry(), m_mapView->mapScale() * m_mapZoomFactor));
}
void Animate3DSymbols::viewWidthChanged(bool sceneViewIsWider)
{
  if (!m_sceneView || !m_mapView)
  {
    return;
  }
  // only show the attribution text on the view with the widest visible extent
  m_sceneView->setAttributionTextVisible(sceneViewIsWider);
  m_mapView->setAttributionTextVisible(!sceneViewIsWider);
}
bool Animate3DSymbols::missionReady() const
{
  if (!m_missionData)
    return false;
  return m_missionData->ready();
}
int Animate3DSymbols::missionSize() const
{
  if (!m_missionData)
    return 0;
  return (int)m_missionData->size();
}
int Animate3DSymbols::missionFrame() const
{
  return m_frame;
}
double Animate3DSymbols::zoom() const
{
  return m_followingController ? m_followingController->cameraDistance() : 200.0;
}
double Animate3DSymbols::angle() const
{
  return m_followingController ? m_followingController->cameraDistance() : 45.0;
}
double Animate3DSymbols::minZoom() const
{
  return m_followingController ? m_followingController->minCameraDistance() : 0;
}