Wednesday, April 2, 2008

Consuming FalconView DTED in the GIS Editor

The FalconView 4.2 ArcGIS Editor will have the capability of pulling DTED from FalconView into an ESRI ArcMap document so that users can perform geoprocessing tasks with the terrain data. In this blog posting, I will examine this capability that will be in FalconView 4.2 and will give an overview of how this capability was implemented technically. The reader will learn how to retrieve coverage data from the FalconView map data manager and how to create a feature class from this coverage data.

Users add DTED tiles to their FalconView coverage database through the FalconView map data manager. Once the DTED is loaded into FalconView, users can draw elevation data visually, use elevation data in route planning, calculate terrain masking of radar systems, and so on. While FalconView has numerous uses for terrain elevation data, ArcGIS has a generalized geoprocessing framework which allows GIS intelligence officers to perform a wide variety of specialized calculations for their mission planning. The integration of FalconView and ArcGIS will provide a simplified framework for mission planning which requires geoprocessing with terrain data.

The screen capture to the left shows FalconView drawing DTED two ways (the DTED view is zoomed out to 25% to remove detail so that the screen capture is suitable for posting to a public forum.) The black and white background map is the normal DTED base map drawn by FalconView. The blue and white tile in the center is a mosaic of four DTED tiles being drawn by the GIS Editor. The FalconView base map is optimized for a viewing experience that shows the general layout of the terrain. The GIS Editor splotch in the center of the map is optimized for a viewing experience that highlights the high and low areas of the terrain.

For GIS Editor users, creating a mosaic of FalconView DTED is a two step process. First, the user creates a vector overlay of polygons which describe the DTED coverage available from FalconView. Next, the user selects the DTED tiles he wishes to turn into a mosaic and creates the mosaic. The coverage overlay and the mosaic are both automatically created by the GIS Editor and are stored in temporary geodatabases. The screen capture to the right shows the tiles of DTED available on my development system. Notice the little raster blotch in the Eastern US. This is a mosaic of four DTED tiles. (It may be necessary to click on the image to the right to see the detail of the overlay.)

Let's explore the technical steps necessary to bring this DTED from FalconView into ArcGIS. (In this blog post we will not examine the steps used to create the DTED mosaic, though we may explore that in a follow-up post.)

1. Creating an ArcGIS feature class to contain the FalconView DTED

ArcGIS always holds feature classes in some sort of geodatabase. In the case of the FalconView DTED, we load the coverage data into a scratch file geodatabase.
// Create the output workspaces

IWorkspaceFactory pOutWorkspaceFactory = new FileGDBWorkspaceFactory();
IWorkspace pOutWorkspace = pOutWorkspaceFactory.OpenFromFile(TempGeodatabaseUtils.TempFileGeodatabase, hWnd);
IFeatureWorkspace pOutFeatWorkspace = (IFeatureWorkspace)pOutWorkspace;
The temporary file geodatabase referred to above is created during the call to the TempFileGeodatabase property, if it didn't exist before.

After we get the reference to the output feature workspace, we must create the Fields that will be stored with the feature class. The fields we create include a shape field to hold the shape of the polygon, a path field which holds the path to the DTED tile, and a level field which indicates the DTED level (0, 1, 2, 3, etc.).
// Create the fields object which will hold the fields for the feature class. I have put much of the field creation
// logic in its own block to demonstrate that the only object that matters after the fields are built is pFields.

IFields pFields = new FieldsClass();

{
// Set up the polygon shape field for the feature class

IFieldsEdit pFieldsEdit = (IFieldsEdit)pFields;

IField pField = new FieldClass();
IFieldEdit pFieldEdit = (IFieldEdit)pField;
pFieldEdit.Name_2 = "Shape";
pFieldEdit.Type_2 = esriFieldType.esriFieldTypeGeometry;

IGeometryDef pGeometryDef = new GeometryDefClass();
IGeometryDefEdit pGeometryDefEdit = (IGeometryDefEdit)pGeometryDef;
pGeometryDefEdit.GeometryType_2 = esriGeometryType.esriGeometryPolygon;

ISpatialReferenceFactory2 pSpaRefFact2 = new SpatialReferenceEnvironmentClass();
IGeographicCoordinateSystem pGeoCoordSys = pSpaRefFact2.CreateGeographicCoordinateSystem(4326); // 4326 is esriSRGeoCS_WGS1984
ISpatialReference pSpatialReference = (ISpatialReference)pGeoCoordSys;

// Set the domain on the coordinate system
// This prevents the error, "The XY domain on the spatial reference is not set or invalid."

ISpatialReferenceResolution pSpatialReferenceResolution = (ISpatialReferenceResolution)pSpatialReference;
pSpatialReferenceResolution.ConstructFromHorizon();

pGeometryDefEdit.SpatialReference_2 = pSpatialReference;
pFieldEdit.GeometryDef_2 = pGeometryDef;
pFieldsEdit.AddField(pField);

// Add the Path field

pField = new FieldClass();
pFieldEdit = pField as IFieldEdit;
pFieldEdit.Name_2 = "Path";
pFieldEdit.Type_2 = esriFieldType.esriFieldTypeString;
pFieldsEdit.AddField(pField);

// Add the Level field

pField = new FieldClass();
pFieldEdit = pField as IFieldEdit;
pFieldEdit.Name_2 = "Level";
pFieldEdit.Type_2 = esriFieldType.esriFieldTypeInteger;
pFieldsEdit.AddField(pField);

// Add other required fields to the feature class

IObjectClassDescription pObjectClassDescription = new FeatureClassDescriptionClass();

for (int i = 0; i < pObjectClassDescription.RequiredFields.FieldCount; i++)
{
pField = pObjectClassDescription.RequiredFields.get_Field(i);
if (pFieldsEdit.FindField(pField.Name) == -1)
pFieldsEdit.AddField(pField);
}
}
Note the loop an the end of this block of code that ensures all required fields are added to the feature class. When creating a feature class from scratch, it is important to add all of the required fields to the class. In a debug session, I see this block of code skips adding the SHAPE field, which we manually created at the beginning of the block, but adds the required OBJECTID field. The image below is a snapshot of the attributes table of the new feature class as seen in the FalconView GIS Editor. If you click on the image, you will see the fields we added to the feature class, along with some of the data which we will add in the next step.
The next step is to create the feature class, which is relatively simple. The code below also shows how we set up some references we will use to add features to the feature class.
// Create the feature class

string sFeatureClassName = "dted" + Guid.NewGuid().ToString("N");
IFeatureClass pFeatureClass = pOutFeatWorkspace.CreateFeatureClass(
sFeatureClassName, pFields, null, null, esriFeatureType.esriFTSimple, "Shape", null);

// Get an insert cursor and a feature buffer

IFeatureCursor pFeatureCursor = pFeatureClass.Insert(true);
IFeatureBuffer pFeatureBuffer = pFeatureClass.CreateFeatureBuffer();

// Get the indicies for the fields of interest

int iPathFieldIndex = pFeatureBuffer.Fields.FindField("Path");
int iLevelFieldIndex = pFeatureBuffer.Fields.FindField("Level");
Note that creating a feature class and adding fields to the class can also be done using several geoprocessing objects, but, once you understand the steps above, it's simpler to do it as described above.

2. Pulling DTED coverage tiles from the FalconView Map Data Manager

The block of code below shows how to enumerate all FalconView map coverage, creating polygons from DTED tiles and adding them to our feature class. Note that there are three nested loops here. The outer loop enumerates over all of the data sources in the FalconView coverage database. The next inner loop enumerates over all of the DTED levels on the data source under consideration (we're only enumerating levels 1 through 3 for now). The innermost loop enumerates over all of the tiles of the particular DTED level on the particular data source. A polygon feature is created for each tile of DTED enumerated in the innermost loop and is added to the feature class using the insert cursor.
// Initialize the coverage rowset

ICoverageRowset coverageRowset = new CoverageRowsetClass();
coverageRowset.Initialize("Dted");

// Enumerate all data sources, cataloging DTED

const int FLUSH_INTERVAL = 100; // Experiment with this value as needed
int iRowsProcessed = 0;
Dictionary<string, object> dictFoldersAdded = new Dictionary<string, object>();

IDataSourcesRowset dataSourcesRowset = new DataSourcesRowsetClass();
dataSourcesRowset.SelectAll(1 /* Online Only */);
while (dataSourcesRowset.m_Identity > 0)
{
string sLocalFolderName = dataSourcesRowset.m_LocalFolderName;

// Enumerate all DTED on this data source

for (int iDtedLevel = 1; iDtedLevel <= 3; iDtedLevel++)
{
coverageRowset.SelectByGeoRectAndDS(dataSourcesRowset.m_Identity, iDtedLevel, -90, -180, 90, 180);

while (coverageRowset.m_LocationSpec != "")
{
// Get the path to this DTED tile and skip it if it's already added

string sPath = sLocalFolderName + '\' + coverageRowset.m_LocationSpec;

if (dictFoldersAdded.ContainsKey(sPath.ToLower()))
goto next_row; // Sometimes can happen when map data server acts up
dictFoldersAdded[sPath.ToLower()] = null;

// Get the bounds of this DTED tile

double dllLat, dllLon, durLat, durLon;
coverageRowset.GetBounds(out dllLat, out dllLon, out durLat, out durLon);

// Create a polygon from the coverage rectangle

IPolygon pPolygon = new PolygonClass();
IPoint pPoint = new PointClass();
IPointCollection pPointCollection = pPolygon as IPointCollection;
object missing = Type.Missing;

pPoint.X = dllLon;
pPoint.Y = dllLat;
pPolygon.FromPoint = pPoint;
pPoint.X = durLon;
pPointCollection.AddPoint(pPoint, ref missing, ref missing);
pPoint.Y = durLat;
pPointCollection.AddPoint(pPoint, ref missing, ref missing);
pPoint.X = dllLon;
pPointCollection.AddPoint(pPoint, ref missing, ref missing);
pPolygon.Close();

// Add the polygon to the feature class

pFeatureBuffer.Shape = pPolygon;
pFeatureBuffer.set_Value(iPathFieldIndex, sPath);
pFeatureBuffer.set_Value(iLevelFieldIndex, iDtedLevel);
pFeatureCursor.InsertFeature(pFeatureBuffer);

// Flush the data from time to time

if (++iRowsProcessed % FLUSH_INTERVAL == 0) pFeatureCursor.Flush();

// Move to the next DTED coverage rowset

next_row: coverageRowset.MoveNext();
}
}

// Move to the next FalconView data source

dataSourcesRowset.MoveNext();
}
The feature cursor is flushed from time to time during its population and again after all of the DTED tiles have been added to the feature class. It's important after this step to manually release all of your COM objects so that the lock on the feature class in the geodatabase is removed. The last step is to create a feature layer from the feature class. (All of these steps are shown below.) The feature layer may be directly added to the map.
// Flush the feature cursor and release COM objects

pFeatureCursor.Flush();

Marshal.ReleaseComObject(pFeatureBuffer);
Marshal.ReleaseComObject(pFeatureCursor);
Marshal.ReleaseComObject(pFeatureClass);
Marshal.ReleaseComObject(pOutFeatWorkspace);
Marshal.ReleaseComObject(pOutWorkspace);
Marshal.ReleaseComObject(pOutWorkspaceFactory);

// Return a DTED feature layer

IWorkspaceFactory pWorkspaceFactory = new FileGDBWorkspaceFactoryClass();
IWorkspace pWorkspace = pWorkspaceFactory.OpenFromFile(TempGeodatabaseUtils.TempFileGeodatabase, hWnd);
IFeatureWorkspace pFeatureWorkspace = (IFeatureWorkspace)pWorkspace;
pFeatureClass = pFeatureWorkspace.OpenFeatureClass(sFeatureClassName);

IFeatureLayer pFeatureLayer = new FeatureLayerClass();
pFeatureLayer.Name = FV_DTED_LAYER_NAME;
pFeatureLayer.FeatureClass = pFeatureClass;

1 comment:

Erik said...

That pesky having to set your spatialreference to default had me tied to my desk for a while. Thansk for providing code to show how to get around it.
A filegeodatabase will just give you the following error -2147024894 [GDB_SystemCatalog] not something helpful like The XY domain not set or invalid.