Import signposts
ImportMultiNetSignsFunction.cs
// 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.
// 

using System;
using System.Runtime.InteropServices;
using System.Collections;

using ESRI.ArcGIS.esriSystem;
using ESRI.ArcGIS.Geodatabase;
using ESRI.ArcGIS.Geoprocessing;
using ESRI.ArcGIS.Geometry;
using ESRI.ArcGIS.Carto;

namespace GPImportSignpostFunctions
{
  /// <summary>
  /// Summary description for ImportNavStreetsSignsFunction.
  /// </summary>
  ///

  [Guid("267C4FBF-E89A-49b7-9727-FFFB5594733E")]
  [ClassInterface(ClassInterfaceType.None)]
  [ProgId("GPImportSignpostFunctions.ImportMultiNetSignsFunction")]

  class ImportMultiNetSignsFunction : IGPFunction
  {
    #region Constants
    // parameter index constants
    private const int InputSITable = 0;
    private const int InputSPTable = 1;
    private const int ReferenceLineFeatures = 2;
    private const int OutFeatureClassName = 3;
    private const int OutStreetsTableName = 4;

    // field names and types
    private static readonly string[] SIFieldNames = new string[] 
                                        { "ID", "SEQNR", "DESTSEQ", "INFOTYP", "RNPART",
                                          "TXTCONT", "TXTCONTLC", "CONTYP", "AMBIG" };
    private static readonly esriFieldType[] SIFieldTypes = new esriFieldType[]
                                        { esriFieldType.esriFieldTypeDouble,
                                          esriFieldType.esriFieldTypeInteger,
                                          esriFieldType.esriFieldTypeInteger,
                                          esriFieldType.esriFieldTypeString,
                                          esriFieldType.esriFieldTypeSmallInteger,
                                          esriFieldType.esriFieldTypeString,
                                          esriFieldType.esriFieldTypeString,
                                          esriFieldType.esriFieldTypeSmallInteger,
                                          esriFieldType.esriFieldTypeSmallInteger};
    private static readonly string[] SPFieldNames = new string[] { "ID", "SEQNR", "TRPELID", "TRPELTYP" };
    private static readonly esriFieldType[] SPFieldTypes = new esriFieldType[]
                                        { esriFieldType.esriFieldTypeDouble,
                                          esriFieldType.esriFieldTypeInteger,
                                          esriFieldType.esriFieldTypeDouble,
                                          esriFieldType.esriFieldTypeSmallInteger};
    private static readonly string LinesIDFieldName = "ID";
    private static readonly esriFieldType LinesIDFieldType = esriFieldType.esriFieldTypeDouble;
    #endregion

    private IArray m_parameters;

    public ImportMultiNetSignsFunction()
    {
    }
    #region IGPFunction Members

