3D dynamic element tracking
TrackDynamicObject.cs
// Copyright 2012 ESRI
// 
// All rights reserved under the copyright laws of the United States
// and applicable international laws, treaties, and conventions.
// 
// You may freely redistribute and use this sample code, with or
// without modification, provided you include the original copyright
// notice and use restrictions.
// 
// See the use restrictions.
// 

using System;
using System.Drawing;
using System.Runtime.InteropServices;
using Microsoft.Win32;
using ESRI.ArcGIS.ADF.BaseClasses;
using ESRI.ArcGIS.ADF.CATIDs;
using ESRI.ArcGIS.Carto;
using ESRI.ArcGIS.DataSourcesFile;
using ESRI.ArcGIS.Display;
using ESRI.ArcGIS.esriSystem;
using ESRI.ArcGIS.Geodatabase;
using ESRI.ArcGIS.Geometry;
using ESRI.ArcGIS.SystemUI;
using ESRI.ArcGIS.GlobeCore;
using ESRI.ArcGIS.Controls;
using ESRI.ArcGIS.Analyst3D;

namespace GlobeDynamicObjectTracking
{
  /// <summary>
  /// This command demonstrates tracking dynamic object in ArcGlobe/GlobeControl with the camera
  /// </summary>
  [Guid("DCB871A1-390A-456f-8A0D-9FDB6A20F721")]
  [ClassInterface(ClassInterfaceType.None)]
  [ProgId("GlobeControlApp.TrackDynamicObject")]
  public sealed class TrackDynamicObject : BaseCommand, IDisposable
  {
    #region COM Registration Function(s)
    [ComRegisterFunction()]
    [ComVisible(false)]
    static void RegisterFunction(Type registerType)
    {
      // Required for ArcGIS Component Category Registrar support
      ArcGISCategoryRegistration(registerType);

      //
      // TODO: Add any COM registration code here
      //
    }

    [ComUnregisterFunction()]
    [ComVisible(false)]
    static void UnregisterFunction(Type registerType)
    {
      // Required for ArcGIS Component Category Registrar support
      ArcGISCategoryUnregistration(registerType);

      //
      // TODO: Add any COM unregistration code here
      //
    }

    #region ArcGIS Component Category Registrar generated code
    /// <summary>
    /// Required method for ArcGIS Component Category registration -
    /// Do not modify the contents of this method with the code editor.
    /// </summary>
    private static void ArcGISCategoryRegistration(Type registerType)
    {
      string regKey = string.Format("HKEY_CLASSES_ROOT\\CLSID\\{{{0}}}", registerType.GUID);
      GMxCommands.Register(regKey);
      ControlsCommands.Register(regKey);

    }
    /// <summary>
    /// Required method for ArcGIS Component Category unregistration -
    /// Do not modify the contents of this method with the code editor.
    /// </summary>
    private static void ArcGISCategoryUnregistration(Type registerType)
    {
      string regKey = string.Format("HKEY_CLASSES_ROOT\\CLSID\\{{{0}}}", registerType.GUID);
      GMxCommands.Unregister(regKey);
      ControlsCommands.Unregister(regKey);

    }

    #endregion
    #endregion

    //class members
    private IGlobeHookHelper        m_globeHookHelper     = null;
    private IGlobeDisplay           m_globeDisplay        = null;
    private ISceneViewer            m_sceneViwer          = null;
    private IGlobeGraphicsLayer     m_globeGraphicsLayer  = null;
    private IRealTimeFeedManager    m_realTimeFeedManager = null;
    private IRealTimeFeed           m_realTimeFeed        = null;
    private bool                    m_bConnected          = false;
    private bool                    m_bTrackAboveTarget   = true;
    private bool                    m_once                = true;
    private int                     m_trackObjectIndex    = -1;
    private string                  m_shapefileName       = string.Empty;


    #region Ctor

    /// <summary>
    /// Class Ctor
    /// </summary>
    public TrackDynamicObject()
    {
      base.m_category = ".NET Samples";
      base.m_caption = "Track Dynamic Object";
      base.m_message = "Tracking a dynamic object";
      base.m_toolTip = "Track Dynamic Object";
      base.m_name = base.m_category + "_" + base.m_caption;

      try
      {
        string bitmapResourceName = GetType().Name + ".bmp";
        base.m_bitmap = new Bitmap(GetType(), bitmapResourceName);
      }
      catch (Exception ex)
      {
        System.Diagnostics.Trace.WriteLine(ex.Message, "Invalid Bitmap");
      }
    }
    #endregion

