Individual Flow Direction Tool
clsFlowDirTool.cls

' Copyright 2008 ESRI
' 
' All rights reserved under the copyright laws of the United States
' and applicable international laws, treaties, and conventions.
' 
' You may freely redistribute and use this sample code, with or
' without modification, provided you include the original copyright
' notice and use restrictions.
' 
' See use restrictions at <your ArcGIS install location>/developerkit/userestrictions.txt.
' 




'The Individual Flow Direction tool allows the user to look up and set the
'flow direction for an individual edge element in the current geometric network.
'In order to be able to set the flow direction, an edit session is required.

Option Explicit

Implements ICommand
Implements ITool

'declare a module-level variable to hold a reference to the ArcMap application
Private m_pApplication As esriFramework.IApplication
'declare a module-level variable to hold a reference to the Utility Network
'Analysis extension
Private m_pUtilityNetworkAnalysisExt As esriEditorExt.IUtilityNetworkAnalysisExt
'declare a module-level variable to hold a reference to the Editor extension
Private m_pEditor As esriEditor.IEditor

'declare a module-level variable to hold a reference to the current geometric network
Private m_pGeometricNetwork As esriGeoDatabase.IGeometricNetwork
'declare a module-level variable to hold the EID that was clicked
Private m_lEID As Long
'declare a module-level variable to hold the geometry of the EID that was clicked
Private m_pGeometry As esriGeometry.IGeometry
'declare a module-level variable to hold the line symbol for drawing
Private m_pLineSymbol As esriDisplay.ILineSymbol

'declare module-level variables to hold references to pictures
Private m_pBitmap As IPictureDisp
Private m_pCursor As IPictureDisp

'declare a module-level variable to handle events from the active view
Private WithEvents m_pActiveViewEvents As esriCarto.Map

'declare module-level variables to handle events from the form
Private WithEvents frmFlowDirToolDialogEvents As Form
Private WithEvents cmdOKButtonEvents As CommandButton

Private Sub Class_Initialize()
  Dim pRgbColor As IRgbColor
  Dim pLineSymbol As ISimpleLineSymbol
  
  'make a blue color object
  Set pRgbColor = New RgbColor
  With pRgbColor
    .Red = 0
    .Green = 0
    .Blue = 255
  End With
  
  'create a new simple line symbol that is blue and has width 4
  Set pLineSymbol = New SimpleLineSymbol
  pLineSymbol.Color = pRgbColor
  pLineSymbol.Width = 4
  
  'set it to the module-level variable
  Set m_pLineSymbol = pLineSymbol
  
  'load the bitmap and cursor images into the module variables
  Set m_pBitmap = LoadResPicture(101, 0)
  Set m_pCursor = LoadResPicture(102, 2)
End Sub

Private Sub Class_Terminate()
  'release the form and active view event variables upon termination of this command
  Set frmFlowDirToolDialogEvents = Nothing
  Set cmdOKButtonEvents = Nothing
  Set m_pActiveViewEvents = Nothing
End Sub

Private Property Get ICommand_Bitmap() As esriSystem.OLE_HANDLE
  ICommand_Bitmap = m_pBitmap
End Property

Private Property Get ICommand_Caption() As String
  ICommand_Caption = "Individual Flow Direction Tool"
End Property

Private Property Get ICommand_Category() As String
  ICommand_Category = "Developer Samples"
End Property

Private Property Get ICommand_Checked() As Boolean
  ICommand_Checked = False
End Property

Private Property Get ICommand_Enabled() As Boolean
'Enable the tool if we are in Data View and the network extension has a network

  Dim pMxDoc As IMxDocument
  Dim pViewManager As IViewManager
  Dim pNetworkAnalysisExt As INetworkAnalysisExt
  
  ICommand_Enabled = False
  
  'if the current active view uses page coordinates, then we are in Layout View
  'do not enable the tool
  Set pMxDoc = m_pApplication.Document
  Set pViewManager = pMxDoc.ActiveView
  If pViewManager.UsesPageCoordinates Then Exit Sub
  
  'if the focus map has at least one network, then enable the tool
  Set pNetworkAnalysisExt = m_pUtilityNetworkAnalysisExt
  If pNetworkAnalysisExt.NetworkCount > 0 Then
    ICommand_Enabled = True
  End If

End Property

Private Property Get ICommand_HelpContextID() As Long

End Property

Private Property Get ICommand_HelpFile() As String

End Property

Private Property Get ICommand_Message() As String
  ICommand_Message = "Get and set the flow direction of an individual edge element."
End Property

Private Property Get ICommand_Name() As String
  ICommand_Name = "IFDTool.clsFlowDirTool"
End Property

Private Sub ICommand_OnClick()

End Sub

Private Sub ICommand_OnCreate(ByVal hook As Object)
  Dim pUID_UNA As New esriSystem.UID
  Dim pUID_Editor As New esriSystem.UID
  
  'set a reference to the application
  Set m_pApplication = hook
  'obtain a reference to the Utility Network Analysis extension
  pUID_UNA = "esriEditorExt.UtilityNetworkAnalysisExt"
  Set m_pUtilityNetworkAnalysisExt = m_pApplication.FindExtensionByCLSID(pUID_UNA)
  'obtain a reference to the Editor extension
  pUID_Editor = "esriEditor.Editor"
  Set m_pEditor = m_pApplication.FindExtensionByCLSID(pUID_Editor)
End Sub

Private Property Get ICommand_Tooltip() As String
  ICommand_Tooltip = "Get and set the flow direction of an individual edge element."
End Property

Private Property Get ITool_Cursor() As esriSystem.OLE_HANDLE
    ITool_Cursor = m_pCursor
End Property

Private Function ITool_Deactivate() As Boolean
  ITool_Deactivate = True
End Function

Private Function ITool_OnContextMenu(ByVal X As Long, ByVal Y As Long) As Boolean

End Function

Private Sub ITool_OnDblClick()

End Sub

Private Sub ITool_OnKeyDown(ByVal KeyCode As Long, ByVal Shift As Long)

End Sub

Private Sub ITool_OnKeyUp(ByVal KeyCode As Long, ByVal Shift As Long)

End Sub

Private Sub ITool_OnMouseDown(ByVal Button As Long, ByVal Shift As Long, ByVal X As Long, ByVal Y As Long)
  Dim pMxDoc As IMxDocument
  Dim pMap As IMap
  Dim pNetworkAnalysisExt As INetworkAnalysisExt
  Dim lSnapTolerance As Long
  Dim dSearchRadius As Double
  Dim pPoint As IPoint
  Dim pPointToEID As IPointToEID
  Dim pLoc As IPoint
  Dim dPercent As Double
  Dim pNetElements As INetElements
  Dim pEnumNetEIDBuilder As IEnumNetEIDBuilder
  Dim pEnumNetEID As IEnumNetEID
  Dim pActiveView As IActiveView
  Dim pEIDHelper As IEIDHelper
  Dim pEnumEIDInfo As IEnumEIDInfo
  Dim lEIDInfoCount As Long
  Dim pEIDInfo As IEIDInfo
  
  On Error GoTo ErrorHandler
  
  'initialize the form event variables
  Set frmFlowDirToolDialogEvents = frmFlowDirToolDialog
  Set cmdOKButtonEvents = frmFlowDirToolDialog.OKButton
  
  'initialize the active view event variable
  Set pMxDoc = m_pApplication.Document
  Set pMap = pMxDoc.FocusMap
  Set m_pActiveViewEvents = pMap
  
  'get parameters from the Utility Network Analysis Extension
  Set pNetworkAnalysisExt = m_pUtilityNetworkAnalysisExt
  Set m_pGeometricNetwork = pNetworkAnalysisExt.CurrentNetwork
  lSnapTolerance = pNetworkAnalysisExt.SnapTolerance
  
  'if there is no geometric network, display an error
  If m_pGeometricNetwork Is Nothing Then
    MsgBox "There is no current geometric network."
    Exit Sub
  End If
  
  'convert the X,Y coordinate to a point in the map
  Set pPoint = pMxDoc.ActiveView.ScreenDisplay.DisplayTransformation.ToMapPoint(X, Y)
  
  'convert the snap tolerance (in pixels) to a search radius in map units
  Set pActiveView = pMxDoc.ActiveView
  dSearchRadius = Pixels2MapUnits(lSnapTolerance, pActiveView)
  
  'find the nearest edge element to the point
  Set pPointToEID = New PointToEID
  Set pPointToEID.GeometricNetwork = m_pGeometricNetwork
  Set pPointToEID.SourceMap = pMap
  pPointToEID.SnapTolerance = dSearchRadius
  pPointToEID.GetNearestEdge pPoint, m_lEID, pLoc, dPercent
  
  Set pNetElements = m_pGeometricNetwork.Network
  If Not pNetElements.IsValidElement(m_lEID, esriETEdge) Then
    'beep and display an error message in ArcMap's status bar
    Beep
    m_pApplication.StatusBar.Message(esriStatusMain) = _
                                    "No edge feature found at this location"
    Exit Sub
  End If
  
  'create a new EnumNetEIDArray object and add m_lEID to it
  Set pEnumNetEIDBuilder = New EnumNetEIDArray
  Set pEnumNetEIDBuilder.Network = m_pGeometricNetwork.Network
  pEnumNetEIDBuilder.ElementType = esriETEdge
  pEnumNetEIDBuilder.Add m_lEID
  Set pEnumNetEID = pEnumNetEIDBuilder
  
  'create a new EIDHelper object and set its parameters to return only the geometries
  Set pEIDHelper = New EIDHelper
  Set pEIDHelper.GeometricNetwork = m_pGeometricNetwork
  Set pEIDHelper.DisplayEnvelope = pActiveView.Extent
  Set pEIDHelper.OutputSpatialReference = pActiveView.FocusMap.SpatialReference
  pEIDHelper.PartialComplexEdgeGeometry = True
  pEIDHelper.ReturnGeometries = True
  pEIDHelper.ReturnFeatures = False
  
  'create the EIDInfo enumeration with the geometries and get its count
  Set pEnumEIDInfo = pEIDHelper.CreateEnumEIDInfo(pEnumNetEID)
  lEIDInfoCount = pEnumEIDInfo.Count
  
  'if there is more than 1 EIDInfo object, then there is an error!
  If lEIDInfoCount <> 1 Then
    MsgBox "Error: " & lEIDInfoCount & " geometries were returned by the EIDHelper!"
    Exit Sub
  End If
  
  'get the geometry and set it to the module variable
  pEnumEIDInfo.Reset
  Set pEIDInfo = pEnumEIDInfo.Next
  If pEIDInfo Is Nothing Then
    MsgBox "Error: EIDInfo Enumeration is empty!"
    Exit Sub
  End If
  If pEIDInfo.Geometry Is Nothing Then
    MsgBox "Error: The geometry is empty!"
    Exit Sub
  End If
  Set m_pGeometry = pEIDInfo.Geometry
  
  'call partial refresh on the graphics to draw the geometry to the screen
  MyPartialRefresh pActiveView, m_pGeometry
  
  'show the form
  frmFlowDirToolDialog.Show vbModal
  Exit Sub
ErrorHandler:
  MsgBox "Error # " & Str(Err.Number) & " was generated by " _
         & Err.Source & Chr(13) & Err.Description
End Sub

Private Sub ITool_OnMouseMove(ByVal Button As Long, ByVal Shift As Long, ByVal X As Long, ByVal Y As Long)

End Sub

Private Sub ITool_OnMouseUp(ByVal Button As Long, ByVal Shift As Long, ByVal X As Long, ByVal Y As Long)

End Sub

Private Sub ITool_Refresh(ByVal hDc As esriSystem.OLE_HANDLE)

End Sub

