Using Visual Basic ActiveX
components to customize a new tool on ArcMap
Yasir H. Kaheil, Yasir@cc.usu.edu
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.
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.
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.
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)
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
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)
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)
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
[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.