Import signposts
ImportDynamapSignsFunction.vb
' Copyright 2010 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 the use restrictions.
' 

Imports System
Imports System.Runtime.InteropServices

Imports ESRI.ArcGIS.esriSystem
Imports ESRI.ArcGIS.Geodatabase
Imports ESRI.ArcGIS.Geoprocessing
Imports ESRI.ArcGIS.Geometry

Namespace GPImportSignpostFunctions
  <Guid("3C5D851A-A98F-4bd4-911C-3296B094DDE8")> _
  <ClassInterface(ClassInterfaceType.None)> _
  <ProgId("GPImportSignpostFunctions.ImportDynamapSignsFunction")> _
  Public Class ImportDynamapSignsFunction
    Implements IGPFunction

#Region "Constants"
    ' parameter index constants
    Private Const InputTable As Integer = 0
    Private Const ReferenceLineFeatures As Integer = 1
    Private Const OutFeatureClassName As Integer = 2
    Private Const OutStreetsTableName As Integer = 3

    ' field names and types
    Private Shared ReadOnly FieldNames() As String = New String() _
    {"EXIT_ID", "SEQUENCE", "FROM_ID", "FROM_NAME", "EXIT_NUM", _
     "TO_ID", "TO_NAME", "SHIELD", "HWY_NUM", "DIRECTION", "TO_LOCALE", _
     "ACCESS", "EXIT_ONLY", "LONGITUDE", "LATITUDE"}

    Private Shared ReadOnly FieldTypes() As esriFieldType = New esriFieldType() _
    {esriFieldType.esriFieldTypeDouble, _
     esriFieldType.esriFieldTypeSmallInteger, _
     esriFieldType.esriFieldTypeDouble, _
     esriFieldType.esriFieldTypeString, _
     esriFieldType.esriFieldTypeString, _
     esriFieldType.esriFieldTypeDouble, _
     esriFieldType.esriFieldTypeString, _
     esriFieldType.esriFieldTypeString, _
     esriFieldType.esriFieldTypeString, _
     esriFieldType.esriFieldTypeString, _
     esriFieldType.esriFieldTypeString, _
     esriFieldType.esriFieldTypeString, _
     esriFieldType.esriFieldTypeString, _
     esriFieldType.esriFieldTypeDouble, _
     esriFieldType.esriFieldTypeDouble}

    Private Shared ReadOnly LinesIDFieldName As String = "Dynamap_ID"
    Private Shared ReadOnly LinesIDFieldType As esriFieldType = esriFieldType.esriFieldTypeDouble

#End Region

    Private m_parameters As IArray

    Public Sub New()
    End Sub

#Region "IGPFunction Members"

    Public ReadOnly Property ParameterInfo() As IArray Implements IGPFunction.ParameterInfo
      Get
        Dim gpParamArray As IArray = New ESRI.ArcGIS.esriSystem.Array

        ' 1 - input_highway_sign_features

        Dim paramEdit As IGPParameterEdit = New GPParameter
        paramEdit.DataType = New DETableType
        paramEdit.Value = New DETable
        paramEdit.Direction = esriGPParameterDirection.esriGPParameterDirectionInput
        paramEdit.DisplayName = "Input Highway Sign Features"
        paramEdit.Enabled = True
        paramEdit.Name = "input_highway_sign_features"
        paramEdit.ParameterType = esriGPParameterType.esriGPParameterTypeRequired

        gpParamArray.Add(paramEdit)

        ' 2 - reference_street_features

        paramEdit = New GPParameter
        paramEdit.DataType = New DEFeatureClassType
        paramEdit.Value = New DEFeatureClass
        paramEdit.Direction = esriGPParameterDirection.esriGPParameterDirectionInput
        paramEdit.DisplayName = "Input Street Features"
        paramEdit.Enabled = True
        paramEdit.Name = "reference_street_features"
        paramEdit.ParameterType = esriGPParameterType.esriGPParameterTypeRequired

        Dim lineFeatureClassDomain As IGPFeatureClassDomain = New GPFeatureClassDomain
        lineFeatureClassDomain.AddType(esriGeometryType.esriGeometryLine)
        lineFeatureClassDomain.AddType(esriGeometryType.esriGeometryPolyline)

        paramEdit.Domain = CType(lineFeatureClassDomain, IGPDomain)

        gpParamArray.Add(paramEdit)

        ' 3 - out_feature_class_name

        paramEdit = New GPParameter
        paramEdit.DataType = New GPStringType
        Dim stringVal As IGPString = New GPString
        stringVal.Value = "Signpost"
        paramEdit.Value = CType(stringVal, IGPValue)
        paramEdit.Direction = esriGPParameterDirection.esriGPParameterDirectionInput
        paramEdit.DisplayName = "Output Signpost Feature Class Name"
        paramEdit.Enabled = True
        paramEdit.Name = "out_feature_class_name"
        paramEdit.ParameterType = esriGPParameterType.esriGPParameterTypeRequired

        gpParamArray.Add(paramEdit)

        ' 4 - out_table_name

        paramEdit = New GPParameter
        paramEdit.DataType = New GPStringType
        stringVal = New GPString
        stringVal.Value = "SignpostSt"
        paramEdit.Value = CType(stringVal, IGPValue)
        paramEdit.Direction = esriGPParameterDirection.esriGPParameterDirectionInput
        paramEdit.DisplayName = "Output Signpost Streets Table Name"
        paramEdit.Enabled = True
        paramEdit.Name = "out_streets_table_name"
        paramEdit.ParameterType = esriGPParameterType.esriGPParameterTypeRequired

        gpParamArray.Add(paramEdit)

        ' TODO: add two derived output parameters for chaining this function
        '       in models

        Return gpParamArray
      End Get
    End Property

    Public Function Validate(ByVal paramvalues As IArray, ByVal updateValues As Boolean, ByVal envMgr As IGPEnvironmentManager) As IGPMessages Implements IGPFunction.Validate

      ' Create the GPUtilities Object

      Dim gpUtils As IGPUtilities = New GPUtilities

      ' Initialize a copy of our parameters

      If m_parameters Is Nothing Then
        m_parameters = ParameterInfo
      End If

      ' Call InternalValidate to check for required parameters

      Dim validateMessages As IGPMessages = gpUtils.InternalValidate(m_parameters, paramvalues, updateValues, True, envMgr)

      ' Verify chosen input table has the expected fields

      Dim gpParam As IGPParameter = CType(paramvalues.Element(InputTable), IGPParameter)
      Dim tableValue As IGPValue = gpUtils.UnpackGPValue(gpParam)

      ' CheckForTableFields will report errors by modifying the relevant GPMessage

      If Not tableValue.IsEmpty() Then
        Dim inputDETable As IDETable = gpUtils.DecodeDETable(tableValue)
        CheckForTableFields(inputDETable, validateMessages.GetMessage(InputTable))
      End If

      ' Verify chosen reference_line_features has expected id field

      gpParam = CType(paramvalues.Element(ReferenceLineFeatures), IGPParameter)
      Dim featureClassValue As IGPValue = gpUtils.UnpackGPValue(gpParam)

      If Not featureClassValue.IsEmpty() Then
        Dim inputDETable As IDETable = gpUtils.DecodeDETable(featureClassValue)
        CheckForLinesIDField(inputDETable, validateMessages.GetMessage(ReferenceLineFeatures))
      End If

      Return validateMessages
    End Function

    Public Sub Execute(ByVal paramvalues As IArray, ByVal trackcancel As ITrackCancel, ByVal envMgr As IGPEnvironmentManager, ByVal messages As IGPMessages) Implements IGPFunction.Execute
      Try
        ' VALIDATE OUR VALUES, INITIALIZE UTILITIES
        Dim validateMessages As IGPMessages = Validate(paramvalues, False, envMgr)
        If CType(validateMessages, IGPMessage).IsError() Then
          messages.AddError(1, "Validate failed")
          Return
        End If

        Dim gpUtils As IGPUtilities = New GPUtilities

        ' OPEN INPUT DATASETS (UNPACK VALUES)

        Dim inputTableAsITable As ITable
        Dim inputLineFeatures As IFeatureClass

        Dim gpParam As IGPParameter = CType(paramvalues.Element(InputTable), IGPParameter)
        Dim inputTableValue As IGPValue = gpUtils.UnpackGPValue(gpParam)
        Dim dataset As IDataset = gpUtils.OpenDataset(inputTableValue)

        If dataset IsNot Nothing Then
          inputTableAsITable = CType(dataset, ITable)
        Else
          messages.AddError(1, "Could not open input table.")
          Return
        End If

        gpParam = CType(paramvalues.Element(ReferenceLineFeatures), IGPParameter)
        Dim inputFeaturesValue As IGPValue = gpUtils.UnpackGPValue(gpParam)
        dataset = gpUtils.OpenDataset(inputFeaturesValue)

        If dataset IsNot Nothing Then
          inputLineFeatures = CType(dataset, IFeatureClass)
        Else
          messages.AddError(1, "Could not open input line features.")
          Return
        End If

        ' CHECK FOR INDEX
        ' check if streets table is indexed by ID and add a GPWarning message if not

        Dim indexEnum As IEnumIndex = inputLineFeatures.Indexes.FindIndexesByFieldName(LinesIDFieldName)
        indexEnum.Reset()
        Dim index As IIndex = indexEnum.Next()
        While index IsNot Nothing
          If index.Fields.FieldCount <> 1 Then
            Continue While
          Else
            Exit While
          End If
          index = indexEnum.Next()
        End While

        If index Is Nothing Then
          messages.AddWarning("Warning: " + LinesIDFieldName + " is not indexed.")
        End If

        ' TODO: check if output exists and raise error or delete depending
        '       on overwrite outputs geoprocessing environment info

        ' CREATE OUTPUT DATASETS

        gpParam = CType(paramvalues.Element(OutFeatureClassName), IGPParameter)
        Dim outputNameValue As IGPValue = gpUtils.UnpackGPValue(gpParam)
        Dim outputName As String = CType(outputNameValue, IGPString).Value

        Dim outputSignsFeatureClass As IFeatureClass = SignpostUtilities.CreateSignsFeatureClass(inputLineFeatures, outputName)

        gpParam = CType(paramvalues.Element(OutStreetsTableName), IGPParameter)
        outputNameValue = gpUtils.UnpackGPValue(gpParam)
        outputName = CType(outputNameValue, IGPString).Value

        Dim outputSignDetailTable As ITable = SignpostUtilities.CreateSignsDetailTable(inputLineFeatures, outputName)

        ' POPULATE DATA

        PopulateData(inputTableAsITable, inputLineFeatures, outputSignsFeatureClass, outputSignDetailTable, messages, trackcancel)
      Catch e As COMException
        messages.AddError(1, e.Message)
      End Try
    End Sub

    Public ReadOnly Property DisplayName() As String Implements IGPFunction.DisplayName
      Get
        Return "Import Dynamap Signs"
      End Get
    End Property

    Public ReadOnly Property MetadataFile() As String Implements IGPFunction.MetadataFile
      Get
        Return "ImportDynamapSignsHelp.xml"
      End Get
    End Property

    Public ReadOnly Property FullName() As IName Implements IGPFunction.FullName
      Get
        Dim functionFactory As IGPFunctionFactory = New SignpostGPFunctionFactory
        Return CType(functionFactory.GetFunctionName(Me.Name), IName)
      End Get
    End Property

    Public Function IsLicensed() As Boolean Implements IGPFunction.IsLicensed
      Return True
    End Function

    Public ReadOnly Property DialogCLSID() As UID Implements IGPFunction.DialogCLSID
      Get
        Return Nothing
      End Get
    End Property

    Public ReadOnly Property Name() As String Implements IGPFunction.Name
      Get
        Return "ImportDynamapSigns"
      End Get
    End Property

    Public ReadOnly Property HelpContext() As Integer Implements IGPFunction.HelpContext
      Get
        Return 0
      End Get
    End Property

    Public ReadOnly Property HelpFile() As String Implements IGPFunction.HelpFile
      Get
        Return Nothing
      End Get
    End Property

    Public Function GetRenderer(ByVal gpParam As IGPParameter) As Object Implements IGPFunction.GetRenderer
      Return Nothing
    End Function
