Saturday, May 16, 2009

Exploiting the ESRI Projection Engine

The ESRI products ArcGIS Explorer, ArcGIS Desktop and ArcGIS Engine ship with an extremely useful C DLL called the projection engine Library.  The projection engine is used by these products to perform geodetic (or ellipsoidal) computations.

The projection engine is fully documented in the SDE C API and there is a sample VB6 wrapper for two functions on the EDN web site.

The VB6 sample be easily ported to C# using DLLImportAttribute.  However there are two disadvantages of using DLLImport.

  1. DLLs are loaded into memory each time a function is called,
  2. The file and path name to the DLL is hard coded at design time.

However with .NET 2.0 a new method called GetDelegateForFunctionPointer was added to the Marshal class to allow late binding. This meant that external DLLs could be discovered and loaded at runtime.

Below is a C# wrapper to the ESRI Projection Engine.  The wrapper is developed as a singleton so that the DLL is only loaded once per application instance.  Additionally, the location of the projection engine library is discovered at runtime from the registry.

Here is a example using the projection engine singleton for ArcGIS Explorer to calculate the geodetic distance between London and Paris.  The geodetic distance is the shortest distance between two points on the surface of an ellipsoid.

public class ProjectionEngineTest{
    public void Main() {
        double distance = 0d;
        double azimuthForward = 0d;
        double azimuthBack = 0d;
        ProjectionEngine projectionEngine = ProjectionEngineExplorer.GetInstance();
        projectionEngine.GetGeodesicDistance(
            ProjectionEngine.GLOBERADIUS,
            0,
            ProjectionEngine.ToRadians(51.50d),
            ProjectionEngine.ToRadians(-0.12d),
            ProjectionEngine.ToRadians(48.86d),
            ProjectionEngine.ToRadians(2.35d),
            out distance,
            out azimuthForward,
            out azimuthBack);
        MessageBox.Show(string.Format("London to Paris is {0} meters", distance.ToString()));               
    }
}

image

Here is the source code to the wrappers.

Please note that the code and methodology used in this post is not endorsed or supported by ESRI. Use this code at your own risk.

using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;
using Microsoft.Win32;

namespace ESRI.ArcGIS.Sample {
    /// <summary>
    /// Projection Engine Abstract Class
    /// </summary>
    public abstract class ProjectionEngine {
        public const double GLOBERADIUS = 6367444.65712259d;
        private GeodesicDistance m_gd = null;
        private GeodesicCoordinate m_gc = null;

        [DllImport("kernel32.dll")]
        private static extern IntPtr LoadLibrary(String dllname);

        [DllImport("kernel32.dll")]
        private static extern IntPtr GetProcAddress(IntPtr hModule, String procname);

        private delegate void GeodesicDistance(
            [In] double semiMajorAxis,
            [In] double eccentricity,
            [In] double longitude1,
            [In] double latitude1,
            [In] double longitude2,
            [In] double latitude2,
            [Out] out double distance,
            [Out] out double azimuthForward,
            [Out] out double azimuthBack);

