Using Visual Basic ActiveX components to customize a new tool on ArcMap

Yasir H. Kaheil, Yasir@cc.usu.edu

Background

Introduction

Processing

Example

References

Background (TOP)

OOP (Object Oriented Programming) is just like real life, it allows developers to mimic real world items and concepts. In an OOP world, there are objects, these objects know what information is used to describe them and what actions they can perform using this information. OOP allows developers to create reusable objects that interact with each other to form complex entities. COM (Microsoft Component Object Model) COM (and component-oriented design in general) was created with object-oriented programming in mind. This is a methodology that promotes thinking about software 'objects' and the way those objects interact with each other, rather than their implementation. COM is the language that a developer may use to link two software components with each other or to create new ones or to use some software components (that already exist) to develop or “customize” a new tool on the same software program. The latter is what I did in this project. COM is mainly used to link programs. Some developers refer to COM as the glue for Microsoft applications which COM really is. When it comes to COM, there are so many terms out there, which may confuse most of the people, like Components. Components are pre compiled interacting pieces of software acting like building blocks. The same component could be used on different applications (if you could provide the same functionality). Other terms like Interfaces, the interfaces of a component are the mechanism by which its functionality can be used by another component. The precise structure of an interface is defined by COM, but in essence it's just a list of functions implemented by the component that can be called by other pieces of code. That’s why when you work with some object model (in my case, ArcGISTM 8.2 Raster Object Model) understanding this object model is everything you need.

 

Introduction (TOP)

In most of the raster based models, a mask file is used to determine the area where the model applies. The mask is an array that –usually- gives zero value where the model doesn't apply and one value where it does[1]. But, in case we are able to clip the exact piece of the raster layer, then there will be no reason to use these mask files anymore.  The new tool that I have developed, clips any raster layer with selected polygons on a polygon-feature layer overlaying this raster layer with the same projection. User might select any number of polygons and then just click the ClipRasterNew button and he will get the piece (or the pieces) of the raster layer for these polygons.  Polygon feature layers-in water applications- are usually different kinds of HUC coverages (6, 8, 10, and 12 digit HUCs) which were developed from the raster layer in the first place. So using these layers to clip raster layers (which are usually DEMs) will guarantee that there is no edge water contamination - which means that some of the cells on the edge of the raster layer belong to different catchment areas.

 


Objective (TOP)

The objective of this work is just to make life easier for those who work with big-sized grid layers and they don’t have to manipulate the whole area. With this tool, they can clip the area that they want and work on it. That of course means that the main objective is to clip a raster with selected objects on a feature layer.

Another objective is to print out the pixel block of this clipping area on a CVS file –TXT option is available too- which allows manipulating this data on Microsoft Excel if there was a need to do so.

Processing (TOP)

Outline

The procedure can be outlined in 4 main steps

1.      Defining the extent of the selected features which will be the extent of the output layer and saving the object IDs of the selected features in an array (which will be used later).

2.      Reading the safe array of the original raster layer in this extent (with an option of saving this block of the safe array.)

3.      Transforming the polygon feature layer into a raster layer. pixel values in this raster layer will be the same as the containing polygon object IDs.

4.      Creating a new raster dataset (which will be the output) with the defined extent. the pixel values in this raster will be the same as those in the original raster if the correspondent pixel value of the transformed layer (step 3) is equal to any value in the object ID array (step 1) else the pixel value of the output raster will be NoData (transparent)

Step 1 (TOP)

The following block of the code defines the selected features and then checks if there is no selected features, if this condition was true then the program will give a message box that tells the user what to do, then (Release memory) and exit and the user have to start it again with some selected features.

Set pEnumFeat = pMap.FeatureSelection
Set pFeatpol = pEnumFeat.Next
If pFeatpol Is Nothing Then 'If there is no selected features
MsgBox ("Please select at least one polygon feature")
GoTo RlsMm
Exit Function
End If

