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.Triangles
Dim 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 oid
drops.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)
/ 3
Dim cy As Double = (triangle.Vertex1.Location.Y
+ triangle.Vertex2.Location.Y
+ triangle.Vertex3.Location.Y)
/ 3
Return 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 Double
Dim longest As Double = 0
For Each id As ObjectId In drops
Dim 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 Point3dCollection
Dim sinks As New Point3dCollection()
For Each id As ObjectId In drops
Dim drop As Polyline3d = _
TryCast(id.GetObject(OpenMode.ForRead), Polyline3d)
If drop.Length > (longest * 0.25) Then
sinks.Add(drop.EndPoint)
Dim msg As String = [String].Format( _
"Sink located at: ({0},{1})" & _
vbLf, drop.EndPoint.X, drop.EndPoint.Y)
write(msg)
End If
Next
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 = 10
For Each sink As Point3d In sinks
Dim 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.