Loop Construct
LoopConstruct.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.
' 




' LoopConstructTool
'
' implements ITool and ICommand
'
' handles all graphic feedback in DrawFeedback - rubber bands a line and
' draws the the snap cursor
'
Option Explicit

Implements ICommand
Implements ITool

Private iPtCount As Integer
Private m_dOffset As Double
Private m_lContructOption As Long
' these are the member variables for our component
'
Dim m_pApp As IApplication
Dim m_pEditor As IEditor            ' our friend, the editor
Dim m_pSketch As IEditSketch        ' a 2nd interface on the editor for working with the edit sketch
Dim m_pDpy As IAppDisplay           ' the application's display, which we draw into frequently
Dim m_pLoc As IPoint                ' the current mouse location at any time
Dim m_pSnapFeature As IFeature
Dim m_pNoSnapLoc As IPoint
Dim m_pFinalPolyline As IPolyline
  
Dim m_pPolyline As IGeometry        ' for feedback only - an optimization to avoid object creation
Dim m_pLine1 As ILine                ' the one segment for m_pPolyline - also an optimization
Dim m_pLine2 As ILine
Dim m_pLine3 As ILine
Dim m_pSym As ISimpleLineSymbol     ' a rubber banding symbol for our line

Private Declare Function GetKeyState Lib "user32" (ByVal nVirtKey As Long) As Integer
Private Const VK_SHIFT = &H10
Private Const VK_SPACE = &H20

Private Sub Class_Initialize()
  ' here's where we initialize the object
  
  On Error Resume Next
  
  ' create the polyline and attach a line segment
  ' note: these geometric primitives are only necessary
  ' to optimize drawing by avoiding creating objects all the time
  '
  Set m_pLine1 = New esriGeometry.Line
  Set m_pLine2 = New esriGeometry.Line
  Set m_pLine3 = New esriGeometry.Line
  Set m_pPolyline = New Polyline
  Dim pSegColl As ISegmentCollection
  Set pSegColl = m_pPolyline
  pSegColl.AddSegment m_pLine1
  pSegColl.AddSegment m_pLine2
  pSegColl.AddSegment m_pLine3
  
  ' set up a symbol for feedback
  ' also a drawing optimization
  '
  Dim m_test As ISymbol
  Set m_test = New SimpleLineSymbol
  m_test.ROP2 = esriROPXOrPen
  Set m_pSym = m_test
  m_pSym.Style = esriSLSDot
  m_pSym.Width = 3
  Dim c As IColor
  Set c = m_pSym.Color
  c.RGB = 32768
  m_pSym.Color = c
  
  ' initialize m_pLoc
  Set m_pLoc = New Point
  Set m_pNoSnapLoc = New Point
  
End Sub

Private Property Get ICommand_Bitmap() As esriSystem.OLE_HANDLE
  ICommand_Bitmap = frmResources.picLotOffset.Picture.Handle
End Property

Private Property Get ICommand_Caption() As String
  ICommand_Caption = "Loop Construction Entry"
End Property

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

Private Property Get ICommand_Checked() As Boolean

End Property

Private Property Get ICommand_Enabled() As Boolean
  ' enable the tool only if we're editing and the current
  ' geometry type is polyline
  
  ICommand_Enabled = False
  If Not m_pEditor.Map Is Nothing Then
    ICommand_Enabled = m_pSketch.GeometryType = esriGeometryPolyline
  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

End Property

Private Property Get ICommand_Name() As String
  ICommand_Name = "Loop Construct Tool"
End Property

Private Sub ICommand_OnClick()
  'Make sure we are not trying to add to a simple edge feature class
  Dim pEdLayer As IEditLayers
  Set pEdLayer = m_pEditor
  If pEdLayer.CurrentLayer.FeatureClass.FeatureType = esriFTSimpleEdge Then
    MsgBox "Tool can only be used for adding complex edges and simple line features!!"
    Set pEdLayer = Nothing
    Set m_pApp.CurrentTool = Nothing
    Exit Sub
  End If
  Set pEdLayer = Nothing
  
  frmOffset.lContructOption = m_lContructOption
  frmOffset.dRailOffset = m_dOffset
  frmOffset.Show vbModal
  m_dOffset = frmOffset.dRailOffset
  m_lContructOption = frmOffset.lContructOption
  Unload frmOffset
  
  iPtCount = 0
End Sub

Private Sub ICommand_OnCreate(ByVal hook As Object)
  ' attach our members to the application, the display, and the editor
  '
  Dim pMXApp As IMxApplication
  Set m_pApp = hook
  Set pMXApp = m_pApp
  Set m_pDpy = pMXApp.Display
  
  Dim pUID As New UID
  pUID = "esriEditor.editor"
  Set m_pEditor = m_pApp.FindExtensionByCLSID(pUID)
  Set m_pSketch = m_pEditor
  
  m_lContructOption = 0
  m_dOffset = 20
End Sub

Private Property Get ICommand_ShortCutKey() As String

End Property

Private Property Get ICommand_Tooltip() As String
  ICommand_Tooltip = "Loop Construct Tool"
End Property

Private Property Get ITool_Cursor() As esriSystem.OLE_HANDLE

End Property

Private Function ITool_Deactivate() As Boolean
  ' clean up
  DrawFeedback 0
  m_pLoc.SetEmpty
  
  ITool_Deactivate = True  ' this tells mapstudio that we're done
End Function

Private Function ITool_OnContextMenu(ByVal x As Long, ByVal y As Long) As Boolean

End Function

Private Sub ITool_OnDblClick()
  ' here's where we provide a nice shortcut to finishing
  ' the edit sketch, or to finish a single part
  '
  m_pLoc.SetEmpty
  
  ' either finish the part or the sketch, depending on whether
  ' or not the shift key is down
  If GetKeyState(VK_SHIFT) < 0 Then
    m_pSketch.FinishSketchPart
  Else
    m_pSketch.FinishSketch
  End If
  
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)
On Error GoTo ErrorHandler
  If button <> 1 Or m_dOffset = 0 Then
    frmOffset.lContructOption = m_lContructOption
    frmOffset.dRailOffset = m_dOffset
    frmOffset.Show vbModal
    m_dOffset = frmOffset.dRailOffset
    m_lContructOption = frmOffset.lContructOption
    Unload frmOffset
    
    If button <> 1 Then Exit Sub
  End If
  
  'Make sure the point is snapped
  Dim pSnap As ISnapEnvironment
  Set pSnap = m_pEditor
  If Not pSnap.SnapPoint(m_pLoc) Then
    MsgBox "First point must snap to an edge feature!!!"
    Exit Sub
  End If
  
  If iPtCount <= 0 Or iPtCount > 1 Then
    m_pSketch.AddPoint m_pLoc, False

    Dim pFeatLayer As IFeatureLayer, pEdLayer As IEditLayers, pFeat As IFeature
    Dim pSpatialFilter As ISpatialFilter, pFeatCursor As IFeatureCursor
    Dim pFC As IFeatureClass, pPolygon As IPolygon
    Dim pTopoOp As ITopologicalOperator
    Set pSpatialFilter = New SpatialFilter
    Set pEdLayer = m_pEditor
    Set pFeatLayer = pEdLayer.CurrentLayer
    Set pFC = pFeatLayer.FeatureClass

    Set pTopoOp = m_pLoc         '  For a search on SDE must use a polygon(etb)
    Set pPolygon = pTopoOp.Buffer(m_dOffset)
    Set pSpatialFilter.Geometry = pPolygon  ' Modified (etb)
    pSpatialFilter.SpatialRel = esriSpatialRelIntersects
    pSpatialFilter.GeometryField = pFC.ShapeFieldName  ' SDE needs the Geometry Field on a spatial filter (etb)

    Set pFeatCursor = pFeatLayer.Search(pSpatialFilter, True)
    Set pFeat = pFeatCursor.NextFeature
    Set m_pSnapFeature = pFeat
    'For some unknown reason it is necessary to get the shape from the feature here
    Dim pPolyLine As IPolyline
    Set pPolyLine = pFeat.Shape
        
    iPtCount = 1
  Else
    'add second point
    m_pSketch.AddPoint m_pLine2.FromPoint, False
    m_pSketch.AddPoint m_pLine2.ToPoint, False
    m_pSketch.AddPoint m_pLoc, False
    Dim pClone As IClone
    Set pClone = m_pSketch.Geometry
    Set m_pFinalPolyline = pClone.Clone
    
    m_pLoc.SetEmpty
    m_pSketch.FinishSketch
    m_pEditor.StartOperation
    'Perform the split to integrate the new segment into the network
    FinishConstruct
    m_pEditor.StopOperation "Loop Construct"
    Set m_pApp.CurrentTool = Nothing
    Dim pDoc As IMxDocument
    Set pDoc = m_pApp.Document
    pDoc.ActiveView.Refresh
    
    iPtCount = 0
  End If
  
  Exit Sub
  
ErrorHandler:
  MsgBox "Mouse Down - " & Err.Description
  m_pEditor.StopOperation "Loop Construct"
  Exit Sub
End Sub

Private Sub ITool_OnMouseMove(ByVal button As Long, ByVal shift As Long, ByVal x As Long, ByVal y As Long)
  ' erase existing feedback if necessary
  If Not m_pLoc.IsEmpty Then DrawFeedback 0
  
  ' calculate the new location with always handy ToMapPoint method
  Set m_pLoc = m_pDpy.DisplayTransformation.ToMapPoint(x, y)
  Dim pClone As IClone
  Set pClone = m_pLoc
  Set m_pNoSnapLoc = pClone.Clone
  
  ' snap the point if the space key isn't down
  If GetKeyState(VK_SPACE) >= 0 Then
    Dim pSnapEnv As ISnapEnvironment
    Set pSnapEnv = m_pEditor
    pSnapEnv.SnapPoint m_pLoc
  End If
  
  DrawFeedback 0  ' draw feedback at new location
  
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)
  DrawFeedback hdc
End Sub

Private Sub DrawFeedback(hdc As esriSystem.OLE_HANDLE)
  If m_pLoc.IsEmpty Then Exit Sub     ' nothing to do
  
  m_pEditor.InvertAgent m_pLoc, hdc
  
  ' get the anchor point
  Dim anchor As IPoint
  Set anchor = m_pSketch.LastPoint
  If anchor Is Nothing Then
    Exit Sub  ' no line to draw
  End If
  
  If m_pSnapFeature Is Nothing Then
    Exit Sub
  End If
  
  'Get 90 degree angle based on current cursor position
  Dim mSecondPT As IPoint
  Set mSecondPT = FindPerpPt(anchor)
  If mSecondPT Is Nothing Then
    Exit Sub
  End If
  
  ' get the intermediate point
  Dim mTurnPT As IConstructPoint
  Set mTurnPT = New esriGeometry.Point
  Dim dOutAngle As Double, pLine As ILine
  Set pLine = New esriGeometry.Line
  pLine.PutCoords anchor, mSecondPT
  dOutAngle = pLine.Angle
  mTurnPT.ConstructAngleDistance m_pLoc, dOutAngle, m_dOffset
  
  ' draw the rubberband line feedback
  ' grab the activeCache setting in order to restore it later
  Dim ac As Integer
  ac = m_pDpy.ActiveCache
  
  ' draw without a cache - directly on the window
  m_pDpy.ActiveCache = esriNoScreenCache
  m_pDpy.StartDrawing hdc, esriNoScreenCache
  
  m_pDpy.SetSymbol m_pSym
  m_pLine1.PutCoords anchor, mSecondPT
  m_pLine2.PutCoords mSecondPT, mTurnPT
  m_pLine3.PutCoords mTurnPT, m_pLoc
  m_pDpy.DrawPolyline m_pPolyline
  
  ' finish drawing and restore the cache setting
  m_pDpy.FinishDrawing
  m_pDpy.ActiveCache = ac
End Sub

Private Function FindPerpPt(pAnchor As IPoint) As IPoint
On Error GoTo ErrHand:
  If m_pSnapFeature Is Nothing Then
    Set FindPerpPt = Nothing
    Exit Function
  End If
  
  'Find distance along line, then tangent line at that point
  Dim pPolyLine As IPolyline, pOutPt As IPoint, dAlong As Double
  Dim dFrom As Double, bSide As Boolean, pLine As ILine, dLoc As Double
  Set pOutPt = New esriGeometry.Point
  Set pLine = New esriGeometry.Line
  Set pPolyLine = m_pSnapFeature.Shape
  pPolyLine.QueryPointAndDistance esriNoExtension, pAnchor, False, pOutPt, dAlong, dFrom, bSide
  'Determine the side the cursor is currently on
  pPolyLine.QueryPointAndDistance esriExtendTangents, m_pNoSnapLoc, False, pOutPt, dLoc, dFrom, bSide
  If bSide Then
    pPolyLine.QueryNormal esriNoExtension, dAlong, False, m_dOffset, pLine
  Else
    pPolyLine.QueryNormal esriNoExtension, dAlong, False, (-1 * m_dOffset), pLine
  End If
  
  Set FindPerpPt = pLine.ToPoint
  
  Exit Function
  
ErrHand:
  MsgBox "findPerpPt - " & Err.Description
  Set FindPerpPt = Nothing
  Exit Function
End Function

Private Sub FinishConstruct()
On Error GoTo ErrHand:
  Dim pFeatEdit As IFeatureEdit, pResultSet As ISet
  Set pFeatEdit = m_pSnapFeature
  Set pResultSet = pFeatEdit.Split(m_pFinalPolyline.FromPoint)
  If pResultSet.Count <> 2 Then Exit Sub
  pResultSet.Reset
  
  'Find the piece intersected by the end point of the loop construct
  Dim pLoop As Long, pProx As IProximityOperator
  Dim dDist1 As Double, dDist2 As Double
  Dim pNewFeature As IFeature, pCurFeature As IFeature
  For pLoop = 0 To 1
    Set pCurFeature = pResultSet.Next
    Set pProx = pCurFeature.Shape
    If pLoop = 0 Then
      dDist1 = pProx.ReturnDistance(m_pFinalPolyline.ToPoint)
      Set pNewFeature = pCurFeature
    Else
      dDist2 = pProx.ReturnDistance(m_pFinalPolyline.ToPoint)
    End If
    If dDist2 < dDist1 Then
      Set pNewFeature = pCurFeature
    End If
  Next pLoop
  
  'Split the piece intersected by the end point
  Dim pFeatEdit2 As IFeatureEdit, pResultSet2 As ISet
  Set pFeatEdit2 = pNewFeature
  Set pResultSet2 = pFeatEdit2.Split(m_pFinalPolyline.ToPoint)
  If pResultSet2 Is Nothing Then
    MsgBox "no set"
    Exit Sub
  End If
  If pResultSet2.Count <> 2 Then Exit Sub
  pResultSet2.Reset
  
  'Find the piece in the middle and modify or delete it
  Dim pFinalFeat As IFeature, pFinalLine As IPolyline
  Dim pSpan As IPolyline, dLength1 As Double, dLength2 As Double
  Dim pMiddleFeat As IFeature, pProxPt As IProximityOperator
  Set pSpan = New Polyline
  Set pFinalFeat = pResultSet2.Next
  Set pFinalLine = pFinalFeat.Shape
  Set pMiddleFeat = pFinalFeat
  Set pProxPt = pFinalLine.FromPoint
  dLength1 = pProxPt.ReturnDistance(pFinalLine.ToPoint)
  Set pFinalFeat = pResultSet2.Next
  Set pFinalLine = pFinalFeat.Shape
  Set pProxPt = pFinalLine.FromPoint
  dLength2 = pProxPt.ReturnDistance(pFinalLine.ToPoint)
  pSpan.FromPoint = m_pFinalPolyline.FromPoint
  pSpan.ToPoint = m_pFinalPolyline.ToPoint
  If Abs(pSpan.Length - dLength1) > Abs(pSpan.Length - dLength2) Then
    Set pMiddleFeat = pFinalFeat
  End If
  
  If m_lContructOption = 0 Then
    Dim pRow As IRow
    Set pRow = pMiddleFeat
    pRow.Delete
  Else
    'Snip 10% of each end
    Dim pInCurve As ICurve, pOutCurve As ICurve
    If pMiddleFeat Is Nothing Then
      MsgBox "Something is wrong with the middle feature."
    End If
    If TypeOf pMiddleFeat Is INetworkFeature Then
      Dim pNetFeat As INetworkFeature
      Set pNetFeat = pMiddleFeat
      pNetFeat.Disconnect
    End If
    pMiddleFeat.Store
    
    Set pInCurve = pMiddleFeat.Shape
    pInCurve.GetSubcurve 0.1, 0.9, True, pOutCurve
    Set pMiddleFeat.Shape = pOutCurve
    pMiddleFeat.Store
  End If
  
  Exit Sub
  
ErrHand:
  MsgBox "final Construct - " & Err.Description
  m_pEditor.StopOperation "Loop Construct"
  Exit Sub
End Sub