    public IArray ParameterInfo
    {
      // create and return the parameters for this function:
      // 1 - Input Sign Information Table
      // 2 - Input Sign Path Table
      // 3 - Input Street Features
      // 4 - Output Signpost Feature Class Name
      // 5 - Output Signpost Streets Table Name

      get
      {
        IArray paramArray = new ArrayClass();

        // 1 - input_sign_information_table

        IGPParameterEdit paramEdit = new GPParameterClass();
        paramEdit.DataType = new DETableTypeClass() as IGPDataType;
        paramEdit.Value = new DETableClass() as IGPValue;
        paramEdit.Direction = esriGPParameterDirection.esriGPParameterDirectionInput;
        paramEdit.DisplayName = "Input Sign Information Table";
        paramEdit.Enabled = true;
        paramEdit.Name = "input_sign_information_table";
        paramEdit.ParameterType = esriGPParameterType.esriGPParameterTypeRequired;

        paramArray.Add(paramEdit as object);

        // 2 - input_sign_path_table

        paramEdit = new GPParameterClass();
        paramEdit.DataType = new DETableTypeClass() as IGPDataType;
        paramEdit.Value = new DETableClass() as IGPValue;
        paramEdit.Direction = esriGPParameterDirection.esriGPParameterDirectionInput;
        paramEdit.DisplayName = "Input Sign Path Table";
        paramEdit.Enabled = true;
        paramEdit.Name = "input_sign_path_table";
        paramEdit.ParameterType = esriGPParameterType.esriGPParameterTypeRequired;

        paramArray.Add(paramEdit as object);

        // 3 - reference_street_features

        paramEdit = new GPParameterClass();
        paramEdit.DataType = new DEFeatureClassTypeClass() as IGPDataType;
        paramEdit.Value = new DEFeatureClass() as IGPValue;
        paramEdit.Direction = esriGPParameterDirection.esriGPParameterDirectionInput;
        paramEdit.DisplayName = "Input Street Features";
        paramEdit.Enabled = true;
        paramEdit.Name = "reference_street_features";
        paramEdit.ParameterType = esriGPParameterType.esriGPParameterTypeRequired;

        IGPFeatureClassDomain lineFeatureClassDomain = new GPFeatureClassDomainClass();
        lineFeatureClassDomain.AddType(esriGeometryType.esriGeometryLine);
        lineFeatureClassDomain.AddType(esriGeometryType.esriGeometryPolyline);

        paramEdit.Domain = lineFeatureClassDomain as IGPDomain;

        paramArray.Add(paramEdit as object);

        // 4 - out_feature_class_name

        paramEdit = new GPParameterClass();
        paramEdit.DataType = new GPStringTypeClass() as IGPDataType;
        IGPString stringVal = new GPStringClass();
        stringVal.Value = "Signpost";
        paramEdit.Value = stringVal as 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;

        paramArray.Add(paramEdit as object);

        // 5 - out_table_name

        paramEdit = new GPParameterClass();
        paramEdit.DataType = new GPStringTypeClass() as IGPDataType;
        stringVal = new GPStringClass();
        stringVal.Value = "SignpostSt";
        paramEdit.Value = stringVal as 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;

        paramArray.Add(paramEdit as object);

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

        return paramArray;
      }
    }

    public IGPMessages Validate(IArray paramvalues, bool updateValues, IGPEnvironmentManager envMgr)
    {

      // Create the GPUtilities Object

      IGPUtilities gpUtils = new GPUtilitiesClass();

      // Initialize a copy of our parameters

      if (m_parameters == null) m_parameters = ParameterInfo;

      // Call InternalValidate to check for required parameters

      IGPMessages validateMessages = gpUtils.InternalValidate(m_parameters, paramvalues, updateValues, true, envMgr);

      // Verify chosen input SI table has the expected fields

      IGPParameter gpParam = paramvalues.get_Element(InputSITable) as IGPParameter;
      IGPValue tableValue = gpUtils.UnpackGPValue(gpParam);

      // CheckForTableFields will report errors by modifying the relevant GPMessage

      if (!tableValue.IsEmpty())
      {
        IDETable inputTable = gpUtils.DecodeDETable(tableValue);
        CheckForTableFields(inputTable, SIFieldNames, SIFieldTypes, validateMessages.GetMessage(InputSITable));
      }

      // Verify chosen input SP table has the expected fields

      gpParam = paramvalues.get_Element(InputSPTable) as IGPParameter;
      tableValue = gpUtils.UnpackGPValue(gpParam);

      // CheckForTableFields will report errors by modifying the relevant GPMessage

      if (!tableValue.IsEmpty())
      {
        IDETable inputTable = gpUtils.DecodeDETable(tableValue);
        CheckForTableFields(inputTable, SPFieldNames, SPFieldTypes, validateMessages.GetMessage(InputSPTable));
      }

      // Verify chosen reference_line_features has expected id field

      gpParam = paramvalues.get_Element(ReferenceLineFeatures) as IGPParameter;
      IGPValue featureClassValue = gpUtils.UnpackGPValue(gpParam);

      if (!featureClassValue.IsEmpty())
      {
        IDETable inputTable = gpUtils.DecodeDETable(featureClassValue);
        CheckForLinesIDField(inputTable, validateMessages.GetMessage(ReferenceLineFeatures));
      }

      return validateMessages;
    }

    public void Execute(IArray paramvalues, ITrackCancel trackcancel,
              IGPEnvironmentManager envMgr, IGPMessages messages)
    {
      try
      {
        #region Validate our values, initialize utilities
        IGPMessages validateMessages = Validate(paramvalues, false, envMgr);
        if ((validateMessages as IGPMessage).IsError())
        {
          messages.AddError(1, "Validate failed");
          return;
        }

        IGPUtilities gpUtils = new GPUtilitiesClass();
        #endregion

        #region Open input datasets (unpack values)

        ITable inputSITable;
        ITable inputSPTable;
        IFeatureClass inputLineFeatures;

        IGPParameter gpParam = paramvalues.get_Element(InputSITable) as IGPParameter;
        IGPValue inputSITableValue = gpUtils.UnpackGPValue(gpParam);
        IDataset dataset = gpUtils.OpenDataset(inputSITableValue);

        if (dataset != null)
          inputSITable = dataset as ITable;
        else
        {
          messages.AddError(1, "Could not open input sign information table.");
          return;
        }

        gpParam = paramvalues.get_Element(InputSPTable) as IGPParameter;
        IGPValue inputSPTableValue = gpUtils.UnpackGPValue(gpParam);
        dataset = gpUtils.OpenDataset(inputSPTableValue);

        if (dataset != null)
          inputSPTable = dataset as ITable;
        else
        {
          messages.AddError(1, "Could not open input sign path table.");
          return;
        }

        gpParam = paramvalues.get_Element(ReferenceLineFeatures) as IGPParameter;
        IGPValue inputFeaturesValue = gpUtils.UnpackGPValue(gpParam);
        dataset = gpUtils.OpenDataset(inputFeaturesValue);

        if (dataset != null)
          inputLineFeatures = dataset as IFeatureClass;
        else
        {
          messages.AddError(1, "Could not open input line features.");
          return;
        }
        #endregion

        #region Check for index
        // check if streets table is indexed by ID and add a GPWarning message if not

        IEnumIndex indexEnum = inputLineFeatures.Indexes.FindIndexesByFieldName(LinesIDFieldName);
        indexEnum.Reset();
        IIndex index;
        while ((index = indexEnum.Next()) != null)
        {
          if (index.Fields.FieldCount != 1)
            continue;
          else
            break;
        }

        if (index == null)
          messages.AddWarning("Warning: " + LinesIDFieldName + " is not indexed.");
        #endregion

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

        #region Create Output datasets

        gpParam = paramvalues.get_Element(OutFeatureClassName) as IGPParameter;
        IGPValue outputNameValue = gpUtils.UnpackGPValue(gpParam);
        string outputName = (outputNameValue as IGPString).Value;

        IFeatureClass outputSignsFeatureClass = SignpostUtilities.CreateSignsFeatureClass(inputLineFeatures, outputName);

        gpParam = paramvalues.get_Element(OutStreetsTableName) as IGPParameter;
        outputNameValue = gpUtils.UnpackGPValue(gpParam);
        outputName = (outputNameValue as IGPString).Value;

        ITable outputSignDetailTable = SignpostUtilities.CreateSignsDetailTable(inputLineFeatures, outputName);
        #endregion

        #region Populate data

        PopulateData(inputSITable, inputSPTable, inputLineFeatures, outputSignsFeatureClass, outputSignDetailTable, messages, trackcancel);
        #endregion
      }
      catch (COMException e)
      {
        messages.AddError(1, e.Message);
      }
    }

    public string DisplayName
    {
      get
      {
        return "Import MultiNet Signs";
      }
    }

    public string MetadataFile
    {
      get
      {
        return "ImportMultiNetSignsHelp.xml";
      }
    }

    public IName FullName
    {
      get
      {
        IGPFunctionFactory functionFactory = new SignpostGPFunctionFactory();
        return functionFactory.GetFunctionName(this.Name) as IName;
      }
    }

    public bool IsLicensed()
    {
      return true;
    }

    public UID DialogCLSID
    {
      get
      {
        return null;
      }
    }

    public string Name
    {
      get
      {
        return "ImportMultiNetSigns";
      }
    }

    public int HelpContext
    {
      get
      {
        return 0;
      }
    }

    public string HelpFile
    {
      get
      {
        return null;
      }
    }

    public object GetRenderer(IGPParameter gpParam)
    {
      return null;
    }
    #endregion

    private bool CheckForTableFields(IDETable inputTable, string[] fieldNames, esriFieldType[] fieldTypes,
                     IGPMessage gpMessage)
    {
      IFields fields = inputTable.Fields;
      int fieldIndex;

      for (int i = 0; i < fieldNames.Length - 1; i++)
      {
        fieldIndex = fields.FindField(fieldNames[i]);
        if (fieldIndex == -1)
        {
          gpMessage.Type = esriGPMessageType.esriGPMessageTypeError;
          gpMessage.Description = "Field named " + fieldNames[i] + " not found.";
          return false;
        }

        if (fields.get_Field(fieldIndex).Type != fieldTypes[i])
        {
          gpMessage.Type = esriGPMessageType.esriGPMessageTypeError;
          gpMessage.Description = "Field named " + fieldNames[i] + " is not the expected type.";
          return false;
        }
      }
      return true;
    }

    private bool CheckForLinesIDField(IDETable pInputTable, IGPMessage pMessage)
    {
      IFields fields = pInputTable.Fields;
      int fieldIndex = fields.FindField(LinesIDFieldName);
      if (fieldIndex == -1)
      {
        pMessage.Type = esriGPMessageType.esriGPMessageTypeError;
        pMessage.Description = "Field named " + LinesIDFieldName + " not found.";
        return false;
      }

      if (fields.get_Field(fieldIndex).Type != LinesIDFieldType)
      {
        pMessage.Type = esriGPMessageType.esriGPMessageTypeError;
        pMessage.Description = "Field named " + LinesIDFieldName + " is not the expected type.";
        return false;
      }

      return true;
    }

