Import signposts
ImportDynamapSignsFunction.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 ESRI.ArcGIS.esriSystem;
using ESRI.ArcGIS.Geodatabase;
using ESRI.ArcGIS.Geoprocessing;
using ESRI.ArcGIS.Geometry;

namespace GPImportSignpostFunctions
{
  /// <summary>
  /// 
  /// </summary>
  /// 
  [Guid("DD0FD598-2622-4240-96B6-6C9FFA43B177")]
  [ClassInterface(ClassInterfaceType.None)]
  [ProgId("GPImportSignpostFunctions.ImportDynamapSignsFunction")]

  public class ImportDynamapSignsFunction : IGPFunction
  {

    #region Constants
    // parameter index constants
    private const int InputTable = 0;
    private const int ReferenceLineFeatures = 1;
    private const int OutFeatureClassName = 2;
    private const int OutStreetsTableName = 3;

    // field names and types
    private static readonly string[] FieldNames = 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 static readonly esriFieldType[] FieldTypes = 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 static readonly string LinesIDFieldName = "Dynamap_ID";
    private static readonly esriFieldType LinesIDFieldType = esriFieldType.esriFieldTypeDouble;

    #endregion

    IArray m_parameters;

    public ImportDynamapSignsFunction()
    {
    }

    #region IGPFunction Members

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

      get
      {
        IArray paramArray = new ArrayClass();

        // 1 - input_highway_sign_features

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

        paramArray.Add(paramEdit as object);

        // 2 - 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);

        // 3 - 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);

        // 4 - 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 table has the expected fields

      IGPParameter gpParam = paramvalues.get_Element(InputTable) 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, validateMessages.GetMessage(InputTable));
      }

      // 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 inputTable;
        IFeatureClass inputLineFeatures;

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

        if (dataset != null)
          inputTable = dataset as ITable;
        else
        {
          messages.AddError(1, "Could not open input 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(inputTable, inputLineFeatures, outputSignsFeatureClass, outputSignDetailTable, messages, trackcancel);
        #endregion
      }
      catch (COMException e)
      {
        messages.AddError(1, e.Message);
      }
    }

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