The following block sets the initial extent to be the extent of the first selected feature, then loops through the other selected features and keep on redefining the attribute values of the extent. That means in different words if you have less value than the minimum X value of the initial extent, then this should be the new minimum X value of the extent, and so forth. The object IDs of these features are saved in an array too for later use (Step 3)

Set pfExtent = pFeatpol.Extent
Dim NumObj As Integer
NumObj = 0
'loop thru selected features and redefine the extent accordingly
While Not pFeatpol Is Nothing
    With pFeatpol.Extent
    If .XMax > pfExtent.XMax Then pfExtent.XMax = .XMax
    If .XMin < pfExtent.XMin Then pfExtent.XMin = .XMin
    If .YMax > pfExtent.YMax Then pfExtent.YMax = .YMax
    If .YMin < pfExtent.YMin Then pfExtent.YMin = .YMin
    End With
    objID(NumObj) = pFeatpol.OID
    NumObj = NumObj + 1 'number of selected objects
    Set pFeatpol = pEnumFeat.Next
Wend

Step 2 (TOP)

The following block of code includes some calculations on the original grid to get the cell size as well as the number of cells will be in the new raster (m,n). to read the pixel array you need to start from the top left corner which is NOT defined by the spatial reference. The top left corner is a double point (g,f) g is the number cells from Xmin of the new grid layer to Xmin of the original grid layer, f is the number of cells from Ymax of the new grid layer to Ymax of the original grid layer. With some variable declarations, the safe array of the pixel block at the area of the interest (the extent of the selected polygons) could be gotten.

I have to mention the Redefinition of the Extent of the new grid layer which gives exact pixel alignment between the new and the original grid layers. This alignment is very important to get the wanted pixels only.

 

 

'Create a DblPnt to hold the PixelBlock size
Dim pSize As IPnt
Set pSize = New DblPnt

Dim CellSizeX, CellSizeY As Double
CellSizeX = pRasterProp.MeanCellSize.X
CellSizeY = pRasterProp.MeanCellSize.Y

Dim m, n As Long
m = CLng((pfExtent.XMax - pfExtent.XMin + 1) / CellSizeX)
n = CLng((pfExtent.YMax - pfExtent.YMin + 1) / CellSizeY)
pSize.SetCoords m, n

Dim pTopLeftCrn As IPnt
Set pTopLeftCrn = New DblPnt
Dim g, f As Long
g = (pfExtent.XMin - pRasterProp.Extent.XMin) / CellSizeX
g = Abs(CLng(g))
f = (pfExtent.YMax - pRasterProp.Extent.YMax) / CellSizeY
f = Abs(CLng(f))
'Redefine the Extent to guarantee exact alignment with the original layer
pfExtent.XMax = CDbl(pRasterProp.Extent.XMin) + CDbl(g + m) * CellSizeX
pfExtent.YMin = CDbl(pRasterProp.Extent.YMax) - CDbl(f + n) * CellSizeY
pfExtent.XMin = CDbl(pRasterProp.Extent.XMin) + CDbl(g) * CellSizeX
pfExtent.YMax = CDbl(pRasterProp.Extent.YMax) - CDbl(f) * CellSizeY

pTopLeftCrn.SetCoords g, f
Dim pBlock As IPixelBlock
'pRawPixel.Read pTopLeftCrn, pBlock
'Set pBlock = pRawPixel.CreatePixelBlock(pSize)
Set pBlock = pRasterNew.CreatePixelBlock(pSize)
Dim pRawPixel As IRawPixels
Set pRawPixel = pBand
pRawPixel.Read pTopLeftCrn, pBlock
'pRasterNew.Read pTopLeftCrn, pBlock
Dim pOrigSafeArray As Variant
pOrigSafeArray = pBlock.SafeArray(0)

 

Step 3 (TOP)