#End Region

    Private Function CheckForTableFields(ByVal inputDETable As IDETable, ByVal gpMessage As IGPMessage) As Boolean
      Dim fields As IFields = inputDETable.Fields
      Dim fieldIndex As Integer

      For i As Integer = 0 To FieldNames.Length - 2
        fieldIndex = fields.FindField(FieldNames(i))
        If fieldIndex = -1 Then
          gpMessage.Type = esriGPMessageType.esriGPMessageTypeError
          gpMessage.Description = "Field named " + FieldNames(i) + " not found."
          Return False
        End If

        If fields.Field(fieldIndex).Type <> FieldTypes(i) Then
          gpMessage.Type = esriGPMessageType.esriGPMessageTypeError
          gpMessage.Description = "Field named " + FieldNames(i) + " is not the expected type."
          Return False
        End If
      Next i
      Return True
    End Function

    Private Function CheckForLinesIDField(ByVal inputDETable As IDETable, ByVal gpMessage As IGPMessage) As Boolean
      Dim fields As IFields = inputDETable.Fields
      Dim fieldIndex As Integer = fields.FindField(LinesIDFieldName)
      If fieldIndex = -1 Then
        gpMessage.Type = esriGPMessageType.esriGPMessageTypeError
        gpMessage.Description = "Field named " + LinesIDFieldName + " not found."
        Return False
      End If

      If fields.Field(fieldIndex).Type <> LinesIDFieldType Then
        gpMessage.Type = esriGPMessageType.esriGPMessageTypeError
        gpMessage.Description = "Field named " + LinesIDFieldName + " is not the expected type."
        Return False
      End If

      Return True
    End Function

    Private Sub PopulateData(ByVal inputSignsTable As ITable, ByVal inputLineFeatures As IFeatureClass, _
         ByVal outputSignFeatures As IFeatureClass, ByVal outputSignDetailTable As ITable, _
         ByVal messages As IGPMessages, ByVal trackcancel As ITrackCancel)
      'FIND FIELDS
      '(Validate checked that these exist)
      Dim inputTableFields As IFields = inputSignsTable.Fields
      Dim inExitIDFI As Integer = inputTableFields.FindField("EXIT_ID")
      Dim inSequenceFI As Integer = inputTableFields.FindField("SEQUENCE")
      Dim inFromIDFI As Integer = inputTableFields.FindField("FROM_ID")
      Dim inExitNumFI As Integer = inputTableFields.FindField("EXIT_NUM")
      Dim inToIDFI As Integer = inputTableFields.FindField("TO_ID")
      Dim inToNameFI As Integer = inputTableFields.FindField("TO_NAME")
      Dim inDirectionFI As Integer = inputTableFields.FindField("DIRECTION")
      Dim inToLocaleFI As Integer = inputTableFields.FindField("TO_LOCALE")
      Dim inAccessFI As Integer = inputTableFields.FindField("ACCESS")

      ' Find output fields (we just made these)

      Dim outputSignFeatureFields As IFields = outputSignFeatures.Fields
      Dim outExitNameFI As Integer = outputSignFeatureFields.FindField("ExitName")

      Dim outBranchXFI() As Integer = New Integer(SignpostUtilities.MaxBranchCount) {}
      Dim outBranchXDirFI() As Integer = New Integer(SignpostUtilities.MaxBranchCount) {}
      Dim outBranchXLngFI() As Integer = New Integer(SignpostUtilities.MaxBranchCount) {}
      Dim outTowardXFI() As Integer = New Integer(SignpostUtilities.MaxBranchCount) {}
      Dim outTowardXLngFI() As Integer = New Integer(SignpostUtilities.MaxBranchCount) {}

      Dim indexString As String

      For i As Integer = 0 To SignpostUtilities.MaxBranchCount - 1
        indexString = Convert.ToString(i)

        outBranchXFI(i) = outputSignFeatureFields.FindField("Branch" + indexString)
        outBranchXDirFI(i) = outputSignFeatureFields.FindField("Branch" + indexString + "Dir")
        outBranchXLngFI(i) = outputSignFeatureFields.FindField("Branch" + indexString + "Lng")
        outTowardXFI(i) = outputSignFeatureFields.FindField("Toward" + indexString)
        outTowardXLngFI(i) = outputSignFeatureFields.FindField("Toward" + indexString + "Lng")
      Next i

      Dim outputTableFields As IFields = outputSignDetailTable.Fields
      Dim outTblSignpostIDFI As Integer = outputTableFields.FindField("SignpostID")
      Dim outTblSequenceFI As Integer = outputTableFields.FindField("Sequence")
      Dim outTblEdgeFCIDFI As Integer = outputTableFields.FindField("EdgeFCID")
      Dim outTblEdgeFIDFI As Integer = outputTableFields.FindField("EdgeFID")
      Dim outTblEdgeFrmPosFI As Integer = outputTableFields.FindField("EdgeFrmPos")
      Dim outTblEdgeToPosFI As Integer = outputTableFields.FindField("EdgeToPos")

      ' Find ID fields on referenced lines

      Dim inLinesOIDFI As Integer = inputLineFeatures.FindField(inputLineFeatures.OIDFieldName)
      Dim inLinesUserIDFI As Integer = inputLineFeatures.FindField(LinesIDFieldName)
      Dim inLinesShapeFI As Integer = inputLineFeatures.FindField(inputLineFeatures.ShapeFieldName)


      ' Fetch all line features referenced by the input signs table.  We do the
      ' "join" this hard way to support all data sources in the sample. 
      ' Also, for large numbers of sign records, this strategy of fetching all
      ' related features and holding them in RAM could be a problem.  To fix
      ' this, one could process the input sign records in batches.

      Dim lineFeaturesList As System.Collections.Hashtable = SignpostUtilities.FillFeatureCache(inputSignsTable, inFromIDFI, inToIDFI, inputLineFeatures, LinesIDFieldName, trackcancel)

      ' Create output feature/row buffers

      Dim featureBuffer As IFeatureBuffer = outputSignFeatures.CreateFeatureBuffer()
      Dim feature As IFeature = CType(featureBuffer, IFeature)
      Dim featureRowBuffer As IRowBuffer = featureBuffer

      Dim tableBuffer As IRowBuffer = outputSignDetailTable.CreateRowBuffer()
      Dim row As IRow = CType(tableBuffer, IRow)
      Dim tableRowBuffer As IRowBuffer = tableBuffer

      ' Create insert cursors.

      Dim featureInsertCursor As IFeatureCursor = outputSignFeatures.Insert(True)
      Dim tableInsertCursor As ICursor = outputSignDetailTable.Insert(True)

      ' Create input cursor for the signs table we are importing

      Dim tableSort As ITableSort = New TableSort
      tableSort.Fields = "EXIT_ID, SEQUENCE"
      tableSort.Ascending("EXIT_ID") = True
      tableSort.Ascending("SEQUENCE") = True
      tableSort.QueryFilter = Nothing
      tableSort.Table = inputSignsTable
      tableSort.Sort(Nothing)
      Dim inputCursor As ICursor = tableSort.Rows

      Dim inputTableRow As IRow
      Dim numInput As Integer = 0
      Dim numOutput As Integer = 0
      Dim inSequenceValue As Integer
      Dim fromIDVal As Long, toIDVal As Long

      Dim nextBranchNum As Integer = -1, nextTowardNum As Integer = -1

      ' these are initialized to prevent uninitialized variable compiler error

      Dim fromFeatureData As SignpostUtilities.FeatureData = New SignpostUtilities.FeatureData(-1, Nothing)
      Dim toFeatureData As SignpostUtilities.FeatureData = New SignpostUtilities.FeatureData(-1, Nothing)

      Dim newOID As Object, accessVal As Object
      Dim outputText As String, outputDirText As String

      Dim fromEdgeCurve As ICurve, toEdgeCurve As ICurve
      Dim fromEdgeStart As IPoint, fromEdgeEnd As IPoint, toEdgeStart As IPoint, toEdgeEnd As IPoint

      Dim refLinesFCID As Integer = inputLineFeatures.ObjectClassID
      Dim outputSignGeometry As IGeometry

      Dim lastSignID As Double = -1.0, currentSignID As Double = -1.0
      Dim fromEdgeFromPos As Double = 0.0
      Dim fromEdgeToPos As Double = 1.0
      Dim toEdgeFromPos As Double = 0.0
      Dim toEdgeToPos As Double = 1.0

      inputTableRow = inputCursor.NextRow()
      While inputTableRow IsNot Nothing
        currentSignID = Convert.ToInt32(inputTableRow.Value(inExitIDFI))

        ' If we have a new sign ID, we need to insert the signpost feature in progress
        ' and write the detail records.
        ' (identical code is also after the while loop for the last sign record)

        If currentSignID <> lastSignID And lastSignID <> -1 Then
          ' clean up unused parts of the row and pack toward/branch items

          SignpostUtilities.CleanUpSignpostFeatureValues(featureBuffer, nextBranchNum - 1, nextTowardNum - 1, _
            outBranchXFI, outBranchXDirFI, outBranchXLngFI, _
            outTowardXFI, outTowardXLngFI)

          ' save sign feature record

          newOID = featureInsertCursor.InsertFeature(featureBuffer)

          ' set streets table values

          tableRowBuffer.Value(outTblSignpostIDFI) = newOID
          tableRowBuffer.Value(outTblSequenceFI) = 1
          tableRowBuffer.Value(outTblEdgeFCIDFI) = refLinesFCID
          tableRowBuffer.Value(outTblEdgeFIDFI) = fromFeatureData.OID
          tableRowBuffer.Value(outTblEdgeFrmPosFI) = fromEdgeFromPos
          tableRowBuffer.Value(outTblEdgeToPosFI) = fromEdgeToPos

          ' insert first detail record

          tableInsertCursor.InsertRow(tableRowBuffer)

          tableRowBuffer.Value(outTblSequenceFI) = 2
          tableRowBuffer.Value(outTblEdgeFIDFI) = toFeatureData.OID
          tableRowBuffer.Value(outTblEdgeFrmPosFI) = toEdgeFromPos
          tableRowBuffer.Value(outTblEdgeToPosFI) = toEdgeToPos

          ' insert second detail record

          tableInsertCursor.InsertRow(tableRowBuffer)

          numOutput += 1
          If (numOutput Mod 100) = 0 Then
            ' check for user cancel

            If Not trackcancel.Continue() Then
              Throw (New COMException("Function cancelled."))
            End If
          End If
        End If

        lastSignID = currentSignID

        inSequenceValue = Convert.ToInt32(inputTableRow.Value(inSequenceFI))
        If inSequenceValue = 1 Then
          ' We are starting a sequence of records for a new sign.
          ' nextBranchNum and nextTowardNum keep track of which branch and
          ' toward item numbers we have used and are not necessarily the same
          ' as inSequenceValue.

          nextBranchNum = 0
          nextTowardNum = 0

          fromIDVal = Convert.ToInt64(inputTableRow.Value(inFromIDFI))
          toIDVal = Convert.ToInt64(inputTableRow.Value(inToIDFI))

          ' If the signpost references a line feature that is not in the lines
          ' feature class, add a warning message and keep going.
          ' Only warn for the first 100 not found.

          numInput += 1

          Try
            fromFeatureData = CType(lineFeaturesList(fromIDVal), SignpostUtilities.FeatureData)
            toFeatureData = CType(lineFeaturesList(toIDVal), SignpostUtilities.FeatureData)
          Catch ex As Exception
            If (numInput - numOutput < 100) Then
              messages.AddWarning("Line feature not found for sign with FromID: " + _
                Convert.ToString(fromIDVal) + ", ToID: " + Convert.ToString(toIDVal))
            End If

            inputTableRow = inputCursor.NextRow()
            Continue While
          End Try

          ' To set from and to position in the detail table and to construct geometry
          ' for the output signs feature class, we need see where and 
          ' if the two edge features connect to figure out their digitized direction.

          fromEdgeCurve = CType(fromFeatureData.feature, ICurve)
          toEdgeCurve = CType(toFeatureData.feature, ICurve)

          fromEdgeStart = fromEdgeCurve.FromPoint
          fromEdgeEnd = fromEdgeCurve.ToPoint
          toEdgeStart = toEdgeCurve.FromPoint
          toEdgeEnd = toEdgeCurve.ToPoint

          fromEdgeFromPos = 0.0
          fromEdgeToPos = 1.0
          toEdgeFromPos = 0.0
          toEdgeToPos = 1.0

          ' flip the from edge?

          If EqualPoints(fromEdgeStart, toEdgeStart) Or EqualPoints(fromEdgeStart, toEdgeEnd) Then
            fromEdgeFromPos = 1.0
            fromEdgeToPos = 0.0
          End If

          ' flip the to edge?

          If EqualPoints(toEdgeEnd, fromEdgeStart) Or EqualPoints(toEdgeEnd, fromEdgeEnd) Then
            toEdgeFromPos = 1.0
            toEdgeToPos = 0.0
          End If

          ' set sign feature values

          ' construct shape - the only purpose of the shape is visualization and it can be null

          outputSignGeometry = MakeSignGeometry(fromEdgeCurve, toEdgeCurve, fromEdgeFromPos = 1.0, toEdgeFromPos = 1.0)

          featureBuffer.Shape = outputSignGeometry

          featureBuffer.Value(outExitNameFI) = inputTableRow.Value(inExitNumFI)
        End If

        ' Populate branch or toward item depending upon the value in the ACCESS field:
        ' if ACCESS == "D" (direct), populate branch(s)
        ' if ACCESS == "I" (direct), populate a toward(s)

        accessVal = inputTableRow.Value(inAccessFI)

        If CType(accessVal, String) = "D" Then
          ' check for schema overflow
          If nextBranchNum > SignpostUtilities.MaxBranchCount - 1 Then
            inputTableRow = inputCursor.NextRow()
            Continue While
          End If

          outputText = CType(inputTableRow.Value(inToNameFI), String).Trim()
          If outputText.Length > 0 Then

            ' set values
            featureBuffer.Value(outBranchXFI(nextBranchNum)) = outputText
            featureBuffer.Value(outBranchXDirFI(nextBranchNum)) = inputTableRow.Value(inDirectionFI)
            featureBuffer.Value(outBranchXLngFI(nextBranchNum)) = "en"

            ' get ready for next branch
            nextBranchNum += 1
          End If

          ' there are rare cases when we'll have Access == D (TO_NAME) AND data for TO_LOCALE
          outputText = CType(inputTableRow.Value(inToLocaleFI), String).Trim()
          If outputText.Length > 0 Then
            ' set values
            featureBuffer.Value(outBranchXFI(nextBranchNum)) = outputText
            featureBuffer.Value(outBranchXDirFI(nextBranchNum)) = Nothing
            featureBuffer.Value(outBranchXLngFI(nextBranchNum)) = "en"

            ' get ready for next branch
            nextBranchNum += 1
          End If
        ElseIf CType(accessVal, String) = "I" Then
          ' check for schema overflow
          If nextTowardNum > SignpostUtilities.MaxBranchCount - 1 Then
            inputTableRow = inputCursor.NextRow()
            Continue While
          End If

          outputText = CType(inputTableRow.Value(inToNameFI), String).Trim()
          If outputText.Length > 0 Then
            outputDirText = CType(inputTableRow.Value(inDirectionFI), String).Trim()
            If outputDirText.Length > 0 Then
              outputText += " "
              outputText += outputDirText
            End If

            ' set values
            featureBuffer.Value(outTowardXFI(nextTowardNum)) = outputText
            featureBuffer.Value(outTowardXLngFI(nextTowardNum)) = "en"

            ' get ready for next toward
            nextTowardNum += 1
          End If

          ' there are rare cases when we'll have Access == I (TO_LOCALE) AND data for TO_NAME
          outputText = CType(inputTableRow.Value(inToLocaleFI), String).Trim()
          If outputText.Length > 0 Then
            ' set values
            featureBuffer.Value(outTowardXFI(nextTowardNum)) = outputText
            featureBuffer.Value(outTowardXLngFI(nextTowardNum)) = "en"

            ' get ready for next toward
            nextTowardNum += 1
          End If
        Else
          inputTableRow = inputCursor.NextRow()
          Continue While    ' not expected
        End If

        inputTableRow = inputCursor.NextRow()
      End While
      ' each input table record

      ' add the last signpost feature and detail records (same code as above)

      ' clean up unused parts of the row

      SignpostUtilities.CleanUpSignpostFeatureValues(featureBuffer, nextBranchNum - 1, nextTowardNum - 1, _
        outBranchXFI, outBranchXDirFI, outBranchXLngFI, _
        outTowardXFI, outTowardXLngFI)

      ' save sign feature record

      newOID = featureInsertCursor.InsertFeature(featureBuffer)

      ' set streets table values

      tableRowBuffer.Value(outTblSignpostIDFI) = newOID
      tableRowBuffer.Value(outTblSequenceFI) = 1
      tableRowBuffer.Value(outTblEdgeFCIDFI) = refLinesFCID
      tableRowBuffer.Value(outTblEdgeFIDFI) = fromFeatureData.OID
      tableRowBuffer.Value(outTblEdgeFrmPosFI) = fromEdgeFromPos
      tableRowBuffer.Value(outTblEdgeToPosFI) = fromEdgeToPos

      ' insert first detail record

      tableInsertCursor.InsertRow(tableRowBuffer)

      tableRowBuffer.Value(outTblSequenceFI) = 2
      tableRowBuffer.Value(outTblEdgeFIDFI) = toFeatureData.OID
      tableRowBuffer.Value(outTblEdgeFrmPosFI) = toEdgeFromPos
      tableRowBuffer.Value(outTblEdgeToPosFI) = toEdgeToPos

      ' insert second detail record

      tableInsertCursor.InsertRow(tableRowBuffer)

      numOutput += 1

      ' add a summary message

      messages.AddMessage(Convert.ToString(numOutput) + " of " + Convert.ToString(numInput) + " signposts added.")

      Return
    End Sub

    Private Function EqualPoints(ByVal p1 As IPoint, ByVal p2 As IPoint) As Boolean
      Return ((p1.X = p2.X) And (p1.Y = p2.Y))
    End Function

    Private Function MakeSignGeometry(ByVal fromEdgeCurve As ICurve, ByVal toEdgeCurve As ICurve, _
     ByVal reverseFromEdge As Boolean, ByVal reverseToEdge As Boolean) As IGeometry
      Dim resultSegments As ISegmentCollection = New Polyline
      Dim fromResultCurve As ICurve, toResultCurve As ICurve

      ' add the part from the first line

      If reverseFromEdge Then
        fromEdgeCurve.GetSubcurve(0.0, 0.25, True, fromResultCurve)
        fromResultCurve.ReverseOrientation()
      Else
        fromEdgeCurve.GetSubcurve(0.75, 1.0, True, fromResultCurve)
      End If

      resultSegments.AddSegmentCollection(CType(fromResultCurve, ISegmentCollection))


      ' add the part from the second line

      If reverseToEdge Then
        toEdgeCurve.GetSubcurve(0.75, 1.0, True, toResultCurve)
        toResultCurve.ReverseOrientation()
      Else
        toEdgeCurve.GetSubcurve(0.0, 0.25, True, toResultCurve)
      End If

      resultSegments.AddSegmentCollection(CType(toResultCurve, ISegmentCollection))

      Return CType(resultSegments, IGeometry)
    End Function
  End Class
End Namespace