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;
            }
        }
    }
}

ArcGIS Diagrammer - Reverse Engineering a Geodatabase to a Diagram

This post will describe how to create a ready-to-print diagram of your geodatabase in four easy steps.

This exercise will use ArcGIS Diagrammer (free) which is available from ArcScripts.  Prerequisites for ArcGIS Diagrammer include ArcGIS Desktop 9.2 and Microsoft .NET Framework 2.0.

Step 1: Export an existing geodatabase to an ESRI XML Workspace Document

Right click on a personal geodatabase, file geodatabase or an SDE connection. Select Export > XML Workspace Document...

image

Select Schema Only and enter an output file name.  For example C:\Temp\Montgomery.xml. Click Next.

image

Click Finish to start the export.

image

Step 2: Load the XML Workspace Document into ArcGIS Diagrammer

Start ArcGIS Diagrammer by clicking:
START > All Programs > ArcGIS > ArcGIS Diagrammer > ArcGIS Diagrammer

clip_image002

Drag and drop the XML file created by ArcCatalog into the ArcGIS Diagrammer canvas.

image

ArcGIS Diagrammer will render all the geodatabase objects and associations.  You may want to change the diagram scale, for example, to change the zooming scale to 25% click View > Zoom > 25%.

image

Step 3: Arrange geodatabase objects

All geodatabase objects can be re-arranged to suit your needs.  Also, some objects like subtypes, domains and feature classes can be expanded to reveal additional details.

image

Step 4: Print the Diagram

Before printing or plotting the diagram you may want to specify a printer/plotter and paper size.  Click File > Print Setup...

image

In the Printer Setup dialog select a printer and paper size.

image

Finally, click File > Print to send the diagram for printing/plotting.

image

Accessing ESRI Style Files using ADO.NET

ESRI's products ArcGIS Engine, ArcGIS Desktop and ArcGIS Server use style files to store and manage collections of symbols. A symbol is a graphic used to represent a geographic feature or class of features. Styles contains named and categorized symbols for various geographic entities like points, lines and polygons.

ArcGIS Engine and ArcGIS Desktop style files are Microsoft Access databases with a style file extension.

ArcGIS Server style files use a proprietary file type and have a serverstyle file extension. Developers can only use the ArcObjects API to access this file.

ArcGIS Engine developers can use the SymbologyControl to browse symbols contained in a server style. Whereas ArcGIS Desktop developers can re-use the desktop dialogs like the StyleManagerDialog, SymbolEditor and the SymbolPickerDialog.

In this post I will present a sample that demonstrates how to access ESRI style files using ADO.NET. The sample will also show how to convert an ESRI symbol into a .NET bitmap. This sample may be useful to developers that want to:
1) access styles without using ArcObjects, or
2) store custom information in an ESRI style file, or
3) convert ESRI symbols to bitmaps, or
4) create a custom symbol browser.

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.

This sample was developed in C# using Microsoft Visual Studio 2005.  The windows application consists of four controls and one component, these are:
1) textBox1: Path name to the style file,
2) button1: Opens the style file and adds entries to the ListView,
3) listView1: Display symbol icon and descriptions,
4) axLicenseControl1: Checks out an ArcGIS Engine or ArcGIS Desktop at runtime,
5) imageList1: Store a collection of images for the ListView items.

image

When the form loads:
1) A default path name is added to the style TextBox,
2) Four columns are added to the ListView,
3) The ListView is view style is set to details,
4) The ImageList is assigned to the SmallImageList property of the ListView.

The application, when loaded, looks like this.

image

If you run this application in debug mode, Microsoft Visual Studio 2005 may throw a PInvokeStackImbalance exception.  This can be overcome by running the application normally from the executable or by disabling this exception.  This and other debug exceptions can be ignored using the Exception window (Debug > Exceptions...).

To load the point, line and polygon symbols from the style to the ListView click GO!

image

Here is the source code for the sample:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Data.OleDb;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using stdole;

using ESRI.ArcGIS.Display;
using ESRI.ArcGIS.Geometry;
using ESRI.ArcGIS.Framework;

