Translating between AutoCAD drawing points and geographic locations using .NET – Part 2

In yesterday's post we saw a simple implementation of two commands to translate between geographical locations (latitude-longitude values) and drawing points inside AutoCAD.

In this post we're extending that to access the current coordinate system, as returned by the GeoLocation object attached to the current drawing. Which in some ways should be simple, but then the CoordinateSystem property actually returns XML data, not just the simple coordinate system name you probably passed in when choosing it (see the IGR command, below, to see what I mean):

<?xml version="1.0" encoding="utf-16" standalone="no" ?>

<Dictionary

  version="1.0"

  xmlns="http://www.osgeo.org/mapguide/coordinatesystem">

  <ProjectedCoordinateSystem id="SWISS">

    <Name>SWISS</Name>

    <Description>

      Deprecated as duplicate of CH1903.LV03/01

    </Description>

    <Authority>Bundesamt fur Landestopographie</Authority>

    <AdditionalInformation>

      <ParameterItem type="CsMap">

        <Key>CSQuadrantSimplified</Key>

        <IntegerValue>1</IntegerValue>

      </ParameterItem>

    </AdditionalInformation>

    <DatumId>CH-1903</DatumId>

    <Axis uom="METER">

      <CoordinateSystemAxis>

        <AxisOrder>1</AxisOrder>

        <AxisName>Easting</AxisName>

        <AxisAbbreviation>E</AxisAbbreviation>

        <AxisDirection>east</AxisDirection>

      </CoordinateSystemAxis>

      <CoordinateSystemAxis>

        <AxisOrder>2</AxisOrder>

        <AxisName>Northing</AxisName>

        <AxisAbbreviation>N</AxisAbbreviation>

        <AxisDirection>north</AxisDirection>

      </CoordinateSystemAxis>

    </Axis>

    <Conversion>

      <Projection>

        <OperationMethodId>

          Swiss Oblique Cylindrical

        </OperationMethodId>

        <ParameterValue>

          <OperationParameterId>

            Longitude of false origin

          </OperationParameterId>

          <Value uom="degree">7.43958333333333</Value>

        </ParameterValue>

        <ParameterValue>

          <OperationParameterId>

            Latitude of false origin

          </OperationParameterId>

          <Value uom="degree">46.9524055555556</Value>

        </ParameterValue>

        <ParameterValue>

          <OperationParameterId>False easting</OperationParameterId>

          <Value uom="METER">600000</Value>

        </ParameterValue>

        <ParameterValue>

          <OperationParameterId>False northing</OperationParameterId>

          <Value uom="METER">200000</Value>

        </ParameterValue>

      </Projection>

    </Conversion>

  </ProjectedCoordinateSystem>

  <GeodeticDatum id="CH-1903">

    <Name>CH-1903</Name>

    <Description>

      Swiss National Geodetic System Aug 1990, Switzerland (7 Param)

    </Description>

    <Authority>Bundesamt fur Landestopographie</Authority>

    <PrimeMeridianId>Greenwich</PrimeMeridianId>

    <EllipsoidId>BESSEL</EllipsoidId>

  </GeodeticDatum>

  <Alias id="6801" type="Datum">

    <ObjectId>CH-1903</ObjectId>

    <Namespace>EPSG Code</Namespace>

  </Alias>

  <Ellipsoid id="BESSEL">

    <Name>BESSEL</Name>

    <Description>Bessel, 1841</Description>

    <Authority>

      US Defense Mapping Agency, TR-8350.2-B, December 1987

    </Authority>

    <SemiMajorAxis uom="meter">6377397.155</SemiMajorAxis>

    <SecondDefiningParameter>

      <SemiMinorAxis uom="meter">6356078.963</SemiMinorAxis>

    </SecondDefiningParameter>

  </Ellipsoid>

  <Alias id="7004" type="Ellipsoid">

    <ObjectId>BESSEL</ObjectId>

    <Namespace>EPSG Code</Namespace>

  </Alias>

  <Transformation id="CH-1903_to_WGS84">

    <Name>CH-1903_to_WGS84</Name>

    <Description>

      Swiss National Geodetic System Aug 1990, Switzerland (7 Param)

    </Description>

    <Authority>Bundesamt fur Landestopographie</Authority>

    <CoordinateOperationAccuracy>

      <Accuracy uom="meter">3</Accuracy>

    </CoordinateOperationAccuracy>

    <SourceDatumId>CH-1903</SourceDatumId>

    <TargetDatumId>WGS84</TargetDatumId>

    <IsReversible>true</IsReversible>

    <OperationMethod>

      <OperationMethodId>

        Seven Parameter Transformation

      </OperationMethodId>

      <ParameterValue>

        <OperationParameterId>

          X-axis translation

        </OperationParameterId>

        <Value uom="meter">660.077</Value>

      </ParameterValue>

      <ParameterValue>

        <OperationParameterId>

          Y-axis translation

        </OperationParameterId>

        <Value uom="meter">13.551</Value>

      </ParameterValue>

      <ParameterValue>

        <OperationParameterId>

          Z-axis translation

        </OperationParameterId>

        <Value uom="meter">369.344</Value>

      </ParameterValue>

      <ParameterValue>

        <OperationParameterId>

          X-axis rotation

        </OperationParameterId>

        <Value uom="degree">0.00022356</Value>

      </ParameterValue>

      <ParameterValue>

        <OperationParameterId>

          Y-axis rotation

        </OperationParameterId>

        <Value uom="degree">0.00016047</Value>

      </ParameterValue>

      <ParameterValue>

        <OperationParameterId>

          Z-axis rotation

        </OperationParameterId>

        <Value uom="degree">0.00026451</Value>

      </ParameterValue>

      <ParameterValue>

        <OperationParameterId>

          Scale difference

        </OperationParameterId>

        <Value uom="unity">5.66e-006</Value>

      </ParameterValue>

    </OperationMethod>

  </Transformation>

</Dictionary>

While I'm sure this contains some great information, in our case we're just after the name of the chosen coordinate system (i.e. "SWISS").

To access this, we've integrated some code from here and here, so that we can use a dynamic object to navigate down through the XML DOM and we don't have to worry about namespace prefixes (which would otherwise be the case with the above XML data, for instance).

That allows us to access the contents of the coordinate system XML much more easily. Here's how we access the "id" attribute of the ProjectedCoordinateSystem element, for instance:

csx.ProjectedCoordinateSystem.id;

The same approach might be used to access whatever else you're interested in from the XML coordinate system information, of course.

Here's a Screencast of the updated commands in action:

Here's the C# code that includes the XML parsing code as well as the updated LLFP and PFLL commands:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Geometry;

using Autodesk.AutoCAD.Runtime;

using Microsoft.CSharp.RuntimeBinder;

using System;

using System.Dynamic;

using System.Linq;

using System.Text.RegularExpressions;

using System.Xml.Linq;

 

namespace GeoLocationAPI

{

  public class Commands

  {

    [CommandMethod("IGR")]

    public void InsertGeoRef()

    {

      var doc = Application.DocumentManager.MdiActiveDocument;

      if (doc == null)

        return;

      var ed = doc.Editor;


0;     var db = doc.Database;

      var msId = SymbolUtilityServices.GetBlockModelSpaceId(db);

 

      if (HasGeoData(db))

      {

        // Report and return: could also open the object for

        // write and modify its properties, of course

 

        ed.WriteMessage("\nDrawing already has geo-location data!");

        return;

      }

 

      // Let's create some geolocation data for this drawing,

      // using a handy method to add it to the modelspace

      // (it gets added to the extension dictionary)

 

      var data = new GeoLocationData();

      data.BlockTableRecordId = msId;

      data.PostToDb();

 

      // We're going to define our geolocation in terms of

      // latitude/longitude using the Mercator projection

      // http://en.wikipedia.org/wiki/Mercator_projection

 

      data.CoordinateSystem = "WORLD-MERCATOR";

      data.TypeOfCoordinates = TypeOfCoordinates.CoordinateTypeGrid;

 

      // Use the lat-long for La Tene, my local "beach"

      // (it's on a lake, after all :-)     

 

      var geoPt = new Point3d(7.019438, 47.005247, 0);

 

      // Transform from a geographic to a modelspace point

      // and add the information to our geolocation data

 

      var wcsPt = data.TransformFromLonLatAlt(geoPt);

      data.DesignPoint = wcsPt;

      data.ReferencePoint = geoPt;

 

      // Let's launch the GEOMAP command to show our geographic

      // overlay

 

      ed.Command("_.GEOMAP", "_AERIAL");

 

      // Now we'll add a circle around our location

      // and that will provide the extents for our zoom

 

      using (var tr = db.TransactionManager.StartTransaction())

      {

        var ms =

          tr.GetObject(msId, OpenMode.ForWrite) as BlockTableRecord;

        if (ms != null)

        {

          // Add a red circle of 7K units radius

          // centred on our point

 

          var circle = new Circle(wcsPt, Vector3d.ZAxis, 7000);

          circle.ColorIndex = 1;

          ms.AppendEntity(circle);

          tr.AddNewlyCreatedDBObject(circle, true);

        }

        tr.Commit();

      }

 

      // And we'll zoom to the circle's extents

 

      ed.Command("_.ZOOM", "_OBJECT", "_L", "");

    }

 

