Import signposts
ImportMultiNetSignsFunction.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 System.Collections

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

Namespace GPImportSignpostFunctions
  <Guid("F5AB9C9C-8193-40fa-99B2-369545BCAAEE")> _
  <ClassInterface(ClassInterfaceType.None)> _
  <ProgId("GPImportSignpostFunctions.ImportMultiNetSignsFunction")> _
  Public Class ImportMultiNetSignsFunction
    Implements IGPFunction

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

    ' field names and types
    Private Shared ReadOnly SIFieldNames() As String = New String() _
    {"ID", "SEQNR", "DESTSEQ", "INFOTYP", "RNPART", "TXTCONT", "TXTCONTLC", "CONTYP", "AMBIG"}

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

    Private Shared ReadOnly SPFieldNames() As String = New String() _
    {"ID", "SEQNR", "TRPELID", "TRPELTYP"}

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

    Private Shared ReadOnly LinesIDFieldName As String = "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_sign_information_table

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

        gpParamArray.Add(paramEdit)

        ' 2 - input_sign_path_table

        paramEdit = New GPParameter
        paramEdit.DataType = New DETableType
        paramEdit.Value = New DETable
        paramEdit.Direction = esriGPParameterDirection.esriGPParameterDirectionInput
        paramEdit.DisplayName = "Input Sign Path Table"
        paramEdit.Enabled = True
        paramEdit.Name = "input_sign_path_table"
        paramEdit.ParameterType = esriGPParameterType.esriGPParameterTypeRequired

        gpParamArray.Add(paramEdit)

        ' 3 - 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)

        ' 4 - 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)

        ' 5 - 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 SI table has the expected fields

      Dim gpParam As IGPParameter = CType(paramvalues.Element(InputSITable), 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, SIFieldNames, SIFieldTypes, validateMessages.GetMessage(InputSITable))
      End If

      ' Verify chosen input SP table has the expected fields

      gpParam = CType(paramvalues.Element(InputSPTable), IGPParameter)
      tableValue = 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, SPFieldNames, SPFieldTypes, validateMessages.GetMessage(InputSPTable))
      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 inputSITableAsITable As ITable
        Dim inputSPTableAsITable As ITable
        Dim inputLineFeatures As IFeatureClass

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

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

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

        If dataset IsNot Nothing Then
          inputSPTableAsITable = CType(dataset, ITable)
        Else
          messages.AddError(1, "Could not open input sign path 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(inputSITableAsITable, inputSPTableAsITable, 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 MultiNet Signs"
      End Get
    End Property

    Public ReadOnly Property MetadataFile() As String Implements IGPFunction.MetadataFile
      Get
        Return "ImportMultiNetSignsHelp.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 "ImportMultiNetSigns"
      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 pParam As IGPParameter) As Object Implements IGPFunction.GetRenderer
      Return Nothing
    End Function