namespace BrowseEsriSymbols {
    public partial class Form1 : Form {
        private const string GDB_TABLE_MARKER = "MARKER SYMBOLS";
        private const string GDB_TABLE_LINE = "LINE SYMBOLS";
        private const string GDB_TABLE_AREA = "FILL SYMBOLS";
        private const string JET_SCHEMA_TABLE = "TABLE_NAME";
        private const string JET_SCHEMA_COLUMN = "COLUMN_NAME";
        private const string FIELD_ID = "ID";
        private const string FIELD_CATEGORY = "CATEGORY";
        private const string FIELD_NAME = "NAME";

        public Form1() {
            // Initialize Components
            InitializeComponent();
        }
        private void Form1_Load(object sender, EventArgs e) {
            // Set Button Name from Resource
            this.textBox1.Text = "C:\\Program Files\\ArcGIS\\Styles\\Conservation.style";
            this.listView1.SmallImageList = this.imageList1;
            this.listView1.View = View.Details;
            this.listView1.Columns.Add("Table");
            this.listView1.Columns.Add("Id");
            this.listView1.Columns.Add("Category");
            this.listView1.Columns.Add("Name");
        }
        private void button1_Click(object sender, EventArgs e) {
            // Get Drawing Context
            Graphics graphics = this.CreateGraphics();

            // Display Hourglass Cursor
            this.Cursor = Cursors.WaitCursor;

            // Remove all rows (if any)
            this.listView1.Items.Clear();

            // Stop ListView Redraws
            this.listView1.BeginUpdate();

            // Connect to Style (using ADO.NET)
            string connection = "Provider=Microsoft.Jet.OleDb.4.0;Data Source=" + this.textBox1.Text + ";";
            OleDbConnection oleConnection = new OleDbConnection(connection);
            oleConnection.Open();

            // Style Hardcode Table Names and Fields
            string[] tables = new string[] { GDB_TABLE_MARKER, GDB_TABLE_LINE, GDB_TABLE_AREA };

            // Loop For Each Symbol Table in Style
            foreach (string table in tables) {
                // Construct SQL statement
                string query = "SELECT * FROM [" + table + "] ";

                // Connect to the Access File and create reader
                OleDbCommand command = new OleDbCommand(query, oleConnection);
                OleDbDataReader dataReader = command.ExecuteReader();
                if (!dataReader.HasRows) { continue; }

                // Find column indexes
                int indexId = dataReader.GetOrdinal(FIELD_ID);
                int indexCategory = dataReader.GetOrdinal(FIELD_CATEGORY);
                int indexName = dataReader.GetOrdinal(FIELD_NAME);

                // Read each row in table
                while (dataReader.Read()) {
                    // Get row values
                    int id = dataReader.GetInt32(indexId);
                    string category = dataReader.GetString(indexCategory);
                    string name = dataReader.GetString(indexName);

                    // Get ESRI Symbol
                    ISymbol symbol = WindowsAPI.GetSymbol(this.textBox1.Text, table, id);

                    // Convert ESRI Symbol to Bitmap
                    Bitmap bitmap = WindowsAPI.SymbolToBitmap(
                        symbol,
                        new Size(16, 16),
                        graphics,
                        ColorTranslator.ToWin32(this.listView1.BackColor));

                    // Add Image to the listview's image list
                    int index = this.listView1.SmallImageList.Images.Add(bitmap, Color.Transparent);

                    // Add ListViewItem to ListView
                    string[] cells = new string[] { table, id.ToString(), category, name };
                    ListViewItem item = new ListViewItem(cells);
                    this.listView1.Items.Add(item);
                    item.ImageIndex = index;
                }

                // Close Reader
                dataReader.Close();
            }

            // Close Connection
            oleConnection.Close();

            // Resume ListView Drawing
            this.listView1.EndUpdate();

            // Display Default Cursor
            this.Cursor = Cursors.Default;
        }
    }

    public static class WindowsAPI {
        private const int COLORONCOLOR = 3;
        private const int HORZSIZE = 4;
        private const int VERTSIZE = 6;
        private const int HORZRES = 8;
        private const int VERTRES = 10;
        private const int ASPECTX = 40;
        private const int ASPECTY = 42;
        private const int LOGPIXELSX = 88;
        private const int LOGPIXELSY = 90;