    #region Overriden Class Methods

    /// <summary>
    /// Occurs when this command is created
    /// </summary>
    /// <param name="hook">Instance of the application</param>
    public override void OnCreate(object hook)
    {
      //initialize the hook-helper
      if (m_globeHookHelper == null)
        m_globeHookHelper = new GlobeHookHelper();

      //set the hook
      m_globeHookHelper.Hook = hook;

      
      //get the ArcGIS path from the registry
      RegistryKey key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\ESRI\ArcObjectsSDK10.1");
      string path = Convert.ToString(key.GetValue("InstallDir"));

      //set the path to the featureclass used by the GPS simulator
      m_shapefileName = System.IO.Path.Combine(path, "Samples\\data\\USAMajorHighways\\usa_major_highways.shp");

      //get the GlobeDisplsy from the hook helper
      m_globeDisplay = m_globeHookHelper.GlobeDisplay;

      //initialize the real-time manager
      if (null == m_realTimeFeedManager)
        m_realTimeFeedManager = new RealTimeFeedManagerClass();

      //use the built in simulator of the real-time manager
      m_realTimeFeedManager.RealTimeFeed = m_realTimeFeedManager.RealTimeFeedSimulator as IRealTimeFeed;

      m_realTimeFeed = m_realTimeFeedManager.RealTimeFeed;
    }
    
    /// <summary>
    /// Occurs when this command is clicked
    /// </summary>
    public override void OnClick()
    {
      try
      {

        if (!m_bConnected)
        {
          //show the tracking type selection dialog (whether to track the element from above or follow it from behind)
          TrackSelectionDlg dlg = new TrackSelectionDlg();
          if (System.Windows.Forms.DialogResult.OK != dlg.ShowDialog())
            return;

          //get the required tracking mode
          m_bTrackAboveTarget = dlg.UseOrthoTrackingMode;

          //do only once initializations
          if (m_once)
          {
            //create the graphics layer to manage the dynamic object
            m_globeGraphicsLayer = new GlobeGraphicsLayerClass();
            ((ILayer)m_globeGraphicsLayer).Name = "DynamicObjects";

            IScene scene = (IScene)m_globeDisplay.Globe;

            //add the new graphic layer to the globe
            scene.AddLayer((ILayer)m_globeGraphicsLayer, false);

            //activate the graphics layer
            scene.ActiveGraphicsLayer = (ILayer)m_globeGraphicsLayer;

            //redraw the GlobeDisplay
            m_globeDisplay.RefreshViewers();

            //open a polyline featurelayer that would serve the real-time feed GPS simulator
            IFeatureLayer featureLayer = GetFeatureLayer();
            if (featureLayer == null)
              return;
            
            //assign the featurelayer to the GPS simulator
            m_realTimeFeedManager.RealTimeFeedSimulator.FeatureLayer = featureLayer;

            m_once = false;
          }

          //get the GlobeViewUtil which is needed for coordinate transformations
          m_sceneViwer = m_globeDisplay.ActiveViewer;

          //Set the globe mode to terrain mode, since otherwise it will not be possible to set the target position
         ((IGlobeCamera)m_sceneViwer.Camera).OrientationMode = esriGlobeCameraOrientationMode.esriGlobeCameraOrientationLocal;


          //set the simulator elapsed time
          m_realTimeFeedManager.RealTimeFeedSimulator.TimeIncrement = 0.1; //sec

          //wire the real-time feed PositionUpdate event
          ((IRealTimeFeedEvents_Event)m_realTimeFeed).PositionUpdated += new IRealTimeFeedEvents_PositionUpdatedEventHandler(OnPositionUpdated);

          //start the real-time listener
          m_realTimeFeed.Start();
        }
        else
        {
          //stop the real-time listener
          m_realTimeFeed.Stop();

          //un-wire the PositionUpdated event handler
          ((IRealTimeFeedEvents_Event)m_realTimeFeed).PositionUpdated -= new IRealTimeFeedEvents_PositionUpdatedEventHandler(OnPositionUpdated);
        }

        //switch the connection flag
        m_bConnected = !m_bConnected;
      }
      catch (Exception ex)
      {
        System.Diagnostics.Trace.WriteLine(ex.Message);
      }
    }

