ProConcepts 3D Analyst Data - Esri/arcgis-pro-sdk GitHub Wiki
This concepts document covers the data considerations for the three types of 3D Analyst data - TIN, Terrain and LAS. All API objects are based in the ArcGIS.Core.Data.Analyst3D namespace. For reference information relevant to layers for these data types, refer to ProConcepts 3D Analyst Layers.
Language: C#
Subject: Geodatabase
Contributor: ArcGIS Pro SDK Team <[email protected]>
Organization: Esri, http://www.esri.com
Date: 10/06/2024
ArcGIS Pro: 3.4
Visual Studio: 2022
A triangular irregular network (TIN) is a form of vector-based digital geographic data that represents continuous surfaces such as terrain elevation or temperature gradients and is constructed by triangulating a set of nodes (points). The nodes are connected by a series of edges to form a network of contiguous, nonoverlapping triangles. Because TIN nodes can be placed irregularly over a surface, TINs can have higher resolution in areas where a surface is highly variable or where more detail is desired and a lower resolution in areas that are less variable. The edges in a TIN can be used to capture the position of linear features that play an important role in the surface, such as a ridge line or stream course.
TINs are based on a Delaunay triangulation or constrained Delaunay. Delaunay conforming triangulations are recommended over constrained triangulations. This is because the resulting TINs are likely to contain fewer long, skinny triangles, which are undesirable for surface analysis.
Here are some explanations of commonly used terms found when working with TINs.
- Super nodes - When a TIN is initially constructed it contains four generated points that reside far outside the declared data extent. These are connected into two triangles and this initial triangulation is what the data is added to. The four generated points are referred to as super nodes.
- Super node extent - The super node extent is the minimum bounding rectangle that surrounds the 4 super nodes.
- “inside”, “outside” - TIN elements can be classified as “inside” or “outside” the TIN. When TINs are initially built the only elements that are "outside" are the four super nodes and their incident edges and triangles. If clip or erase polygons are added to a TIN, they further modify what is considered to be "inside" or "outside”.
- Data area - This is the area that defines the boundary between the “inside” and “outside” elements. It is also known as the interpolation zone. When TIN’s are initially built from points the data area is a convex hull that surrounds all the user entered data. If clip or erase polygons exist or are added to the TIN, then the data area will be modified and defined as a multipart polygon with the clipped or erased elements being excluded.
Super nodes and their associated edges and triangles are not typically displayed as they are outside the TIN's data area.
In the pictures below, the super nodes have been added to the overlay and are displayed as bright green circles, the super node extent is the area denoted by the red border and the data area is denoted by the black border. In the second image you can see that the TIN's data area has a hole; an area has been cut from the TIN. The purple lines inside the hole denote "outside" edges and triangles whereas the other elements are "inside".
A TIN is stored as a series of standalone files on disk. You access a TIN workspace via the FileSystemDatastore
and FileSystemConnectionPath
objects using a datastore type of FileSystemDatastoreType.Tin
.
await ArcGIS.Desktop.Framework.Threading.Tasks.QueuedTask.Run(() =>
{
string path = @"d:\Data\Tin";
var fileConnection = new FileSystemConnectionPath(new Uri(path), FileSystemDatastoreType.Tin);
using (FileSystemDatastore dataStore = new FileSystemDatastore(fileConnection))
{
}
});
Once you have connected to the TIN workspace you can open the TinDataset via the OpenDataset
method on the FIleSystemDatastore. Or obtain the TinDatasetDefinition via the GetDefinition
method.
await ArcGIS.Desktop.Framework.Threading.Tasks.QueuedTask.Run(() =>
{
string path = @"d:\Data\Tin";
var fileConnection = new FileSystemConnectionPath(new Uri(path), FileSystemDatastoreType.Tin);
using (FileSystemDatastore dataStore = new FileSystemDatastore(fileConnection))
{
// TIN is in a folder at d:\Data\Tin\TinDataset
string dsName = "TinDataset";
using (var dataset = dataStore.OpenDataset<ArcGIS.Core.Data.Analyst3D.TinDataset>(dsName))
{
}
using (var def = dataStore.GetDefinition<ArcGIS.Core.Data.Analyst3D.TinDatasetDefinition>(dsName))
{
}
}
});
The TinDataset
and TinDatasetDefinition
objects provide you with access to the spatial reference via the TinDatasetDefinition.GetSpatialReference method, extent via TinDatasetDefinition.GetExtent and other dataset related information such as the TIN version (GetVersion) and whether the TIN is defined using the Constrained Delaunay triangulation technique. (UsesConstrainedDelaunay).
As mentioned above, TIN elements can be classified as “inside” or “outside”. The area that defines the boundary between these “inside” and “outside” elements is the interpolation zone or "data area" and can be obtained via the GetDataArea method. Obtain the "inside" and "outside" node, edge and triangle counts using GetNodeCount, GetOutsideNodeCount, GetEdgeCount, GetOutsideEdgeCount, GetTriangleCount, GetOutsideTriangleCount methods.
Use the GetSuperNodeExtent method to obtain the full extent of the TIN including the super nodes.
// get the super node extent
var superNodeExtent = tinDataset.GetSuperNodeExtent();
// get the data area
var dataArea = tinDataset.GetDataArea();
// get inside and outside node counts
var nodeCount = tinDataset.GetNodeCount();
var outsideNodeCount = tinDataset.GetOusideNodeCount();
A TIN consists of 3 different element types:
- Nodes are represented by the TinNode class
- Edges represented by the TinEdge class
- Triangles are represented by the TinTriangle class.
Each of these three classes inherit from the base TinElement class. Each element has an ElementType, a NodeCount and an Index. If known, the Index can be used to retrieve the given element (see Accessing TIN Elements). Each element also knows whether it is "inside" or "outside" via its IsInsideDataArea property.
Nodes store the x, y, z values. The TinNode
class provides methods to retrieve these coordinates via the Coordinate2D, Coordinate3D or ToMapPoint methods. Other class methods provide functionalities that include finding nodes that are connected to the current node (GetAdjacentNodes or GetAdjacentNodeIndices), finding the edges that share this node (GetIncidentEdges or GetIncidentEdgeIndices), or finding the triangles that reference the node (GetIncidentTriangles or GetIncidentTriangleIndices).
Edges are comprised of two nodes. Use the TinEdge
Nodes property to obtain the two nodes. Represent the edge as a polyline using the ToPolyline method. The EdgeType property determines the edge classification and how it is used in the triangulation. Edges can be regular, hard or soft; a hard edge is one that represents an abrupt transition in slope values whereas soft edges represent a gradual transition in slope values. Regular edges are produced by the triangulation algorithm.
Navigate the TIN structure to find neighboring edges using other class methods such as GetNeighbor, GetNextCounterClockwiseEdge, GetNextClockwiseEdge, GetNextEdgeInTriangle or GetPreviousEdgeInTriangle. Obtain the triangles for an edge using the LeftTriangle and RightTriangle methods.
Triangles are comprised of 3 nodes and 3 edges oriented in a clockwise direction. Use the Nodes and Edges properties on a TinTriangle
instance to retrieve these elements. The geometry of a triangle can be obtained using the ToPolygon method. Other key attributes of a triangle can be retrieved using the Aspect, Intensity and Slope properties.
Find adjacent triangles using GetAdjacentTriangles or GetAdjacentTriangleIndices. Other useful methods are GetCentroid, GetCircumCircle and GetNormal.
Using Element Index
You can retrieve specific elements from the TIN using the element’s index; note: the base index for TIN elements is 1 (not '0' zero). GetNodeByIndex, GetEdgeByIndex, GetTriangleByIndex provide these functionalities and are available on the TinDataset
object.
using (ArcGIS.Core.Data.Analyst3D.TinNode node = tinDataset.GetNodeByIndex(23))
{
var mapPoint = node.ToMapPoint();
// get adjacent nodes index
var nodeIndices = node.GetAdjacentNodesIndices();
// get incident edges indexes
var edgeIndices = node.GetIncidentEdgeIndices();
}
using (ArcGIS.Core.Data.Analyst3D.TinEdge edge = tinDataset.GetEdgeByIndex(45))
{
var edgeShape = edge.ToPolyline();
var edgeType = edge.EdgeType;
// get left, right triangles
var right = edge.RightTriangle;
var left = edge.LeftTriangle;
var nodes = edge.Nodes;
}
using (ArcGIS.Core.Data.Analyst3D.TinTriangle triangle = tinDataset.GetTriangleByIndex(22))
{
var triangleShape = triangle.ToPolygon();
var aspect = triangle.Aspect;
var slope = triangle.Slope;
}
Using MapPoint
Alternatively if you have a mapPoint
(e.g. through an identify action on the map or otherwise) you can use the GetNearestNode, GetNearestEdge or GetTriangleByPoint methods to retrieve the required / nearest elements.
Other methods that also take a MapPoint geometry allow you to find its natural neighbors (the nodes it would connect with to form triangles if the point were inserted into the triangulation) via GetNaturalNeighbors, Or triangles whose circumscribed circle contains the point with GetTriangleNeighborhood.
// get nearest node, edge, triangle to an identifyPoint
using (var nearestNode = tinDataset.GetNearestNode(identifyPoint))
{
}
using (var nearestEdge = tinDataset.GetNearestEdge(identifyPoint))
{
}
using (var triangle = tinDataset.GetTriangleByPoint(identifyPoint))
{
}
Using a Filter
You can also search the TinDataest
for elements using the TinDataset.SearchNodes, TinDataset.SearchEdges and TinDataset.SearchTriangles methods. They use a pattern similar to how you would search a feature class for features. These methods return a TinNodeCursor, TinEdgeCursor and TinTriangleCursor respectively to iterate through the search results. Each of the search methods takes a TIN element filter object as a parameter. For example to search for nodes use the SearchNodes method with a TinNodeFilter; to search for edges use the SearchEdges method with a TinEdgeFilter; and to search for triangles use the SearchTriangles method with a TinTriangleFilter.
Here are some simple snippets showing the searching pattern. As a convenience you can also pass null
as the filter parameter; meaning that you will be searching with the default filter. The default filter behavior is to use the TIN’s data extent to find all elements in that extent.
// search for nodes
using (ArcGIS.Core.Data.Analyst3D.TinNodeCursor nodeCursor = tinDataset.SearchNodes(null))
{
while (nodeCursor.MoveNext())
{
using (ArcGIS.Core.Data.Analyst3D.TinNode node = nodeCursor.Current)
{
}
}
}
// search for nodes
ArcGIS.Core.Data.Analyst3D.TinNodeFilter nodeFilter = new ArcGIS.Core.Data.Analyst3D.TinNodeFilter();
using (ArcGIS.Core.Data.Analyst3D.TinNodeCursor nodeCursor = tinDataset.SearchNodes(nodeFilter))
{
while (nodeCursor.MoveNext())
{
using (ArcGIS.Core.Data.Analyst3D.TinNode node = nodeCursor.Current)
{
}
}
}
// search for edges
using (ArcGIS.Core.Data.Analyst3D.TinEdgeCursor edgeCursor = tinDataset.SearchEdges(null))
{
while (edgeCursor.MoveNext())
{
using (ArcGIS.Core.Data.Analyst3D.TinEdge edge = edgeCursor.Current)
{
}
}
}
// search for edges
ArcGIS.Core.Data.Analyst3D.TinEdgeFilter edgeFilter = new ArcGIS.Core.Data.Analyst3D.TinEdgeFilter();
using (ArcGIS.Core.Data.Analyst3D.TinEdgeCursor edgeCursor = tinDataset.SearchEdges(edgeFilter))
{
while (edgeCursor.MoveNext())
{
using (ArcGIS.Core.Data.Analyst3D.TinEdge edge = edgeCursor.Current)
{
}
}
}
// search for triangles
using (ArcGIS.Core.Data.Analyst3D.TinTriangleCursor triangleCursor = tinDataset.SearchTriangles(null))
{
while (triangleCursor.MoveNext())
{
using (ArcGIS.Core.Data.Analyst3D.TinTriangle triangle = triangleCursor.Current)
{
}
}
}
// search for triangles
ArcGIS.Core.Data.Analyst3D.TinTriangleFilter triangleFilter = new ArcGIS.Core.Data.Analyst3D.TinTriangleFilter();
using (ArcGIS.Core.Data.Analyst3D.TinTriangleCursor triangleCursor = tinDataset.SearchTriangles(triangleFilter))
{
while (triangleCursor.MoveNext())
{
using (ArcGIS.Core.Data.Analyst3D.TinTriangle triangle = triangleCursor.Current)
{
}
}
}
As mentioned, the default filter behavior is to use the TIN’s data extent to find all elements in that extent. The filter objects have properties that allow you to change this behavior. Use the FilterEnvelope property to set an alternative extent to search. Use a combination of FilterType and DataElementsOnly to alter the category of elements returned by the search. The following table describes these combinations.
DataElementsOnly = true | DataElementsOnly = false | |
---|---|---|
FilterType = All | Find all "inside" elements in the FilterEnvelope. This will NOT include super nodes or "outside" elements. |
Find all elements in the FilterEnvelope. This could include super nodes or "outside" elements depending on the extent. |
FilterType = InsideTin | Find all "inside" elements in the TIN in the FilterEnvelope except super nodes. This will NOT include super nodes or "outside" elements. |
Find all elements in the TIN in the FilterEnvelope except super nodes. This could include "outside" elements depending on the extent. |
FilterType = InsideDataArea | Find all elements in the TIN's data area that are in the FilterEnvelope. Elements that are in the TIN’s data area are by definition classified as “Inside” so the DataElementsOnly property has no effect. This will NOT include super nodes or "outside" elements. |
Find all elements in the TIN's data area that are in the FilterEnvelope. Elements that are in the TIN’s data area are by definition classified as “Inside” so the DataElementsOnly property has no effect. This will NOT include super nodes or "outside" elements. |
Here are some examples which illustrate the DataElementsOnly
property. Consider the following where a region has been cut from the TIN introducing an "outside" area. The purple colored edges are "outside", the blue colored edges are "hard" edges; that is they represent a breakline.
Using the following code snippet the following counts are obtained.
- nodeCountInside = 8; nodeCountAll = 8
- edgeCountInside = 26; edgeCountAll = 31
- triCountInside = 18; triCountAll = 24
var nodeCountInside = 0;
// search "inside" nodes within an extent
ArcGIS.Core.Data.Analyst3D.TinNodeFilter nodeFilter = new ArcGIS.Core.Data.Analyst3D.TinNodeFilter();
nodeFilter.FilterEnvelope = bookmarkExtent;
nodeFilter.DataElementsOnly = true;
//nodeFilter.FilterType = ArcGIS.Core.Data.Analyst3D.TinFilterType.All; //All is the default so no need to set
using (ArcGIS.Core.Data.Analyst3D.TinNodeCursor nodeCursor = tinDataset.SearchNodes(nodeFilter))
{
while (nodeCursor.MoveNext())
{
using (ArcGIS.Core.Data.Analyst3D.TinNode node = nodeCursor.Current)
{
nodeCountInside++;
}
}
}
int nodeCountAll = 0;
// search for all nodes within an extent
// this will include "outside" and superNodes depending upon the envelope
ArcGIS.Core.Data.Analyst3D.TinNodeFilter nodeFilterAll = new ArcGIS.Core.Data.Analyst3D.TinNodeFilter();
nodeFilterAll.FilterEnvelope = bookmarkExtent;
nodeFilterAll.DataElementsOnly = false;
//nodeFilterAll.FilterType = ArcGIS.Core.Data.Analyst3D.TinFilterType.All;//All is the default so no need to set
using (ArcGIS.Core.Data.Analyst3D.TinNodeCursor nodeCursor = tinDataset.SearchNodes(nodeFilterAll))
{
while (nodeCursor.MoveNext())
{
using (ArcGIS.Core.Data.Analyst3D.TinNode node = nodeCursor.Current)
{
nodeCountAll++;
}
}
}
var edgeCountInside = 0;
// search "inside" edges within an extent
ArcGIS.Core.Data.Analyst3D.TinEdgeFilter edgeFilter = new ArcGIS.Core.Data.Analyst3D.TinEdgeFilter();
edgeFilter.FilterEnvelope = bookmarkExtent;
edgeFilter.DataElementsOnly = true;
using (ArcGIS.Core.Data.Analyst3D.TinEdgeCursor edgeCursor = tinDataset.SearchEdges(edgeFilter))
{
while (edgeCursor.MoveNext())
{
using (ArcGIS.Core.Data.Analyst3D.TinEdge edge = edgeCursor.Current)
{
edgeCountInside++;
}
}
}
int edgeCountAll = 0;
// search all edges within an extent
// this could include outside or edges attached to super nodes depending upon the extent
ArcGIS.Core.Data.Analyst3D.TinEdgeFilter edgeFilterAll = new ArcGIS.Core.Data.Analyst3D.TinEdgeFilter();
edgeFilterAll.FilterEnvelope = bookmarkExtent;
edgeFilterAll.DataElementsOnly = false;
using (ArcGIS.Core.Data.Analyst3D.TinEdgeCursor edgeCursor = tinDataset.SearchEdges(edgeFilterAll))
{
while (edgeCursor.MoveNext())
{
using (ArcGIS.Core.Data.Analyst3D.TinEdge edge = edgeCursor.Current)
{
edgeCountAll++;
}
}
}
int triCountInside = 0;
// search "inside" triangles within an extent
ArcGIS.Core.Data.Analyst3D.TinTriangleFilter triangleFilter =
new ArcGIS.Core.Data.Analyst3D.TinTriangleFilter();
triangleFilter.FilterEnvelope = bookmarkExtent;
triangleFilter.DataElementsOnly = true;
using (ArcGIS.Core.Data.Analyst3D.TinTriangleCursor triangleCursor = tinDataset.SearchTriangles(triangleFilter))
{
while (triangleCursor.MoveNext())
{
using (ArcGIS.Core.Data.Analyst3D.TinTriangle triangle = triangleCursor.Current)
{
triCountInside++;
}
}
}
int triCountAll = 0;
// search all triangles within an extent
// this could include outside or triangles attached to super nodes depending upon the extent
var triangleFilterAll = new ArcGIS.Core.Data.Analyst3D.TinTriangleFilter();
triangleFilterAll.FilterEnvelope = bookmarkExtent;
triangleFilterAll.DataElementsOnly = false;
using (ArcGIS.Core.Data.Analyst3D.TinTriangleCursor triangleCursor =
tinDataset.SearchTriangles(triangleFilterAll))
{
while (triangleCursor.MoveNext())
{
using (ArcGIS.Core.Data.Analyst3D.TinTriangle triangle = triangleCursor.Current)
{
triCountAll++;
}
}
}
The following code snippet produces the same inside counts; note the setting of the FilterType to TinFilterType.InsideDataArea
instead of the previously default of TinFilterType.All
. The DataElementsOnly
flag is ignored when using this filter type.
var nodeCountInside = 0;
// search nodes inside data area within an extent
ArcGIS.Core.Data.Analyst3D.TinNodeFilter nodeFilter = new ArcGIS.Core.Data.Analyst3D.TinNodeFilter();
nodeFilter.FilterEnvelope = bookmarkExtent;
nodeFilter.FilterType = ArcGIS.Core.Data.Analyst3D.TinFilterType.InsideDataArea;
using (ArcGIS.Core.Data.Analyst3D.TinNodeCursor nodeCursor = tinDataset.SearchNodes(nodeFilter))
{
while (nodeCursor.MoveNext())
{
using (ArcGIS.Core.Data.Analyst3D.TinNode node = nodeCursor.Current)
{
nodeCountInside++;
}
}
}
var edgeCountInside = 0;
// search edges inside data area within an extent
ArcGIS.Core.Data.Analyst3D.TinEdgeFilter edgeFilter = new ArcGIS.Core.Data.Analyst3D.TinEdgeFilter();
edgeFilter.FilterEnvelope = bookmarkExtent;
edgeFilter.FilterType = ArcGIS.Core.Data.Analyst3D.TinFilterType.InsideDataArea;
using (ArcGIS.Core.Data.Analyst3D.TinEdgeCursor edgeCursor = tinDataset.SearchEdges(edgeFilter))
{
while (edgeCursor.MoveNext())
{
using (ArcGIS.Core.Data.Analyst3D.TinEdge edge = edgeCursor.Current)
{
edgeCountInside++;
}
}
}
int triCountInside = 0;
// search triangles inside data area within an extent
ArcGIS.Core.Data.Analyst3D.TinTriangleFilter triangleFilter = new ArcGIS.Core.Data.Analyst3D.TinTriangleFilter();
triangleFilter.FilterEnvelope = bookmarkExtent;
triangleFilter.FilterType = ArcGIS.Core.Data.Analyst3D.TinFilterType.InsideDataArea;
using (ArcGIS.Core.Data.Analyst3D.TinTriangleCursor triangleCursor = tinDataset.SearchTriangles(triangleFilter))
{
while (triangleCursor.MoveNext())
{
using (ArcGIS.Core.Data.Analyst3D.TinTriangle triangle = triangleCursor.Current)
{
triCountInside++;
}
}
}
Use the FilterType of TinFilterType.InsideTin
when the search extent includes some part outside the TIN's data area and you wish to exclude super nodes and their incident edges or triangles from the search.
// search all nodes in the TIN in the extent
// this will include "outside" nodes depending upon the envelope
// it will not include super nodes even if the envelope encompasses part of the superNode extent
var nodeFilter = new ArcGIS.Core.Data.Analyst3D.TinNodeFilter();
nodeFilter.FilterType = ArcGIS.Core.Data.Analyst3D.TinFilterType.InsideTin;
nodeFilter.FilterEnvelope = envelope;
nodeFilter.DataElementsOnly = false;
using (ArcGIS.Core.Data.Analyst3D.TinNodeCursor nodeCursor = tinDataset.SearchNodes(nodeFilter))
{
while (nodeCursor.MoveNext())
{
using (ArcGIS.Core.Data.Analyst3D.TinNode node = nodeCursor.Current)
{
}
}
}
The TinNodeFilter
has an additional property SuperNode allowing you to specifically search for the super nodes. Remember to set the FilterEnvelope
to the super node extent and DataElementsOnly
to false,
// search for super nodes only
var nodeFilter = new ArcGIS.Core.Data.Analyst3D.TinNodeFilter();
nodeFilter.FilterEnvelope = tinDataset.GetSuperNodeExtent();
nodeFilter.DataElementsOnly = false;
nodeFilter.SuperNode = true;
using (ArcGIS.Core.Data.Analyst3D.TinNodeCursor nodeCursor = tinDataset.SearchNodes(nodeFilter))
{
while (nodeCursor.MoveNext())
{
using (ArcGIS.Core.Data.Analyst3D.TinNode node = nodeCursor.Current)
{
}
}
}
If you wish to search by edge type, use the TinEdgeFilter
with the following properties FilterByEdgeType and EdgeType.
// search for hard edges in the TIN
var edgeFilter = new ArcGIS.Core.Data.Analyst3D.TinEdgeFilter();
edgeFilter.FilterByEdgeType = true;
edgeFilter.EdgeType = ArcGIS.Core.Data.Analyst3D.TinEdgeType.HardEdge;
using (ArcGIS.Core.Data.Analyst3D.TinEdgeCursor edgeCursor = tinDataset.SearchEdges(edgeFilter))
{
while (edgeCursor.MoveNext())
{
using (ArcGIS.Core.Data.Analyst3D.TinEdge edge = edgeCursor.Current)
{
}
}
}
A terrain dataset is a multiresolution, TIN-based surface. That is, it has a series of TINs, each of which is used within a specific map scale range. A coarse-grained TIN is used at larger map extents (such as when you are zoomed out to the entire study area). More surface points and increasing levels of detail are used as you zoom in and focus on larger map scales and specific map extents. Terrains reside in the geodatabase inside feature datasets with the features used to construct them. The measurements are typically made from LiDAR, sonar, and photogrammetric sources.
Terrains use pyramids to represent the multiple levels of resolution. Pyramid levels take advantage of the fact that accuracy requirements diminish with scale. For each successive pyramid level, fewer measurements are used, and the accuracy requirements necessary to display the surface drops accordingly. The original source measurements are still used in coarser pyramids, but there are fewer of them. Essentially, the pyramid levels are used as a form of scale-dependent generalization that are used to improve efficiencies.
Since a terrain exists in a geodatabase, you can access the terrain using the standard way to connect to a geodatabase in the Pro SDK. That is, in the case of a file geodatabase use the FileGeodatabaseConnectionPath
and Geodatabase
objects to open a connection. Once you have connected to the data source use the OpenDataset method to open and retrieve the Terrain. The TerrainDefinition can also be obtained via the "standard" GetDefinition method (on the data store).
Here's a snippet showing how to retrieve the Terrain
and TerrainDefinition
objects.
await ArcGIS.Desktop.Framework.Threading.Tasks.QueuedTask.Run(() =>
{
string path = @"d:\Data\Terrain\filegdb_Containing_A_Terrain.gdb";
var fileConnection = new FileGeodatabaseConnectionPath(new Uri(path));
using (Geodatabase gdb = new Geodatabase(fileConnection))
{
string dsName = "nameOfTerrain";
using (var terrain = gdb.OpenDataset<ArcGIS.Core.Data.Analyst3D.Terrain>(dsName))
{
}
using (var terrainDef = dataStore.GetDefinition<ArcGIS.Core.Data.Analyst3D.TerrainDefinition>(dsName))
{
}
}
});
Once you have the Terrain
object, you can use the GetFeatureDataset method to obtain the parent feature dataset and the GetDataSources method to obtain information about the participating feature classes. The return value of GetDataSources is a read-only list of TerrainDataSources. A TerrainDataSource class encapsulates the names of the data sources along with information about how the features are used to define the surface through a range of scales.
IReadOnlyList<ArcGIS.Core.Data.Analyst3D.TerrainDataSource> dataSources = terrain.GetDataSources();
foreach (var ds in dataSources)
{
var dsName = ds.DataSourceName;
var surfaceType = ds.SurfaceType;
var maxResolution = ds.MaximumResolution;
var minResolution = ds.MinimumResolution;
}
Pyramid level information can be obtained with the GetPyramidLevels method. It returns a set of TerrainPyrramidLevel. TerrainPyrramidLevel provides the Resolution and MaximumScale which defines the amount of detail represented by the level and the display scale at which the level becomes active.
IReadOnlyList<ArcGIS.Core.Data.Analyst3D.TerrainPyramidLevel> pyramidLevels = terrain.GetPyramidLevels();
foreach (var pyramidLevel in pyramidLevels)
{
var resolution = pyramidLevel.Resolution;
var maxScale = pyramidLevel.MaximumScale;
}
There are two different types of pyramiding algorithms. These are represented by the TerrainPyramidType enumeration. Determine the pyramid type of the terrain with GetPyramidType from the TerrainDefinition
. Find the pyramid properties using GetPyramidWindowSizeProperties.
TerrainPyramidType pyramidType = terrainDefinition.GetPyramidType();
TerrainWindowSizeProperties properties = terrainDefinition.GetPyramidWindowSizeProperties();
var method = properties.Method;
var zThreshold = properties.ZThreshold;
var zThresholdStrategy = properties.ZThresholdStrategy;
A LAS file is an industry-standard binary format for storing airborne LiDAR data. ArcGIS Pro can use LAS files directly, but often these come as a collection of adjacent files. To work with a collection of LAS files, a LAS dataset (.lasd) is created which references one or more LAS files on disk, as well as optional surface constraint features that define various surface characteristics. The LAS dataset allows you to examine the LAS files and inspect the aerial coverage of the LiDAR data contained in the LAS files as well. Note that Optimized LAS (.ZLAS) files are also supported by the LAS dataset.
The LAS dataset is a stand alone file that resides in a folder. You access a LAS via the FileSystemDatastore
and FileSystemConnectionPath
objects using a datastore type of FileSystemDatastoreType.LasDataset
.
await ArcGIS.Desktop.Framework.Threading.Tasks.QueuedTask.Run(() =>
{
string path = @"d:\Data\LASDataset";
var fileConnection = new FileSystemConnectionPath(new Uri(path), FileSystemDatastoreType.LasDataset);
using (FileSystemDatastore dataStore = new FileSystemDatastore(fileConnection))
{
}
});
Once connected to the LAS file-system data source, open the LasDataset using the standard OpenDataset
method. You can also access the LasDatasetDefinition with the GetDefinition
method.
await ArcGIS.Desktop.Framework.Threading.Tasks.QueuedTask.Run(() =>
{
string path = @"d:\Data\LASDataset";
var fileConnection = new FileSystemConnectionPath(new Uri(path), FileSystemDatastoreType.LasDataset);
using (FileSystemDatastore dataStore = new FileSystemDatastore(fileConnection))
{
string name = "somedataset.lasd"; // can specify with or without the .lasd extension
using (ArcGIS.Core.Data.Analyst3D.LasDataset dataset =
dataStore.OpenDataset<ArcGIS.Core.Data.Analyst3D.LasDataset>(name))
{
}
using (ArcGIS.Core.Data.Analyst3D.LasDatasetDefinition def =
dataStore.GetDefinition<ArcGIS.Core.Data.Analyst3D.LasDatasetDefinition>(name))
{
}
}
});
The LasDataset
object gives you access to the information about the individual LAS or ZLAS files in the dataset. Use the GetFileCounts method to obtain the count of each type of file. And GetFiles to obtain the collection of files that are referenced by the LAS dataset. Individual file information is encapsulated in a LasFile class that has FileName, FilePath, MajorVersion, MinorVersion, PointCount and ZMin and ZMax properties.
var (lasFileCount, zLasFileCount) = lasDataset.GetFileCounts();
IReadOnlyList<ArcGIS.Core.Data.Analyst3D.LasFile> fileInfos = lasDataset.GetFiles();
foreach (ArcGIS.Core.Data.Analyst3D.LasFile fileInfo in fileInfos)
{
var path = fileInfo.FilePath;
var name = fileInfo.FileName;
var ptCount = fileInfo.PointCount;
var zMin = fileInfo.ZMin;
var zMax = fileInfo.ZMax;
}
Surface constraint features that define the LAS surface characteristics can be obtained using the GetSurfaceConstraints method. Breaklines, water polygons, or area boundaries are all examples of surface constraints. Each SurfaceConstraint is defined by it’s DataSourceName, WorkspacePath, HeightField and SurfaceType. The SurfaceType defines how the geometry of the surface constraint features are incorporated into the triangulation of the surface.
var constraintCount = lasDataset.GetSurfaceConstraintCount();
IReadOnlyList<ArcGIS.Core.Data.Analyst3D.SurfaceConstraint> constraints = lasDataset.GetSurfaceConstraints();
foreach (ArcGIS.Core.Data.Analyst3D.SurfaceConstraint constraint in constraints)
{
var dsName = constraint.DataSourceName;
var wksPath = constraint.WorkspacePath;
var heightField = constraint.HeightField;
var surfaceType = constraint.SurfaceType;
}
Each LasPoint contains an x, y, z value. You can retrieve these coordinates with the Coordinate2D, Coordinate3D or ToMapPoint methods. You identify a point using the PointID along with the FileIndex; the index of the LAS file that it resides in.
Other key attributes of a LasPoint
are it's classification code, classification flags and return values. These concepts are explained below.
Classification codes:
Classification codes are used to define the type of surface, or surfaces, that reflected the lidar pulse. Classification codes follow the American Society for Photogrammetry and Remote Sensing (ASPRS)* for LAS formats 1.1, 1.2, 1.3, and 1.4 and include codes for building, ground, water, vegetation, and so on. The set of classification codes present in a given LasDataset
can be retrieved via the LasDataset.GetUniqueClassCodes method. Retrieve the point's classification using the ClassCode property.
*The complete set of available classification codes from ASPRS.
Return Values
When a lidar pulse is emitted from a sensor, it can have multiple return values depending on the nature of the surfaces the pulses encounter. The first returns will typically be associated with the highest objects encountered (eg tops of trees or buildings) and the last returns with the lowest objects encountered (e.g. the ground).
Use the ReturnNumber and NumberOfReturns properties to obtain the information for a LAS point. The return values that can be specified are represented by the LasReturnType enumeration. Determine the set of return values present in the dataset via the LasDataset.GetUniqueReturns method.
Classification flags
In many cases, when a classification is carried out on lidar data, points can fall into more than one classification category. In these cases, classification flags are specified in the lidar data to provide a secondary description or classification for the points. Classification flag values include Synthetic, key-point, withheld, and overlap (see below).
The set of flags and their description is as follow:
Flag | Notes |
---|---|
Synthetic | The point was created by a technique other than LIDAR collection such as digitized from a photogrammetric stereo model or by traversing a waveform |
Key-point | The point is considered to be a model key-point and thus generally should not be withheld in a thinning algorithm |
Withheld | The point should not be included in processing (synonymous with Deleted) |
Overlap | The point is within the overlap region of two or more swaths or takes. Setting this bit is not mandatory (unless, of course, it is mandated by a particular delivery specification) but allows Classification of overlap points to be preserved. |
Determine the classification flags of the LasPoint
by using the IsKeyPoint, IsOverlapPoint, IsSyntheticPoint and IsWithheld properties.
Using Point ID
Retrieve specific elements from the LAS dataset using the GetPointByID method. The base index for a LAS point is 1, meaning that the first record in a file has a PointID of 1. Because the LAS Dataset can reference multiple LAS files, more than one point with the specified point ID can be found. It is also possible to specify an extent to limit the search area. Alternatively, if you know the specific file that the point resides in, use a different version of the GetPointByID method that takes the file index as a parameter in addition to the point ID.
// access by ID
IReadOnlyList<ArcGIS.Core.Data.Analyst3D.LasPoint> pts = lasDataset.GetPointByID(123456);
// access by ID and envelope
pts = lasDataset.GetPointByID(123456, envelope);
ArcGIS.Core.Data.Analyst3D.LasPoint pt = pts.FirstOrDefault();
// access by ID and file Index
pt = lasDataset.GetPointByID(2, 123456);
// get point coordinates
var coords = pt.Coordinate3D;
var mapPoint = pt.ToMapPoint();
Using a Filter
Search the LasDataset
for points using the SearchPoints method which returns a LasPointCursor. The SearchPoints
method uses a LasPointFilter object to allow you to define a set of parameters to refine the search. Pass null
to retrieve all points or create a LasPointFilter
with a FilterGeometry to search (for points) in a specific area. You can further refine the search by specifying a set of classification codes or return values via the ClassCodes and/or Returns properties. You can also filter by the point classification flags if desired.
Iterate through the LasPointCursor
and retrieve the individual LasPoint
objects with the MoveNext. Alternatively if you are processing large numbers of results and interested in only the coordinates or file index and point ID identifiers, use the MoveNextArray method with pre-initialized arrays to gain faster performance. You cannot mix using MoveNext and MoveNextArray within the same search result.
Here are a few snippets illustrating SearchPoints with different LasPointFilter options:
// search all points
using (ArcGIS.Core.Data.Analyst3D.LasPointCursor ptCursor = lasDataset.SearchPoints(null))
{
while (ptCursor.MoveNext())
{
using (ArcGIS.Core.Data.Analyst3D.LasPoint point = ptCursor.Current)
{
}
}
}
// search within an extent
ArcGIS.Core.Data.Analyst3D.LasPointFilter pointFilter = new ArcGIS.Core.Data.Analyst3D.LasPointFilter();
pointFilter.FilterGeometry = envelope;
using (ArcGIS.Core.Data.Analyst3D.LasPointCursor ptCursor = lasDataset.SearchPoints(pointFilter))
{
while (ptCursor.MoveNext())
{
using (ArcGIS.Core.Data.Analyst3D.LasPoint point = ptCursor.Current)
{
}
}
}
// search within an extent and limited to specific classification codes
ArcGIS.Core.Data.Analyst3D.LasPointFilter pointFilter = new ArcGIS.Core.Data.Analyst3D.LasPointFilter();
pointFilter.FilterGeometry = envelope;
pointFilter.ClassCodes = new List<int> { 4, 5 };
using (ArcGIS.Core.Data.Analyst3D.LasPointCursor ptCursor = lasDataset.SearchPoints(pointFilter))
{
while (ptCursor.MoveNext())
{
using (ArcGIS.Core.Data.Analyst3D.LasPoint point = ptCursor.Current)
{
}
}
}
And examples using the MoveNextArray
method.
// search all points and process with a set size of array retrieving only coordinates
using (ArcGIS.Core.Data.Analyst3D.LasPointCursor ptCursor = lasDataset.SearchPoints(null))
{
int count;
Coordinate3D[] lasPointsRetrieved = new Coordinate3D[10000];
while (ptCursor.MoveNextArray(lasPointsRetrieved, null, null, null, out count))
{
var points = lasPointsRetrieved.ToList();
// ...
}
}
// search within an extent
// use MoveNextArray retrieving coordinates, fileIndex and pointIds
ArcGIS.Core.Data.Analyst3D.LasPointFilter pointFilter = new ArcGIS.Core.Data.Analyst3D.LasPointFilter();
pointFilter.FilterGeometry = envelope;
using (ArcGIS.Core.Data.Analyst3D.LasPointCursor ptCursor = lasDataset.SearchPoints(pointFilter))
{
int count;
Coordinate3D[] lasPointsRetrieved = new Coordinate3D[50000];
int[] fileIndexes = new int[50000];
double[] pointIds = new double[50000];
while (ptCursor.MoveNextArray(lasPointsRetrieved, null, fileIndexes, pointIds, out count))
{
var points = lasPointsRetrieved.ToList();
}
}