Private Sub frmFlowDirToolDialogEvents_Load()
  Dim pNetElements As INetElements
  Dim lFCID As Long, lFID As Long, lSubID As Long
  Dim pFCContainer As IFeatureClassContainer
  Dim pFeatureClass As IFeatureClass
  Dim pDataset As IDataset
  Dim sFCName As String
  Dim pUtilityNetwork As IUtilityNetwork
  
  'get the Feature Class ID, Feature ID, and Sub ID for m_lEID
  Set pNetElements = m_pGeometricNetwork.Network
  pNetElements.QueryIDs m_lEID, esriETEdge, lFCID, lFID, lSubID
  
  'get the name of the feature class for this FCID
  Set pFCContainer = m_pGeometricNetwork
  Set pFeatureClass = pFCContainer.ClassByID(lFCID)
  Set pDataset = pFeatureClass
  sFCName = pDataset.Name
  
  With frmFlowDirToolDialog
    'populate the text labels on the form
    .lblFCNameData = sFCName
    .lblFCIDData = CStr(lFCID)
    .lblFIDData = CStr(lFID)
    .lblSubIDData = CStr(lSubID)
    .lblEIDData = CStr(m_lEID)

    'look up the current flow for m_lEID and click the appropriate option button
    Set pUtilityNetwork = m_pGeometricNetwork.Network
    .ClickFlowDirButton pUtilityNetwork.GetFlowDirection(m_lEID)
   
    'if we are not in an edit session for this map then do not enable radio buttons
    If m_pEditor.EditState <> esriStateEditing Then
      .SetFlowButtonsEnabled False
    End If

    'disable the OK button, since no edits have been made yet
    .OKButton.Enabled = False
  End With
End Sub

Private Sub frmFlowDirToolDialogEvents_Unload(Cancel As Integer)
  Dim pGeometry As IGeometry
  Dim pMxDoc As IMxDocument
  
  'make a copy of the EID's geometry before clearing the module-level variable
  Set pGeometry = m_pGeometry
  Set m_pGeometry = Nothing

  'call partial refresh on the graphics to clear the geometry from the screen
  Set pMxDoc = m_pApplication.Document
  MyPartialRefresh pMxDoc.ActiveView, pGeometry
End Sub

Private Sub cmdOKButtonEvents_Click()
  'set the flow direction to the network
  SetFlowDirection
  
  'close the form
  Unload frmFlowDirToolDialog
End Sub

'when the after draw event is fired for the esriViewGraphics phase,
'draw the edge element's geometry (m_pGeometry) using the blue line symbol (m_pLineSymbol)
Private Sub m_pActiveViewEvents_AfterDraw(ByVal Display As IDisplay, ByVal phase As esriViewDrawPhase)
  If phase = esriViewGraphics Then
    Display.StartDrawing 0, esriNoScreenCache
    Display.SetSymbol m_pLineSymbol
    If Not m_pGeometry Is Nothing Then
      Display.DrawPolyline m_pGeometry
    End If
    Display.FinishDrawing
  End If
End Sub