#End Region

    Private Function CheckForTableFields(ByVal inputDETable As IDETable, ByVal fieldNames As String(), ByVal fieldTypes As esriFieldType(), _
       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
      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 inputSignInformationTable As ITable, ByVal inputSignPathTable 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 inputSITableFields As IFields = inputSignInformationTable.Fields
      Dim inSiIdFI As Integer = inputSITableFields.FindField("ID")
      Dim inSiInfoTypFI As Integer = inputSITableFields.FindField("INFOTYP")
      Dim inSiTxtContFI As Integer = inputSITableFields.FindField("TXTCONT")
      Dim inSiConTypFI As Integer = inputSITableFields.FindField("CONTYP")
      Dim inputSPTableFields As IFields = inputSignPathTable.Fields
      Dim inSpIdFI As Integer = inputSPTableFields.FindField("ID")
      Dim inSpSeqNrFI As Integer = inputSPTableFields.FindField("SEQNR")
      Dim inSpTrpElIdFI As Integer = inputSPTableFields.FindField("TRPELID")
      Dim inSpTrpElTypFI As Integer = inputSPTableFields.FindField("TRPELTYP")

      ' 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

      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(inputSignPathTable, inSpTrpElIdFI, -1, 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 cursors for the sign tables we are importing

      Dim spTableSort As ITableSort = New TableSort()
      spTableSort.Fields = "ID, SEQNR"
      spTableSort.Ascending("ID") = True
      spTableSort.Ascending("SEQNR") = True
      spTableSort.QueryFilter = Nothing
      spTableSort.Table = inputSignPathTable
      spTableSort.Sort(Nothing)
      Dim spInputCursor As ICursor = spTableSort.Rows

      Dim siTableSort As ITableSort = New TableSort()
      siTableSort.Fields = "ID, SEQNR, DESTSEQ, RNPART"
      siTableSort.Ascending("ID") = True
      siTableSort.Ascending("SEQNR") = True
      siTableSort.Ascending("DESTSEQ") = True
      siTableSort.Ascending("RNPART") = True
      siTableSort.QueryFilter = Nothing
      siTableSort.Table = inputSignInformationTable
      siTableSort.Sort(Nothing)
      Dim siInputCursor As ICursor = siTableSort.Rows

      Dim inputSpTableRow As IRow
      Dim inputSiTableRow As IRow
      Dim currentID As Long = -1, loopSpID As Long, loopSiID As Long

      Dim numOutput As Integer = 0
      Dim numInput As Integer = 0
      Dim fetchFeatureDataSucceeded As Boolean
      Dim idVals As ArrayList = New System.Collections.ArrayList(2)
      Dim edgesData As ArrayList = New System.Collections.ArrayList(2)
      Dim reverseEdge As ArrayList = New System.Collections.ArrayList(2)
      Dim currentFeatureData As SignpostUtilities.FeatureData = New SignpostUtilities.FeatureData(-1, Nothing)
      Dim earlierEdgeCurve As ICurve, laterEdgeCurve As ICurve
      Dim earlierEdgeStart As IPoint, earlierEdgeEnd As IPoint
      Dim laterEdgeStart As IPoint, laterEdgeEnd As IPoint

      Dim nextBranchNum As Integer = -1, nextTowardNum As Integer = -1
      Dim infoTypText As String, txtContText As String
      Dim directionalAsText As String = ""
      Dim towardText As String
      Dim conTypVal As Integer

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

      inputSpTableRow = spInputCursor.NextRow()
      inputSiTableRow = siInputCursor.NextRow()
      While inputSpTableRow IsNot Nothing And inputSiTableRow IsNot Nothing
        currentID = Convert.ToInt64(inputSpTableRow.Value(inSpIdFI))

        ' fetch the edge ID values from the SP table for the current sign ID

        idVals.Clear()
        While (True)
          idVals.Add(Convert.ToInt64(inputSpTableRow.Value(inSpTrpElIdFI)))

          inputSpTableRow = spInputCursor.NextRow()
          If (inputSpTableRow Is Nothing) Then
            Exit While    ' we've reached the end of the SP table
          End If

          loopSpID = Convert.ToInt64(inputSpTableRow.Value(inSpIdFI))
          If (loopSpID <> currentID) Then
            Exit While    ' we're now on a new ID value          
          End If
        End While

        numInput += 1

        ' fetch the FeatureData for each of these edges

        edgesData.Clear()
        fetchFeatureDataSucceeded = True
        For Each currentIDVal As Long In idVals
          Try
            currentFeatureData = CType(lineFeaturesList(currentIDVal), SignpostUtilities.FeatureData)
            edgesData.Add(currentFeatureData)
          Catch
            fetchFeatureDataSucceeded = False
            If (numInput - numOutput < 100) Then
              messages.AddWarning("Line feature not found for sign with ID: " + _
                Convert.ToString(currentIDVal))
            End If
            Exit For
          End Try
        Next
        If Not fetchFeatureDataSucceeded Then Continue While

        ' determine the orientation for each of these edges

        reverseEdge.Clear()
        For i As Integer = 1 To edgesData.Count - 1
          ' get the endpoints of the earlier curve
          currentFeatureData = CType(edgesData(i - 1), SignpostUtilities.FeatureData)
          earlierEdgeCurve = CType(currentFeatureData.feature, ICurve)
          earlierEdgeStart = earlierEdgeCurve.FromPoint
          earlierEdgeEnd = earlierEdgeCurve.ToPoint

          ' get the endpoints of the later curve
          currentFeatureData = CType(edgesData(i), SignpostUtilities.FeatureData)
          laterEdgeCurve = CType(currentFeatureData.feature, ICurve)
          laterEdgeStart = laterEdgeCurve.FromPoint
          laterEdgeEnd = laterEdgeCurve.ToPoint

          ' determine the orientation of the first edge
          ' (first edge is reversed if its Start point is coincident with either point of the second edge)
          If (i = 1) Then
            reverseEdge.Add(EqualPoints(earlierEdgeStart, laterEdgeStart) Or EqualPoints(earlierEdgeStart, laterEdgeEnd))
          End If

          ' determine the orientation of the i'th edge
          ' (i'th edge is reversed if its End point is coincident with either point of the previous edge)
          reverseEdge.Add(EqualPoints(laterEdgeEnd, earlierEdgeStart) Or EqualPoints(laterEdgeEnd, earlierEdgeEnd))
        Next i

        ' write out the sign geometry to the featureBuffer

        outputSignGeometry = MakeSignGeometry(edgesData, reverseEdge)
        featureBuffer.Shape = outputSignGeometry

        ' fetch the signpost information from the SI table for the current sign ID

        nextBranchNum = 0
        nextTowardNum = 0
        featureBuffer.Value(outExitNameFI) = ""

        While inputSiTableRow IsNot Nothing
          loopSiID = Convert.ToInt64(inputSiTableRow.Value(inSiIdFI))
          If (loopSiID < currentID) Then
            inputSiTableRow = siInputCursor.NextRow()
            Continue While
          ElseIf (loopSiID > currentID) Then
            Exit While    ' we're now on a new ID value
          End If

          infoTypText = CType(inputSiTableRow.Value(inSiInfoTypFI), String)
          txtContText = CType(inputSiTableRow.Value(inSiTxtContFI), String)
          conTypVal = Convert.ToInt32(inputSiTableRow.Value(inSiConTypFI))

          Select Case infoTypText
            Case "4E"   ' exit number
              featureBuffer.Value(outExitNameFI) = txtContText

            Case "9D", "4I"     ' place name or other destination

              ' check for schema overflow
              If (nextTowardNum > SignpostUtilities.MaxBranchCount - 1) Then
                inputSiTableRow = siInputCursor.NextRow()
                Continue While
              End If

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

              ' get ready for next toward
              nextTowardNum += 1

            Case "6T", "RN"     ' street name or route number

              If (conTypVal = 2) Then     ' toward
                ' check for schema overflow
                If (nextTowardNum > SignpostUtilities.MaxBranchCount - 1) Then
                  inputSiTableRow = siInputCursor.NextRow()
                  Continue While
                End If

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

                ' get ready for next toward
                nextTowardNum += 1
              Else  ' branch
                ' check for schema overflow
                If (nextBranchNum > SignpostUtilities.MaxBranchCount - 1) Then
                  inputSiTableRow = siInputCursor.NextRow()
                  Continue While
                End If

                ' set values
                featureBuffer.Value(outBranchXFI(nextBranchNum)) = txtContText
                featureBuffer.Value(outBranchXDirFI(nextBranchNum)) = ""
                featureBuffer.Value(outBranchXLngFI(nextBranchNum)) = "en"

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

            Case "7G"   ' route directional

              ' convert the directional value to a string
              Select Case txtContText
                Case "1"
                  directionalAsText = "N"
                Case "2"
                  directionalAsText = "E"
                Case "3"
                  directionalAsText = "S"
                Case "4"
                  directionalAsText = "W"
              End Select

              If (conTypVal = 2) Then     ' toward
                ' check for schema underflow
                If (nextTowardNum = 0) Then
                  inputSiTableRow = siInputCursor.NextRow()
                  Continue While
                End If

                ' append directional text to the previous toward text value
                towardText = CType(featureBuffer.Value(outTowardXFI(nextTowardNum - 1)), String)
                towardText = towardText + " " + directionalAsText
                featureBuffer.Value(outTowardXFI(nextTowardNum - 1)) = towardText
              Else  ' branch
                ' check for schema underflow
                If (nextBranchNum = 0) Then
                  inputSiTableRow = siInputCursor.NextRow()
                  Continue While
                End If

                ' set value of the Dir field on the previous branch
                featureBuffer.Value(outBranchXDirFI(nextBranchNum - 1)) = directionalAsText
              End If

          End Select

          inputSiTableRow = siInputCursor.NextRow()

        End While  ' each SI table record

        ' 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(outTblEdgeFCIDFI) = refLinesFCID
        For i As Integer = 0 To edgesData.Count - 1
          currentFeatureData = CType(edgesData(i), SignpostUtilities.FeatureData)
          tableRowBuffer.Value(outTblSequenceFI) = i + 1
          tableRowBuffer.Value(outTblEdgeFIDFI) = currentFeatureData.OID
          If (CType(reverseEdge(i), Boolean)) Then
            tableRowBuffer.Value(outTblEdgeFrmPosFI) = 1.0
            tableRowBuffer.Value(outTblEdgeToPosFI) = 0.0
          Else
            tableRowBuffer.Value(outTblEdgeFrmPosFI) = 0.0
            tableRowBuffer.Value(outTblEdgeToPosFI) = 1.0
          End If

          ' insert detail record

          tableInsertCursor.InsertRow(tableRowBuffer)
        Next i

        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 While  ' outer while

      ' 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 edgesData As ArrayList, ByVal reverseEdge As ArrayList) As IGeometry
      Dim resultSegments As ISegmentCollection = New Polyline()
      Dim currentFeatureData As SignpostUtilities.FeatureData = New SignpostUtilities.FeatureData(-1, Nothing)
      Dim currentCurve As ICurve, resultCurve As ICurve

      For i As Integer = 0 To edgesData.Count - 1
        ' fetch the curve and reverse it as needed

        currentFeatureData = CType(edgesData(i), SignpostUtilities.FeatureData)
        currentCurve = CType(currentFeatureData.feature, ICurve)
        If (CType(reverseEdge(i), Boolean)) Then
          currentCurve.ReverseOrientation()
        End If

        ' trim the first and last geometries so that they only cover 25% of the street feature

        If (i = 0) Then
          currentCurve.GetSubcurve(0.75, 1.0, True, resultCurve)
        ElseIf (i = (edgesData.Count - 1)) Then
          currentCurve.GetSubcurve(0.0, 0.25, True, resultCurve)
        Else
          resultCurve = currentCurve
        End If

        ' add the resulting geometry to the collection

        resultSegments.AddSegmentCollection(CType(resultCurve, ISegmentCollection))
      Next i

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