        private enum PictureTypeConstants {
            picTypeNone = 0,
            picTypeBitmap = 1,
            picTypeMetafile = 2,
            picTypeIcon = 3,
            picTypeEMetafile = 4
        }
        private struct PICTDESC {
            public int cbSizeOfStruct;
            public int picType;
            public IntPtr hPic;
            public IntPtr hpal;
            public int _pad;
        }
        private struct RECT {
            public int Left;
            public int Top;
            public int Right;
            public int Bottom;
        }

        [DllImport("olepro32.dll", EntryPoint = "OleCreatePictureIndirect", PreserveSig = false)]
        private static extern int OleCreatePictureIndirect(
            ref PICTDESC pPictDesc, ref Guid riid, bool fOwn, out IPictureDisp ppvObj);

        [DllImport("gdi32.dll", EntryPoint = "CreateCompatibleDC", ExactSpelling = true, SetLastError = true)]
        private static extern IntPtr CreateCompatibleDC(IntPtr hDC);

        [DllImport("gdi32.dll", EntryPoint = "DeleteDC", ExactSpelling = true, SetLastError = true)]
        private static extern bool DeleteDC(IntPtr hdc);

        [DllImport("gdi32.dll", EntryPoint = "SelectObject", ExactSpelling = true, SetLastError = true)]
        private static extern IntPtr SelectObject(IntPtr hDC, IntPtr hObject);

        [DllImport("gdi32.dll", EntryPoint = "DeleteObject", ExactSpelling = true, SetLastError = true)]
        private static extern bool DeleteObject(IntPtr hObject);

        [DllImport("gdi32.dll", EntryPoint = "CreateCompatibleBitmap", ExactSpelling = true,
            SetLastError = true)]
        private static extern IntPtr CreateCompatibleBitmap(IntPtr hObject, int width, int height);

        [DllImport("user32.dll", EntryPoint = "GetDC", ExactSpelling = true, SetLastError = true)]
        private static extern IntPtr GetDC(IntPtr ptr);

        [DllImport("user32.dll", EntryPoint = "ReleaseDC", ExactSpelling = true, SetLastError = true)]
        private static extern IntPtr ReleaseDC(IntPtr hWnd, IntPtr hDc);

        [DllImport("gdi32", EntryPoint = "CreateSolidBrush", ExactSpelling = true, SetLastError = true)]
        private static extern IntPtr CreateSolidBrush(int crColor);

        [DllImport("user32", EntryPoint = "FillRect", ExactSpelling = true, SetLastError = true)]
        private static extern int FillRect(IntPtr hdc, ref RECT lpRect, IntPtr hBrush);

        [DllImport("GDI32.dll", EntryPoint = "GetDeviceCaps", ExactSpelling = true, SetLastError = true)]
        private static extern int GetDeviceCaps(IntPtr hdc, int nIndex);

        [DllImport("user32", EntryPoint = "GetClientRect", ExactSpelling = true, SetLastError = true)]
        private static extern int GetClientRect(IntPtr hwnd, ref RECT lpRect);

