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;
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.)