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

Tuesday, October 28, 2008

Inter-Process Communication (IPC) with Autodesk Map3d

Author: Jonio, Dennis

Prospective and Environment
Everything that follows was done on and with Windows XP SP2, Visual Studio 2005 Professional SP1, Oracle XE 10.2, Oracle ODP.NET 11g(2.111.6.20), Autodesk Map3d 2008 SP1, Autodesk ObjectARX 2008 and lots of reading and lots of research. Since I have not done it myself I cannot attest as to the viability of this solution in a vanilla AutoCAD 200x environment. I am however inclined to believe that it will work just fine. I make neither guarantees nor claims of fitness for any purpose. It is “as-is” so proceed at your own risk.

Overview
Simply stated, this was the problem, I wished to have a generic way in which to communicate with a DWG. All I wished to do was PUT an SDO_GEOMETRY and GET a selected entity back as an SDO_GEOMETRY. This seemed to me to be a not unreasonable request. Just think about this a second: AutoCAD as a COLUMN editor? I could build whatever user interface I wished, using whatever tools I wished. Include all the business logic needed and then just “plumb” this stand-alone application. I didn’t have to solve the world’s problems either. Points, lines and surfaces were all I was interested in. Well good luck! First off there was no .NET DWG to SDO_GEOMETRY translator that I could get my hands on. Second, this just is not how Autodesk has written their applications. Don’t misunderstand me here. This is not a flame against Autodesk. Their applications do what they were designed to do and do this very well. So this series is then about getting one of two major things accomplished: 1) A 2-dimensional, SDO_GEOMETRY translator that supported arcs and 2) A communications interface.

This is about the communications piece. In my mind this would be the most technically challenging and if I was going to fail it would be on this.

Of course the issue of multithreading arose numerous times but it always seemed, by its nature, to be somewhat limiting. Inter-process communication (IPC) was the answer but I found no ready made solution. I had to piece this together myself. I did a fair amount of investigating and found named pipes to be the most generic and robust approach to take.

This is not really a story about pipes but you must look at this article: “Inter-Process Communication in .NET Using Named Pipes” by Ivan Latunov.
link to article The author explains it all much better than I ever could and his implementation is the backbone of this implementation. By the way, I believe that Microsoft has now included Named Pipe support in the 3.5 .NET framework. Since MS pipes are a Client/Server architecture and I was totally intimidated by “overlapping” ,“bidirectional” and the other more complex implementations I decided to really simplify and incorporate, in each executable or dll, a pipe Server and a pipe Client. Thank you Ivan. The significant change that I incorporated was the use of Events on the Server side( the “owner” side if you will ). When you compare Ivan’s code to mine you will see some other minor changes. A simple piccy to illustrate the idea.


Since experience has taught me that the Autodesk API’s are not trivial especially when it comes to the MDI interface structure I really needed to isolate this application as much as possible. The solution was a simple data exchange mechanism, a data “bridge”, if you will. Let the bridge become the generic between the other executables! My NETLOADed module and the “bridge” will communicate with simple Events and Queue objects! The Autodesk API’s understands Forms, Forms are, by nature, multi-threaded! The bridge will then communicate to whatever is at the other end of the pipe. BINGO! We have it all!

Below is a clip where these ideas are put together. Very simple and straightforward but it should give you some "food for thought":

Full-size video clip is available for download at:


The Communications Data Definition

OK… so now we have settled on the communications “plumbing”, a full blown IO channel no less, what is it that we wish to communicate?
Well in reading Ivan’s work and the work of some others I became aware that pipes can move megabytes of data in sub-second times. Thusly then, the amount was not the issue the format was. Whatever that format was it had to be serializable and it had to be generic. I had to accomplish two(2) things: 1) construct a request that I wished my NETLOADed module to perform 2) provide for data interchange. The solution here is a plain’ol .NET dataset. In fact, all I really had to do was encapsulate a dataset inside a field in a “control” dataset. I could then pass one(1) serializabe object that had both a request/task and corresponding data. In my mind at least, very clean, very straightforward, and very generic.

The following is the structure I decided upon for my environment:


public static DataSet MaterializeNPControlDataSet()
{
DataSet _ds = new DataSet("NPCONTROLDATASET");
_ds.RemotingFormat = SerializationFormat.Binary;
_ds.SchemaSerializationMode = SchemaSerializationMode.IncludeSchema;

_ds.Tables.Add("NPCONTROLTABLE");
DataColumn dc1 = new DataColumn("ID", typeof(Int32));
DataColumn dc2 = new DataColumn("TEXT", typeof(string));
DataColumn dc3 = new DataColumn("PAYLOADTEXT", typeof(string));
DataColumn dc4 = new DataColumn("PAYLOAD", typeof(object));
_ds.Tables[0].Columns.Add(dc1);
_ds.Tables[0].Columns.Add(dc2);
_ds.Tables[0].Columns.Add(dc3);
_ds.Tables[0].Columns.Add(dc4);
return _ds;
}


To support the manipulation of this structure there is one(1) class. All of the serialization/de-seriaization and encapsulations are done with this class. Below are the “putter” and “getter” methods for the dataset that is encapsulated within the “PAYLOAD” field. In addition there are numerous other helper methods within the class.
”putter”

public static void SetNPControlTablePayLoad(ref DataSet _NPds, ref DataSet _payload)
{
byte[] ba = SerializeDataSet(ref _payload);
_NPds.Tables["NPCONTROLTABLE"].Rows[0]["PAYLOAD"] = (object)ba;
}

“getter”

public static DataSet ExtractNPCTablePayLoad(ref DataSet _NPds)
{
DataSet ads = null;
object o = (object)_NPds.Tables["NPCONTROLTABLE"].Rows[0]["PAYLOAD"];
if (o != System.DBNull.Value)
{
ads = ExtractSerializedDataSet((byte[])o);
if (ads != null)
return ads;
else
return null;
}
else
return null;
}

