Home arrow GIS Data & Resources arrow Scripts and Code arrow Visual Basic / VBA arrow VBA: Centerline Editing - Address Range Split Tool
VBA: Centerline Editing - Address Range Split Tool PDF Print E-mail

Written by Bert Granberg,

This code is adapted from the HouseNumber tool that ESRI published on its ArcScripts web site. The HouseNumber tool was originally designed to work in pre-9.1 versions of ArcMap and was quite extensive in its functionality. I spoke with an ESRI staffer a few months back who indicated that, unfortunately, the code had not been adapted yet for the 9.2 reorganization of the ArcObjects component library and beyond (and there were no plans to do so). This may be why the original tool doesn't always install and run as expected.

Not wanting to install VB 6.0 to modify the HouseNumber project and compile a new DLL either, I took what I needed and adapted it to work in the VBA environment. So far so good.

What does this tool do:

  1. Select a street feature in an editing workspace
  2. With the Address Range Splitter (ARS) custom UI tool selected, click on a location on a selected street feature with the four standard address range attributes
  3. The existing feature will be split into two features at the point of your click and new address range attributes will be calculated using the a proportional interpolation based on feature length

Notes:

  • The snapping environment IS honored although you will not see your your map tool's cursor snapping at all.
  • Address Ranges are assumed to be mixed parity only. This tool will not work where mixed parity is not desired.
  • The script does NOT yet take into account the address coordinate alias of intersecting streets at the click point
  • The script depends on the MxDocument.OpenDocument having run since the last code interuption so if you are modifying/customizing and you get an error OR you are running the script for the first time, you'll need to run the MxDocument.OpenDocument script manually before proceeding.
  • Check the results to make sure the script is producing the desired results.


How to install and configure in an ArcMap Project:

Part I. Paste code into VBA Editor

1) From the ArcMap menu, navigate to Tools --> Macros --> Visual Basic Editor to open the VBA editor

2) In the VBA Editor's Project Window, expand the Project --> Arcmap Objects folder and double click on ThisDocument

3) Paste the code (below) in the window that opens and close the VBA editor.

4) Make sure that the 3 module level variables are at the top of your code window if you have any prexisting code already in your project.

5) At the first 10 lines of the sub procedure doCenterlineSplit, modify the field names (if necessary) so they correspond to your centerline dataset's address range attribute field names.

6) Save your project

Part II. Create and configure custom ArcMap Tool

1) From the ArcMap menu, navigate to Tools -->Customize

2) Click the Commands tab.

3) Click the drop-down arrow on the Save in combo box, then change the selected item so it is the current name of your ArcMap project (ex. Untitled.mxd)

4) Click [UIControls] in the Categories list. then Click the New UIControl button.

5) Click the UIToolControl radio button and then click Create.

6) The name of the new tool appears in the Commands list. Single click the newly created UIControl, then click it again to activate in-place renamin, then type a new name for the control so it reads Project.AddressRangeSplitter.

7) Click and drag the newly created UIControl and drop it on an ArcMap toolbar or menu.

8) Keep the customize dialog box open. On the toolbar or menu you dragged the tool on to, right-click the command to set its image, caption, and other properties.

9) Save Project. Close ArcMap. Reopen the project and start using the tool.

'CODE 5/11/09 AGRC

'THESE 3 LINES NEED TO GO AT THE VERY TOP OF YOUR CODE WINDOW ABOVE
'PRE_EXISTING SUBS OR FUNCTIONS
Private m_pEditor As IEditor
Private m_pEditSketch As IEditSketch
Private WithEvents EditorEvents As Editor


Private Function MxDocument_opendocument() As Boolean
    Dim pUID As New UID
    pUID = "esriEditor.Editor"
    Set m_pEditor = Application.FindExtensionByCLSID(pUID)
    Set m_pEditSketch = m_pEditor
    Set EditorEvents = m_pEditor