        public static ISymbol GetSymbol(string style, string classname, int id) {
            ISymbol symbol = null;
            IStyleGallery styleGallery = new StyleGalleryClass();
            IStyleGalleryStorage styleGalleryStorage = (IStyleGalleryStorage)styleGallery;
            styleGalleryStorage.TargetFile = style;
            IEnumStyleGalleryItem styleGalleryItems = styleGallery.get_Items(classname, style, "");
            styleGalleryItems.Reset();
            IStyleGalleryItem styleGalleryItem = styleGalleryItems.Next();
            while (styleGalleryItem != null) {
                if (styleGalleryItem.ID == id) {
                    symbol = (ISymbol)styleGalleryItem.Item;
                    break;
                }
                styleGalleryItem = styleGalleryItems.Next();
            }
            styleGalleryItem = null;
            styleGalleryStorage = null;
            styleGallery = null;

            return symbol;
        }
        private static IPictureDisp CreatePictureFromSymbol(IntPtr hDCOld, ref IntPtr hBmpNew,
            ISymbol pSymbol, Size size, int lGap, int backColor) {
            IntPtr hDCNew = IntPtr.Zero;
            IntPtr hBmpOld = IntPtr.Zero;
            try {
                hDCNew = CreateCompatibleDC(hDCOld);
                hBmpNew = CreateCompatibleBitmap(hDCOld, size.Width, size.Height);
                hBmpOld = SelectObject(hDCNew, hBmpNew);

                // Draw the symbol to the new device context.
                bool lResult = DrawToDC(hDCNew, size, pSymbol, lGap, backColor);

                hBmpNew = SelectObject(hDCNew, hBmpOld);
                DeleteDC(hDCNew);

                // Return the Bitmap as an OLE Picture.
                return CreatePictureFromBitmap(hBmpNew);
            }
            catch (Exception error) {
                if (pSymbol != null) {
                    pSymbol.ResetDC();
                    if ((hBmpNew != IntPtr.Zero) && (hDCNew != IntPtr.Zero) && (hBmpOld != IntPtr.Zero)) {
                        hBmpNew = SelectObject(hDCNew, hBmpOld);
                        DeleteDC(hDCNew);
                    }
                }

                throw error;
            }
        }
        private static IPictureDisp CreatePictureFromBitmap(IntPtr hBmpNew) {
            try {
                Guid iidIPicture = new Guid("7BF80980-BF32-101A-8BBB-00AA00300CAB");

                PICTDESC picDesc = new PICTDESC();
                picDesc.cbSizeOfStruct = Marshal.SizeOf(picDesc);
                picDesc.picType = (int)PictureTypeConstants.picTypeBitmap;
                picDesc.hPic = (IntPtr)hBmpNew;
                picDesc.hpal = IntPtr.Zero;

                // Create Picture object.
                IPictureDisp newPic;
                OleCreatePictureIndirect(ref picDesc, ref iidIPicture, true, out newPic);

                // Return the new Picture object.
                return newPic;
            }
            catch (Exception error) {
                throw error;
            }
        }
        private static bool DrawToWnd(IntPtr hWnd, ISymbol pSymbol, int lGap, int backColor) {
            IntPtr hDC = IntPtr.Zero;
            try {
                if (hWnd != IntPtr.Zero) {
                    // Calculate size of window.
                    RECT udtRect = new RECT();
                    int lResult = GetClientRect(hWnd, ref udtRect);

                    if (lResult != 0) {
                        int lWidth = (udtRect.Right - udtRect.Left);
                        int lHeight = (udtRect.Bottom - udtRect.Top);

                        hDC = GetDC(hWnd);
                        // Must release the DC afterwards.
                        if (hDC != IntPtr.Zero) {
                            bool ok = DrawToDC(hDC, new Size(lWidth, lHeight), pSymbol, lGap, backColor);

                            // Release cached DC obtained with GetDC.
                            ReleaseDC(hWnd, hDC);

                            return ok;
                        }
                    }
                }
            }
            catch {
                if (pSymbol != null) {
                    // Try resetting DC, in case we have already called SetupDC for this symbol.
                    pSymbol.ResetDC();

                    if ((hWnd != IntPtr.Zero) && (hDC != IntPtr.Zero)) {
                        ReleaseDC(hWnd, hDC); // Try to release cached DC obtained with GetDC.
                    }
                }
                return false;
            }
            return true;
        }
        private static bool DrawToDC(IntPtr hDC, Size size, ISymbol pSymbol, int lGap, int backColor) {
            try {
                if (hDC != IntPtr.Zero) {
                    // First clear the existing device context.
                    if (!Clear(hDC, backColor, 0, 0, size.Width, size.Height)) {
                        throw new Exception("Could not clear the Device Context.");
                    }

                    // Create the Transformation and Geometry required by ISymbol::Draw.
                    ITransformation pTransformation = CreateTransFromDC(hDC, size.Width, size.Height);
                    IEnvelope pEnvelope = new EnvelopeClass();
                    pEnvelope.PutCoords(lGap, lGap, size.Width - lGap, size.Height - lGap);
                    IGeometry pGeom = CreateSymShape(pSymbol, pEnvelope);

                    // Perform the Draw operation.
                    if ((pTransformation != null) && (pGeom != null)) {
                        pSymbol.SetupDC(hDC.ToInt32(), pTransformation);
                        pSymbol.Draw(pGeom);
                        pSymbol.ResetDC();
                    }
                    else {
                        throw new Exception("Could not create required Transformation or Geometry.");
                    }
                }
            }
            catch {
                if (pSymbol != null) {
                    pSymbol.ResetDC();
                }
                return false;
            }

            return true;
        }
        private static bool Clear(IntPtr hDC, int backgroundColor, int xmin, int ymin, int xmax, int ymax) {
            // This function fill the passed in device context with a solid brush,
            // based on the OLE color passed in.
            IntPtr hBrushBackground = IntPtr.Zero;
            int lResult;
            bool ok;

            try {
                RECT udtBounds;
                udtBounds.Left = xmin;
                udtBounds.Top = ymin;
                udtBounds.Right = xmax;
                udtBounds.Bottom = ymax;

                hBrushBackground = CreateSolidBrush(backgroundColor);
                if (hBrushBackground == IntPtr.Zero) {
                    throw new Exception("Could not create GDI Brush.");
                }
                lResult = FillRect(hDC, ref udtBounds, hBrushBackground);
                if (hBrushBackground == IntPtr.Zero) {
                    throw new Exception("Could not fill Device Context.");
                }
                ok = DeleteObject(hBrushBackground);
                if (hBrushBackground == IntPtr.Zero) {
                    throw new Exception("Could not delete GDI Brush.");
                }
            }
            catch {
                if (hBrushBackground != IntPtr.Zero) {
                    ok = DeleteObject(hBrushBackground);
                }
                return false;
            }

            return true;
        }
        private static ITransformation CreateTransFromDC(IntPtr hDC, int lWidth, int lHeight) {
            // Calculate the parameters for the new transformation,
            // based on the dimensions passed to this function.
            try {
                IEnvelope pBoundsEnvelope = new EnvelopeClass();
                pBoundsEnvelope.PutCoords(0.0, 0.0, (double)lWidth, (double)lHeight);

                tagRECT deviceRect;
                deviceRect.left = 0;
                deviceRect.top = 0;
                deviceRect.right = lWidth;
                deviceRect.bottom = lHeight;

                int dpi = GetDeviceCaps(hDC, LOGPIXELSY);
                if (dpi == 0) {
                    throw new Exception("Could not retrieve Resolution from device context.");
                }

                // Create a new display transformation and set its properties.
                IDisplayTransformation newTrans = new DisplayTransformationClass();
                newTrans.VisibleBounds = pBoundsEnvelope;
                newTrans.Bounds = pBoundsEnvelope;
                newTrans.set_DeviceFrame(ref deviceRect);
                newTrans.Resolution = dpi;

                return newTrans;
            }
            catch {
                return null;
            }
        }
        private static IGeometry CreateSymShape(ISymbol pSymbol, IEnvelope pEnvelope) {
            // This function returns an appropriate Geometry type depending on the
            // Symbol type passed in.
            try {
                if (pSymbol is IMarkerSymbol) {
                    // For a MarkerSymbol return a Point.
                    IArea pArea = (IArea)pEnvelope;
                    return pArea.Centroid;
                }
                else if ((pSymbol is ILineSymbol) || (pSymbol is ITextSymbol)) {
                    // For a LineSymbol or TextSymbol return a Polyline.
                    IPolyline pPolyline = new PolylineClass();
                    pPolyline.FromPoint = pEnvelope.LowerLeft;
                    pPolyline.ToPoint = pEnvelope.UpperRight;
                    return pPolyline;
                }
                else {
                    // For any FillSymbol return an Envelope.
                    return pEnvelope;
                }
            }
            catch {
                return null;
            }
        }
        public static Bitmap SymbolToBitmap(ISymbol userSymbol, Size size, Graphics gr, int backColor) {
            IntPtr graphicsHdc = gr.GetHdc();
            IntPtr hBitmap = IntPtr.Zero;
            IPictureDisp newPic = CreatePictureFromSymbol(
                graphicsHdc, ref hBitmap, userSymbol, size, 1, backColor);
            Bitmap newBitmap = Bitmap.FromHbitmap(hBitmap);
            gr.ReleaseHdc(graphicsHdc);

            return newBitmap;
        }
    }
}