    [CommandMethod("CGI")]

    public void CreateGeoMapImage()

    {

      var doc = Application.DocumentManager.MdiActiveDocument;

      if (doc == null)

        return;

      var ed = doc.Editor;

      var db = doc.Database;

 

      // Get the first corner of our area to convert to a

      // GeomapImage

 

      var ppo = new PromptPointOptions("\nSpecify first corner");

      var ppr = ed.GetPoint(ppo);

      if (ppr.Status != PromptStatus.OK)

        return;

 

      var first = ppr.Value;

 

      // And get the second point as a corner (to rubber-band

      // the selection)

 

      var pco =

        new PromptCornerOptions("\nSpecify second corner", first);

      ppr = ed.GetCorner(pco);

 

      if (ppr.Status != PromptStatus.OK)

        return;

 

      var second = ppr.Value;

 

      // We'll use an event handler on the Database to check for

      // GeomapImage entities being added

      // (we'll use a lambda but assigned to a variable to be

      // able to remove it, afterwards)

 

      ObjectId giId = ObjectId.Null;

      ObjectEventHandler handler =

        (s, e) =>

        {

          if (e.DBObject is GeomapImage)

          {

            giId = e.DBObject.ObjectId;

          }

        };

 

      // Simply call the GEOMAPIMAGE command with the two points

 

      db.ObjectAppended += handler;

      ed.Command("_.GEOMAPIMAGE", first, second);

      db.ObjectAppended -= handler;

 

      // Only continue if we've collected a valid ObjectId

 

      if (giId == ObjectId.Null)

        return;

 

      // Open the entity and change some values

 

      try

      {

        using (var tr = doc.TransactionManager.StartTransaction())

        {

          // Get each object and check if it's a GeomapImage

 

          var gi =

            tr.GetObject(giId, OpenMode.ForWrite) as GeomapImage;

          if (gi != null)

          {

            // Let's adjust the brightmess/contrast/fade of the

            // GeomapImage

 

            gi.Brightness = 90;

            gi.Contrast = 40;

            gi.Fade = 20;

 

            // And make sure it's at the right resolution and

         
   // shows both aerial and road information

 

            gi.Resolution = GeomapResolution.Optimal;

            gi.MapType = GeomapType.Hybrid;

 

            gi.UpdateMapImage(true);

          }

 

          tr.Commit();

        }

      }

      catch (Autodesk.AutoCAD.Runtime.Exception)

      {

        ed.WriteMessage(

          "\nUnable to update geomap image entity." +

          "\nPlease check your internet connectivity and call " +

          "GEOMAPIMAGEUPDATE."

        );

      }

    }

 

    [CommandMethod("LLFP")]

    public void LatLongFromPoint()

    {

      var doc = Application.DocumentManager.MdiActiveDocument;

      if (doc == null)

        return;

      var ed = doc.Editor;

      var db = doc.Database;

 

      if (!HasGeoData(db))

      {

        ed.WriteMessage(

          "\nCurrent drawing has no geo-location information."

        );

        return;

      }

 

      // Get the drawing point to be translated into a lat-lon

 

      var ppo = new PromptPointOptions("\nSpecify point");

      var ppr = ed.GetPoint(ppo);

      if (ppr.Status != PromptStatus.OK)

        return;

 

      var dwgPt = ppr.Value;

 

      // Translate the drawing point to a lat-lon

 

      var res = TranslateGeoPoint(db, dwgPt, true);

 

      // Print any coordinate system information

 

      PrintCoordinateSystem(ed, res.Item2);

 

      // And then the point itself

 

      var lonlat = res.Item1;

      ed.WriteMessage(

        "\nLatitude-longitude is {0},{1}", lonlat.Y, lonlat.X

      );

    }

 

    [CommandMethod("PFLL")]

    public void PointFromLatLong()

    {

      var doc = Application.DocumentManager.MdiActiveDocument;

      if (doc == null)

        return;

      var ed = doc.Editor;

      var db = doc.Database;

 

      if (!HasGeoData(db))

      {

        ed.WriteMessage(

          "\nCurrent drawing has no geo-location information."

        );

        return;

      }

 

      // Get the latitude and longitude to be translated

      // to a drawing point

 

      var pdo = new PromptDoubleOptions("\nEnter latitude");

      var pdr = ed.GetDouble(pdo);

      if (pdr.Status != PromptStatus.OK)

        return;

 

      var lat = pdr.Value;

 

      pdo.Message = "\nEnter longitude";

      pdr = ed.GetDouble(pdo);

      if (pdr.Status != PromptStatus.OK)

        return;

 

      var lon = pdr.Value;

 

      var lonlat = new Point3d(lon, lat, 0.0);

 

      // Translate the lat-lon to a drawing point

 

      var res = TranslateGeoPoint(db, lonlat, false);

 

      // Print any coordinate system information

 

      ed.WriteMessage(res.Item2);

 

      PrintCoordinateSystem(ed, res.Item2);

 

      // And then the point itself

 

      var dwgPt = res.Item1;

      ed.WriteMessage(

        "\nDrawing point is {0},{1},{2}", dwgPt.X, dwgPt.Y, dwgPt.Z

      );

    }

 

    private static void PrintCoordinateSystem(Editor ed, string xml)

    {

      try

      {

        dynamic csx = DynamicXml.Parse(xml);

        var cs = csx.ProjectedCoordinateSystem.id;

        ed.WriteMessage("\nCoordinate system: {0}", cs);

      }

      catch (RuntimeBinderException)

      {

        ed.WriteMessage("\nNo coordinate system information.");

      }

    }

 

    private Tuple<Point3d, string> TranslateGeoPoint(

      Database db, Point3d inPt, bool fromDwg

    )

    {

      using (

        var tr = db.TransactionManager.StartOpenCloseTransaction()

      )

      {

        // Get the drawing's GeoLocation object

 

        var gd =

          tr.GetObject(db.GeoDataObject, OpenMode.ForRead)

            as GeoLocationData;

 

        // Get the output point...

        // dwg2lonlat if fromDwg is true,

        // lonlat2dwg otherwise

 

        var outPt =

          (fromDwg ?

            gd.TransformToLonLatAlt(inPt) :

            gd.TransformFromLonLatAlt(inPt)

          );

 

        var cs = gd.CoordinateSystem;

 

        tr.Commit();

 

        return new Tuple<Point3d, string>(outPt, cs);

      }

    }

 

    private static bool HasGeoData(Database db)

    {

      // Check whether the drawing already has geolocation data

 

      bool hasGeoData = false;

      try

      {

        var gdId = db.GeoDataObject;

        hasGeoData = true;

      }

      catch { }

      return hasGeoData;

    }

  }

 

  // From

  // http://stackoverflow.com/questions/13704752/

  // deserialize-xml-to-object-using-dynamic

  // and

  // http://social.msdn.microsoft.com/Forums/en-US/

  // bed57335-827a-4731-b6da-a7636ac29f21/xdocument-remove-namespace

 

  public class DynamicXml : DynamicObject

  {

    XElement _root;

    private DynamicXml(XElement root)

    {

      _root = root;

    }

 

    public static DynamicXml Parse(

      string xmlString, bool stripNamespaces = true

    )

    {

      var doc = XDocument.Parse(xmlString);

      if (stripNamespaces)

      {

        doc = StripDocumentNamespaces(doc);

      }

      return new DynamicXml(doc.Root);

    }

 

    public static DynamicXml Load(

      string filename, bool stripNamespaces = true

    )

    {

      var doc = XDocument.Load(filename);

      if (stripNamespaces)

      {

        doc = StripDocumentNamespaces(doc);

      }

      return new DynamicXml(doc.Root);

    }

 

    private static XDocument StripDocumentNamespaces(

      XDocument doc

    )

    {

      // Remove all xmlns:* instances from the passed XDocument

 

      return

        XDocument.Parse(

          Regex.Replace(

            doc.ToString(), @"(xmlns:?[^=]*=[""][^""]*[""])", "",

            RegexOptions.IgnoreCase | RegexOptions.Multiline

          )

        );

    }

 

    public override bool TryGetMember(

      GetMemberBinder binder, out object result

    )

    {

      result = null;

 

  
;    // Do we have an attribute with this name?

 

      var att = _root.Attribute(binder.Name);

      if (att != null)

      {

        result = att.Value;

        return true;

      }

 

      // Do we have a list of elements with this name?

 

      var nodes = _root.Elements(binder.Name);

      if (nodes.Count() > 1)

      {

        result = nodes.Select(n => new DynamicXml(n)).ToList();

        return true;

      }

 

      // Do we have a single element with this name?

 

      var node = _root.Element(binder.Name);

      if (node != null)

      {

        if (node.HasElements)

        {

          result = new DynamicXml(node);

        }

        else

        {

          result = node.Value;

        }

        return true;

      }

 

      return true;

    }

  }

}

If you're attending AU 2014 and are interested in learning more about this API. I recommend signing up for a class being delivered by my friend and colleague, Mads Paulin: Building Location-Aware Applications in AutoCAD 2015. Mads is the software architect on the AutoCAD team who is responsible for this feature, so it's a great opportunity to get information straight from the source. (Unfortunately this session conflicts with a class at which I'm co-speaking, otherwise I'd be there, too.)

Leave a Reply

Your email address will not be published. Required fields are marked *