    public string MetadataFile
    {
      get
      {
        return "ImportDynamapSignsHelp.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 "ImportDynamapSigns";
      }
    }

    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, 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 inputTable, IGPMessage gpMessage)
    {
      IFields fields = inputTable.Fields;
      int fieldIndex = fields.FindField(LinesIDFieldName);
      if (fieldIndex == -1)
      {
        gpMessage.Type = esriGPMessageType.esriGPMessageTypeError;
        gpMessage.Description = "Field named " + LinesIDFieldName + " not found.";
        return false;
      }

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

      return true;
    }

    private void PopulateData(ITable inputSignsTable, IFeatureClass inputLineFeatures,
                  IFeatureClass outputSignFeatures, ITable outputSignDetailTable,
                  IGPMessages messages, ITrackCancel trackcancel)
    {
      #region Find fields
      //(Validate checked that these exist)
      IFields inputTableFields = inputSignsTable.Fields;
      int inExitIDFI = inputTableFields.FindField("EXIT_ID");
      int inSequenceFI = inputTableFields.FindField("SEQUENCE");
      int inFromIDFI = inputTableFields.FindField("FROM_ID");
      int inExitNumFI = inputTableFields.FindField("EXIT_NUM");
      int inToIDFI = inputTableFields.FindField("TO_ID");
      int inToNameFI = inputTableFields.FindField("TO_NAME");
      int inDirectionFI = inputTableFields.FindField("DIRECTION");
      int inToLocaleFI = inputTableFields.FindField("TO_LOCALE");
      int inAccessFI = inputTableFields.FindField("ACCESS");

      /// 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(inputSignsTable, inFromIDFI, inToIDFI, 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 cursor for the signs table we are importing

      ITableSort tableSort = new TableSortClass();
      tableSort.Fields = "EXIT_ID, SEQUENCE";
      tableSort.set_Ascending("EXIT_ID", true);
      tableSort.set_Ascending("SEQUENCE", true);
      tableSort.QueryFilter = null;
      tableSort.Table = inputSignsTable;
      tableSort.Sort(null);
      ICursor inputCursor = tableSort.Rows;

      IRow inputTableRow;
      int numInput = 0;
      int numOutput = 0;
      int inSequenceValue;
      long fromIDVal, toIDVal;

      int nextBranchNum = -1, nextTowardNum = -1;

      // these are initialized to prevent uninitialized variable compiler error

      SignpostUtilities.FeatureData fromFeatureData = new SignpostUtilities.FeatureData(-1, null);
      SignpostUtilities.FeatureData toFeatureData = new SignpostUtilities.FeatureData(-1, null);

      object newOID, accessVal;
      string outputText, outputDirText;

      ICurve fromEdgeCurve, toEdgeCurve;
      IPoint fromEdgeStart, fromEdgeEnd, toEdgeStart, toEdgeEnd;

      int refLinesFCID = inputLineFeatures.ObjectClassID;
      IGeometry outputSignGeometry;

      double lastSignID = -1.0, currentSignID = -1.0;
      double fromEdgeFromPos = 0.0;
      double fromEdgeToPos = 1.0;
      double toEdgeFromPos = 0.0;
      double toEdgeToPos = 1.0;

      while ((inputTableRow = inputCursor.NextRow()) != null)
      {
        currentSignID = Convert.ToInt32(inputTableRow.get_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 && lastSignID != -1)
        {
          // 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(outTblSequenceFI, 1);
          tableRowBuffer.set_Value(outTblEdgeFCIDFI, refLinesFCID);
          tableRowBuffer.set_Value(outTblEdgeFIDFI, fromFeatureData.OID);
          tableRowBuffer.set_Value(outTblEdgeFrmPosFI, fromEdgeFromPos);
          tableRowBuffer.set_Value(outTblEdgeToPosFI, fromEdgeToPos);

          // insert first detail record

          tableInsertCursor.InsertRow(tableRowBuffer);

          tableRowBuffer.set_Value(outTblSequenceFI, 2);
          tableRowBuffer.set_Value(outTblEdgeFIDFI, toFeatureData.OID);
          tableRowBuffer.set_Value(outTblEdgeFrmPosFI, toEdgeFromPos);
          tableRowBuffer.set_Value(outTblEdgeToPosFI, toEdgeToPos);

          // insert second detail record

          tableInsertCursor.InsertRow(tableRowBuffer);

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

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

        lastSignID = currentSignID;

        inSequenceValue = Convert.ToInt32(inputTableRow.get_Value(inSequenceFI));
        if (inSequenceValue == 1)
        {
          // 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.get_Value(inFromIDFI));
          toIDVal = Convert.ToInt64(inputTableRow.get_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++;

          try
          {
            fromFeatureData = (SignpostUtilities.FeatureData)lineFeaturesList[fromIDVal];
            toFeatureData = (SignpostUtilities.FeatureData)lineFeaturesList[toIDVal];
          }
          catch
          {
            if (numInput - numOutput < 100)
            {
              messages.AddWarning("Line feature not found for sign with FromID: " +
                Convert.ToString(fromIDVal) + ", ToID: " + Convert.ToString(toIDVal));
            }
            continue;
          }

          // 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 = fromFeatureData.feature as ICurve;
          toEdgeCurve = toFeatureData.feature as 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) || EqualPoints(fromEdgeStart, toEdgeEnd))
          {
            fromEdgeFromPos = 1.0;
            fromEdgeToPos = 0.0;
          }

          // flip the to edge?

          if (EqualPoints(toEdgeEnd, fromEdgeStart) || EqualPoints(toEdgeEnd, fromEdgeEnd))
          {
            toEdgeFromPos = 1.0;
            toEdgeToPos = 0.0;
          }

          // 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.set_Value(outExitNameFI, inputTableRow.get_Value(inExitNumFI));
        }

        // 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.get_Value(inAccessFI);

        if ((accessVal as string) == "D")
        {
          // check for schema overflow
          if (nextBranchNum > SignpostUtilities.MaxBranchCount - 1)
            continue;

          outputText = (inputTableRow.get_Value(inToNameFI) as String).Trim();
          if (outputText.Length > 0)
          {

            // set values
            featureBuffer.set_Value(outBranchXFI[nextBranchNum], outputText);
            featureBuffer.set_Value(outBranchXDirFI[nextBranchNum], inputTableRow.get_Value(inDirectionFI));
            featureBuffer.set_Value(outBranchXLngFI[nextBranchNum], "en");

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

          // there are rare cases when we'll have Access == D (TO_NAME) AND data for TO_LOCALE
          outputText = (inputTableRow.get_Value(inToLocaleFI) as String).Trim();
          if (outputText.Length > 0)
          {
            // set values
            featureBuffer.set_Value(outBranchXFI[nextBranchNum], outputText);
            featureBuffer.set_Value(outBranchXDirFI[nextBranchNum], null);
            featureBuffer.set_Value(outBranchXLngFI[nextBranchNum], "en");

            // get ready for next branch
            nextBranchNum++;
          }
        }
        else if ((accessVal as string) == "I")
        {
          // check for schema overflow
          if (nextTowardNum > SignpostUtilities.MaxBranchCount - 1)
            continue;

          outputText = (inputTableRow.get_Value(inToNameFI) as String).Trim();
          if (outputText.Length > 0)
          {
            outputDirText = (inputTableRow.get_Value(inDirectionFI) as String).Trim();
            if (outputDirText.Length > 0)
            {
              outputText += " ";
              outputText += outputDirText;
            }

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

            // get ready for next toward
            nextTowardNum++;
          }

          // there are rare cases when we'll have Access == I (TO_LOCALE) AND data for TO_NAME
          outputText = (inputTableRow.get_Value(inToLocaleFI) as String).Trim();
          if (outputText.Length > 0)
          {
            // set values
            featureBuffer.set_Value(outTowardXFI[nextTowardNum], outputText);
            featureBuffer.set_Value(outTowardXLngFI[nextTowardNum], "en");

            // get ready for next toward
            nextTowardNum++;
          }
        }
        else
          continue;    // not expected

      }  // 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.set_Value(outTblSignpostIDFI, newOID);
      tableRowBuffer.set_Value(outTblSequenceFI, 1);
      tableRowBuffer.set_Value(outTblEdgeFCIDFI, refLinesFCID);
      tableRowBuffer.set_Value(outTblEdgeFIDFI, fromFeatureData.OID);
      tableRowBuffer.set_Value(outTblEdgeFrmPosFI, fromEdgeFromPos);
      tableRowBuffer.set_Value(outTblEdgeToPosFI, fromEdgeToPos);

      // insert first detail record

      tableInsertCursor.InsertRow(tableRowBuffer);

      tableRowBuffer.set_Value(outTblSequenceFI, 2);
      tableRowBuffer.set_Value(outTblEdgeFIDFI, toFeatureData.OID);
      tableRowBuffer.set_Value(outTblEdgeFrmPosFI, toEdgeFromPos);
      tableRowBuffer.set_Value(outTblEdgeToPosFI, toEdgeToPos);

      // insert second detail record

      tableInsertCursor.InsertRow(tableRowBuffer);

      numOutput++;

      // 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(ICurve fromEdgeCurve, ICurve toEdgeCurve,
                       bool reverseFromEdge, bool reverseToEdge)
    {
      ISegmentCollection resultSegments = new PolylineClass();
      ICurve fromResultCurve, toResultCurve;

      // add the part from the first line

      if (reverseFromEdge)
      {
        fromEdgeCurve.GetSubcurve(0.0, 0.25, true, out fromResultCurve);
        fromResultCurve.ReverseOrientation();
      }
      else
      {
        fromEdgeCurve.GetSubcurve(0.75, 1.0, true, out fromResultCurve);
      }

      resultSegments.AddSegmentCollection(fromResultCurve as ISegmentCollection);


      // add the part from the second line

      if (reverseToEdge)
      {
        toEdgeCurve.GetSubcurve(0.75, 1.0, true, out toResultCurve);
        toResultCurve.ReverseOrientation();
      }
      else
      {
        toEdgeCurve.GetSubcurve(0.0, 0.25, true, out toResultCurve);
      }

      resultSegments.AddSegmentCollection(toResultCurve as ISegmentCollection);

      return resultSegments as IGeometry;
    }

  }
}