        private delegate void GeodesicCoordinate(
            [In] double semiMajorAxis,
            [In] double eccentricity,
            [In] double longitude1,
            [In] double latitude1,
            [In] double distance,
            [In] double azimuth,
            [Out] out double longitude2,
            [Out] out double latitude2);
        //
        // CONSTRUCTOR
        //
        protected ProjectionEngine() {
            IntPtr pe = ProjectionEngine.LoadLibrary(this.DLLPath);
            IntPtr gd = ProjectionEngine.GetProcAddress(pe, "pe_geodesic_distance");
            IntPtr gc = ProjectionEngine.GetProcAddress(pe, "pe_geodesic_coordinate");

            this.m_gd = (GeodesicDistance)Marshal.GetDelegateForFunctionPointer(
                gd, typeof(GeodesicDistance));
            this.m_gc = (GeodesicCoordinate)Marshal.GetDelegateForFunctionPointer(
                gc, typeof(GeodesicCoordinate));
        }
        //
        // PUBLIC METHODS
        //
        public abstract string DLLPath { get;}
        //
        // PUBLIC METHODS
        //
        /// <summary>
        /// Returns the geodestic azimuth and distance between two geographic locations.
        /// </summary>
        /// <param name="semiMajorAxis">Semi Major Axis</param>
        /// <param name="eccentricity">Globe Eccentricity</param>
        /// <param name="longitude1">From Longitude (radians)</param>
        /// <param name="latitude1">From Latitude (radians)</param>
        /// <param name="longitude2">To Longitude (radians)</param>
        /// <param name="latitude2">To Latitude (radians)</param>
        /// <param name="distance">Returned Geodetic Distance</param>
        /// <param name="azimuthForward">Returned Forward Azimuth (radians)</param>
        /// <param name="azimuthBack">Returned Reverse Azimuth (radians)</param>
        public void GetGeodesicDistance(
            double semiMajorAxis,
            double eccentricity,
            double longitude1,
            double latitude1,
            double longitude2,
            double latitude2,
            out double distance,
            out double azimuthForward,
            out double azimuthBack) {
            this.m_gd(
                semiMajorAxis,
                eccentricity,
                longitude1,
                latitude1,
                longitude2,
                latitude2,
                out distance,
                out azimuthForward,
                out azimuthBack);
        }
        /// <summary>
        /// Returns the geographic location based on the azimuth and distance from another geographic location
        /// </summary>
        /// <param name="semiMajorAxis">Semi Major Axis</param>
        /// <param name="eccentricity">Globe Eccentricity</param>
        /// <param name="longitude1">From Longitude (radians)</param>
        /// <param name="latitude1">From Latitude (radians)</param>
        /// <param name="distance">Distance from "From Location"</param>
        /// <param name="azimuth">Azimuth from "From Location"</param>
        /// <param name="longitude2">Out Logitude (in radians)</param>
        /// <param name="latitude2">Out Latitude (in radians)</param>
        public void GetGeodesicCoordinate(
            double semiMajorAxis,
            double eccentricity,
            double longitude1,
            double latitude1,
            double distance,
            double azimuth,
            out double longitude2,
            out double latitude2) {
            this.m_gc(
                semiMajorAxis,
                eccentricity,
                longitude1,
                latitude1,
                distance,
                azimuth,
                out longitude2,
                out latitude2);
        }
        /// <summary>
        /// Converts Radians to Decimal Degrees.
        /// </summary>
        /// <param name="degrees">In angle in decimal degrees.</param>
        /// <returns>Returns angle in radians.</returns>
        public static double ToRadians(double degrees) { return degrees * (Math.PI / 180d); }
        /// <summary>
        /// Converts Radians to Decimal Degrees.
        /// </summary>
        /// <param name="radians">In angle in radians.</param>
        /// <returns>Returns angle in decimal degrees.</returns>
        public static double ToDegrees(double radians) { return radians * (180d / Math.PI); }
    }
    /// <summary>
    /// Projection Engine Singleton for ArcGIS Explorer
    /// </summary>
    public sealed class ProjectionEngineExplorer : ProjectionEngine {
        private static ProjectionEngineExplorer projectionEngine;
        //
        // PUBLIC METHODS
        //
        public static ProjectionEngineExplorer GetInstance() {
            if (projectionEngine == null) {
                projectionEngine = new ProjectionEngineExplorer();
            }
            return projectionEngine;
        }
        public override string DLLPath {
            get {
                RegistryKey coreRuntime = Registry.LocalMachine.OpenSubKey(
                    @"SOFTWARE\ESRI\E2\CoreRuntime", false);
                object installDir = coreRuntime.GetValue("InstallDir", null);
                coreRuntime.Close();
                string folder = installDir.ToString();
                string bin = Path.Combine(folder, "bin");
                string pedll = Path.Combine(bin, "pe.dll");
                return pedll;
            }
        }
    }
    /// <summary>
    /// Projection Engine Singleton for ArcGIS Desktop
    /// </summary>
    public sealed class ProjectionEngineDesktop : ProjectionEngine {
        private static ProjectionEngineDesktop projectionEngine;
        //
        // PUBLIC METHODS
        //
        public static ProjectionEngineDesktop GetInstance() {
            if (projectionEngine == null) {
                projectionEngine = new ProjectionEngineDesktop();
            }
            return projectionEngine;
        }
        public override string DLLPath {
            get {
                RegistryKey coreRuntime = Registry.LocalMachine.OpenSubKey(
                    @"SOFTWARE\ESRI\CoreRuntime", false);
                object installDir = coreRuntime.GetValue("InstallDir", null);
                coreRuntime.Close();
                string folder = installDir.ToString();
                string bin = Path.Combine(folder, "bin");
                string pedll = Path.Combine(bin, "pe.dll");
                return pedll;
            }
        }
    }
}

1 comment: