Globe Fly tool
Fly.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.Collections.Generic;
using System.Text;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using System.Drawing;
using ESRI.ArcGIS.Analyst3D;
using ESRI.ArcGIS.Geometry;
using ESRI.ArcGIS.ADF.BaseClasses;
using ESRI.ArcGIS.ADF.CATIDs;
using ESRI.ArcGIS.GlobeCore;
using ESRI.ArcGIS.esriSystem;
using ESRI.ArcGIS.SystemUI;

namespace GlobeFlyTool
{
    public class Fly : ESRI.ArcGIS.Desktop.AddIns.Tool
    {
        #region DllImport

        [DllImport("user32")]
        public static extern int SetCursor(int hCursor);
        [DllImport("user32")]
        public static extern int GetClientRect(int hwnd, ref  Rectangle lpRect);
        [DllImport("user32")]
        static extern bool GetCursorPos(ref System.Drawing.Point lpPoint);
        [DllImport("user32")]
        public static extern int GetWindowRect(int hwnd, ref  Rectangle lpRect);

        #endregion

        #region Member Variables

        private IGlobe globe;
        private IGlobeDisplay globeDisplay;
        private IGlobeCamera globeCamera;
        private ICamera camera;
        private IScene scene;
        private bool inUse;
        bool bCancel = false;
        bool orbitalFly = false;
        private long mouseX;
        private long mouseY;
        private double motion = 2; //speed of the scene fly through in scene units
        private double distance; //distance between target and observer
        private double currentElevation; //normal fly angles in radians
        private double currentAzimut;  //normal fly angles in radians
        private int speed;
        private System.Windows.Forms.Cursor flyCur;
        private System.Windows.Forms.Cursor moveFlyCur;
        private Microsoft.VisualBasic.Devices.Clock theClock;
        private long lastClock;
        GlobeFlyTool.PointZ observer;
        GlobeFlyTool.PointZ target;
        GlobeFlyTool.PointZ viewVec;

        #endregion

        #region Constructor/Destructor

        public Fly()
        {
            globe = ArcGlobe.Globe;
            scene = globe as IScene;
            globeDisplay = globe.GlobeDisplay;
            camera = globeDisplay.ActiveViewer.Camera;
            globeCamera = camera as IGlobeCamera;
            theClock = new Microsoft.VisualBasic.Devices.Clock();
            flyCur = new System.Windows.Forms.Cursor(GetType().Assembly.GetManifestResourceStream("GlobeFlyTool.Fly.cur"));
            moveFlyCur = new System.Windows.Forms.Cursor(GetType().Assembly.GetManifestResourceStream("GlobeFlyTool.fly1.cur"));
            speed = 0;
        }

        ~Fly()
        {
            flyCur = null;
            moveFlyCur = null;
        }

        #endregion

        protected override void OnUpdate()
        {
            Enabled = ArcGlobe.Application != null;

            if (inUse)
                Cursor = moveFlyCur;
            else
                Cursor = flyCur;
        }

        #region Tool overrides

        protected override void OnMouseUp(ESRI.ArcGIS.Desktop.AddIns.Tool.MouseEventArgs arg)
        {
            if (arg.Button == MouseButtons.Left || arg.Button == MouseButtons.Right)
            {
                if (!inUse)
                {
                    mouseX = arg.X;
                    mouseY = arg.Y;

                    if (speed == 0)
                        StartFlight(arg.X, arg.Y);
                }
                else
                {
                    //Set the speed
                    if (arg.Button == MouseButtons.Left)
                        speed = speed + 1;
                    else if (arg.Button == MouseButtons.Right)
                        speed = speed - 1;
                }
            }
            else
            {
                //EndFlight();
                inUse = false;
                bCancel = true;
            }
        }

        protected override void OnMouseMove(ESRI.ArcGIS.Desktop.AddIns.Tool.MouseEventArgs arg)
        {
            if (!inUse) return;

            mouseX = arg.X;
            mouseY = arg.Y;
        }

        protected override void OnKeyUp(ESRI.ArcGIS.Desktop.AddIns.Tool.KeyEventArgs arg)
        {
            if (inUse == true)
            {
                //Slow down the speed of the fly through
                if (arg.KeyCode == Keys.Down || arg.KeyCode == Keys.Left)
                    motion = motion / 2;
                //Speed up the speed of the fly through
                else if (arg.KeyCode == Keys.Up || arg.KeyCode == Keys.Right)
                    motion = motion * 2;
                else if (arg.KeyCode == Keys.Escape)
                    bCancel = true;

            }
        }

        protected override void OnKeyDown(ESRI.ArcGIS.Desktop.AddIns.Tool.KeyEventArgs arg)
        {
            if (arg.KeyCode == Keys.Escape) //ESC is pressed
            {
                bCancel = true;
            }
        }

        #endregion

        #region Fly Functions

        public void StartFlight(double x, double y)
        {
            inUse = true;

            globeDisplay.IsNavigating = true;
            ESRI.ArcGIS.GlobeCore.esriGlobeCameraOrientationMode camOrientMode =
                globeCamera.OrientationMode;

            orbitalFly = (camOrientMode == ESRI.ArcGIS.GlobeCore.esriGlobeCameraOrientationMode.esriGlobeCameraOrientationLocal) ? true : false;

            IPoint pObs = camera.Observer;
            IPoint pTar = camera.Target;

            observer = new GlobeFlyTool.PointZ(pObs.X, pObs.Y, pObs.Z);
            target = new GlobeFlyTool.PointZ(pTar.X, pTar.Y, pTar.Z);

            viewVec = target - observer;
            distance = viewVec.Norm();

            //avoid center of globe
            if (target.Norm() < 0.25)
            {
                target = target + viewVec;
                distance = distance * 2;
            }

            currentElevation = Math.Atan(viewVec.z / Math.Sqrt((viewVec.x * viewVec.x) + (viewVec.y + viewVec.y)));
            currentAzimut = Math.Atan2(viewVec.y, viewVec.x);//2.26892;//

            //Windows API call to get windows client coordinates
            System.Drawing.Point pt = new System.Drawing.Point();
            bool ans = GetCursorPos(ref pt);
            Rectangle rect = new Rectangle();
            if (GetWindowRect(globeDisplay.ActiveViewer.hWnd, ref rect) == 0) return;

            mouseX = pt.X - rect.Left;
            mouseY = pt.Y - rect.Top;

            if (!orbitalFly)
            {
                globeCamera.OrientationMode = esriGlobeCameraOrientationMode.esriGlobeCameraOrientationGlobal;
            }
            else
            {
                globeCamera.OrientationMode = esriGlobeCameraOrientationMode.esriGlobeCameraOrientationLocal;
            }
            globeCamera.NavigationType = esriGlobeNavigationType.esriGlobeNavigationFree;
            globeCamera.RollFactor = 1.0;

            globeDisplay.IsNavigating = true;
            globeDisplay.IsNavigating = false;
            globeDisplay.IsNavigating = true;

            lastClock = theClock.TickCount;

            //Windows API call to set cursor
            SetCursor(moveFlyCur.Handle.ToInt32());
            //Continue the flight
            Flight();
        }

