NOTE: As you may have noticed, I have been very busy lately, which has prevented me from posting more often to this blog. The good news is that while I have been slacking on my articles, a couple colleagues have put together a great example on how to use the API to create water-drops and analyze rainfall. I want to thank Matt Anderson (@MattAnderson) and Drew Avis for this great contribution on what it is the first guest post to Civilized Development, and hopefully, the first of many more.
The new Surface APIs introduced in Civil 3D 2012 expose a lot of useful functionality and allow us to do some interesting things. In this post I will walk through using the SurfaceAnalysis.CreateWaterdrop() method to write a command that creates a rainfall analysis for a TIN surface. This sample also uses the Triangles collection of a TIN surface, another part of the new API.
In the Civil 3D GUI, the Waterdrop command is a tool to verify flow paths of a surface. You select or enter a point on a surface, and Civil draws a polyline representing the flow path of water from that point. This example creates a water-drop path from the centroid of all triangles in a surface, which can help you visualize the path rainwater runoff takes across a surface. There are a few reasons to perform this sort of analysis:
The collection of water-drop endpoints could be used as Discharge Points for the Catchment Command.
This command can provide a visualization of the surface and Civil 3D Inlets locations- Does the surface drain to the inlet, or is the inlet in the incorrect location?
The density of flow paths provides a visualization of flow accumulation. An interruption of that flow might be a “sink” or a watershed term for a “puddle”, or simply the need to verify the TIN triangles and/or the need to add break-lines along valleys.
Let’s look at some code. I’ve left out the tedious bits but you can download the complete source here.
First, we prompt for a TIN surface to operate on, and iterate through all the triangles in the surface. For each triangle, we calculate the 2d point that is the centroid (the geometric center) of the triangle. The CreateWaterdrop() method takes a 2d point, so we do not need to calculate the z dimension of the centroid. CreateWaterdrop() returns a collection of ObjectIds for any created polylines (and will be empty if there are none, for example if the target location is on a flat area). We save all of these in the “drops” collection to look at later.
C#
ObjectIdCollection drops = new ObjectIdCollection();
foreach (TinSurfaceTriangle triangle in surface.Triangles){Point2d centroid = getTriangleCentroid(triangle);// calculate water-drop for the centroid
ObjectIdCollection oid =surface.Analysis.CreateWaterdrop(centroid,Autodesk.Civil.Land.WaterdropObjectType.Polyline3D);// Save all the water-drops
foreach (ObjectId id in oid){drops.Add(id);}}
VB.NET
Dim drops As New ObjectIdCollection()For Each triangle As TinSurfaceTriangle In surface.TrianglesDim centroid As Point2d = _getTriangleCentroid(triangle)' calculate water-drop for the centroid
Dim oid As ObjectIdCollection = _surface.Analysis.CreateWaterdrop(centroid, _Autodesk.Civil.Land.WaterdropObjectType.Polyline3D)' Save all the water-drops
For Each id As ObjectId In oiddrops.Add(id)Next
Next
The centroid is calculated in the following way:
C#
private Point2d getTriangleCentroid(TinSurfaceTriangle triangle)
{// The centroid is calculated from the cx and cy being:
// cx = (v1x + v2x + v3x) / 3
// cy = (v1y + v2y + v3y) / 3
double cx = (triangle.Vertex1.Location.X
+ triangle.Vertex2.Location.X+ triangle.Vertex3.Location.X)/ 3;double cy = (triangle.Vertex1.Location.Y
+ triangle.Vertex2.Location.Y+ triangle.Vertex3.Location.Y)/ 3;return new Point2d(cx, cy);}
VB.NET
Private Function getTriangleCentroid(triangle As TinSurfaceTriangle) As Point2d' The centroid is calculated from the cx and cy being:
' cx = (v1x + v2x + v3x) / 3
' cy = (v1y + v2y + v3y) / 3
Dim cx As Double = (triangle.Vertex1.Location.X _+ triangle.Vertex2.Location.X _+ triangle.Vertex3.Location.X)/ 3Dim cy As Double = (triangle.Vertex1.Location.Y+ triangle.Vertex2.Location.Y+ triangle.Vertex3.Location.Y)/ 3Return New Point2d(cx, cy)End Function
Now we can do some processing on the collection of water-drops. First we find the longest one, and then we filter out any that are shorter than 25% the length of the longest, and save the endpoints in a collection of 3D points called “sinks”. Of course, a better implementation would be to make the 25% figure user-configurable, but I’ll leave that as an exercise for you to try.
The following code shows how we calculate the longest.
C#
private double calculateLongestWaterDrop(ObjectIdCollection drops){double longest = 0;
foreach (ObjectId id in drops)
{Polyline3d drop = id.GetObject(OpenMode.ForRead)as Polyline3d;
longest = drop.Length > longest ? drop.Length : longest;}return longest;
}
VB.NET
Private Function calculateLongestWaterDrop(drops As ObjectIdCollection)As DoubleDim longest As Double = 0For Each id As ObjectId In dropsDim drop As Polyline3d =TryCast(id.GetObject(OpenMode.ForRead), Polyline3d)
longest = If(drop.Length > longest, drop.Length, longest)
Next
Return longest
End Function
And now, we filter the sinks:
C#
private Point3dCollection filterSinks(ObjectIdCollection drops,
double longest)
{Point3dCollection sinks = new Point3dCollection();
foreach (ObjectId id in drops){Polyline3d drop = id.GetObject(OpenMode.ForRead)as Polyline3d;
if (drop.Length > (longest * .25))
{sinks.Add(drop.EndPoint);string msg = String.Format(
"Sink located at: ({0},{1})\n",
drop.EndPoint.X, drop.EndPoint.Y);write(msg);}}return sinks;
}
VB.NET
Private Function filterSinks(drops As ObjectIdCollection, _longest As Double) As Point3dCollectionDim sinks As New Point3dCollection()For Each id As ObjectId In dropsDim drop As Polyline3d = _TryCast(id.GetObject(OpenMode.ForRead), Polyline3d)
If drop.Length > (longest * 0.25) Thensinks.Add(drop.EndPoint)Dim msg As String = [String].Format( _"Sink located at: ({0},{1})" & _
vbLf, drop.EndPoint.X, drop.EndPoint.Y)write(msg)End IfNext
Return sinks
End Function
We cast each ObjectId in the drops collection to a Polyline3d because we specified that CreateWaterdrop() should create 3D polylines. You can also create 2d polylines if you do not care about the z-dimension elevation of the water-drop endpoint. In this case we do, because we want to create a collection of 3D points and mark them. Also, the 3D polyline water-drop is “draped” over the surface, which makes for a better visualization in model view.
Finally, we mark each endpoint with a marker in MODELSPACE. This part is just to demonstrate using the collection of water-drop endpoints, and there are probably more useful things you can do with this data. For example, you could export the collection of “sinks” to a file, which you can later use to create catchments. Unfortunately, catchment creation is not yet exposed in the API, so we can’t create them automatically in code.
C#
private void markSinks(Transaction ts, Point3dCollection sinks){// now lets mark each endpoint
BlockTable acBlkTbl = _currentDb.BlockTableId.GetObject(OpenMode.ForRead) as BlockTable;
BlockTableRecord acBlkTblRec =acBlkTbl[BlockTableRecord.ModelSpace].GetObject(OpenMode.ForWrite) as BlockTableRecord;
// set the point style
_currentDoc.Database.Pdmode = 35;_currentDoc.Database.Pdsize = 10;foreach (Point3d sink in sinks){DBPoint sinkPoint = new DBPoint(sink);
sinkPoint.Color = Color.FromRgb(0, 255, 255);sinkPoint.SetDatabaseDefaults();acBlkTblRec.AppendEntity(sinkPoint);ts.AddNewlyCreatedDBObject(sinkPoint, true);
}}
VB.NET
Private Sub markSinks(ts As Transaction, sinks As Point3dCollection)' now lets mark each endpoint
Dim acBlkTbl As BlockTable = TryCast(ts.GetObject( __currentDoc.Database.BlockTableId, OpenMode.ForRead), _BlockTable)Dim acBlkTblRec As BlockTableRecord = TryCast(ts.GetObject( _acBlkTbl(BlockTableRecord.ModelSpace), OpenMode.ForWrite), _BlockTableRecord)' set the point style
_currentDoc.Database.Pdmode = 35_currentDoc.Database.Pdsize = 10For Each sink As Point3d In sinksDim sinkPoint As New DBPoint(sink)sinkPoint.Color = Color.FromRgb(0, 255, 255)sinkPoint.SetDatabaseDefaults()acBlkTblRec.AppendEntity(sinkPoint)ts.AddNewlyCreatedDBObject(sinkPoint, True)
Next
End Sub
And that’s it, a simple rainfall visualization.
A surface before:
And after (zooming into the marked areas):
You can download the complete example here.
Can this be used in Civil 3D 2011 as well?
Thanks!
Posted by: Fadi Antoon | 09/29/2011 at 12:18 AM
The Surfaces .NET API is new in 2012, so the code will not compile in 2011.
Posted by: Isaac Rodriguez | 09/29/2011 at 05:15 AM
This is an interesting approach for dealing with watersheds.
I would be so nice to be able to test it … unfortunately I have only little experience in using vb.net.
Would you be so kind to give me a helping hand?
Thank a lot for all your posts.
Posted by: Severin Cazanescu | 10/06/2011 at 02:21 AM