End Function
Private Sub AddressRangeSplitter_MouseDown(ByVal button As Long, ByVal shift As Long, ByVal x As Long, ByVal y As Long)
    
    Dim pDisplayTransformation As IDisplayTransformation
    Dim pScreenDisplay As IScreenDisplay
    Dim pSelFeature As IFeature
    Dim pSplitPnt As IPoint
    Dim count As Long
    Dim bol As Boolean
    Dim pMxdoc As IMxDocument
    Dim pActiveView As IActiveView
    Dim pSnapEnv As ISnapEnvironment
    
    Set pMxdoc = ThisDocument
    Set pActiveView = pMxdoc.ActiveView
      
    bol = MxDocument_opendocument()

    If Not m_pEditor.EditState = esriStateEditing Then
        MsgBox "You must currently be in an edit session to run this script.", vbOKOnly, "Exiting"
        Exit Sub
    End If

    Set pSelFeature = m_pEditor.EditSelection.Next
    
    If pSelFeature Is Nothing Then  ' should never be true due to enabling.
        MsgBox "The esriEditor.Editor does not have a feature selected.", vbCritical, "Can not Split"
        Exit Sub
    End If
    If Not TypeOf pSelFeature.Shape Is IPolyline Then   ' should never be true due to enabling.
        MsgBox "The Editor's selected feature is not a polyline.", vbCritical, "Can not Split"
        Exit Sub
    End If
    
    Set pSnapEnv = m_pEditor
        
    Set pSplitPnt = New Point
    
    Set pScreenDisplay = pActiveView.ScreenDisplay
    Set pDisplayTransformation = pScreenDisplay.DisplayTransformation
    Set pSplitPnt = pDisplayTransformation.ToMapPoint(x, y)
    
    'snap point to existing snap environment
    pSnapEnv.SnapPoint pSplitPnt
    
    Call doCenterlineSplit(pSelFeature, pSplitPnt)

    Dim pEnv As IEnvelope
    Set pEnv = pSelFeature.Extent
    pEnv.Expand 1.2, 1.2, True
    ' if the parent arc is horizontal or vertical, refresh the full screen.
    If pEnv.Width < pMxdoc.ActiveView.Extent.Width / 100 Then Set pEnv = pMxdoc.ActiveView.Extent
    If pEnv.Height < pMxdoc.ActiveView.Extent.Height / 100 Then Set pEnv = pMxdoc.ActiveView.Extent
    pActiveView.PartialRefresh 6, Nothing, pEnv

End Sub

 

Private Sub doCenterlineSplit(pParentFeature As IFeature, pSplitPnt As IPoint)

  On Error GoTo EH
  Dim leftFromFN As String, leftToFN As String, rightFromFN As String, rightToFN As String

  '**** Set these for each address range field
  leftFromFN = "L_F_ADD"    'LEFT FROM
  leftToFN = "L_T_ADD"      'LEFT TO
  rightFromFN = "R_F_ADD"   'RIGHT FROM
  rightToFN = "R_T_ADD"     'RIGHT TO
  '****
 
  Dim pParentCurve As ICurve
  Set pParentCurve = pParentFeature.Shape
  If pParentCurve Is Nothing Then ' true if user has selected a polyline with Null geometry.
    MsgBox "The selected polyline does not have a valid shape.", vbCritical, "Can not Split"
    Exit Sub
  End If
  If pParentCurve.IsEmpty = True Then
    MsgBox "The selected polyline has an empty geometry.", vbCritical, "Can not Split"
    Exit Sub
  End If
  If pParentCurve.Length = 0 Then
    MsgBox "The selected polyline has a length of zero.", vbCritical, "Can not Split"
    Exit Sub
  End If
  ' make sure the required fields which contain the house numbers have been specified.
  Dim pFields As IFields
  Set pFields = pParentFeature.Fields

  Dim leftFromFI As Long, leftToFI As Long, rightFromFI As Long, rightToFI As Long
  leftFromFI = pFields.FindField(leftFromFN)
  leftToFI = pFields.FindField(leftToFN)
  rightFromFI = pFields.FindField(rightFromFN)
  rightToFI = pFields.FindField(rightToFN)
 
  ' check for Null values in the 4 house number fields (modGlobals has comments on g_pExtension).
  If IsNull(pParentFeature.Value(leftFromFI)) Or _
     IsNull(pParentFeature.Value(leftToFI)) Or _
     IsNull(pParentFeature.Value(rightFromFI)) Or _
     IsNull(pParentFeature.Value(rightToFI)) Then
    
    MsgBox "One or more of the house numbers are Null for the selected line.", _
            vbCritical, "Can not Split"
    Exit Sub
 
  End If
 
  ' try to get a valid house number from each of the 4 house number fields.
  ' this is especially important if the fields are Text, which is common for geocoding data.
  Dim lngFrom_Left_HouseNum As Long, lngFrom_Right_HouseNum As Long, lngTo_Left_HouseNum As Long, lngTo_Right_HouseNum As Long
  lngFrom_Left_HouseNum = TryToGetValidHouseNum(pParentFeature.Value(leftFromFI))
  lngFrom_Right_HouseNum = TryToGetValidHouseNum(pParentFeature.Value(rightFromFI))
  lngTo_Left_HouseNum = TryToGetValidHouseNum(pParentFeature.Value(leftToFI))
  lngTo_Right_HouseNum = TryToGetValidHouseNum(pParentFeature.Value(rightToFI))
 
  If lngFrom_Left_HouseNum = -1 Or lngFrom_Right_HouseNum = -1 Or _
     lngTo_Left_HouseNum = -1 Or lngTo_Right_HouseNum = -1 Then
    
    MsgBox "One or more of the house numbers are invalid for the selected line.", _
            vbCritical, "Can not Split"
    Exit Sub
 
  End If
 
  Dim mixedParity As Boolean
  mixedParity = False
 
  If Not mixedParity Then ' the default, test and fail if mixed parity is found.
    ' make sure both house numbers on each side of the street are either even or odd.
    Dim blnFromIsEven As Boolean, blnToIsEven As Boolean
    blnFromIsEven = isEven(lngFrom_Left_HouseNum)
    blnToIsEven = isEven(lngTo_Left_HouseNum)
    If blnFromIsEven <> blnToIsEven Then
      MsgBox "The left side of the selected line has both even and odd numbers.", _
              vbCritical, "Mixed Parity"
      Exit Sub ' fail if mixed parity.
    End If
    blnFromIsEven = isEven(lngFrom_Right_HouseNum)
    blnToIsEven = isEven(lngTo_Right_HouseNum)
    If blnFromIsEven <> blnToIsEven Then
      MsgBox "The right side of the selected line has both even and odd numbers.", _
              vbCritical, "Mixed Parity"
      Exit Sub ' fail if mixed parity.
    End If
    ' verify the 2 sides of the polyline have opposite parity (unless one side has 2 zeros).
    Dim blnLeftIsEven As Boolean
    blnLeftIsEven = isEven(lngFrom_Left_HouseNum)
    Dim blnOneSideHasZeros As Boolean
    If (lngFrom_Left_HouseNum = 0 And lngTo_Left_HouseNum = 0) Or (lngFrom_Right_HouseNum = 0 And lngTo_Right_HouseNum = 0) Then
      blnOneSideHasZeros = True
    End If
    If blnOneSideHasZeros = False Then
      If blnLeftIsEven = blnFromIsEven Then
        If blnLeftIsEven = False Then
          MsgBox "Both sides of the selected line have odd house numbers.", vbCritical, "Mixed Parity"
        Else
          MsgBox "Both sides of the selected line have even house numbers.", vbCritical, "Mixed Parity"
        End If
        Exit Sub
      End If
    End If
  End If
  ' create the point that will be used to split the selected polyline.
 
  If pSplitPnt Is Nothing Then ' should never be true.
    MsgBox "A valid split point could not be created.", vbCritical, "Can not Split"
    Exit Sub
  End If
  If pSplitPnt.IsEmpty = True Then ' should never be true.
    MsgBox "A valid split point could not be created.", vbCritical, "Can not Split"
    Exit Sub
  End If
  ' split the parent feature into 2 offspring features. we use IFeatureEdit::Split since
  ' it respects geodatabase behaviour (subtypes, domains, split policies etc). it also maintains
  ' M and Z values, and works for geometric networks and topological ArcInfo coverages.
  Dim pFeatEdit As IFeatureEdit
  Set pFeatEdit = pParentFeature
  Dim pNewSet As ISet
  On Error GoTo ErrorAfterStartOperation
  m_pEditor.StartOperation
  Set pNewSet = pFeatEdit.Split(pSplitPnt)
  If pNewSet.count <> 2 Then
    MsgBox "Unknown error - two offspring lines could not be produced.", _
           vbCritical, "Can not Split"
    m_pEditor.AbortOperation
    Exit Sub
  End If
  ' we now need to modify the house numbers of the 2 offspring polylines. before doing this,
  ' we must be sure pNewFeature1 is the offspring line that contains the parent's from vertex,
  ' or our logic will not work. Since ISet::Next does not return the features in any particular
  ' order, we must test and switch if needed.
  Dim pNewFeature1 As IFeature, pNewFeature2 As IFeature
  Set pNewFeature1 = pNewSet.Next ' will be wrong 50% of the time.
  Dim pNewFeat1Curve As ICurve, pNewFeat2Curve As ICurve
  Set pNewFeat1Curve = pNewFeature1.Shape
  Dim pParentFromPnt As IPoint, pNewFeature1Pnt As IPoint
  Set pParentFromPnt = pParentCurve.FromPoint
  Set pNewFeature1Pnt = pNewFeat1Curve.FromPoint
  Dim pRelOp As IRelationalOperator
  Set pRelOp = pParentFromPnt
  If pRelOp.Equals(pNewFeature1Pnt) = True Then ' no need to switch.
    Set pNewFeature2 = pNewSet.Next
  Else ' will happen 50% of the time, need to switch.
    Set pNewFeature2 = pNewFeature1
    Set pNewFeature1 = pNewSet.Next
    Set pNewFeat1Curve = pNewFeature1.Shape
  End If
  Set pNewFeat2Curve = pNewFeature2.Shape
    Dim pNewFeature2Pnt As esriGeometry.IPoint
    Set pNewFeature2Pnt = pNewFeat2Curve.FromPoint
    'MsgBox "Parent: " & pParentCurve.SpatialReference.Name & " X = " & pParentFromPnt.x & Chr(13) & _
           "SplitPoint: " & pSplitPnt.SpatialReference.Name & " X = " & pSplitPnt.x & Chr(13) & _
           "Offspring1: " & pNewFeat1Curve.SpatialReference.Name & " X = " & pNewFeature1Pnt.x & Chr(13) & _
           "Offspring2: " & pNewFeat2Curve.SpatialReference.Name & " X = " & pNewFeature2Pnt.x
  ' get the distance along the curve (as a ratio) where the split point falls. we will need
  ' this soon for the interpolation of house numbers.
  Dim dblDistAlongCurve As Double
  dblDistAlongCurve = pNewFeat1Curve.Length / (pNewFeat1Curve.Length + pNewFeat2Curve.Length)
  ' fix the 4 house numbers that are not correct (main part of code). the other 4 numbers are
  ' already correct (the FROM_LEFT and FROM_RIGHT of the first feature, and the TO_LEFT and
  ' TO_RIGHT of the second feature).
  Dim lngLeftNum As Long, lngRightNumber As Long
  lngLeftNum = getInterpolatedHouseNumber(lngFrom_Left_HouseNum, lngTo_Left_HouseNum, dblDistAlongCurve)
  lngRightNumber = getInterpolatedHouseNumber(lngFrom_Right_HouseNum, lngTo_Right_HouseNum, dblDistAlongCurve)
  ' the following 10 lines set the TO_LEFT and TO_RIGHT numbers of the first feature.
  If lngFrom_Left_HouseNum = lngTo_Left_HouseNum Then ' if parent had no range of house numbers.
    pNewFeature1.Value(leftToFI) = lngFrom_Left_HouseNum
  Else
    pNewFeature1.Value(leftToFI) = lngLeftNum
  End If
  If lngFrom_Right_HouseNum = lngTo_Right_HouseNum Then
    pNewFeature1.Value(rightToFI) = lngFrom_Right_HouseNum
  Else
    pNewFeature1.Value(rightToFI) = lngRightNumber
  End If
  ' the following lines set the FROM_LEFT and FROM_RIGHT numbers of the second feature.
  If lngFrom_Left_HouseNum < lngTo_Left_HouseNum Then
    If mixedParity = False Then
      pNewFeature2.Value(leftFromFI) = pNewFeature1.Value(leftToFI) + 2
    Else
      pNewFeature2.Value(leftFromFI) = pNewFeature1.Value(leftToFI) + 1
    End If
  ElseIf lngFrom_Left_HouseNum = lngTo_Left_HouseNum Then ' if parent had no range of house numbers.
    pNewFeature2.Value(leftFromFI) = lngFrom_Left_HouseNum
  Else ' if house numbers run opposite to the polyline's digitized direction.
    If mixedParity = False Then
      pNewFeature2.Value(leftFromFI) = pNewFeature1.Value(leftToFI) - 2
    Else
      pNewFeature2.Value(leftFromFI) = pNewFeature1.Value(leftToFI) - 1
    End If
  End If
  If lngFrom_Right_HouseNum < lngTo_Right_HouseNum Then
    If mixedParity = False Then
      pNewFeature2.Value(rightFromFI) = pNewFeature1.Value(rightToFI) + 2
    Else
      pNewFeature2.Value(rightFromFI) = pNewFeature1.Value(rightToFI) + 1
    End If
  ElseIf lngFrom_Right_HouseNum = lngTo_Right_HouseNum Then
    pNewFeature2.Value(rightFromFI) = lngFrom_Right_HouseNum
  Else ' if house numbers run opposite to the polyline's digitized direction.
    If mixedParity = False Then
      pNewFeature2.Value(rightFromFI) = pNewFeature1.Value(rightToFI) - 2
    Else
      pNewFeature2.Value(rightFromFI) = pNewFeature1.Value(rightToFI) - 1
    End If
  End If
  pNewFeature1.Store
  pNewFeature2.Store
  m_pEditor.StopOperation "Split house numbers"
  On Error GoTo AfterStopOperation
  ' partially refresh the screen.
  Exit Sub
EH:
  MsgBox Err.Number & Chr(13) & Err.Description, vbCritical, "Can not Split"
  Exit Sub
ErrorAfterStartOperation:
  MsgBox Err.Number & Chr(13) & Err.Description, vbCritical, "Can not Split"
  m_pEditor.AbortOperation ' do not do Store any of the edits unless all are successful.
  Exit Sub
AfterStopOperation:
  MsgBox Err.Number & Chr(13) & Err.Description & Chr(13) & _
        "The line was split, but there was a problem with refreshing the screen.", _
        vbCritical, "Refresh problem"
  Exit Sub
End Sub

 

Function TryToGetValidHouseNum(strHouseNum As Variant) As Long
  ' attempt to get a valid Long Interger from the supplied candidate.
  ' returns -1 if not possible.
  On Error GoTo EH
  TryToGetValidHouseNum = -1
  ' do not allow candidates containing only blank spaces.
  If Trim(strHouseNum) = "" Then
    Exit Function
  End If
  strHouseNum = Trim(strHouseNum) ' strip away any blank spaces.
  ' drop non-numeric characters, if present.
  Dim lngHouseNumber As Long
  If IsNumeric(strHouseNum) Then
    lngHouseNumber = CLng(strHouseNum)
  Else
    Dim strNewCandidate As String
    Dim i As Integer
    Dim oneChar As String
    For i = 1 To Len(strHouseNum)
      oneChar = Mid(strHouseNum, i, 1)
      If IsNumeric(oneChar) Then
        strNewCandidate = strNewCandidate & oneChar
      End If
    Next
    If Len(strNewCandidate) = 0 Then ' if none of the characters were numbers.
      Exit Function
    End If
    lngHouseNumber = CLng(strNewCandidate)
  End If
  ' lngHouseNumber will be used by the main code.
  TryToGetValidHouseNum = lngHouseNumber
  Exit Function