    /// <summary>
    /// The Checked property indicates the state of this Command. 
    /// </summary>
    /// <remarks>If a command item appears depressed on a commandbar, the command is checked.</remarks>
    public override bool Checked
    {
      get
      {
        return m_bConnected;
      }
    }

    #endregion

    #region helper methods

    /// <summary>
    /// get a featurelayer that would be used by the real-time simulator
    /// </summary>
    /// <returns></returns>
    private IFeatureLayer GetFeatureLayer()
    {
      //instantiate a new featurelayer
      IFeatureLayer featureLayer = new FeatureLayerClass();     

      //set the layer's name
      featureLayer.Name = "GPS Data";

      //open the featureclass
      IFeatureClass featureClass = OpenFeatureClass();
      if (featureClass == null)
        return null;

      //set the featurelayer featureclass
      featureLayer.FeatureClass = featureClass;

      //return the featurelayer
      return featureLayer;
    }

    /// <summary>
    /// Opens a shapefile polyline featureclass
    /// </summary>
    /// <returns></returns>
    private IFeatureClass OpenFeatureClass()
    {

      string fileName = System.IO.Path.GetFileNameWithoutExtension(m_shapefileName);
      
      //instantiate a new workspace factory
      IWorkspaceFactory workspaceFactory = new ShapefileWorkspaceFactoryClass();

      //get the workspace directory
      string path = System.IO.Path.GetDirectoryName(m_shapefileName);

      //open the workspace containing the featureclass
      IFeatureWorkspace featureWorkspace = workspaceFactory.OpenFromFile(path, 0) as IFeatureWorkspace;

      //open the featureclass
      IFeatureClass featureClass = featureWorkspace.OpenFeatureClass(fileName);

      //make sure that the featureclass type is polyline
      if (featureClass.ShapeType != esriGeometryType.esriGeometryPolyline)
      {
        featureClass = null;
      }

      //return the featureclass
      return featureClass;
    }

    /// <summary>
    /// Adds a sphere element to the given graphics layer at the specified position
    /// </summary>
    /// <param name="globeGraphicsLayer"></param>
    /// <param name="position"></param>
    /// <returns></returns>
    private int AddTrackElement(IGlobeGraphicsLayer globeGraphicsLayer, esriGpsPositionInfo position)
    {
      if (null == globeGraphicsLayer)
        return -1;

      //create a new point at the given position
      IPoint point = new PointClass();
      ((IZAware)point).ZAware = true;
      point.X = position.longitude;
      point.Y = position.latitude;
      point.Z = 0.0;

      //set the color for the element (red)
      IRgbColor color = new RgbColorClass();
      color.Red = 255;
      color.Green = 0;
      color.Blue = 0;

      //create a new 3D marker symbol
      IMarkerSymbol markerSymbol = new SimpleMarker3DSymbolClass();

      //set the marker symbol's style and resolution
      ((ISimpleMarker3DSymbol)markerSymbol).Style = esriSimple3DMarkerStyle.esriS3DMSSphere;
      ((ISimpleMarker3DSymbol)markerSymbol).ResolutionQuality = 1.0;

      //set the symbol's size and color
      markerSymbol.Size = 700;
      markerSymbol.Color = color as IColor;

      //crate the graphic element
      IElement trackElement = new MarkerElementClass();

      //set the element's symbol and geometry (location and shape)
      ((IMarkerElement)trackElement).Symbol = markerSymbol;
      trackElement.Geometry = point as IPoint;


      //add the element to the graphics layer
      int elemIndex = 0;
      ((IGraphicsContainer)globeGraphicsLayer).AddElement(trackElement, 0);

      //get the element's index
      globeGraphicsLayer.FindElementIndex(trackElement, out elemIndex);
      return elemIndex;
    }