The ESRI Generic Get Position Tool

ESRI's ArcGIS Desktop product includes the applications ArcCatalog, ArcMap, ArcScene and ArcGlobe.  The user interfaces of all four applications can be heavily customized by developers, for example, a developer can deploy custom toolbars and buttons.

A button (or command in ESRI terminology) is a COM class that implements the ICommand interface. Some commands require geographic interaction, for example, clicking on a country in a world map.  These commands must implement the ITool interface.

Normally, developers create sets of commands that are specific to each project.  In this article I will discuss an out-of-the-box command called Generic Get Point Tool that can be used for map/globe interaction. Using this command in future projects may reduce your development time.

To demonstrate this command I have created an ArcGIS Engine-based windows application sample using Microsoft .Net Framework 2.0.  The application consists of a map, an invisible toolbar and a button that will activate the Generic Get Point Tool.  After the map is clicked, a message box will display the map coordinates of the clicked location and then deactivate the tool.

The sample windows application (C#) was created using Microsoft Visual Studio 2005.  It consists of a map control, toolbar control, license control and a button.  The map control is assigned to the toolbar control's buddy property and the map control's is assigned a map document to load at runtime.

image 

When the form loads, the Generic Get Point Tool is added to the toolbar and then by casting the command to the IToolPositionInitializer the form is assigned as the callback (because it supports IToolPositionCallback).

image

When the button is clicked the Generic Get Point Tool is located on the toolbar and then assigned as the current tool.  This essentially activates the tool and deactivates the previously active tool (if any).

image

When the user clicks on the map display with the Generic Get Point Tool activated, the command will called the MouseClicked method on the IToolPositionCallback interface.  In the sample, the form implements this interface and displays a message box displaying the map coordinates.  Rather than leaving the tool active, setting IToolbarControl::CurrentTool (or IMapControl::CurrentTool) to null effectively deactivate the tool.

image

The sample code follows:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

using ESRI.ArcGIS.Controls;
using ESRI.ArcGIS.esriSystem;
using ESRI.ArcGIS.SystemUI;
using ESRI.ArcGIS.Geometry;

namespace WindowsApplication1 {
    public partial class Form1 : Form, IToolPositionCallback{
        public Form1() {
            InitializeComponent();
        }
        private void Form1_Load(object sender, EventArgs e) {
            // Hide ESRI Toolbar
            this.axToolbarControl1.Hide();

            // Add Generic Get Point Tool
            UID uid = new UIDClass();
            uid.Value = "{30DBCBEF-951E-4904-89DE-5A5F1AF571EE}";
            IToolbarControl toolbarControl = (IToolbarControl)this.axToolbarControl1.Object;
            toolbarControl.AddItem(uid, -1, -1, false, -1, esriCommandStyles.esriCommandStyleIconOnly);

            // Find Generic Get Point Tool on Toolbar
            int index = toolbarControl.Find(uid);
            IToolbarItem toolbarItem = toolbarControl.GetItem(index);
            ICommand command = toolbarItem.Command;

            // Assign Callback to Generic Get Point Tool
            IToolPositionInitializer toolPositionInitializer = (IToolPositionInitializer)command;
            toolPositionInitializer.Initialize(this, -1);
        }
        private void Button_Click(object sender, EventArgs e) {
            if (sender == this.buttonGetPoint) {
                // Find Generic Get Point Tool
                UID uid = new UIDClass();
                uid.Value = "{30DBCBEF-951E-4904-89DE-5A5F1AF571EE}";
                IToolbarControl toolbarControl = (IToolbarControl)this.axToolbarControl1.Object;
                int index = toolbarControl.Find(uid);
                IToolbarItem toolbarItem = toolbarControl.GetItem(index);
                ICommand command = toolbarItem.Command;
                ITool tool = (ITool)command;

                // Set Generic Get Point Tool as the current tool
                toolbarControl.CurrentTool = tool;
            }
        }
        #region IToolPositionCallback Members
        public void Deactivated() { }
        public void MouseClicked(int lCookie, int button, int shift, int X, int Y, double mapX, double mapY,
            ISpatialReference pSpace) {
            // Display Message
            string message = string.Format(
                "Map location" + Environment.NewLine +
                "X: {0}" + Environment.NewLine +
                "Y: {1}",
                mapX.ToString(),
                mapY.ToString());
            MessageBox.Show(message, "Test", MessageBoxButtons.OK, MessageBoxIcon.Information);

            // Deactivate Generic Get Point Tool
            IToolbarControl toolbarControl = (IToolbarControl)this.axToolbarControl1.Object;
            toolbarControl.CurrentTool = null;
        }
        #endregion
    }
} 

ArcGIS Tablet Editor

Recently posted on ESRI's ArcScripts website is a set of commands for ArcMap targeting the capabilities of a Tablet PC.  Specifically, digital ink, text recognition, text-to-speech (TTS) and speech recognition.

The ArcGIS Tablet Editor can be downloaded from here:
http://arcscripts.esri.com/details.asp?dbid=14921 (full source code provided)

Prerequisites:

Microsoft Windows XP TabletPC Edition
ESRI ArcGIS Desktop 9.2
Microsoft .NET Framework 2.0
ESRI .NET Support (click here to learn how to install)

How to Install:

Download the ZIP file from ArcScripts
Unzip the download
Double click on the "msi" file.  Follow the install wizard instructions.

How to get started?

Start ArcMap (Windows: Start > ArcGIS > ArcMap)
Display the Tablet Editor toolbar (ArcMap: View > Toolbars > Tablet Editor)
Load a map document.  To show some of the advanced capabilities add one or more layers with a unique renderer.

Demonstration Screenshots:

 
The Feature Selector Tool
To select features using ink first click on a layer in the ArcMap table of contents and then activate the Feature Selection tool on the Tablet Editor toolbar.  Using your pointing device drag your mouse over the screen.  As soon as you lift the mouse/pen, features from the selected layer will be selected (if inside the ink).


The Verbal Dimensioning Tool
Click on the Verbal Dimensioning Tool (third from left) on the Tablet Editor Toolbar.  Drag straight ink line on screen.  The computer will speak the length in the current map units, for example. "Eighty Four Meters".  Lengths will be rounded to the nearest map unit.


The Add Features Tool
Now, start an edit session (click Editor > Start Editing from the Editor Toolbar).  Select the layer in the ArcMap table of contents that you would like to add features to.  In the screenshot above the user would like to add more light poles.  Click the Add Features Tool (first button on Tablet Editor toolbar) and draw small marks on the screen as shown above.  When done, make a right-up gesture with your pointing device.  This will cause the small ink dashes to turn into new light poles.


The Attribute Transfer Tool
Using the Add Feature Tool in the previous step, four new Light Poles were added.  The new light poles adopted whatever default values were associated with the parent featureclass (and/or subtype).  With the Light Poles layer still selected in the ArcMap table of contents click the Attribute Transfer Tool (second button on the Tablet Editor Toolbar).  Draw two ink rings, each circling a light pole.  When the right-up gestures is used, attributes from the pole selected with the first ink stroke will be transfer to the pole(s) selected with the second ink stroke.


The Ink Updater
Again, whilst still in an edit session and with the Light Poles layer selected in the ArcMap table of contents select a few poles with the Feature Selector Tool (as show previously).  Now click the Ink Updater tool (the fifth button on the Tablet Editor toolbar).  Because the selected layer is using a unique renderer the user can reclassify the selected poles by writing "f" followed by a right-up gesture.  The Tablet Editor will use the ink text to find the first matching description (if any) in the layer and update the attributes accordingly.  If the layer does not have a unique renderer or if you want to explicitly select a field to update then you can click the Field Drop down menu to pick a field to edit.  The ink Updater is smart enough to convert ink into the appropriate data format, for example, ink will be converted to a date if a date field is selected.


The Speech Updater
This is probably the coolest tool on the Tablet Editor toolbar.  The user is still in an edit session, there are three light poles selected in the map and the Light Poles layer is selected in the ArcMap table of contents.  Click the Speech Updater button (sixth button on the Tablet Editor toolbar).  Now, say "Good".  The Tablet Editor will automatically reclassify the poles as "Good".  This tool will work with ANY layer that has a unique value rendered with descriptions in readable English.