Now we have to convert this feature layer into a grid (raster) layer. And read its pixel block array too. The converted raster layer will have the same extent and the same cell size too (which means the same number of rows and columns in its attributed pixel matrix). The pixel values in this pixel block safe array are the object ID of the correspondent polygon. The convert raster layer array, however, will contain some unwanted polygons (not selected but still in the wanted extent or not selected but parts of it lie in the wanted extent). That’s why we had to save the object IDs of the selected polygons in step one. The array that is saved in step one will tell if there is a pixel value for this pixel or a NoData value, move on to step 4

Dim pWS As IWorkspace
Set pWS = SetRasterWorkspace(sPath)

Dim pEnv As IRasterAnalysisEnvironment
Set pEnv = New RasterAnalysis
Dim pConv As IConversionOp
Set pConv = New RasterConversionOp
Set pEnv = pConv
pEnv.SetCellSize esriRasterEnvValue, CDbl(pRasterProp.MeanCellSize.X)
pEnv.SetExtent esriRasterEnvValue, pfExtent


Dim pTempDS As IGeoDataset
Set pTempDS = pFeatLyr.FeatureClass

'Delete the same file if existed before
Dim FS
Set FS = CreateObject("Scripting.FileSystemObject")
If FS.FileExists(sPath + "\" + "TempCov.img") Then
FS.Deletefile (sPath + "\" + "TempCov.img")
End If

Dim polRDS As IRasterDataset
Set polRDS = New RasterDataset
Set polRDS = pConv.ToRasterDataset(pTempDS, "IMAGINE Image", pWS, "Tempcov.img")
PB.Value = 60
Dim pNewRaster As IRaster
Set pNewRaster = polRDS.CreateDefaultRaster
Dim pNewRasProps As IRasterProps
Set pNewRasProps = pNewRaster

' Get RasterBand from the raster
Dim pNewBand As IRasterBand
Dim pNewBands As IRasterBandCollection
Set pNewBands = pNewRaster
Set pNewBand = pNewBands.Item(0)


' Create a DblPnt to hold the PixelBlock size
Dim pNewSize As IPnt
Set pNewSize = New DblPnt
Dim pOrigin As IPnt
Set pOrigin = New DblPnt
pNewSize.SetCoords pNewRasProps.Width, pNewRasProps.Height
pOrigin.SetCoords 0, 0

'QI RawPixel interface
Dim pRawPixel2 As IRawPixels
Set pRawPixel2 = pNewBand
Dim pBlock2 As IPixelBlock
Set pBlock2 = pNewRaster.CreatePixelBlock(pNewSize)
pRawPixel2.Read pOrigin, pBlock2
Dim pNewArray As Variant
pNewArray = pBlock2.SafeArray(0)

 

Step 4 (TOP)

In this step I’m creating a new raster dataset. For less confusion now here is what we have: matrix A, the associated pixel matrix of the original raster layer, B, the associated pixel matrix of the converted feature layer, this matrix contains the object IDs of the polygon feature layer. and pOutSafeArray the output raster layer associated matrix. The condition that clipping will be based on is for any i, j if B(i,j) is equal to any value in the ObjID array, then that means this pixel should be there in the new raster layer, which means that we have to give it a value which means pOutSafeArray(i,j)=A(i,j).  but what if B(i,j) wasn’t equal to any value in the object ID array, this means that it’s a pixel of the unwanted objects that lied accidentally on the same extent but they are not selected. So pOutSafeArray= NoData

 

Dim pRWS As IRasterWorkspace2
Dim pWSF As IWorkspaceFactory
Set pWSF = New RasterWorkspaceFactory
Set pRWS = pWSF.OpenFromFile(sPath, 0)
Dim OutPutRDS As IRasterDataset
Dim ColCount, RCount As Long
ColCount = m
RCount = n
Dim Spat As ISpatialReference
Set Spat = pNewRasProps.SpatialReference
pNewRasProps.Extent = pfExtent
Dim pOrigin2 As IPoint
Set pOrigin2 = New Point
pOrigin2.X = pfExtent.XMin
pOrigin2.Y = pfExtent.YMin
PB.Value = 80
Set OutPutRDS = pRWS.CreateRasterDataset(sFileName3, "GRID", pOrigin2, ColCount, RCount, _
CellSizeX, CellSizeY, 1, PT_LONG, Spat, True)
PB.Value = 90
' Create a default raster and QI raster properties interface
Dim pOutRaster As IRaster
Set pOutRaster = OutPutRDS.CreateDefaultRaster
Dim pOutBandCol As IRasterBandCollection
Set pOutBandCol = pOutRaster
Dim pOutBand As IRasterBand
Set pOutBand = pOutBandCol.Item(0)
Dim pOutRasProps As IRasterProps
Set pOutRasProps = pOutBand

' QI RawPixel interface
Dim pOutRawPixel As IRawPixels
Set pOutRawPixel = pOutBand

' Create a DblPnt to hold the PixelBlock size
Dim pOutSize As IPnt
Set pOutSize = New DblPnt
pOutSize.SetCoords pOutRasProps.Width, pOutRasProps.Height
'pRasProps.NoDataValue = 0
' Create PixelBlock with defined size
Dim pOutBlock As IPixelBlock
Set pOutBlock = pOutRawPixel.CreatePixelBlock(pOutSize)

' Get the SafeArray associated with the first band
'(It does not work with raster that is 16 bit unsigned integer since VB does not support this data type).
Dim pOutSafeArray As Variant
pOutSafeArray = pOutBlock.SafeArray(0)
'Setting the nodata value to some odd value  for display reasons
pOutRasProps.NoDataValue = -9999

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Dim ii, j, k As Long

For ii = 0 To pNewSize.X - 1
For j = 0 To pNewSize.Y - 1

For k = 0 To NumObj - 1
If B_(ii, j) = CLng(objID(k)) Then
pOutSafeArray(ii, j) = CDbl(A_(ii, j))
GoTo sss:
End If
Next k
pOutSafeArray(ii, j) = CDbl(pOutRasProps.NoDataValue)

sss:
Next j
PB.Value = 90 + 9 * ii / (pNewSize.X - 1)
Next ii
'pOutBlock.SafeArray(0) = pOutSafeArray
pOrigin.SetCoords 0, 0

pOutRawPixel.Write pOrigin, pOutBlock

Dim pRasPyramid As IRasterPyramid
Set pRasPyramid = OutPutRDS
' Create the pyramid
If Not pRasPyramid.Present Then
pRasPyramid.Create
End If
'Add the raster layer
Dim pOutputRasLy As IRasterLayer
Set pOutputRasLy = New RasterLayer
pOutputRasLy.CreateFromDataset OutPutRDS
'pOutputRasLy.Name = "Clip"
pMap.ClearSelection
pMap.AddLayer pOutputRasLy
pMxDoc.ActiveView.Refresh


Example (TOP)

  1. Adding the Dll file (Download DLL file)
  1. Now your ClipRasterNew button is on the menu.  (TOP)

  1. Add a Raster layer and a polygon feature layer to your map (make sure they have the same projection) (TOP)

  1. Select  at least one polygon and click on ClipRasterNew button. A form will appear. make sure that the input raster layer and the input feature layer are the same as you want. if not select them from the combo boxes (TOP)

  1. Click clip.., answer yes if you want to see the safe array of the input raster at selected extent... wait as the tool continues clipping (TOP)
  2. Browse where you want to save your new raster layer and give it a name (make sure its different from all the names that you have in this directory). Click Save. (TOP)

 

  1. You’re done! The new raster layer will automatically be added to the TOC. (TOP)

 

References (TOP)

 

 



[1] Some mask files though, have other values than zeros and ones, like “-1” that gives a third option to do something else. I’m only discussing the two option mask files.