'performs a partial refresh that expands m_pGeometry's envelope to take into account
'the line symbol width and (if displayed) the flow direction symbol size
Private Sub MyPartialRefresh(pActiveView As IActiveView, pGeometry As IGeometry)
  Dim pEnvelope As IEnvelope
  Dim dLineExp As Double
  Dim dXExp As Double, dYExp As Double
  Dim pUNAExtFlow As IUtilityNetworkAnalysisExtFlow
  Dim dCurrentSymbolSize As Double, dMaxSymbolSize As Double
  Dim dMaxSymbolSizeInMapUnits As Double
  
  'get a copy of the geometry's envelope
  Set pEnvelope = pGeometry.Envelope
  
  'find out how much we need to expand the envelope
  'find the expansion needed to take into account the width of the line symbol
  dLineExp = pActiveView.ScreenDisplay.DisplayTransformation.FromPoints(m_pLineSymbol.Width)
  dXExp = dLineExp
  dYExp = dLineExp
  
  'get a reference to the Utility Network Analysis Extension and QI for the
  'IUtilityNetworkAnalysisExtFlow interface
  Set pUNAExtFlow = m_pUtilityNetworkAnalysisExt
  
  'if flow direction indicators are being drawn in the map, then
  If pUNAExtFlow.ShowFlow Then
    'find the largest symbol size used for indicating flow direction
    dMaxSymbolSize = pUNAExtFlow.ArrowSymbol.Size
    dCurrentSymbolSize = pUNAExtFlow.IndeterminateSymbol.Size
    If dCurrentSymbolSize > dMaxSymbolSize Then dMaxSymbolSize = dCurrentSymbolSize
    dCurrentSymbolSize = pUNAExtFlow.UninitializedSymbol.Size
    If dCurrentSymbolSize > dMaxSymbolSize Then dMaxSymbolSize = dCurrentSymbolSize
  
    'find out the size of the largest symbol in map units for the current display
    dMaxSymbolSizeInMapUnits = pActiveView.ScreenDisplay.DisplayTransformation.FromPoints(dMaxSymbolSize)
  
    'if the envelope size after taking into account the width of line symbol is still
    'too small to enclose the flow direction symbol, adjust the expansion values
    If (pEnvelope.Width + dLineExp) < dMaxSymbolSizeInMapUnits Then
      dXExp = dMaxSymbolSizeInMapUnits - pEnvelope.Width
    End If
    If (pEnvelope.Height + dLineExp) < dMaxSymbolSizeInMapUnits Then
      dYExp = dMaxSymbolSizeInMapUnits - pEnvelope.Height
    End If
  End If
  
  'expand the envelope
  pEnvelope.Expand dXExp, dYExp, False

  'refresh the graphics layer to redraw the edge element
  pActiveView.PartialRefresh esriViewGraphics, Nothing, pEnvelope
End Sub

'converts the specified pixel distance into a map unit distance
Private Function Pixels2MapUnits(lPixelDistance As Long, pActiveView As IActiveView) As Double
  Dim pDT As IDisplayTransformation
  Dim deviceRECT As tagRECT
  Dim lExtentInPixels As Long
  Dim pEnvelope As IEnvelope
  Dim dExtentInMapUnits As Double
  Dim dOnePixelInMapUnits As Double
  
  Set pDT = pActiveView.ScreenDisplay.DisplayTransformation
  
  'find the width of the device frame in pixels
  deviceRECT = pDT.DeviceFrame
  lExtentInPixels = deviceRECT.Right - deviceRECT.Left
  
  'find the width of the device frame in map units
  Set pEnvelope = pDT.VisibleBounds
  dExtentInMapUnits = pEnvelope.Width
  
  'calculate what one pixel is in map units
  dOnePixelInMapUnits = dExtentInMapUnits / lExtentInPixels
  
  'multiply by the pixel distance to find the distance in map units
  Pixels2MapUnits = lPixelDistance * dOnePixelInMapUnits
End Function

'sets the flow direction for the m_lEID based on the button pressed on the form
'NOTE: this Sub does not refresh the display (refresh occurs when the form is Unloaded)
Private Sub SetFlowDirection()
  Dim pUtilityNetwork As esriGeoDatabase.IUtilityNetwork
  
  'change the mouse pointer to an hourglass during setting flow direction and redraw
  frmFlowDirToolDialog.MousePointer = vbHourglass
  
  'create an edit operation enabling an undo for this operation
  m_pEditor.StartOperation
  
  'copy the flow direction info from the option button to the network
  Set pUtilityNetwork = m_pGeometricNetwork.Network
  pUtilityNetwork.SetFlowDirection m_lEID, frmFlowDirToolDialog.getFlowDirButton
  
  'stop the edit operation and specify the name of this edit operation
  m_pEditor.StopOperation "Set Flow Direction"
  
  'change the mouse pointer back to default
  frmFlowDirToolDialog.MousePointer = vbDefault
End Sub