    private void PopulateData(ITable inputSignInformationTable, ITable inputSignPathTable, IFeatureClass inputLineFeatures,
                  IFeatureClass outputSignFeatures, ITable outputSignDetailTable,
                  IGPMessages messages, ITrackCancel trackcancel)
    {
      #region Find fields
      //(Validate checked that these exist)
      IFields inputSITableFields = inputSignInformationTable.Fields;
      int inSiIdFI = inputSITableFields.FindField("ID");
      int inSiInfoTypFI = inputSITableFields.FindField("INFOTYP");
      int inSiTxtContFI = inputSITableFields.FindField("TXTCONT");
      int inSiConTypFI = inputSITableFields.FindField("CONTYP");
      IFields inputSPTableFields = inputSignPathTable.Fields;
      int inSpIdFI = inputSPTableFields.FindField("ID");
      int inSpSeqNrFI = inputSPTableFields.FindField("SEQNR");
      int inSpTrpElIdFI = inputSPTableFields.FindField("TRPELID");
      int inSpTrpElTypFI = inputSPTableFields.FindField("TRPELTYP");

      // Find output fields (we just made these)

      IFields outputSignFeatureFields = outputSignFeatures.Fields;
      int outExitNameFI = outputSignFeatureFields.FindField("ExitName");

      int[] outBranchXFI = new int[SignpostUtilities.MaxBranchCount];
      int[] outBranchXDirFI = new int[SignpostUtilities.MaxBranchCount];
      int[] outBranchXLngFI = new int[SignpostUtilities.MaxBranchCount];
      int[] outTowardXFI = new int[SignpostUtilities.MaxBranchCount];
      int[] outTowardXLngFI = new int[SignpostUtilities.MaxBranchCount];

      string indexString;

      for (int i = 0; i < SignpostUtilities.MaxBranchCount; i++)
      {
        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");
      }

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

      // Find ID fields on referenced lines

      int inLinesOIDFI = inputLineFeatures.FindField(inputLineFeatures.OIDFieldName);
      int inLinesUserIDFI = inputLineFeatures.FindField(LinesIDFieldName);
      int inLinesShapeFI = inputLineFeatures.FindField(inputLineFeatures.ShapeFieldName);

      #endregion

      // 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.

      System.Collections.Hashtable lineFeaturesList = SignpostUtilities.FillFeatureCache(inputSignPathTable, inSpTrpElIdFI, -1, inputLineFeatures, LinesIDFieldName, trackcancel);

      // Create output feature/row buffers

      IFeatureBuffer featureBuffer = outputSignFeatures.CreateFeatureBuffer();
      IFeature feature = featureBuffer as IFeature;
      IRowBuffer featureRowBuffer = featureBuffer as IRowBuffer;

      IRowBuffer tableBuffer = outputSignDetailTable.CreateRowBuffer();
      IRow row = tableBuffer as IRow;
      IRowBuffer tableRowBuffer = tableBuffer as IRowBuffer;

      // Create insert cursors.

      IFeatureCursor featureInsertCursor = outputSignFeatures.Insert(true);
      ICursor tableInsertCursor = outputSignDetailTable.Insert(true);

      // Create input cursors for the sign tables we are importing

      ITableSort spTableSort = new TableSortClass();
      spTableSort.Fields = "ID, SEQNR";
      spTableSort.set_Ascending("ID", true);
      spTableSort.set_Ascending("SEQNR", true);
      spTableSort.QueryFilter = null;
      spTableSort.Table = inputSignPathTable;
      spTableSort.Sort(null);
      ICursor spInputCursor = spTableSort.Rows;

      ITableSort siTableSort = new TableSortClass();
      siTableSort.Fields = "ID, SEQNR, DESTSEQ, RNPART";
      siTableSort.set_Ascending("ID", true);
      siTableSort.set_Ascending("SEQNR", true);
      siTableSort.set_Ascending("DESTSEQ", true);
      siTableSort.set_Ascending("RNPART", true);
      siTableSort.QueryFilter = null;
      siTableSort.Table = inputSignInformationTable;
      siTableSort.Sort(null);
      ICursor siInputCursor = siTableSort.Rows;

      IRow inputSpTableRow;
      IRow inputSiTableRow;
      long currentID = -1, loopSpID, loopSiID;

      int numOutput = 0;
      int numInput = 0;
      bool fetchFeatureDataSucceeded;
      ArrayList idVals = new System.Collections.ArrayList(2);
      ArrayList edgesData = new System.Collections.ArrayList(2);
      ArrayList reverseEdge = new System.Collections.ArrayList(2);
      SignpostUtilities.FeatureData currentFeatureData = new SignpostUtilities.FeatureData(-1, null);
      ICurve earlierEdgeCurve, laterEdgeCurve;
      IPoint earlierEdgeStart, earlierEdgeEnd;
      IPoint laterEdgeStart, laterEdgeEnd;

      int nextBranchNum = -1, nextTowardNum = -1;
      string infoTypText, txtContText;
      string directionalAsText = "";
      string towardText;
      int conTypVal;

      int refLinesFCID = inputLineFeatures.ObjectClassID;
      IGeometry outputSignGeometry;
      object newOID;

      inputSpTableRow = spInputCursor.NextRow();
      inputSiTableRow = siInputCursor.NextRow();
      while (inputSpTableRow != null && inputSiTableRow != null)
      {
        currentID = Convert.ToInt64(inputSpTableRow.get_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.get_Value(inSpTrpElIdFI)));

          inputSpTableRow = spInputCursor.NextRow();
          if (inputSpTableRow == null)
            break;    // we've reached the end of the SP table

          loopSpID = Convert.ToInt64(inputSpTableRow.get_Value(inSpIdFI));
          if (loopSpID != currentID)
            break;    // we're now on a new ID value          
        }