EH:
  MsgBox Err.Number & Chr(13) & Err.Description, vbCritical, "Error in TryToGetValidHouseNum"
  Exit Function
End Function

 

Public Function getInterpolatedHouseNumber(lngFrom As Long, lngTo As Long, dblDist As Double) As Long
  ' interpolate the next lowest whole number given lngFrom and lngTo. Makes sure it is on the
  ' correct side of the street if MixedParity is False.
  ' start by returning the raw (Double) interpolated house number.
  On Error GoTo EH
  Dim dblHouseNum As Double
  Dim lngNextLowestHouseNumber As Long
  Dim mixedParity As Boolean
  mixedParity = False
  If lngFrom < lngTo Then
    dblHouseNum = ((lngTo - lngFrom) * dblDist) + lngFrom
    lngNextLowestHouseNumber = Int(dblHouseNum) ' Int will return a Long.
  Else ' if house numbers run opposite to line's digitized direction.
    dblHouseNum = lngFrom - ((lngFrom - lngTo) * dblDist)
    lngNextLowestHouseNumber = Int(dblHouseNum) + 1 ' Int will return a Long.
  End If
  If Not mixedParity Then ' the default.
    ' make sure the interpolated number is on the correct side of the street.
    Dim blnFromIsEven As Boolean, blnCandidateNumberIsEven As Boolean
    blnFromIsEven = isEven(lngFrom)
    blnCandidateNumberIsEven = isEven(lngNextLowestHouseNumber)
    If blnFromIsEven <> blnCandidateNumberIsEven Then
      If lngFrom < lngTo Then
        lngNextLowestHouseNumber = lngNextLowestHouseNumber - 1
      Else
        lngNextLowestHouseNumber = lngNextLowestHouseNumber + 1
      End If
    End If
  End If
  getInterpolatedHouseNumber = lngNextLowestHouseNumber
  Exit Function
EH:
  MsgBox Err.Number & Chr(13) & Err.Description, vbCritical, "Error in getInterpolatedHouseNumber"
  Exit Function
End Function

 

Function isEven(num As Long) As Boolean
  ' returns True if num is even.
  isEven = False
  If num Mod 2 = 0 Then
    isEven = True
  End If
End Function


Users' Comments  
 

No comment posted

Add your comment

11, May. 2009
Last Updated ( 12, May. 2009 )
 
< Prev   Next >

AGRC Contacts | UGIC Contacts

feed image feed image

Utah GIS Portal © 2009 AGRC

Optimized for