    /// <summary>
    /// The real-time feed position updated event handler
    /// </summary>
    /// <param name="position">a GPS position information</param>
    /// <param name="estimate">indicates whether this is an estimated time or real time</param>
    void OnPositionUpdated(ref esriGpsPositionInfo position, bool estimate)
    {
      try
      {
        //add the tracking element to the tracking graphics layer (should happen only once)
        if (-1 == m_trackObjectIndex)
        {
          int index = AddTrackElement(m_globeGraphicsLayer, position);
          if (-1 == index)
            throw new Exception("could not add tracking object");

          //cache the element's index
          m_trackObjectIndex = index;

          return;
        }
        //get the element by its index
        IElement elem = ((IGraphicsContainer3D)m_globeGraphicsLayer).get_Element(m_trackObjectIndex);

        //keep the previous location
        double lat, lon, alt;
        ((IPoint)elem.Geometry).QueryCoords(out lon, out lat);
        alt = ((IPoint)elem.Geometry).Z;

        //update the element's position
        IPoint point = elem.Geometry as IPoint;
        point.X = position.longitude;
        point.Y = position.latitude;
        point.Z = alt;
        elem.Geometry = (IGeometry)point;

        //update the element in the graphics layer.
        lock (m_globeGraphicsLayer)
        {
          m_globeGraphicsLayer.UpdateElementByIndex(m_trackObjectIndex);
        }

        IGlobeCamera globeCamera = m_sceneViwer.Camera as IGlobeCamera;

        //set the camera position in order to track the element
        if (m_bTrackAboveTarget)
          TrackAboveTarget(globeCamera, point);
        else
          TrackFollowTarget(globeCamera, point.X, point.Y, point.Z, lon, lat, alt);

      }
      catch (Exception ex)
      {
        System.Diagnostics.Trace.WriteLine(ex.Message);
      }
    }
    void TrackDynamicObject_PositionUpdated(ref esriGpsPositionInfo position, bool estimate)
    {
      
    }

    /// <summary>
    /// If the user chose to track the element from behind, set the camera behind the element 
    /// so that the camera will be placed on the line connecting the previous and the current element's position.
    /// </summary>
    /// <param name="globeCamera"></param>
    /// <param name="newLon"></param>
    /// <param name="newLat"></param>
    /// <param name="newAlt"></param>
    /// <param name="oldLon"></param>
    /// <param name="oldLat"></param>
    /// <param name="oldAlt"></param>
    private void TrackFollowTarget(IGlobeCamera globeCamera, double newLon, double newLat, double newAlt, double oldLon, double oldLat, double oldAlt)
    {
      //make sure that the camera position is not directly above the element. Otherwise it can lead to
      //an ill condition
      if (newLon == oldLon && newLat == oldLat)
      {
        newLon += 0.00001;
        newLat += 0.00001;
      }

      //calculate the azimuth from the previous position to the current position
      double azimuth = Math.Atan2(newLat - oldLat, newLon - oldLon) * (Math.PI / 180.0);

      //the camera new position, right behind the element
      double obsX = newLon - 0.04 * Math.Cos(azimuth * (Math.PI / 180));
      double obsY = newLat - 0.04 * Math.Sin(azimuth * (Math.PI / 180));

      //set the camera position. The camera must be locked in order to prevent a dead-lock caused by the cache manager
      lock (globeCamera)
      {
        globeCamera.SetTargetLatLonAlt(newLat, newLon, newAlt / 1000.0);
        globeCamera.SetObserverLatLonAlt(obsY, obsX, newAlt / 1000.0 + 0.7);
        m_sceneViwer.Camera.Apply();
      }

      //refresh the globe display
      m_globeDisplay.RefreshViewers();
    }

    /// <summary>
    /// should the user choose to track the element from above, set the camera above the element
    /// </summary>
    /// <param name="globeCamera"></param>
    /// <param name="objectLocation"></param>
    private void TrackAboveTarget(IGlobeCamera globeCamera, IPoint objectLocation)
    {
      //Update the observer as well as the camera position
      //The camera must be locked in order to prevent a dead-lock caused by the cache manager
      lock (globeCamera)
      {

        globeCamera.SetTargetLatLonAlt(objectLocation.Y, objectLocation.X, objectLocation.Z / 1000.0);

        //The camera must nut be located exactly above the target, since it results in poor orientation computation
        //and therefore the camera gets jumpy.
        globeCamera.SetObserverLatLonAlt(objectLocation.Y - 0.000001, objectLocation.X - 0.000001, objectLocation.Z / 1000.0 + 30.0);
        m_sceneViwer.Camera.Apply();
      }
      m_globeDisplay.RefreshViewers();
    }

    #endregion

    #region IDisposable Members

    public void Dispose()
    {
    }

    #endregion
  }
}