        public void Flight()
        {
            //speed in scene units
            double motionUnit = (0.000001 + Math.Abs(observer.Norm() - 1.0) / 200.0) * motion;
            //Get IMessageDispatcher interface
            IMessageDispatcher pMessageDispatcher;
            pMessageDispatcher = new MessageDispatcherClass();

            //Set the ESC key to be seen as a cancel action
            pMessageDispatcher.CancelOnClick = false;
            pMessageDispatcher.CancelOnEscPress = true;
            bCancel = false;
            do
            {
                //Get the elapsed time
                long currentClock = theClock.TickCount;
                double lastFrameDuration = (double)(currentClock - lastClock) / 1000;
                lastClock = currentClock;

                if (lastFrameDuration < 0.01)
                    lastFrameDuration = 0.01;

                if (lastFrameDuration > 1)
                    lastFrameDuration = 0.1;

                System.Diagnostics.Debug.Print(lastFrameDuration.ToString());

                //Windows API call to get windows client coordinates
                Rectangle rect = new Rectangle();
                if (GetClientRect(globeDisplay.ActiveViewer.hWnd, ref rect) == 0) return;

                //Get normal vectors
                double dXMouseNormal, dYMouseNormal;

                dXMouseNormal = 2 * ((double)mouseX / (double)(rect.Right - rect.Left)) - 1;
                dYMouseNormal = 2 * ((double)mouseY / (double)(rect.Bottom - rect.Top)) - 1;

                PointZ dir = this.RotateNormal(lastFrameDuration, dXMouseNormal, dYMouseNormal);

                PointZ visTarget = new PointZ(observer.x + distance * dir.x, observer.y + distance * dir.y, observer.z + distance * dir.z);
                target.x = visTarget.x;
                target.y = visTarget.y;
                target.z = visTarget.z;

                if (speed != 0)
                {
                    int speedFactor = (speed > 0) ? (1 << speed) : -(1 << (-speed));

                    //Move the camera in the viewing directions
                    observer.x = observer.x + (lastFrameDuration * (2 ^ speedFactor) * motionUnit * dir.x);
                    observer.y = observer.y + (lastFrameDuration * (2 ^ speedFactor) * motionUnit * dir.y);
                    observer.z = observer.z + (lastFrameDuration * (2 ^ speedFactor) * motionUnit * dir.z);
                    target.x = target.x + (lastFrameDuration * (2 ^ speedFactor) * motionUnit * dir.x);
                    target.y = target.y + (lastFrameDuration * (2 ^ speedFactor) * motionUnit * dir.y);
                    target.z = target.z + (lastFrameDuration * (2 ^ speedFactor) * motionUnit * dir.z);
                }

                ESRI.ArcGIS.GlobeCore.IGlobeViewUtil globeViewUtil = globeCamera as ESRI.ArcGIS.GlobeCore.IGlobeViewUtil;
                double obsLat;
                double obsLon;
                double obsAlt;
                double tarLat;
                double tarLon;
                double tarAlt;

                globeViewUtil.GeocentricToGeographic(observer.x, observer.y, observer.z, out obsLon, out obsLat, out obsAlt);
                globeViewUtil.GeocentricToGeographic(target.x, target.y, target.z, out tarLon, out tarLat, out tarAlt);
                globeCamera.SetObserverLatLonAlt(obsLat, obsLon, obsAlt / 1000);
                globeCamera.SetTargetLatLonAlt(tarLat, tarLon, tarAlt / 1000);

                globeCamera.SetAccurateViewDirection(target.x - observer.x, target.y - observer.y, target.z - observer.z);

                double rollAngle = 0;
                if (speed > 0)
                {
                    rollAngle = 10 * dXMouseNormal * Math.Abs(dXMouseNormal);
                }
                camera.RollAngle = rollAngle;

                //Redraw the scene viewer 
                globeDisplay.RefreshViewers();

                //Dispatch any waiting messages: OnMouseMove / OnMouseUp / OnKeyUp events
                object objCancel = bCancel as object;
                pMessageDispatcher.Dispatch(globeDisplay.ActiveViewer.hWnd, false, out objCancel);

                //End flight if ESC key pressed
                if (bCancel == true)
                    EndFlight();

            }
            while (inUse == true && bCancel == false);

            bCancel = false;
        }