No, I did not implement any compression of the datasets, just the BinaryFormatter.

public static byte[] SerializeDataSet(ref DataSet _ds)
{
_ds.RemotingFormat = SerializationFormat.Binary;
_ds.SchemaSerializationMode = SchemaSerializationMode.IncludeSchema;
MemoryStream ms = new MemoryStream();
BinaryFormatter bf = new BinaryFormatter();
bf.Serialize(ms, _ds);
if (ms.Length > 0)
return ms.ToArray();
else
return null;
}

The Named Pipes
Ivan’s implementation is a work of art. I really hated to touch it. (Not only because it was well put together but I really did not fully comprehend his threading architecture). However, I really needed event hooks because this wasn’t going to be a “classic” Client/Server application. The applications that were going to be incorporating this were going to be operating in and supporting the user interface world. Since Ivan had architected this with interfaces I went about some touch up. (four lines of code with some big ideas ...)
Source code (C#):

using System;
namespace AppModule.InterProcessComm {
// Delegates for events that your component will use to communicate
// values to the form
public delegate void ReceiveCompleteHandler(byte[] ba);
public delegate void StatusActivityHandler(string info);

public interface IChannelManager {
// events that this (class)component will use to communicate with the app
event ReceiveCompleteHandler OnReceiveComplete;
event StatusActivityHandler OnStatusActivity;

void Initialize();
void Stop();
string HandleRequest(string request);
bool Listen {get; set;}
void WakeUp();
void RemoveServerChannel(object param);
}
}

Since I had decided to have both a Client and a Server incorporated within each app I made it easier on myself and just constructed an “A” and “B” dispatcher (I wanted to get rid of this Client/Server label). One would be owned by the “bridge” the other would be owned by the “other” application.
They are really nothing more than containers for Ivan’s public static IChannelManager PipeManager; I just added a few constants to make life easier and to set the “ReadOn” pipe and the “SendOn” pipe. I really didn’t want any confusion later on.

PipeManager’s most significant modifications had to do with the Events hooks:
Source code (C#):

if (numChannels <= NumberPipes) // =>
{
ServerNamedPipe pipe = new ServerNamedPipe(PipeName, OutBuffer, InBuffer, MAX_READ_BYTES, false);
// subscribe to the events raised when data has been gotten
pipe.GotNewByteArray += new OnNewByteArrayHandler(pipe_GotNewByteArray);
pipe.GotActivity += new OnActivityToReportHandler(pipe_GotActivity);
try
{
pipe.Connect();
pipe.LastAction = DateTime.Now;
System.Threading.Interlocked.Increment(ref numChannels);
pipe.Start();
Pipes.Add(pipe.PipeConnection.NativeHandle, pipe);
}
catch (InterProcessIOException ex) {
RemoveServerChannel(pipe.PipeConnection.NativeHandle);
// unsubscribe to the events raised when data has been gotten
pipe.GotNewByteArray -= new OnNewByteArrayHandler(pipe_GotNewByteArray);
pipe.GotActivity -= new OnActivityToReportHandler(pipe_GotActivity);
pipe.Dispose();
}
}
else {
Mre.Reset();
Mre.WaitOne(1000, false);
}

Now, I just pass it on up the chain to “whatever” has hooked up.
Source code (C#):

void pipe_GotActivity(object sender, string activity)
{
if (OnStatusActivity.Target is System.Windows.Forms.Control)
{
// make sure execution is done on the UI thread
System.Windows.Forms.Control t = OnStatusActivity.Target as System.Windows.Forms.Control;
t.BeginInvoke(OnStatusActivity, new Object[] { activity });
}
else
// object from the invocation list isn't a UI thread.
OnStatusActivity(activity);
}
}
void pipe_GotNewByteArray(object sender, byte[] ba)
{
string rcvszdttm = string.Format("Received: {0} bytes at {1}", ba.Length.ToString(), DateTime.Now);
// tell whoever that a worker has just sent us a byte array
pipe_GotActivity(this, rcvszdttm);
//
// determine if the object on which this delegate was invoked is a UI thread
// if the object is a control, then the object is a UI thread
//
if (OnReceiveComplete.Target is System.Windows.Forms.Control)
{
// make sure execution is done on the UI thread
System.Windows.Forms.Control t = OnReceiveComplete.Target as System.Windows.Forms.Control;
t.BeginInvoke(OnReceiveComplete, new Object[] { ba });
}
else // object from the invocation list isn't a UI thread.
OnReceiveComplete(ba);
}

Finally public sealed class ServerNamedPipe had to be tweaked. In this implementation I always send an acknowledgment string after a receipt. It is client/server after all. So it is a receive binary / send a string.
Source code (C#):

public event OnNewByteArrayHandler GotNewByteArray;
public event OnActivityToReportHandler GotActivity;
private void PipeListener() {
CheckIfDisposed();
try {
Listen = DspchrHandlerA.PipeManager.Listen;
string myName = "Pipe_" + this.PipeConnection.NativeHandle.ToString();
GotActivity(new object(), myName + " :new pipe started");
while (Listen)
{
LastAction = DateTime.Now;
byte[] request = PipeConnection.ReadBytes();
LastAction = DateTime.Now;

// pipe manager gets the data
GotNewByteArray(this, request);
// this goes to the sender's listener to signal OK
PipeConnection.Write("ACKNOWLEDGEMENT " + request.Length.ToString() + " bytes");
//pipe manager get more info
GotActivity(new object(), myName + " acknowledgement sent for receipt of " + request.Length.ToString() + " bytes");

LastAction = DateTime.Now;
PipeConnection.Disconnect();
if (Listen) {
GotActivity(new object(), myName + " :listening");
Connect();
}
//
DspchrHandlerA.PipeManager.WakeUp();
}
}
catch (System.Threading.ThreadAbortException ex) { }
catch (System.Threading.ThreadStateException ex) { }
catch (Exception ex) {
// Log exception
}
finally {
this.Close();
}
}

Believe me named pipes and threads are NOT my specialty. I just had to know and do just enough to make it work for me. Yes, I am sure there are different and probably better ways. But I am a pragmatist and was not doing this for fun.

So we now have our pipes and we have an A and a B configuration. It matters not who is who but obviously partners cannot have the same configuration. As a final note these configurations are coded to operate on one workstation. You can of course run across different machines but that is a whole different topic.

The Data Bridge
This is the Form that will be instantiated from our NETLOADed module. As I mentioned before I wanted this to be as simple and generic as possible. Just a couple public Queue objects and a few events. I did add some UI Controls to play with during debug and never took them out.

How it works:
  1. Receive an entry on “my” pipe
  2. EnQueue the entry into the Suspense Queue
  3. Fire off an OnReceivedQueueEntry event
OR
  1. Receive an OnCompletedTask event
  2. DeQueue the entry from the Process queue
  3. Send it on to the other “Server’
If there is a simpler approach I could not think of it. Since a picture is worth so many words…



And the entire source sans the UI controls for the bridge:

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

using DspchrHandlers;
using Dspchr;

namespace DataBridge
{
public partial class DataBridge : Form
{
public delegate void ReceivedQueueEntryHandler(object sender);
public event ReceivedQueueEntryHandler OnReceivedQueueEntry;

public Queue SuspenseQ;
public Queue ProcessQ;

public SendVia SendViaX;
IDataBridge iDataBridgeClient;

public DataBridge(IDataBridge parent)
{
InitializeComponent();
iDataBridgeClient = parent;
DspchrHandlerA.StartServer();
SuspenseQ = new Queue();
ProcessQ = new Queue();
SendViaX = new SendVia();
DspchrHandlerA.PipeManager.OnReceiveComplete += new AppModule.InterProcessComm.ReceiveCompleteHandler(PipeManager_OnReceiveComplete);
DspchrHandlerA.PipeManager.OnStatusActivity += new AppModule.InterProcessComm.StatusActivityHandler(PipeManager_OnStatusActivity);
iDataBridgeClient.OnCompletedTask += new CompletedTaskHandler(iDataBridgeClient_OnCompletedTask);
}

void iDataBridgeClient_OnCompletedTask(object sender)
{
lblPQCnt.Text = ProcessQ.Count.ToString();
string rtn_result = null;
if (ProcessQ.Count > 0)
{
rtn_result = SendViaX.WriteBytes(DspchrHandlerA.SendOnPipeName, DspchrHandlerA.ServerName, (byte[])ProcessQ.Dequeue());
txtActivity.AppendText(rtn_result + Environment.NewLine);
}
lblPQCnt.Text = ProcessQ.Count.ToString();
lblSQCnt.Text = SuspenseQ.Count.ToString();
}
//
void PipeManager_OnStatusActivity(string info)
{
txtActivity.AppendText(info + Environment.NewLine);
}
void PipeManager_OnReceiveComplete(byte[] ba)
{
SuspenseQ.Enqueue(ba);
lblSQCnt.Text = SuspenseQ.Count.ToString();
Application.DoEvents();
OnReceivedQueueEntry(this);
}
private void DataBridge_FormClosing(object sender, FormClosingEventArgs e)
{
DspchrHandlerA.StopServer();
}
//
// Some form relate stuff to mess around with
//
private void btnRmvSQE_Click(object sender, EventArgs e)
{
if (SuspenseQ.Count > 0) SuspenseQ.Dequeue();
lblSQCnt.Text = SuspenseQ.Count.ToString();
}
private void btnRmvPQE_Click(object sender, EventArgs e)
{
if (ProcessQ.Count > 0) ProcessQ.Dequeue();
lblPQCnt.Text = ProcessQ.Count.ToString();
}
private void btnReturnSQE_Click(object sender, EventArgs e)
{
string rtn_result = null;
if (SuspenseQ.Count > 0)
{
rtn_result = SendViaX.WriteBytes(DspchrHandlerA.SendOnPipeName, DspchrHandlerA.ServerName, (byte[])SuspenseQ.Dequeue());
lblSQCnt.Text = SuspenseQ.Count.ToString();
txtActivity.AppendText(rtn_result + Environment.NewLine);
}
}
private void btnCLEARLOG_Click(object sender, EventArgs e)
{
txtActivity.Clear();
}
}
}
The last piece of the puzzle is this interface object for the DataBridge so it knows that there was something to be done.

using System;
using System.Collections.Generic;
using System.Text;

public delegate void CompletedTaskHandler(object sender);
public interface IDataBridge
{
event CompletedTaskHandler OnCompletedTask;
}
And a skeleton of a NETLOADable dll:

namespace ADSKDATABRIDGE
{
public class WRKR : IDataBridge
{
public event CompletedTaskHandler OnCompletedTask;
public DataBridge.DataBridge DataBridgeForm;

private static object TestForDuplication = null;
public WRKR() { }

[CommandMethod("DOPUTGET")]
public void dome()
{
if (TestForDuplication == null)
{
DataBridgeForm = new DataBridge.DataBridge(this);
TestForDuplication = DataBridgeForm;
DataBridgeForm.FormClosing += new FormClosingEventHandler(DataBridgeForm_FormClosing);
DataBridgeForm.OnReceivedQueueEntry += new DataBridge.DataBridge.ReceivedQueueEntryHandler(DataBridgeForm_OnReceivedQueueEntry);
DataBridgeForm.Show();
}
}
void DataBridgeForm_FormClosing(object sender, FormClosingEventArgs e)
{
DataBridgeForm.OnReceivedQueueEntry -= new DataBridge.DataBridge.ReceivedQueueEntryHandler(DataBridgeForm_OnReceivedQueueEntry);
TestForDuplication = null;
}
Finally … we receive our task and do it

void DataBridgeForm_OnReceivedQueueEntry(object sender)
{
Document doc = Autodesk.AutoCAD.ApplicationServices.Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
bool GONOGO = true;
DataSet CntrlDS;
DataSet ToDoDS;
byte[] RtnVal = null;
if (this.DataBridgeForm.SuspenseQ.Count > 0)
{
while (GONOGO && this.DataBridgeForm.SuspenseQ.Count > 0)
{
object o = DataBridgeForm.SuspenseQ.Dequeue();
try
{
CntrlDS = NPC.ExtractSerializedDataSet((byte[])o);
if (CntrlDS != null)
{
if (NPC.HasPayLoad(ref CntrlDS))
{
ToDoDS = NPC.ExtractNPCTablePayLoad(ref CntrlDS);
if (ToDoDS != null)
{
RtnVal = ProcessRequest(ref CntrlDS, ref ToDoDS);
}
}
}
if (RtnVal != null)
DataBridgeForm.ProcessQ.Enqueue(RtnVal);
OnCompletedTask(this);
RtnVal = null;
}
catch (System.Exception _Ex)
{
ed.WriteMessage("ERROR: " + _Ex.Message + "\r\n");
}
} // end while GONOGO
}// if count in queue
}// OnReceivedQueueEntry
}//eoc WRKR
} // eons

Thursday, October 23, 2008

Harvesting Autodesk DWG Entities

Author: Jonio, Dennis

This kicks off a series of articles on building SDO_GEOMETRY types from DWG entities. By series end you should have at least one working NETLOADable module for a template. I do not pretend that this is THE solution. Since this was completed, I have, of course, realized better ways to do some things and I am sure you will be able to find more. I strongly recommend that you look over the Oracle OTN forums in particular .NET and Spatial. The heart and soul of these UDT’s came from there. Another indispensable forum is the Autodesk Discussion Groups. Their .NET forum is also indispensable.

Oracle SDO_GEOMETRY and a few others as User Defined Types (UDT’s)

Included below are a couple projects NetSdoGeometry and NetSdoDimArray. These are the fundamental classes and I have posted these in the past. However since that time I have added an AsText property and the ToString() method. It became necessary to actually visualize the objects during development. I must also point out that SDO_GEOMETRY is now marked as [Serializable]. You will see later on that this allows for the inclusion of this type into a native .NET serializable dataset. I have included a fair number of non-essential members and methods. They are for convenience so the purist may wish to strip them.

Triplets and Triplet Management

In order to make any sense of this article you will have to have a copy of Oracle’s SDO_GEOMETRY specification and my implementation of the class NetSdoGeometry.sdogeometry.

Let me clarify, in general terms, what this implementation does and does not do in regards the specification

  1. Is Limited to two(2) dimensional geometries. Points, lines, surfaces(polygons)
  2. Does not include “compound-type” geometries
  3. Does include support for curves/arcs

After reviewing the spec you will realize it is all about “triplets”. The SDO_DIM_ELEM_ARRAY is of course a 1-> n series of 3 element integers groups. OFFSET - ETYPE -INTERPRETATION as the named components.

The manipulation of the ordinates array I found to be rather trivial both in terms of composition and decomposition. It is nothing more than that a left to right series of numbers.

“Triple Management” becomes the focus. Since I was learning the spec and writing code to it as I went along, the first class I created was, of course, “Triplet”. This class helped me visualize and understand the key elements of the specification. It is nothing more than codifying the obvious. The only oddity is my use of a GUID to track external or internal rings. The idea here will be explained later. In most cases a triplet acts as a kind’a sort’a header. A header that explains the ordinate array that is part of the SDO_GEOMETRY class. It would be just that simple if all one wished to do was work with linestrings. It is the support for curves/arcs that really adds a level of complexity by using a series of triplets to denote the transitions from line to arc, arc to line, etc.

Class “Pts” is a simple container for holding the ordinates. Of note here is the BulgeValue member. This acts as the placeholder for the arc information gotten from, in my case Map3d.

Finally class “TripletMgr” or Triplet Manager if you will. Before I discuss this class I will give you a chance to see how it is used. I do not use it for points and simple lines. Really unnecessary in those cases. This code decomposes as MPolygon into an sdogeometry. Frankly if you understand this one the rest is easy.

Here is the basic idea:

  
public static sdogeometry PolygonGeometryFromMPolygon(MPolygon mpoly)
{
sdogeometry sdo = null;
Point2dCollection p2dColl = new Point2dCollection();
int hasBulgesCount;
int totalOrdinatesWritten = 0; // The total ordinates in the array
int totalSubElementCount = 0; // Just the total of sub-elements
int OWNERSHIP = 1; // Manage the OFFSET for a triplet
LoopDirection loopdirection;

TripletMgr MasterTripletMgr = new TripletMgr();
int loops = mpoly.NumMPolygonLoops;
for (int i = 0; i < loops; i++)
{
// Gather up points and bulges for this loop
MPolygonLoop mPolygonLoop = mpoly.GetMPolygonLoopAt(i);
loopdirection = mpoly.GetLoopDirection(i);
hasBulgesCount = 0;
// Remove duplicate verticies
for (int d = 1; d < mPolygonLoop.Count - 1; d++)
{
if (mPolygonLoop[d].Vertex.IsEqualTo(mPolygonLoop[d + 1].Vertex))
{
mPolygonLoop.RemoveAt(d + 1);
}
}
//
Point2d[] pts = new Point2d[mPolygonLoop.Count];
double[] blgs = new double[mPolygonLoop.Count];
for (int z = 0; z < mPolygonLoop.Count; z++)
{
if (z == mPolygonLoop.Count - 1)
{
pts[z] = pts[0];
}
else
{
double X = Math.Round(mPolygonLoop[z].Vertex.X, 10, MidpointRounding.AwayFromZero);
double Y = Math.Round(mPolygonLoop[z].Vertex.Y, 10, MidpointRounding.AwayFromZero);
pts[z] = new Point2d(X, Y);
}
blgs[z] = mPolygonLoop[z].Bulge;
if (blgs[z] != 0.0)
hasBulgesCount = hasBulgesCount + 1;
}
// dej 2008/12/17
// Some fixup for starting and/or ending with an arc in that the bulges get duplicated
// along with the vertices. This also seems to be a bigger issue with "Inner" vs "Outer"
// loops. Obviously if we have a series of arcs they won't have dup values anyway.
// We must not decrement hasBulgesCount!!!
if (blgs[0] == blgs[mPolygonLoop.Count - 1])
blgs[mPolygonLoop.Count - 1] = 0.0;

//
// Finished getting all the ordinates and bulges for this loop
//
// Just a dummy triplet to work with and begin the process of formulation
// Each loop needs its own header triplet.
// Establish the ETYPE and the OWNERSHIP(OFFSET) marker for this header
// and its INTERPRETATION
int headerOffset = (i == 0) ? 1 : OWNERSHIP;
Triplet dummy_headerTriplet = new Triplet(new int[] { headerOffset, 0, 0 });
if (hasBulgesCount == 0)
dummy_headerTriplet.INTERPRETATION = 1; // all "lines"
else if (hasBulgesCount == pts.Length)
dummy_headerTriplet.INTERPRETATION = 2; // all "arcs"
// if it is not a 1 or 2 we have to fill the INTERPRETATION value in later
// because it contains the number of subelements
if (loopdirection == LoopDirection.Exterior)
{
// It is an exterior
if (dummy_headerTriplet.INTERPRETATION == 1 || dummy_headerTriplet.INTERPRETATION == 2)
{
dummy_headerTriplet.ETYPE = 1003;
}
}
else
{
// It is an interior
if (dummy_headerTriplet.INTERPRETATION == 1 || dummy_headerTriplet.INTERPRETATION == 2)
{
dummy_headerTriplet.ETYPE = 2003;
}
}
// We have not set the etype yet so it must be a 1005 0R 2005 - compound polygon
// dej 2008/12/17 if (dummy_headerTriplet.ETYPE == 0)
if (dummy_headerTriplet.ETYPE == 0 || hasBulgesCount > 0)
{
if (loopdirection == LoopDirection.Exterior)
dummy_headerTriplet.ETYPE = 1005;
else
dummy_headerTriplet.ETYPE = 2005;
}
//
// Every time we make a transition from/to a curve/line we will make up a triplet
//
// We new-up a triplet mgr for these subelements within this loop
// We do know that the etype MUST be a 2 - line or arc
//
TripletMgr tmgr = new TripletMgr();
Triplet nxtTriplet = null;
int currentINTERPRETATION = 0;
int lastwrittenINTERPRETATION = 0;
Point2d somePointOnArc;
Point2d arc_endpoint;
for (int j = 0; j < mPolygonLoop.Count; j++)
{
double theBulge = blgs[j];
//Test the bulge
if (theBulge != 0.0)
{
// Look ahead to get the endpoint for the arc
if (j == (mPolygonLoop.Count - 1))
arc_endpoint = pts[0];
else
arc_endpoint = pts[j + 1];

CircularArc2d ca2d = new CircularArc2d(pts[j], arc_endpoint, theBulge, false);
//
// Get the center point on the arc ....
// As an alternative:/ Point2d[] somePointSOnArc = ca2d.GetSamplePoints(3);
// The somePointSOnArc[1] element will have the centerpoint on the arc
//
Interval interval_of_arc = ca2d.GetInterval();
somePointOnArc = ca2d.EvaluatePoint(interval_of_arc.Element / 2);
currentINTERPRETATION = 2;
}
else
{
currentINTERPRETATION = 1;
}
// This only happens once per loop/ring because nxtTriplet is no longer null
// We use this to start off the tracking transitions from-to curves/lines
if (nxtTriplet == null)
{
int elementOffset = (i == 0) ? 1 : OWNERSHIP;
nxtTriplet = new Triplet(new int[] { elementOffset, 2, currentINTERPRETATION });
lastwrittenINTERPRETATION = currentINTERPRETATION;
}
// Now collect the points and if we have a bulge add the midpoint only
// and not the endpoint that will be next
p2dColl.Add(pts[j]);
totalOrdinatesWritten = totalOrdinatesWritten + 2;
OWNERSHIP = OWNERSHIP + 2;
if (theBulge != 0.0)
{
p2dColl.Add(new Point2d(somePointOnArc.ToArray()));
totalOrdinatesWritten = totalOrdinatesWritten + 2;
OWNERSHIP = OWNERSHIP + 2;
// Oracle must be made happy.
// OK this is really tricky ... the next item may or may not have a bulge
// if the next item is at the end and it has a bulge this is a circle!
// If it does not it is a line and we must loop back up so we make the proper
// switch on interp and get the OWNERSHIP value correct. Otherwise
// our OWNERSHIP value will be off by 2
//
if ((j + 1) == (mPolygonLoop.Count - 1))
{
if (blgs[j + 1] != 0.0)
{
p2dColl.Add(pts[j + 1]);
totalOrdinatesWritten = totalOrdinatesWritten + 2;
OWNERSHIP = OWNERSHIP + 2;
j = j + 1;
}
}
}
//
// Now we construct an additional triplet in the event we have changed INTERPRETATION
//
if (currentINTERPRETATION != lastwrittenINTERPRETATION)
{
tmgr.TripletList.Add(nxtTriplet);
totalSubElementCount++;
//
// If I am transitioning to an arc/curve my ownership actually started
// 4 ordinates ago or 2 ordinates ago in the case of a line
//
int elementOffset = (currentINTERPRETATION == 2) ? (OWNERSHIP - 4) : OWNERSHIP - 2;
nxtTriplet = new Triplet(new int[] { elementOffset, 2, currentINTERPRETATION });
lastwrittenINTERPRETATION = currentINTERPRETATION;
}
} // end of for-loop on vertex count ... finished with one of the rings.
//
// Add this last triplet no matter what
//
tmgr.TripletList.Add(nxtTriplet);
totalSubElementCount++;
//
// If we are a 1005 or a 2005 we must fix-up the header triplet to reflect
// the total number of triplets(subelements) that are present. In this case it
// gets stuffed into the INTERP position.
// In the end the Master will hold the entire polygon
//
if (tmgr.TripletCount > 1)
{
dummy_headerTriplet.INTERPRETATION = totalSubElementCount;
MasterTripletMgr.TripletList.Add(dummy_headerTriplet);
for (int x = 0; x < tmgr.TripletCount; x++)
{
MasterTripletMgr.TripletList.Add(tmgr.TripletList[x]);
}
}
//
// If we are a 1003 or a 2003
//
if (tmgr.TripletCount == 1)
{
MasterTripletMgr.TripletList.Add(dummy_headerTriplet);
}
// Set for next ring ...
totalSubElementCount = 0;
}// for each loop in the mpolygon
//
// We should have the geometry for the entire MPolygon described
// Construct the sdo_geometry object
//
sdo = new sdogeometry();
sdo.Dimensionality = (int)sdogeometryTypes.DIMENSION.DIM2D;
sdo.ElemArrayOfInts = MasterTripletMgr.ElementInfoToArray;
double[] rtnval = new double[p2dColl.Count * 2];
for (int w = 0, cnt = 0; w < p2dColl.Count; w++)
{
rtnval[cnt++] = p2dColl[w].X; rtnval[cnt++] = p2dColl[w].Y;
}
sdo.OrdinatesArrayOfDoubles = rtnval;
sdo.GeometryType = (int)sdogeometryTypes.GTYPE.POLYGON;
sdo.PropertiesToGTYPE();
return sdo;
}

The method SetPts is the workhorse. Once again it is the management of the triplets. The concept of “ownership” can from Pro Oracle Spatial for Oracle Database 11g, by Kothuri, Godfrind, Beinat. This really helped me put some of the pieces together. I recommend it highly.

Source code (C#) available for download at:

Saturday, August 2, 2008

Working with Oracle UDT’s with AutoCAD Map 3D

Author: Jonio, Dennis

Within this series of articles I have pretty well illustrated Oracle’s ODP.NET working in "harmony" with Autodesk’s Map3d 2008. Also note that I have used Oracle XE throughout. The functionality Locator brings is certainly more than enough to build a reasonable GIS data store. I have not spent any time in outlining any of the other Oracle stored procedures like SDO_GEOM.VALIDATE_GEOMETRY_WITH_CONTEXT() these are all well documented across the internet. As I have said before, FDO is the way to go. But you still need to get that geometry built somewhere and put into a data store. "Map Export to FDO" is obviously a work in progress but hopefully will become a useful tool down the road.

Lessons Learned
  • Always turn Connection Pooling OFF when working with UDT’s.

  • Do NOT use the App.config method for UDT discovery that is described in the ODP.NET documentation. This does no work in all cases. It seems nested classes are not always resolved.

  • Since your UDT class assembly(s) will be referenced from, in the default case: "C:\Program Files\AutoCAD Map 3D 2008", be careful of your naming convention.
Source code (C#):

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.Geometry;
using System.Reflection;
using System.Collections;
using System.IO;
using System.Text;
using System.Data;
using System;
using Oracle.DataAccess.Client;
using Oracle.DataAccess.Types;
using NetSdoGeometry;

[assembly: ExtensionApplication(typeof(YOURCLEVERAPPNAME.Initialization))]
[assembly: CommandClass(typeof(YOURCLEVERAPPNAME.YOURCLEVERAPPNAME))]
namespace YOURCLEVERAPPNAME
{
public class YOURCLEVERAPPNAME
{
public static string LocationOfDll;
public static string forReadDwgFileInit = "forReadDwgFileInit.dwg";

public YOURCLEVERAPPNAME()
{
Assembly callee = Assembly.GetExecutingAssembly();
LocationOfDll = Path.GetDirectoryName(callee.Location);
}

[CommandMethod("YOURCLEVERAPPNAME")]
public void mycleverapp()
{
Document doc = Autodesk.AutoCAD.ApplicationServices.Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;

//
// Must do this before any Oracle.DataAccess method calls.
// If you start getting “First chance exceptions” in VS you know things are hosed.
//
Database Xdb = new Database(false, false);
Xdb.ReadDwgFile(LocationOfDll + "\\" + forReadDwgFileInit,FileShare.ReadWrite, false, null);
Xdb.Dispose();

//
// I do not understand but I needed this HERE to resolve sdo_geometry
//
sdogeometry NEED_HERE_TO_RESOLVE_sdogeometry = new sdogeometry();
using (DocumentLock docLock = doc.LockDocument())
{
// do whatever needs doing
}
}
}//eoc YOURCLEVERAPPNAME

public class Initialization : Autodesk.AutoCAD.Runtime.IExtensionApplication
{
public void Initialize()
{
Editor ed = Autodesk.AutoCAD.ApplicationServices.Application.DocumentManager.MdiActiveDocument.Editor;
Assembly caller = Assembly.GetCallingAssembly();
Assembly callee = Assembly.GetExecutingAssembly();
string callername = caller.FullName;
string calleename = callee.FullName;
ed.WriteMessage("Loader: " + callername + " initialize " + calleename);
}

public void Terminate()
{
Assembly callee = Assembly.GetExecutingAssembly();
string calleename = callee.FullName;
Console.WriteLine("Cleaning up..." + calleename);
}
}//eoc Initialize
}//eons

Friday, May 30, 2008

ODP.NET 11g, Oracle XE 10.2, ObjectARX.NET, Autodesk Map3d 2008, OSGeo FDO 3.2

Author: Jonio, Dennis

Just some basics to let you know my current bias. I do believe that FDO is the way of the way to go. Without a doubt this is the correct track to be on. Just like everything else however, it has a way to go to get to maturity. I do not like the Autodesk FDO provider for Oracle. IMHO, Haris Kurtagic’s King.Oracle Provider is the way to go. Of course his, FDO2FDO is essential and an excellent piece of work.

That being said I really did not wish to deal with all the "weight" that FDO brings to every application. So I thought I would code up an application that moved DWG geometry to an Oracle table. It seemed to me to be a fairly straightforward task since I had put together that UDT for MDSYS.SDO_GEOMETRY. I always wondered why I had not seen some code examples of doing this. Well folks, now I know...

ODP does not play well with Map3d. For those of you who do ObjectARX.NET applications there are significant problems in heap management between ODP and Map3d. The basic premise of this app is to open a DWG as an "aside" using ReadDwgFile(), iterate the geometry, convert it to the correct format and insert it into the table. Let me "cut to the chase" on this one. It ended up that I had to "load" a DWG before any call was made to any ODP.NET classes. (In all fairness to Autodesk they are very clear in that they do not support these foreign database access methods within there architecture.) Once this is done there is no difficulty. I am able to iterate through a list of DWG’s and instantiate the database with ReadDwgFile() any number of times. At least in my case the acad.exe heap is now toast. For example, if after netloading and executing my app I attempt to use FDO within Map 3D it will fail. Just restart Map 3D and all is fine.

I had set up my sdogeometry UDT in a separate class being that I did not wish to duplicate the code across any number of applications. This is of course doable but I had a very frustrating time getting ODP.NET to "see" my UDT class within the Map3d environment. As the documentation reads, Oracle.Access.Client does a reflection on the executing assembly and looks for the OracleCustomTypeMappingAttribute. This works great if your UDT class is internal to your assembly. If you have an external class assembly the alternative is to set up an xml App.config file that Client will look for or add an entry to the machine.config. I never got this to work. I found that I had to actually instantiate an NetSdoGeometry.sdogeometry object within the [CommandMethod("????")] decorated method. Wow … NOT a big sacrifice but it did take me some time to come up with the correct timings.

To do the conversions from Map3d I first used TF.NET to convert to a JTS IGeometry object and to Maksim’s credit this seems to work well. (I did, however, take the time to code up emulating a circle as a linestring.) To do the conversion to sdogeometry I used Oracle’s JGeometry object. Remember I am working with Oracle XE 10.2 and it does not come with the builtin JVM so WKB/WKT are there but return nothing. I choose not to use the monoGIS distribution and did the IKVM byte code conversions myself using the most current JGeometry version I could find. Since I was only interested in the WKB/WKT methods I was not too concerned with the warnings coming out of IKVM. I forget the compile order but I needed these 4: sdoapi, sdoutl, xmlparserv2 and ojdbc5.

At this point a have a fully functional application. Not the fastest piece of code I have done but I certainly do exercise TF.NET. The next step will be to code up the load of the sdogeometry objects directly. But, I am in no hurry. Computer cycles are a lot cheaper than my time.

Oracle's SDO_DIM_ARRAY as UDT and utilizing SDO_MIGRATE.TO_CURRENT as stored procedure

Author: Jonio, Dennis

Utilizing Oracle’s latest ODP for .NET the following is one implementation of the SDO_DIM_ARRAY and SDO_DIM_ELEMENT type(s) as a .NET class.

This one is easy to explain. I needed to use SDO_MIGRATE.TO_CURRENT to fix some ORA-13367: wrong orientation for interior/exterior rings errors. So I needed SDO_DIM_ARRAY and thusly SDO_DIM_ELEMENT because SDO_MIGRATE.TO_CURRENT requires a SDO_DIM_ARRAY as a parameter. I have included my method of utilizing SDO_MIGRATE.TO_CURRENT after the class definitions.

Source code (C#):

using System;
using System.Collections;
using System.Text;
using Oracle.DataAccess.Client;
using Oracle.DataAccess.Types;

namespace NetSdoDimArray
{
///
/// ////////////////////////////////////
/// sdodimarray
/// ////////////////////////////////////
///

public class sdodimarray : IOracleCustomType, INullable
{
[OracleObjectMapping(0)]
public sdodimelement[] _dimelements;
private bool m_bIsNull;

public bool IsNull
{
get
{
return m_bIsNull;
}
}

public sdodimelement[] DimElements
{
get
{
return _dimelements;
}
set
{
_dimelements = value;
}
}

public static sdodimarray Null
{
get
{
sdodimarray obj = new sdodimarray();
obj.m_bIsNull = true;
return obj;
}
}

public override string ToString()
{
if (m_bIsNull)
return "sdodimarray.Null";
else
{
StringBuilder sb = new StringBuilder();
foreach (sdodimelement i in _dimelements)
{
sb.Append("sdodimelement(");
sb.Append(i._dimname + "=");
sb.Append(string.Format("{0:0.#####}", i._lb));
sb.Append(",");
sb.Append(string.Format("{0:0.#####}", i._ub));
sb.Append(",Tol=");
sb.Append(i._tolerance.ToString());
sb.Append(")");
}
return sb.ToString();
}
}

public void ToCustomObject(OracleConnection con, IntPtr pUdt)
{
_dimelements = (sdodimelement[])OracleUdt.GetValue(con, pUdt, 0);
}

public void FromCustomObject(OracleConnection con, IntPtr pUdt)
{
OracleUdt.SetValue(con, pUdt, 0, _dimelements);
}
}

[OracleCustomTypeMapping("MDSYS.SDO_DIM_ARRAY")]
public class sdodimarrayFactory : IOracleArrayTypeFactory, IOracleCustomTypeFactory
{
// IOracleCustomTypeFactory Inteface
public IOracleCustomType CreateObject()
{
return new sdodimarray();
}

// IOracleArrayTypeFactory.CreateArray Inteface
public Array CreateArray(int numElems)
{
return new sdodimelement[numElems];
}

// IOracleArrayTypeFactory.CreateStatusArray
public Array CreateStatusArray(int numElems)
{
// An OracleUdtStatus[] is not required to store null status information
// if there is no NULL attribute data in the element array
return null;
}
}

///
/// ////////////////////////////////////
/// sdodimelement
/// ////////////////////////////////////
///

public class sdodimelement : IOracleCustomType, INullable
{
[OracleObjectMapping(0)]
public string _dimname;

[OracleObjectMapping(1)]
public double _lb;

[OracleObjectMapping(2)]
public double _ub;

[OracleObjectMapping(3)]
public double _tolerance;

private bool m_bIsNull;

public bool IsNull
{
get
{
return m_bIsNull;
}
}

public static sdodimelement Null
{
get
{
sdodimelement obj = new sdodimelement();
obj.m_bIsNull = true;
return obj;
}
}

public override string ToString()
{
if (m_bIsNull)
return "sdodimelement.Null";
else
{
StringBuilder sb = new StringBuilder();
sb.Append("sdodimelement(");
sb.Append(_dimname + "=");
sb.Append(string.Format("{0:0.#####}", _lb));
sb.Append(",");
sb.Append(string.Format("{0:0.#####}", _ub));
sb.Append(",Tol=");
sb.Append(_tolerance.ToString());
sb.Append(")");
return sb.ToString();
}
}

public void ToCustomObject(OracleConnection con, IntPtr pUdt)
{
_dimname = (string)OracleUdt.GetValue(con, pUdt, 0);
_lb = (double)OracleUdt.GetValue(con, pUdt, 1);
_ub = (double)OracleUdt.GetValue(con, pUdt, 2);
_tolerance = (double)OracleUdt.GetValue(con, pUdt, 3);
}

public void FromCustomObject(OracleConnection con, IntPtr pUdt)
{
OracleUdt.SetValue(con, pUdt, 0, _dimname);
OracleUdt.SetValue(con, pUdt, 1, _lb);
OracleUdt.SetValue(con, pUdt, 2, _ub);
OracleUdt.SetValue(con, pUdt, 3, _tolerance);
}
}

[OracleCustomTypeMapping("MDSYS.SDO_DIM_ELEMENT")]
public class sdodimelementFactory : IOracleCustomTypeFactory
{
// IOracleCustomTypeFactory Inteface
public IOracleCustomType CreateObject()
{
sdodimelement sdodimelement = new sdodimelement();
return sdodimelement;
}
}
}
As you see, it utilizes both sdogeometry and sdodimarray...

public static sdogeometry MigrateToCurrentSP(string connectstring, sdogeometry _geoIn, sdodimarray _toleranceIn)
{
//ORA-13367: wrong orientation for interior/exterior rings
sdogeometry rtnval = null;
using (OracleConnection conn = new OracleConnection(connectstring))
{
using (OracleCommand dbcmd = conn.CreateCommand())
{
dbcmd.CommandType = CommandType.StoredProcedure;
dbcmd.CommandText = "SDO_MIGRATE.TO_CURRENT";

OracleParameter cParm1 = new OracleParameter();
cParm1.ParameterName = "geometry";
cParm1.OracleDbType = OracleDbType.Object;
cParm1.Value = _geoIn;
cParm1.UdtTypeName = "MDSYS.SDO_GEOMETRY";
cParm1.Direction = ParameterDirection.Input;

OracleParameter cParm2 = new OracleParameter();
cParm2.ParameterName = "tolerance";
cParm2.OracleDbType = OracleDbType.Object;
cParm2.Value = _toleranceIn;
cParm2.UdtTypeName = "MDSYS.SDO_DIM_ARRAY";
cParm2.Direction = ParameterDirection.Input;

OracleParameter cParm3 = new OracleParameter();
cParm3.ParameterName = "RETURNVALUE";
cParm3.OracleDbType = OracleDbType.Object;
cParm3.UdtTypeName = "MDSYS.SDO_GEOMETRY";
cParm3.Direction = ParameterDirection.ReturnValue;

// Note the order
dbcmd.Parameters.Add(cParm3);
dbcmd.Parameters.Add(cParm1);
dbcmd.Parameters.Add(cParm2);

try
{
conn.Open();
dbcmd.ExecuteNonQuery();
rtnval = (sdogeometry)dbcmd.Parameters["RETURNVALUE"].Value;
}
catch (Exception _Ex)
{
throw (_Ex); // Actually rethrow
}
finally
{
if (conn != null && conn.State == ConnectionState.Open)
{
conn.Close();
}
}
}//using cmd
}//using conn

return rtnval;
}