A camera follows a graphic while the graphic's position and rotation are animated.
      
  
    
How to use the sample
Animation Controls (Top Left Corner):
- Select a mission -- selects a location with a route for plane to fly.
 - Mission progress -- shows how far along the route the plane is. Slide to change keyframe in animation.
 - Play/Stop -- toggles playing and stopping the animation.
 - Follow/Free Cam -- toggles camera following plane.
 
Speed Slider (Top Right Corner):
- 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 
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.getAttributes(). - 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
- ArcGISScene
 - Camera
 - GlobeCameraController
 - Graphic
 - GraphicsOverlay
 - ModelSceneSymbol
 - OrbitGeoElementCameraController
 - Renderer
 - SceneProperties
 - SceneView
 - SurfacePlacement
 
Tags
animation, camera, heading, pitch, roll, rotation, visualize
Sample Code
/*
 * Copyright 2017 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.
 */
package com.esri.samples.animate_3d_graphic;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.fxml.FXML;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ToggleButton;
import javafx.util.Duration;
import com.esri.arcgisruntime.ArcGISRuntimeEnvironment;
import com.esri.arcgisruntime.geometry.Point;
import com.esri.arcgisruntime.geometry.PointCollection;
import com.esri.arcgisruntime.geometry.Polyline;
import com.esri.arcgisruntime.geometry.SpatialReference;
import com.esri.arcgisruntime.geometry.SpatialReferences;
import com.esri.arcgisruntime.mapping.ArcGISMap;
import com.esri.arcgisruntime.mapping.ArcGISScene;
import com.esri.arcgisruntime.mapping.ArcGISTiledElevationSource;
import com.esri.arcgisruntime.mapping.BasemapStyle;
import com.esri.arcgisruntime.mapping.Surface;
import com.esri.arcgisruntime.mapping.Viewpoint;
import com.esri.arcgisruntime.mapping.view.GlobeCameraController;
import com.esri.arcgisruntime.mapping.view.Graphic;
import com.esri.arcgisruntime.mapping.view.GraphicsOverlay;
import com.esri.arcgisruntime.mapping.view.LayerSceneProperties;
import com.esri.arcgisruntime.mapping.view.MapView;
import com.esri.arcgisruntime.mapping.view.OrbitGeoElementCameraController;
import com.esri.arcgisruntime.mapping.view.SceneView;
import com.esri.arcgisruntime.symbology.ModelSceneSymbol;
import com.esri.arcgisruntime.symbology.Renderer;
import com.esri.arcgisruntime.symbology.SimpleLineSymbol;
import com.esri.arcgisruntime.symbology.SimpleMarkerSymbol;
import com.esri.arcgisruntime.symbology.SimpleRenderer;
public class Animate3dGraphicController {
  // injected elements from fxml
  @FXML private AnimationModel animationModel;
  @FXML private SceneView sceneView;
  @FXML private MapView mapView;
  @FXML private ComboBox<String> missionSelector;
  @FXML private ToggleButton playButton;
  @FXML private ToggleButton followButton;
  @FXML private Timeline animation;
  @FXML private Label altitudeLabel;
  @FXML private Label headingLabel;
  @FXML private Label pitchLabel;
  @FXML private Label rollLabel;
  private OrbitGeoElementCameraController orbitCameraController;
  private List<Map<String, Object>> missionData;
  private Graphic plane3D;
  private Graphic plane2D;
  private Graphic routeGraphic;
  private static final SpatialReference WGS84 = SpatialReferences.getWgs84();
  private static final String ELEVATION_IMAGE_SERVICE =
      "https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer";
  /**
   * Called after FXML loads. Sets up scene and map and configures property bindings.
   */
  public void initialize() {
    try {
      // authentication with an API key or named user is required to access basemaps and other location services
      String yourAPIKey = System.getProperty("apiKey");
      ArcGISRuntimeEnvironment.setApiKey(yourAPIKey);
      // create a scene
      ArcGISScene scene = new ArcGISScene(BasemapStyle.ARCGIS_IMAGERY);
      sceneView.setArcGISScene(scene);
      // add elevation data
      Surface surface = new Surface();
      surface.getElevationSources().add(new ArcGISTiledElevationSource(ELEVATION_IMAGE_SERVICE));
      scene.setBaseSurface(surface);
      // create a graphics overlay for the scene
      GraphicsOverlay sceneOverlay = new GraphicsOverlay();
      sceneOverlay.getSceneProperties().setSurfacePlacement(LayerSceneProperties.SurfacePlacement.ABSOLUTE);
      sceneView.getGraphicsOverlays().add(sceneOverlay);
      // create renderer to handle updating plane's orientation
      SimpleRenderer renderer3D = new SimpleRenderer();
      Renderer.SceneProperties renderProperties = renderer3D.getSceneProperties();
      renderProperties.setHeadingExpression("[HEADING]");
      renderProperties.setPitchExpression("[PITCH]");
      renderProperties.setRollExpression("[ROLL]");
      sceneOverlay.setRenderer(renderer3D);
      // set up mini map
      ArcGISMap map = new ArcGISMap(BasemapStyle.ARCGIS_IMAGERY);
      mapView.setMap(map);
      // create a graphics overlay for the mini map
      GraphicsOverlay mapOverlay = new GraphicsOverlay();
      mapView.getGraphicsOverlays().add(mapOverlay);
      // create renderer to rotate the plane graphic in the mini map
      SimpleRenderer renderer2D = new SimpleRenderer();
      SimpleMarkerSymbol plane2DSymbol = new SimpleMarkerSymbol(SimpleMarkerSymbol.Style.TRIANGLE, 0xFF0000FF, 10);
      renderer2D.setSymbol(plane2DSymbol);
      renderer2D.setRotationExpression("[ANGLE]");
      mapOverlay.setRenderer(renderer2D);
      // create a placeholder graphic for showing the mission route in mini map
      SimpleLineSymbol routeSymbol = new SimpleLineSymbol(SimpleLineSymbol.Style.SOLID, 0xFFFF0000, 2);
      routeGraphic = new Graphic();
      routeGraphic.setSymbol(routeSymbol);
      mapOverlay.getGraphics().add(routeGraphic);
      // create a graphic with a blue (0xFF0000FF) triangle symbol to represent the plane on the mini map
      Map<String, Object> attributes = new HashMap<>();
      attributes.put("ANGLE", 0f);
      plane2D = new Graphic(new Point(0, 0, WGS84), attributes);
      mapOverlay.getGraphics().add(plane2D);
      // create a graphic with a ModelSceneSymbol of a plane to add to the scene
      String modelURI = new File(System.getProperty("data.dir"), "./samples-data/bristol/Collada/Bristol.dae").getAbsolutePath();
      ModelSceneSymbol plane3DSymbol = new ModelSceneSymbol(modelURI, 1.0);
      plane3DSymbol.loadAsync();
      plane3D = new Graphic(new Point(0, 0, 0, WGS84), plane3DSymbol);
      sceneOverlay.getGraphics().add(plane3D);
      // create an orbit camera controller to follow the plane
      orbitCameraController = new OrbitGeoElementCameraController(plane3D, 20.0);
      orbitCameraController.setCameraPitchOffset(75.0);
      sceneView.setCameraController(orbitCameraController);
      // setup animation to render a new frame every 20 ms by default
      animation.getKeyFrames().add(new KeyFrame(Duration.millis(20), e -> animate(animationModel.nextKeyframe())));
      // bind button properties
      followButton.textProperty().bind(Bindings.createStringBinding(() -> followButton.isSelected()
          ? "Free cam" : "Follow", followButton.selectedProperty()));
      playButton.textProperty().bind(Bindings.createStringBinding(() -> playButton.isSelected()
          ? "Stop" : "Play", playButton.selectedProperty()));
      // open default mission selection
      changeMission();
    } catch (Exception e) {
      // on any exception, print the stack trace
      e.printStackTrace();
    }
  }
  /**
   * Change the mission data and reset the animation.
   */
  @FXML
  private void changeMission() {
    // clear previous mission data
    missionData = new ArrayList<>();
    // get mission data
    String mission = missionSelector.getSelectionModel().getSelectedItem();
    missionData = getMissionData(mission);
    animationModel.setFrames(missionData.size());
    animationModel.setKeyframe(0);
    // draw mission route on mini map
    PointCollection points = new PointCollection(WGS84);
    points.addAll(missionData.stream().map(m -> (Point) m.get("POSITION")).collect(Collectors.toList()));
    Polyline route = new Polyline(points);
    routeGraphic.setGeometry(route);
    // refresh mini map zoom and show initial keyframe
    mapView.setViewpointScaleAsync(100000).addDoneListener(() -> Platform.runLater(() -> animate(0)));
  }
  /**
   * Loads the mission data from a .csv file into memory.
   *
   * @param mission .csv file name containing the mission data
   * @return ordered list of mapped key value pairs representing coordinates and rotation parameters for each step of
   * the mission
   */
  private List<Map<String, Object>> getMissionData(String mission) {
    // open a file reader to the mission file that automatically closes after read
    try (BufferedReader missionFile = new BufferedReader(
        new InputStreamReader(getClass().getResourceAsStream("/animate_3d_graphic/csv/" + mission)))) {
      return missionFile.lines()
          //ex: -156.3666517,20.6255059,999.999908,83.77659,1.05E-09,-47.766567
          .map(l -> l.split(","))
          .map(l -> {
            // create a map of parameters (ordinates) to values
            Map<String, Object> ordinates = new HashMap<>();
            ordinates.put("POSITION", new Point(Float.valueOf(l[0]), Float.valueOf(l[1]), Float.valueOf(l[2]),
                WGS84));
            ordinates.put("HEADING", Float.valueOf(l[3]));
            ordinates.put("PITCH", Float.valueOf(l[4]));
            ordinates.put("ROLL", Float.valueOf(l[5]));
            return ordinates;
          })
          .collect(Collectors.toList());
    } catch (IOException e) {
      e.printStackTrace();
    }
    throw new RuntimeException("Error reading mission file: " + mission);
  }
  /**
   * Animates a single keyframe corresponding to the index in the mission data profile. Updates the position and
   * rotation of the 2D/3D plane graphic and sets the camera viewpoint.
   *
   * @param keyframe index in mission data to show
   */
  private void animate(int keyframe) {
    // get the next position from the mission data
    Map<String, Object> datum = missionData.get(keyframe);
    Point position = (Point) datum.get("POSITION");
    // update the position parameters pane
    altitudeLabel.setText(String.format("%.2f", position.getZ()));
    headingLabel.setText(String.format("%.2f", (float) datum.get("HEADING")));
    pitchLabel.setText(String.format("%.2f", (float) datum.get("PITCH")));
    rollLabel.setText(String.format("%.2f", (float) datum.get("ROLL")));
    // update plane's position and orientation
    plane3D.setGeometry(position);
    plane3D.getAttributes().put("HEADING", datum.get("HEADING"));
    plane3D.getAttributes().put("PITCH", datum.get("PITCH"));
    plane3D.getAttributes().put("ROLL", datum.get("ROLL"));
    // update mini map plane's position and rotation
    plane2D.setGeometry(position);
    if (followButton.isSelected()) {
      // rotate the map view in the direction of motion to make graphic always point up
      mapView.setViewpoint(new Viewpoint(position, mapView.getMapScale(), 360 + (float) datum.get("HEADING")));
    } else {
      plane2D.getAttributes().put("ANGLE", 360 + (float) datum.get("HEADING") - mapView.getMapRotation());
    }
  }
  /**
   * Switches the animation on or off.
   */
  @FXML
  private void togglePlay() {
    if (playButton.isSelected()) {
      animation.play();
    } else {
      animation.stop();
    }
  }
  /**
   * Switches between the orbiting camera controller and default globe camera controller.
   */
  @FXML
  private void toggleFollow() {
    if (followButton.isSelected()) {
      // reset mini-map plane's rotation to point up
      plane2D.getAttributes().put("ANGLE", 0f);
      // set orbit camera controller
      sceneView.setCameraController(orbitCameraController);
    } else {
      // set camera controller back to default
      sceneView.setCameraController(new GlobeCameraController());
    }
  }
  /**
   * Zoom in mini-map scale.
   */
  @FXML
  private void zoomInMap() {
    mapView.setViewpoint(new Viewpoint((Point) plane2D.getGeometry(), mapView.getMapScale() / 5));
  }
  /**
   * Zoom out mini-map scale.
   */
  @FXML
  private void zoomOutMap() {
    mapView.setViewpoint(new Viewpoint((Point) plane2D.getGeometry(), mapView.getMapScale() * 5));
  }
  /**
   * Stops the animation and disposes of application resources.
   */
  void terminate() {
    animation.stop();
    if (sceneView != null) {
      sceneView.dispose();
    }
    if (mapView != null) {
      mapView.dispose();
    }
  }
}