        public void EndFlight()
        {
            inUse = false;
            bCancel = true;
            speed = 0;
            globeDisplay.IsNavigating = false;

            // reposition target
            PointZ currentObs = new PointZ();
            IPoint newTarget = new PointClass();
            currentObs.x = camera.Observer.X;
            currentObs.y = camera.Observer.Y;
            currentObs.z = camera.Observer.Z;

            int orX = 0;
            int orY = 0;
            int width = 0;
            int height = 0;
            camera.GetViewport(ref orX, ref orY, ref width, ref height);

            object obj1;
            object obj2;
            try
            {
                globeDisplay.Locate(globeDisplay.ActiveViewer, width / 2, height / 2, true, true, out newTarget, out obj1, out obj2);
            }
            catch (System.Exception e)
            {
                MessageBox.Show(e.Message);
                MessageBox.Show(e.StackTrace.ToString());
            }

            if (newTarget == null) // no intersection with globe, but don't let the target to be too far
            {
                newTarget = camera.Target;
                PointZ tar = new PointZ(currentObs.x, currentObs.y, currentObs.z);

                double elevObs = tar.Norm() - 1.0;
                if (elevObs <= 0.0001)
                    elevObs = 0.0001;

                PointZ oldTarget = new PointZ(newTarget.X, newTarget.Y, newTarget.Z);
                PointZ dir = (oldTarget - tar);
                double val = dir.Norm();
                if (val > 0.0)
                {
                    dir.x = dir.x * elevObs * 10 / val;
                    dir.y = dir.y * elevObs * 10 / val;
                    dir.z = dir.z * elevObs * 10 / val;
                }

                tar = tar + dir;
                newTarget.X = tar.x;
                newTarget.Y = tar.y;
                newTarget.Z = tar.z;
            }

            ESRI.ArcGIS.GlobeCore.IGlobeViewUtil globeViewUtil = globeCamera as ESRI.ArcGIS.GlobeCore.IGlobeViewUtil;
            double obsLat;
            double obsLon;
            double obsAlt;
            double tarLat;
            double tarLon;
            double tarAlt;
            globeViewUtil.GeocentricToGeographic(currentObs.x, currentObs.y, currentObs.z, out obsLon, out obsLat, out obsAlt);
            globeViewUtil.GeocentricToGeographic(newTarget.X, newTarget.Y, newTarget.Z, out tarLon, out tarLat, out tarAlt);
            globeCamera.SetObserverLatLonAlt(obsLat, obsLon, obsAlt / 1000);
            globeCamera.SetTargetLatLonAlt(tarLat, tarLon, tarAlt / 1000);
            camera.RollAngle = 0;
            camera.PropertiesChanged();
            globeDisplay.RefreshViewers();

            //Windows API call to set cursor
            SetCursor(moveFlyCur.Handle.ToInt32());
        }

        public PointZ RotateNormal(double lastFrameDuration, double mouseXNorm, double mouseYNorm)
        {
            currentElevation = currentElevation - (lastFrameDuration * mouseYNorm * (Math.Abs(mouseYNorm)));
            currentAzimut = currentAzimut - (lastFrameDuration * mouseXNorm * (Math.Abs(mouseXNorm)));

            if (currentElevation > 0.45 * 3.141592)
            {
                currentElevation = 0.45 * 3.141592;
            }
            if (currentElevation < -0.45 * 3.141592)
            {
                currentElevation = -0.45 * 3.141592;
            }
            while (currentAzimut < 0)
            {
                currentAzimut = currentAzimut + (2 * 3.141592);
            }
            while (currentAzimut > 2 * 3.141592)
            {
                currentAzimut = currentAzimut - (2 * 3.141592);
            }

            double x = Math.Cos(currentElevation) * Math.Cos(currentAzimut);
            double y = Math.Cos(currentElevation) * Math.Sin(currentAzimut);
            double z = Math.Sin(currentElevation);

            GlobeFlyTool.PointZ p = new PointZ(x, y, z);
            return p;
        }

        #endregion
    }

}