ArcObjects Library Reference  

ImportDynamapSignsFunction

About the Import signposts Sample

[C#]

ImportDynamapSignsFunction.cs

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;
		}

	}
}

[Visual Basic .NET]

ImportDynamapSignsFunction.vb

Imports System
Imports System.Runtime.InteropServices

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

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

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

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

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

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

#End Region

		Private m_parameters As IArray

		Public Sub New()
		End Sub

#Region "IGPFunction Members"

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

				' 1 - input_highway_sign_features

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

				gpParamArray.Add(paramEdit)

				' 2 - reference_street_features

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

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

				paramEdit.Domain = CType(lineFeatureClassDomain, IGPDomain)

				gpParamArray.Add(paramEdit)

				' 3 - out_feature_class_name

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

				gpParamArray.Add(paramEdit)

				' 4 - out_table_name

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

				gpParamArray.Add(paramEdit)

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

				Return gpParamArray
			End Get
		End Property

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

			' Create the GPUtilities Object

			Dim gpUtils As IGPUtilities = New GPUtilities

			' Initialize a copy of our parameters

			If m_parameters Is Nothing Then
				m_parameters = ParameterInfo
			End If

			' Call InternalValidate to check for required parameters

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

			' Verify chosen input table has the expected fields

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

			' CheckForTableFields will report errors by modifying the relevant GPMessage

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

			' Verify chosen reference_line_features has expected id field

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

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

			Return validateMessages
		End Function

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

				Dim gpUtils As IGPUtilities = New GPUtilities

				' OPEN INPUT DATASETS (UNPACK VALUES)

				Dim inputTableAsITable As ITable
				Dim inputLineFeatures As IFeatureClass

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

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

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

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

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

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

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

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

				' CREATE OUTPUT DATASETS

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

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

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

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

				' POPULATE DATA

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

			Return True
		End Function

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

			' Find output fields (we just made these)

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

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

			Dim indexString As String

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

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

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

			' Find ID fields on referenced lines

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


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

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

			' Create output feature/row buffers

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

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

			' Create insert cursors.

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

			' Create input cursor for the signs table we are importing

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

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

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

			' these are initialized to prevent uninitialized variable compiler error

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

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

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

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

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

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

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

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

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

					' save sign feature record

					newOID = featureInsertCursor.InsertFeature(featureBuffer)

					' set streets table values

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

					' insert first detail record

					tableInsertCursor.InsertRow(tableRowBuffer)

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

					' insert second detail record

					tableInsertCursor.InsertRow(tableRowBuffer)

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

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

				lastSignID = currentSignID

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

					nextBranchNum = 0
					nextTowardNum = 0

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

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

					numInput += 1

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

						inputTableRow = inputCursor.NextRow()
						Continue While
					End Try

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

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

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

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

					' flip the from edge?

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

					' flip the to edge?

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

					' set sign feature values

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

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

					featureBuffer.Shape = outputSignGeometry

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

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

				accessVal = inputTableRow.Value(inAccessFI)

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

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

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

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

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

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

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

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

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

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

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

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

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

			' clean up unused parts of the row

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

			' save sign feature record

			newOID = featureInsertCursor.InsertFeature(featureBuffer)

			' set streets table values

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

			' insert first detail record

			tableInsertCursor.InsertRow(tableRowBuffer)

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

			' insert second detail record

			tableInsertCursor.InsertRow(tableRowBuffer)

			numOutput += 1

			' add a summary message

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

			Return
		End Sub

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

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

			' add the part from the first line

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

			resultSegments.AddSegmentCollection(CType(fromResultCurve, ISegmentCollection))


			' add the part from the second line

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

			resultSegments.AddSegmentCollection(CType(toResultCurve, ISegmentCollection))

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