        numInput++;

        // fetch the FeatureData for each of these edges

        edgesData.Clear();
        fetchFeatureDataSucceeded = true;
        foreach (long currentIDVal in idVals)
        {
          try
          {
            currentFeatureData = (SignpostUtilities.FeatureData)lineFeaturesList[currentIDVal];
            edgesData.Add(currentFeatureData);
          }
          catch
          {
            fetchFeatureDataSucceeded = false;
            if (numInput - numOutput < 100)
            {
              messages.AddWarning("Line feature not found for sign with ID: " +
                Convert.ToString(currentIDVal));
            }
            break;
          }
        }
        if (!fetchFeatureDataSucceeded)
          continue;

        // determine the orientation for each of these edges

        reverseEdge.Clear();
        for (int i = 1; i < edgesData.Count; i++)
        {
          // get the endpoints of the earlier curve
          currentFeatureData = (SignpostUtilities.FeatureData)edgesData[i - 1];
          earlierEdgeCurve = currentFeatureData.feature as ICurve;
          earlierEdgeStart = earlierEdgeCurve.FromPoint;
          earlierEdgeEnd = earlierEdgeCurve.ToPoint;

          // get the endpoints of the later curve
          currentFeatureData = (SignpostUtilities.FeatureData)edgesData[i];
          laterEdgeCurve = currentFeatureData.feature as 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)
            reverseEdge.Add(EqualPoints(earlierEdgeStart, laterEdgeStart) || EqualPoints(earlierEdgeStart, laterEdgeEnd));

          // 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) || EqualPoints(laterEdgeEnd, earlierEdgeEnd));
        }

        // 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.set_Value(outExitNameFI, "");

        while (inputSiTableRow != null)
        {
          loopSiID = Convert.ToInt64(inputSiTableRow.get_Value(inSiIdFI));
          if (loopSiID < currentID)
          {
            inputSiTableRow = siInputCursor.NextRow();
            continue;
          }
          else if (loopSiID > currentID)
          {
            break;    // we're now on a new ID value
          }

          infoTypText = inputSiTableRow.get_Value(inSiInfoTypFI) as string;
          txtContText = inputSiTableRow.get_Value(inSiTxtContFI) as string;
          conTypVal = Convert.ToInt32(inputSiTableRow.get_Value(inSiConTypFI));

          switch (infoTypText)
          {
            case "4E":    // exit number

              featureBuffer.set_Value(outExitNameFI, txtContText);

              break;
            case "9D":    // place name
            case "4I":    // other destination

              // check for schema overflow
              if (nextTowardNum > SignpostUtilities.MaxBranchCount - 1)
              {
                inputSiTableRow = siInputCursor.NextRow();
                continue;
              }

              // set values
              featureBuffer.set_Value(outTowardXFI[nextTowardNum], txtContText);
              featureBuffer.set_Value(outTowardXLngFI[nextTowardNum], "en");

              // get ready for next toward
              nextTowardNum++;

              break;
            case "6T":    // street name
            case "RN":    // route number

              if (conTypVal == 2)    // toward
              {
                // check for schema overflow
                if (nextTowardNum > SignpostUtilities.MaxBranchCount - 1)
                {
                  inputSiTableRow = siInputCursor.NextRow();
                  continue;
                }

                // set values
                featureBuffer.set_Value(outTowardXFI[nextTowardNum], txtContText);
                featureBuffer.set_Value(outTowardXLngFI[nextTowardNum], "en");

                // get ready for next toward
                nextTowardNum++;
              }
              else    // branch
              {
                // check for schema overflow
                if (nextBranchNum > SignpostUtilities.MaxBranchCount - 1)
                {
                  inputSiTableRow = siInputCursor.NextRow();
                  continue;
                }

                // set values
                featureBuffer.set_Value(outBranchXFI[nextBranchNum], txtContText);
                featureBuffer.set_Value(outBranchXDirFI[nextBranchNum], "");
                featureBuffer.set_Value(outBranchXLngFI[nextBranchNum], "en");

                // get ready for next branch
                nextBranchNum++;
              }

              break;
            case "7G":    // route directional

              // convert the directional value to a string
              switch (txtContText)
              {
                case "1":
                  directionalAsText = "N";
                  break;
                case "2":
                  directionalAsText = "E";
                  break;
                case "3":
                  directionalAsText = "S";
                  break;
                case "4":
                  directionalAsText = "W";
                  break;
              }

              if (conTypVal == 2)    // toward
              {
                // check for schema underflow
                if (nextTowardNum == 0)
                {
                  inputSiTableRow = siInputCursor.NextRow();
                  continue;
                }

                // append directional text to the previous toward text value
                towardText = featureBuffer.get_Value(outTowardXFI[nextTowardNum - 1]) as string;
                towardText = towardText + " " + directionalAsText;
                featureBuffer.set_Value(outTowardXFI[nextTowardNum - 1], towardText);
              }
              else    // branch
              {
                // check for schema underflow
                if (nextBranchNum == 0)
                {
                  inputSiTableRow = siInputCursor.NextRow();
                  continue;
                }

                // set value of the Dir field on the previous branch
                featureBuffer.set_Value(outBranchXDirFI[nextBranchNum - 1], directionalAsText);
              }

              break;
          }  // switch

          inputSiTableRow = siInputCursor.NextRow();

        }  // 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.set_Value(outTblSignpostIDFI, newOID);
        tableRowBuffer.set_Value(outTblEdgeFCIDFI, refLinesFCID);
        for (int i = 0; i < edgesData.Count; i++)
        {
          currentFeatureData = (SignpostUtilities.FeatureData)edgesData[i];
          tableRowBuffer.set_Value(outTblSequenceFI, i + 1);
          tableRowBuffer.set_Value(outTblEdgeFIDFI, currentFeatureData.OID);
          if ((bool)reverseEdge[i])
          {
            tableRowBuffer.set_Value(outTblEdgeFrmPosFI, 1.0);
            tableRowBuffer.set_Value(outTblEdgeToPosFI, 0.0);
          }
          else
          {
            tableRowBuffer.set_Value(outTblEdgeFrmPosFI, 0.0);
            tableRowBuffer.set_Value(outTblEdgeToPosFI, 1.0);
          }

          // insert detail record

          tableInsertCursor.InsertRow(tableRowBuffer);
        }

        numOutput++;
        if ((numOutput % 100) == 0)
        {
          // check for user cancel

          if (!trackcancel.Continue())
            throw (new COMException("Function cancelled."));
        }

      }  // outer while

      // add a summary message

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

      return;
    }

    private bool EqualPoints(IPoint p1, IPoint p2)
    {
      return ((p1.X == p2.X) && (p1.Y == p2.Y));
    }

    private IGeometry MakeSignGeometry(ArrayList edgesData, ArrayList reverseEdge)
    {
      ISegmentCollection resultSegments = new PolylineClass();
      SignpostUtilities.FeatureData currentFeatureData = new SignpostUtilities.FeatureData(-1, null);
      ICurve currentCurve, resultCurve;

      for (int i = 0; i < edgesData.Count; i++)
      {
        // fetch the curve and reverse it as needed

        currentFeatureData = (SignpostUtilities.FeatureData)edgesData[i];
        currentCurve = currentFeatureData.feature as ICurve;
        if ((bool)reverseEdge[i])
          currentCurve.ReverseOrientation();

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

        if (i == 0)
          currentCurve.GetSubcurve(0.75, 1.0, true, out resultCurve);
        else if (i == (edgesData.Count - 1))
          currentCurve.GetSubcurve(0.0, 0.25, true, out resultCurve);
        else
          resultCurve = currentCurve;

        // add the resulting geometry to the collection

        resultSegments.AddSegmentCollection(resultCurve as ISegmentCollection);
      }

      return resultSegments as IGeometry;
    }
  }
}