Thursday, November 12, 2009

(III) New SDO_GEOMETRY <-> Autodesk Map3d 2010

Author: Jonio, Dennis

Before I forget I did put together a simple class to aid in manipulating the three(3) element array structures that comprise SDO_ELEM_INFO_ARRAY.
Source code (C#):

[Serializable]
public class TriInts
{
public int OFFSET
{ set { TriInt[0] = value; } get { return TriInt[0]; } }
public int ETYPE
{ set { TriInt[1] = value; } get { return TriInt[1]; } }
public int INTERP
{ set { TriInt[2] = value; } get { return TriInt[2]; } }
private int[] TriInt;

public TriInts()
{
TriInt = new int[] { 0, 0, 0 };
}
public TriInts(int o, int e, int i)
{
TriInt = new int[3];
TriInt[0] = o;
TriInt[1] = e;
TriInt[2] = i;
}
public TriInts(TriInts triint)
{
TriInt = new int[3];
TriInt[0] = triint.OFFSET;
TriInt[1] = triint.ETYPE;
TriInt[2] = triint.INTERP;
}
public int[] ToArray()
{
return new int[] { OFFSET, ETYPE, INTERP };
}
}//eoc TriInts

So now we get to the primary actor DrawAbleSdoGeometry. The container class for the NetSdoGeometry.sdogeometry type and the supporting cast of players to break sdogeometry down into DrawAble(s) and build a sdogeometry from DrawAble(s). Again the paradigm is all about this intermediate drawable construct so Autodesk/Map3d doesn’t come into play here. That is all handled in a separate class!
Source code (C#):

[Serializable]
public class DrawAbleSdoGeometry
{
private sdogeometry m_Geometry;
public const decimal DefaultSRID2236 = 2236;
public const int DefaultLRS0 = 0;
public const int DefaultDimensionality2D = 2;
public const int DefaultDimensionality3D = 3;
public decimal SRID = DefaultSRID2236;
public int LRS = DefaultLRS0;
public int Dimensionality = DefaultDimensionality2D;
public List Drawables = new List();
public List OrphanComponents = new List();
// Default ctor - nothing to do
public DrawAbleSdoGeometry() { }
// ctor from an existing SDO_GEOMETRY object
public DrawAbleSdoGeometry(sdogeometry aGeometry)
{
this.Geometry = aGeometry;
}
}//eoc DrawAbleSdoGeometry


I set a default Dimensionality, LRS and SRID but have left them totally accessible. I am not one to hide things away in some private corner somewhere unless it just is nonsensical to do otherwise. Like DrawAblesFromGeometry(), the player that decomposes a sdogeometry/SDO_GEOMETRY object into DrawAble(s). It only made sense to me to invoke this directly from the “setter” for the class’ Geometry object.
Note also that within the “setter” I deal with that Optimized Point issue.
Source code (C#):

public sdogeometry Geometry
{
get { return this.m_Geometry; }

set
{
m_Geometry = value;
try
{
m_Geometry.PropertiesFromGTYPE();
Dimensionality = m_Geometry.Dimensionality;

if (m_Geometry.sdo_point != null)
{
m_Geometry.ElemArray = new decimal[] { 1, 1, 1 };
if (m_Geometry.Dimensionality == 2)
m_Geometry.OrdinatesArray = new decimal[] { (decimal)m_Geometry.sdo_point.X, (decimal)m_Geometry.sdo_point.Y };
else if (m_Geometry.Dimensionality == 3)
m_Geometry.OrdinatesArray = new decimal[] { (decimal)m_Geometry.sdo_point.X, (decimal)m_Geometry.sdo_point.Y, (decimal)m_Geometry.sdo_point.Z };
m_Geometry.sdo_point = null;
}
if (m_Geometry.ElemArray != null && m_Geometry.OrdinatesArray != null)
DrawAblesFromGeometry();
}
catch (System.Exception) {/*Just eat it all*/}
}
}

As regards catching and eating any errors at this level I am ambivilant. I really do assume VALID geometry coming in because I produce VALID geometry on the other end. You may see it differently.

To be continued …

(II) New SDO_GEOMETRY <-> Autodesk Map3d 2010

Author: Jonio, Dennis

The DrawAbleType enums were easy. I just had to give them values that could be distinguised via “OR”ing them together.

  • Point = 1,
  • Line = 2,
  • Surface = 4


    Source code (C#):

    public static int InferGeometryType(List _drawables)
    {
    int rtnval = 0;
    if (_drawables.Count == 1)
    {
    switch (_drawables[0].DrawEtype)
    {
    case DrawAbleType.Point:
    rtnval = 1;
    break;
    case DrawAbleType.Line:
    rtnval = 2;
    break;
    case DrawAbleType.Surface:
    rtnval = 3;
    break;
    }
    }
    else
    {
    int ttype = 0;
    foreach (DrawAble d in _drawables)
    ttype = ttype | (int)d.DrawEtype;

    switch (ttype)
    {
    case 1:
    rtnval = 5; //MultiPoint
    break;
    case 2:
    rtnval = 6; //MultiLine
    break;
    case 4:
    rtnval = 7; //MultiPolygon
    break;
    default:
    rtnval = 4; //Collection
    break;
    }
    }
    return rtnval;
    }

    I really wrestled with these DrawAbleSubComponentEType enums. I tried to be really clever with these values and somehow take advantage of the relationship between ETYPE and INTERPRETATION. Tried is the operative word.

  • Point = 1, //1
  • PointCluster = 10, //1 + n
  • PointOriented = 19, //Not supported OrientedPoint
  • SimpleLine = 3, //2 + 1
  • SimpleLineAllCurves = 4, //2 + 2
  • CompoundLine = 40, //4 + n
  • SimpleSurfaceOuterRingLine = 1004, //1003 + 1
  • SimpleSurfaceOuterRingAllCurves = 1005, //1003 + 2
  • SimpleSurfaceOuterRingRectangle = 1006, //1003 + 3
  • SimpleSurfaceOuterRingCircle = 1007, //1003 + 4
  • SimpleSurfaceInnerRingLine = 2004, //2003 + 1
  • SimpleSurfaceInnerRingAllCurves = 2005, //2003 + 2
  • SimpleSurfaceInnerRingRectangle = 2006, //2003 + 3
  • SimpleSurfaceInnerRingCircle = 2007, //2003 + 4
  • CompoundSurfaceOuterRingLine = 10050, //1005 + n
  • CompoundSurfaceInnerRingLine = 20050 //2005 + n
    The specification has to many exceptions for this to work but at least I have my unique values.


    I do wonder alot as to why Oracle set up the unique combinations for Circle, Rectangle and instances of all Curves. I did read the spec on “Oriented Point” and just arbitrarily decided NOT to support it. I will never use it and I guess I am the boss. So if you need that you will have to do it yourself. It should be a straight forward job for someone with the motivation.


    When it gets time to resolve these into the “real” ETYPE and INTERPRETATION values I just set up a couple static methods in a static utility class.
    Source code (C#):

    public static int BasicETYPE(DrawAbleSubComponentEType t)
    {
    int rtnval = 0;
    switch (t)
    {
    case DrawAbleSubComponentEType.Point:
    case DrawAbleSubComponentEType.PointCluster:
    case DrawAbleSubComponentEType.PointOriented:
    rtnval = 1;
    break;
    case DrawAbleSubComponentEType.SimpleLine:
    case DrawAbleSubComponentEType.SimpleLineAllCurves:
    rtnval = 2;
    break;
    case DrawAbleSubComponentEType.CompoundLine:
    rtnval = 4;
    break;
    case DrawAbleSubComponentEType.SimpleSurfaceOuterRingLine:
    case DrawAbleSubComponentEType.SimpleSurfaceOuterRingAllCurves:
    case DrawAbleSubComponentEType.SimpleSurfaceOuterRingCircle:
    case DrawAbleSubComponentEType.SimpleSurfaceOuterRingRectangle:
    rtnval = 1003;
    break;
    case DrawAbleSubComponentEType.SimpleSurfaceInnerRingLine:
    case DrawAbleSubComponentEType.SimpleSurfaceInnerRingAllCurves:
    case DrawAbleSubComponentEType.SimpleSurfaceInnerRingCircle:
    case DrawAbleSubComponentEType.SimpleSurfaceInnerRingRectangle:
    rtnval = 2003;
    break;
    case DrawAbleSubComponentEType.CompoundSurfaceOuterRingLine:
    rtnval = 1005;
    break;
    case DrawAbleSubComponentEType.CompoundSurfaceInnerRingLine:
    rtnval = 2005;
    break;
    default:
    break;
    }
    return rtnval;
    }
    public static int BasicINTERPRETATION(DrawAbleSubComponentEType t)
    {
    int rtnval = 0;
    switch (t)
    {
    case DrawAbleSubComponentEType.Point:
    case DrawAbleSubComponentEType.SimpleLine:
    case DrawAbleSubComponentEType.SimpleSurfaceInnerRingLine:
    case DrawAbleSubComponentEType.SimpleSurfaceOuterRingLine:
    rtnval = 1;
    break;
    case DrawAbleSubComponentEType.SimpleSurfaceInnerRingAllCurves:
    case DrawAbleSubComponentEType.SimpleSurfaceOuterRingAllCurves:
    case DrawAbleSubComponentEType.SimpleLineAllCurves:
    rtnval = 2;
    break;
    case DrawAbleSubComponentEType.SimpleSurfaceOuterRingRectangle:
    case DrawAbleSubComponentEType.SimpleSurfaceInnerRingRectangle:
    rtnval = 3;
    break;
    case DrawAbleSubComponentEType.SimpleSurfaceInnerRingCircle:
    case DrawAbleSubComponentEType.SimpleSurfaceOuterRingCircle:
    rtnval = 4;
    break;
    case DrawAbleSubComponentEType.PointCluster:
    case DrawAbleSubComponentEType.CompoundLine:
    case DrawAbleSubComponentEType.CompoundSurfaceOuterRingLine:
    case DrawAbleSubComponentEType.CompoundSurfaceInnerRingLine:
    rtnval = 0;
    break;
    case DrawAbleSubComponentEType.PointOriented:
    rtnval = 9;
    break;
    default:
    break;
    }
    return rtnval;
    }

    To be continued ...
  • Monday, November 9, 2009

    (I) New SDO_GEOMETRY <-> Autodesk Map3d 2010

    Author: Jonio, Dennis


    Howdy all.
    Well, it has really been awhile since that last post. By the way, I have no idea what happened to Maksim Sestic. Anyone willing to let me know?

    Anyway a lot has transpired over the months not the least of which is that some time ago I built a shiny new Autodesk 2010/Map3d <-> SDO_GEOMETRY translator.

    The learning example I have posted here was/is OK and served its purpose. However, like most software it had to be fixed and/or enhanced (and in this case just thrown away!)
    The new converter supports everything that is in keeping with the Oracle Locator’s - Right To Use license. Obviously then there is no 3D object support. Curves are still fully supported and now ALL of the MULT types are supported. I do include support for the Z axis in this generation. In order to support the MULTI types I forced myself into building some “intermediate” objects. This also lays some of the groundwork for a different geometry object generator in the future. No, we are not having any problems or issues with Autodesk but maybe something else will come along. My NetSdoGeometry.sdogeometry object had to be tweaked a little so that it fully supported the Z axis. Beyond that it is good to go.


    Three(3) primary objectives drove this iteration of the translator.
    I) I wished to support a Z axis value
    II) I wished to support the Oracle MULTI types
    III) A more flexible overall design in general.
    I had to modify my sdogeometry AsText property/ToString method to support the Z axis. If you recall I go out of my way to NOT utilize the "optimized point". I am sure it is a fine thing. For me, logically and programmatically, it is more trouble than its worth. In my way of thinking there is nothing wrong with an SDO_ELEM_INFO_ARRAY(1,1,1) and it is somewhat more consistent with the rest of the standard. I do of course read them in but I never output a SDO_POINT.
    Source code (C#):

    public string AsText
    {
    get
    {
    StringBuilder sb = new StringBuilder();
    sb.Append("MDSYS.SDO_GEOMETRY(");
    sb.Append((sdo_gtype != null) ? sdo_gtype.ToString() : "null");
    sb.Append(",");
    sb.Append((sdo_srid != null) ? sdo_srid.ToString() : "null");
    sb.Append(",");
    // begin point
    if (sdo_point != null)
    {
    sb.Append("MDSYS.SDO_POINT_TYPE(");
    string _tmp = string.Format("{0:0.0#####},{1:0.0#####}{2}{3:#.######}",
    sdo_point.X,
    sdo_point.Y,
    (sdo_point.Z == null) ? null : ",",
    sdo_point.Z);

    sb.Append(_tmp.Trim());
    sb.Append(")");
    }
    else
    {
    sb.Append("null");
    }
    sb.Append(",");
    // begin element array
    if (elemArray != null)
    {
    sb.Append("MDSYS.SDO_ELEM_INFO_ARRAY(");
    for (int i = 0; i < elemArray.Length; i++)
    {
    string _tmp = string.Format("{0}", elemArray[i]);
    sb.Append(_tmp);
    if (i < (elemArray.Length - 1))
    sb.Append(",");
    }
    sb.Append(")");
    }
    else
    {
    sb.Append("null");
    }
    sb.Append(",");
    // begin ordinates array
    if (ordinatesArray != null)
    {
    sb.Append("MDSYS.SDO_ORDINATE_ARRAY(");
    for (int i = 0; i < ordinatesArray.Length; i++)
    {
    string _tmp = string.Format("{0:0.0######}", ordinatesArray[i]);
    sb.Append(_tmp);
    if (i < (ordinatesArray.Length - 1))
    sb.Append(",");
    }
    sb.Append(")");
    }
    else
    {
    sb.Append("null");
    }
    sb.Append(")");
    return sb.ToString();
    }
    }
    public override string ToString()
    {
    return this.AsText;
    }


    Here is the conceptual paradigm that I chose for this iteration:
    An Autodesk drawable entity decomposes into one or more SDO_ELEM_INFO_ARRAY triplet(s) with corresponding ordinates. An Autodesk drawable entity may be composed of one or more SDO_ELEM_INFO_ARRAY triplet(s) with corresponding ordinates.
    With this mental shift from my original tool which is based upon a one-to-one/entity-to-sdogeometry relationship things flowed pretty well. Frankly this was a necessary shift in thinking to support the MULTI types.
    So a class "DrawAble" is composed of "DrawAbleSubComponent"(s) and DrawAbleSubComponents equate to SDO_ELEM_INFO_ARRAY triplet(s) and their corresponding ordinates.
    Source code (C#):

    [Serializable]
    public class DrawAbleSubComponent
    {
    public int[] Elem;
    public decimal[] Ords;
    public DrawAbleSubComponentEType Etype;
    }//eoc DrawAbleSubComponent

    [Serializable]
    public class DrawAble
    {
    private DrawAbleType m_drawtype;
    public DrawAbleType DrawEtype
    { get{return m_drawtype;} set { m_drawtype = value; }}

    private int m_dimensionality = 2;
    public int Dimensionality
    { get { return m_dimensionality; } set { m_dimensionality = value; } }

    public List SubComponents = new List();
    public DrawAble(DrawAbleType _type, int _dimensionality)
    {
    DrawEtype = _type;
    Dimensionality = _dimensionality;
    }
    }//eoc DrawAble

    To be continued ...

    Tuesday, January 20, 2009

    SDO_GEOMETRY - FROM and TO Autodesk Map3d

    Author: Jonio, Dennis


    This Visual Studio 2005 solution contains everything you need to PUT/GET 2d curve SDO_GEOMETRY from/to Autodesk Map3d. It is of course a named-pipe solution. I have tested this particular one briefly on both Map3d 2008 and Map3d 2009. No warranties about anything. I have not included my SDO_GEOMETRY Validation class and anything having to do with SDO_DIM_ARRAY. This is posted elsewhere.
    What you will need: Visual Studio 2005, Map3d 2008/2009, ODP.NET 11g.
    The last and most significant new code included here is the DwgReader and the DwgWriter classes. They are nothing more than convenience containers for the geometry conversion methods contained with these same named classes.
    As I have learned the idiosyncrasies and subtleties of doing conversions from one geometry system to the other are immense. For example: Circles ... what about circles? Just pursue this one issue yourself and you should become somewhat befuddled at what approach to take.
    I am aware that the conversion routine for inner and outer rings of polygons is somewhat flawed in that it does not handle ring direction properly in all cases. This has not been a big concern for me since every piece of production geometry I create gets passed through a "validator" and in the case of ring orientation SDO_MIGRATE.TO_CURRENT works fine for fix up. This was another of those lessons learned. You should always check the geometry you are going to convert. If you were not aware already, you can easily create polygon geometry in Map3d that nothing but Map3d will understand. It is what it is.

    The NETLOADable dll is ADSKDATABRIDGE.dll and the commandline command is: DOPUTGET.

    Startup “ClientBasicIO.exe” and your off and running.

    I have included the file “AGEOM1.sds” with the project. This is a serialized .NET dataset with a few geometries. You can load it within clientbasicio.exe.

    Enough … code speaks for itself …


    You can download the solution here: http://tf-net.googlecode.com/files/aPub.zip

    Saturday, November 15, 2008

    A Simple Named Pipes Solution for Autodesk Map3d

    Author: Jonio, Dennis


    I have recently decided to put together a minimalist IPC namedpipes application. I just could not think of something simplier than what I have here. This is like using a 747 jetliner to commute cross town to work but it does illustrate the ease. It is a complete, zipped up, Visual Studio 2005 solution with the entire source that I have outlined in other posts. To reiterate I use Map3d 2008 and have not tested this against vanilla AutoCAD. You will have to tell me if it works or not. I have no dependences on Map3d that I can think of.

    The NETLOADable dll is, you guessed it, IPCSimple.dll. Load it up and commandline: doit. The “DataBridge” form is minimized at startup.

    Startup “ClientBasicIO.exe” it is pretty obvious what to do from this point.

    Most of what is happening I consider pretty much to be “boilerplate”, again thanks to Ivan Latunov. My particular implementation of serializing/de-serializing the datasets can easily be changed to support whatever construction you like. Sending a text string for “SendStringToExecute” is really kind’a sort’a silly but, like I said, I could not think of anything simpler.

    At this point it is up to your imaginations as to how this can work for you. …enjoy!


    You can download the solution here: http://tf-net.googlecode.com/files/IPCSimple.zip

    Friday, November 14, 2008

    Org.OpenGIS.GeoAPI Revisited - Part I

    Author: Sestic, Maksim

    I've been thinking of starting this article by lamenting on current state of the available managed OGC's GeoAPI interface implementations, but I won't :-) If you're reading this - you have probably already hit their limitations. I'm talking about GeoAPI interface ports here (a la GeoAPI.NET), not concrete implementations (i.e. NetTopologySuite, SharpMap or Proj.NET).

    Not having common managed interface library makes things a lot harder for GIS developers. Present one(s) won't help you much either - most of them got way out of synch when compared to their Java-based raw model maintaned by OGC. In the meantime, Microsoft's .NET Framework itself evolved a lot, etc. To cut the long story short, my idea was to start managed GeoAPI interfaces from scratch, based on present GeoAPI 2.1.1 interfaces specification - but not by simply jumping into it...

    Naming Conventions

    Lets start with something "obvious" - naming conventions used in Java and .NET. For example: org.opengis.geometry namespace becomes Org.OpenGIS.Geometry namespace in .NET. Both camel-case and abbreviation rules apply, starting letter always capitalized.

    When it comes to interface names, since it's mostly about them, each Java class pertaining to an interface gets an "I" prefix. For example: Precision interface becomes IPrecision in .NET.

    Naming and Type Conventions

    Java Get/Set functions become Properties. That is - ReadOnly properties for explicit getter functions, or WriteOnly properties for explicit setter functions. Also, get/set prefix gets stripped from resulting property name, always starting with a capital letter. For example:

    Function getLength() As Double

    becomes:

    ReadOnly Property Length() As Double

    Also:

    Function getLength() As Double
    Sub setLength(d As Double)


    method pair becomes:

    Property Length() As Double

    So, when Java function becomes .NET property, and when it simply remains a method - beyond getter/setter rules stated above? It depends on overall method semantics; what it actually means to hosting class/object. For example:

    Function setLength(d As Double)

    when alone in a class, becomes:

    WriteOnly Property Length() As Double

    Because Length is a class property. But:

    Function ToArray() As Double()

    remains a function, since it's a method to hosting class. Sticking to described semantic rules, even parametrized functions become properties. For example:

    Function getBuffer(d As Double) As IGeometry

    becomes:

    ReadOnly Property Buffer(ByVal d As Double) As IGeometry

    since buffer is a parametrized geometric property (one of many, of course).

    Type Conventions And Enumerators

    Enumerated types seem rather confusing to present managed GeoAPI ports. In fact, they're simple enumerators. A good example for this is org.opengis.geometry.PrecisionType which derives from (extends) generic Java CodeList type. In .NET it becomes:

    Public Enum PrecisionType

    with it's members slightly changed when it comes to naming: DOUBLE becomes Double, FIXED becomes Fixed, etc. If we had a Java member named PIECEWISE_BEZIER, then it would become PiecewiseBezier in .NET. BTW, proposed notation is way closer to original ISO identifier naming rules anyways.

    Type Conventions And Generics

    This is the most challening part of it all. That's also why IKVM.NET won't help you much with GeoAPI in first place. Yes, it's about .NET generics and how do we use them in GeoAPI - keeping in mind future implementations. IMHO, it's very important to introduce strongly typed collections to managed GeoAPI interfaces. Alas, without knowing the nature of each interface it's close to impossible to introduce their generic counterparts properly. In other words - one needs to consult UML definition and think ahead of possible implications on implementers' side.

    Ongoing Activities

    You can check the current development status of managed GeoAPI library based on present GeoAPI 2.1.1 interfaces specification here: Managed OpenGIS GeoAPI Interfaces (look under SVN trunk).

    Tuesday, November 4, 2008

    Extending the IGeometryCollection type with methods provided on the HashSet(T) class

    Author: Baxevanis, Nikos

    Overview

    As of Kim Hamilton’s post, http://blogs.msdn.com/bclteam/archive/2006/11/09/introducing-hashset-t-kim-hamilton.aspx “HashSet determines equality according to the EqualityComparer you specify, or the default EqualityComparer for the type (if you didn’t specify)”.

    The IGeometryCollection type implements the IComparable(T). Using extension methods we “add” methods from the HashSet(T) class so any type that implements the IGeometryCollection appears to have instance methods such as IntersectWith, UnionWith etc.


    The IGeometryCollection type implements the IComparable(T).

    A HashSet(T) operation modifies the set it’s called on and doesn’t create a new set. The extension methods provided with this article return an IGeometryCollection object rather than modifying the caller set.


    Implementation

    “Extension methods are defined as static methods but are called by using instance method syntax. Their first parameter specifies which type the method operates on, and the parameter is preceded by the this modifier. Extension methods are only in scope when you explicitly import the namespace into your source code with a using directive.” http://msdn.microsoft.com/en-us/library/bb359438.aspx

    public static Topology.Geometries.IGeometryCollection UnionWith(this Topology.Geometries.IGeometryCollection geomCol, Topology.Geometries.IGeometryCollection otherCol)
    {
    return Operation(geomCol, otherCol, OperationType.Union);
    }

    public static Topology.Geometries.IGeometryCollection IntersectWith(this Topology.Geometries.IGeometryCollection geomCol, Topology.Geometries.IGeometryCollection otherCol)
    {
    return Operation(geomCol, otherCol, OperationType.Intersection);
    }

    private static Topology.Geometries.IGeometryCollection Operation(Topology.Geometries.IGeometryCollection geomCol, Topology.Geometries.IGeometryCollection otherCol, OperationType typeOp)
    {
    HashSet geomSet =
    new HashSet();

    foreach (Topology.Geometries.IGeometry geom in geomCol)
    geomSet.Add(geom);

    HashSet otherSet =
    new HashSet();

    foreach (Topology.Geometries.IGeometry geom in otherCol)
    otherSet.Add(geom);

    switch (typeOp)
    {
    case OperationType.Intersection:
    geomSet.IntersectWith(otherSet);
    break;

    case OperationType.Union:
    geomSet.UnionWith(otherSet);
    break;

    case OperationType.Except:
    geomSet.ExceptWith(otherSet);
    break;

    case OperationType.SymmetricExcept:
    geomSet.SymmetricExceptWith(otherSet);
    break;

    default:
    return Topology.Geometries.GeometryCollection.Empty;
    }

    Topology.Geometries.IGeometry[] array =
    new Topology.Geometries.IGeometry[geomSet.Count];

    geomSet.CopyTo(array);

    return new Topology.Geometries.GeometryFactory().CreateGeometryCollection(array);
    }
    Using the extension methods in your code

    You have to add a reference to Topology.Geometries.GeometryCollectionExtensions.dll
    To use the HashSet(T) operations first bring them into scope with a using Topology.Geometries.GeometryCollectionExtensions directive.


    Using the HashSet(T) operations


    Available for download here: http://tf-net.googlecode.com/files/GeometryCollectionExtensions-Source.zip