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: