ArcGIS Network Analyst extension barrier location editor
EditorForm.cs
// Copyright 2012 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.Windows.Forms;
using ESRI.ArcGIS.ArcMapUI;
using ESRI.ArcGIS.Display;
using ESRI.ArcGIS.Framework;
using ESRI.ArcGIS.Geodatabase;
using ESRI.ArcGIS.Geometry;
using ESRI.ArcGIS.NetworkAnalyst;

namespace NABarrierLocationEditor
{
  public partial class EditorForm : Form
  {
    #region Member Variables

    private readonly static string EDGE_ALONG = "Along Digitized";
    private readonly static string EDGE_AGAINST = "Against Digitized";

    IApplication m_app;
    INAContext m_context;
    IFeature m_barrier;

    #endregion

    #region Initialization

    public EditorForm(IApplication app, INAContext context, IFeature barrier)
    {
      m_barrier = barrier;
      m_app = app;
      m_context = context;

      InitializeComponent();
      LoadDatagrids();
    }

    /// <summary>
    /// Load both the Edge and Junction dataGrids with the information in the barrier feature
    /// <param name="barrier">The barrier being loaded into dataGrids</param>
    /// </summary>
    void LoadDatagrids()
    {
      // Populate the cell with the direction drop down
      ((DataGridViewComboBoxColumn)dataGridViewEdges.Columns[1]).Items.AddRange(EDGE_ALONG, EDGE_AGAINST); ;

      // get the location ranges out of the barrier feature
      var naLocRangesObject = m_barrier as INALocationRangesObject;
      var naLocRanges = naLocRangesObject.NALocationRanges;
      if (naLocRanges == null)
        throw new Exception("Selected barrier has a null NALocationRanges value");

      // add all of the junctions included in the barrier to the Junctions dataGrid
      long junctionCount = naLocRanges.JunctionCount;
      int junctionEID = -1;
      for (int i = 0; i < junctionCount; i++)
      {
        naLocRanges.QueryJunction(i, ref junctionEID);
        int rowIndex = dataGridViewJunctions.Rows.Add();
        dataGridViewJunctions.Rows[rowIndex].SetValues(junctionEID);
      }

      // add all of the edges included in the barrier to the Edges dataGrid
      long edgeRangeCount = naLocRanges.EdgeRangeCount;
      int edgeEID = -1;
      double fromPosition, toPosition;
      fromPosition = toPosition = -1;
      esriNetworkEdgeDirection edgeDirection = esriNetworkEdgeDirection.esriNEDNone;
      for (int i = 0; i < edgeRangeCount; i++)
      {
        naLocRanges.QueryEdgeRange(i, ref edgeEID, ref edgeDirection, ref fromPosition, ref toPosition);

        string directionValue = "";
        if (edgeDirection == esriNetworkEdgeDirection.esriNEDAlongDigitized) directionValue = EDGE_ALONG;
        else if (edgeDirection == esriNetworkEdgeDirection.esriNEDAgainstDigitized) directionValue = EDGE_AGAINST;

        dataGridViewEdges.Rows.Add(edgeEID, directionValue, fromPosition, toPosition);
      }
    }

    #endregion

    #region Button Clicks

    /// <summary>
    /// Occurs when the user clicks the Cancel button.
    /// <param name="sender">The control raising this event</param>
    /// <param name="e">Arguments associated with the event</param>
    /// </summary>
    private void btnCancel_Click(object sender, EventArgs e)
    {
      this.Close();
    }

    /// <summary>
    /// Occurs when the user clicks the Save button.
    ///   The barrier information is collected out of the junction and edge barrier
    ///   dataGrids, then stored back into the original barrier feature as a replacement
    ///   to the existing barrier information.  The original geometry of the barrier remains 
    ///   unaltered.
    /// <param name="sender">The control raising this event</param>
    /// <param name="e">Arguments associated with the event</param>
    /// </summary>
    private void btnSave_Click(object sender, EventArgs e)
    {
      if (!ValidateDataGrid(dataGridViewEdges)) return;
      if (!ValidateDataGrid(dataGridViewJunctions)) return;

      // The existing NALocationRanges for the barrier will be replaced with a new one
      INALocationRanges naLocRanges = new NALocationRangesClass();

      // First gather the edge ranges
      foreach (DataGridViewRow row in dataGridViewEdges.Rows)
      {
        // ignore the extra row in the dataGrid
        if (row.IsNewRow) continue;

        // gather the EID value for the new range
        int eid = Int32.Parse(row.Cells[0].Value.ToString());

        // gather the edge direction value for the new range
        string directionValue = row.Cells[1].Value.ToString();
        esriNetworkEdgeDirection direction = esriNetworkEdgeDirection.esriNEDNone;
        if (directionValue == EDGE_ALONG) direction = esriNetworkEdgeDirection.esriNEDAlongDigitized;
        else if (directionValue == EDGE_AGAINST) direction = esriNetworkEdgeDirection.esriNEDAgainstDigitized;

        // gather the from and to position values for the new range
        double fromPos = Double.Parse(row.Cells[2].Value.ToString());
        double toPos = Double.Parse(row.Cells[3].Value.ToString());

        // load the values for this range into the NALocationRanges object
        naLocRanges.AddEdgeRange(eid, direction, fromPos, toPos);
      }

      // Now gather the junctions to be included in the barrier
      foreach (DataGridViewRow row in dataGridViewJunctions.Rows)
      {
        // ignore the extra row in the dataGrid
        if (row.IsNewRow) continue;

        // gather the EID value for the junction to include
        int eid = Int32.Parse(row.Cells[0].Value.ToString());

        // load this junction into the NALocationRanges object
        naLocRanges.AddJunction(eid);
      }

      // Cast the barrier feature to INALocationRanges Object, then populate
      //   its NALocationRanges value with the new barrier that was created above.
      //   Then, save the new barrier with a call to Store()
      INALocationRangesObject naLocationRangesObject = m_barrier as INALocationRangesObject;
      naLocationRangesObject.NALocationRanges = naLocRanges;
      m_barrier.Store();

      this.Close();
    }

    /// <summary>
    /// Occurs when the user clicks the Zoom To Barrier Geometry button.
    ///   The map will zoom to the extent of the Shape of the barrier.
    /// <param name="sender">The control raising this event</param>
    /// <param name="e">Arguments associated with the event</param>
    /// </summary>
    private void btnZoomToBarrier_Click(object sender, EventArgs e)
    {
      IMxDocument mxDoc = m_app.Document as IMxDocument;
      mxDoc.ActiveView.Extent = m_barrier.Extent;
      mxDoc.ActiveView.Refresh();
    }

    #endregion

    #region Validation

    /// <summary>
    /// ValidateDataGrid goes row by row and checks that the values in the grid
    ///   are valid
    /// <param name="dgv">The dataGrid to validate</param>
    /// </summary>
    private bool ValidateDataGrid(DataGridView dgv)
    {
      // we do all of our validation when the save button is clicked
      foreach (DataGridViewRow row in dgv.Rows)
      {
        ValidateRow(row);
        if (row.ErrorText != "")
        {
          dgv.FirstDisplayedScrollingRowIndex = row.Index;
          System.Windows.Forms.MessageBox.Show("You cannot save until all row errors are cleared.", "Barrier Location Editor Warning");
          return false;
        }
      }

      return true;
    }

    /// <summary>
    /// ValidateRow checks the cell value in the passed-in row
    /// <param name="row">The row to validate</param>
    /// <param name="columns">The fields to be validated</param>
    /// <param name="datagridviewName">The name of the dataGrid whose row is being validated</param>
    /// </summary>
    void ValidateRow(DataGridViewRow row)
    {
      // the extra row to add values does not need to be validated
      if (row.IsNewRow) return;

      row.ErrorText = "";

      // validate each column
      foreach (DataGridViewColumn column in row.DataGridView.Columns)
      {
        // none of the column values can be empty
        if (row.Cells[column.Name].Value == null || row.Cells[column.Name].Value.ToString() == "")
        {
          row.ErrorText = "There cannot be any empty cells";
          return;
        }

        string value = row.Cells[column.Name].Value.ToString();

        switch (column.Name)
        {
          case "JunctionEID":
            if (!ValidateEID(value, esriNetworkElementType.esriNETJunction))
              row.ErrorText += "  Junction EID must correspond to a valid network junction";
            break;
          case "EdgeEID":
            if (!ValidateEID(value, esriNetworkElementType.esriNETEdge))
              row.ErrorText += "  Edge EID must correspond to a valid network edge";
            break;
          case "Direction":
            if (value != "Along Digitized" && value != "Against Digitized")
              row.ErrorText += "  Direction must be Along or Against Digitized";
            break;
          case "fromPos":
            double fromPos = -1;
            if (!Double.TryParse(value, out fromPos) || fromPos < 0 || fromPos > 1)
              row.ErrorText += "  FromPosition must be a positive number between zero and one";
            break;
          case "toPos":
            double toPos = -1;
            if (!Double.TryParse(value, out toPos) || toPos < 0 || toPos > 1)
              row.ErrorText += "  ToPosition must be a positive number between zero and one";
            break;
          default:
            throw new Exception("Unexpected Column");
        }
      }

      // Now, validate that the from position is always less than the two position
      // FromPosition and ToPosition only matter for edge barriers
      if (row.DataGridView.Name == "dataGridViewEdges")
      {
        if (row.Cells["FromPos"].Value == null || row.Cells["ToPos"].Value == null)
        {
          row.ErrorText += "  FromPosition and ToPosition must have valid decimal values between 0 and 1";
          return;
        }

        double fromPos = -1;
        Double.TryParse(row.Cells["FromPos"].Value.ToString(), out fromPos);

        double toPos = -1;
        Double.TryParse(row.Cells["ToPos"].Value.ToString(), out toPos);

        if (fromPos > toPos)
        {
          row.ErrorText += "  FromPosition must be equal to or less than the ToPosition";
          return;
        }
      }
    }

    /// <summary>
    /// Verify that the EID corresponds to a valid network element
    /// <param name="value">The EID passed as a string</param>
    /// <param name="elementType">The type of element to be verified</param>
    /// </summary>
    bool ValidateEID(string value, esriNetworkElementType elementType)
    {
      // validate that the EID is a valid integer
      int eid = -1;
      if (!Int32.TryParse(value, out eid) || eid < 1)
        return false;

      // QueryEdge and QueryJunction will throw exceptions if the EID doesn't match any elements
      var netQuery = m_context.NetworkDataset as INetworkQuery;
      try
      {
        switch (elementType)
        {
          case esriNetworkElementType.esriNETJunction:
            var junction = netQuery.CreateNetworkElement(esriNetworkElementType.esriNETJunction) as INetworkJunction;
            netQuery.QueryJunction(eid, junction);
            break;
          case esriNetworkElementType.esriNETEdge:
            var edge = netQuery.CreateNetworkElement(esriNetworkElementType.esriNETEdge) as INetworkEdge;
            netQuery.QueryEdge(eid, esriNetworkEdgeDirection.esriNEDAlongDigitized, edge);
            break;
          default:
            return false;
        }
      }
      catch
      {
        return false;
      }

      return true;
    }

    #endregion

    #region Flash the Geometry

    /// <summary>
    /// Occurs when a dataGrid row header is clicked.
    ///   It is used here to flash the geometry of the newly selected row
    /// <param name="sender">The control raising this event</param>
    /// <param name="e">Arguments associated with the event</param>
    /// </summary>
    private void dataGridView_RowHeaderMouseClick(object sender, DataGridViewCellMouseEventArgs e)
    {
      // make sure none of the cells are in edit mode.  When in edit mode, the values obtained
      //  programmatically will not yet match the value the user has changed the cell to
      DataGridView dgv = (DataGridView)sender;
      dgv.EndEdit();

      // Only flash when there is one selected row
      if (dgv.SelectedRows.Count > 1) return;
      DataGridViewRow selectedRow = dgv.SelectedRows[0];

      // If it is the extra dataGrid row or has errors, then don't try to flash it 
      ValidateRow(selectedRow);
      if (selectedRow.IsNewRow || selectedRow.ErrorText != "") return;

      // also, if any of the row's cell have no value, then don't want to flash it
      foreach (DataGridViewCell cell in selectedRow.Cells)
        if (cell.Value == null) return;

      // use the EID to obtain the barrier's corresponding network element and source feature
      INetworkElement element = GetElementByEID(selectedRow.Cells[0].Value.ToString(), dgv.Name);
      if (element == null) return;
      IFeature sourceFeature = GetSourceFeature(element);

      // For an edge, get the part geometry of the barrier covered portion of the source feature
      //  that should be displayed
      INetworkEdge netEdge = element as INetworkEdge;
      esriNetworkEdgeDirection displayDirection = esriNetworkEdgeDirection.esriNEDNone;
      if (netEdge != null)
      {
        sourceFeature.Shape = GetBarrierSubcurve(netEdge, sourceFeature, selectedRow);
        displayDirection = GetDirectionValue(selectedRow);
      }

      // Draw
      FlashFeature(sourceFeature, displayDirection);
    }

    /// <summary>
    /// Determine the esriNetworkEdgeDirection from the value in the dataGridView cell
    /// <param name="selectedRow">The row containing the barrier's location range information</param>
    /// </summary>
    private esriNetworkEdgeDirection GetDirectionValue(DataGridViewRow selectedRow)
    {
      esriNetworkEdgeDirection direction = esriNetworkEdgeDirection.esriNEDNone;
      string textValue = selectedRow.Cells[1].Value.ToString();
      if (textValue == EDGE_ALONG) direction = esriNetworkEdgeDirection.esriNEDAlongDigitized;
      else if (textValue == EDGE_AGAINST) direction = esriNetworkEdgeDirection.esriNEDAgainstDigitized;

      return direction;
    }

    /// <summary>
    /// Take a network edge, a source feature, and a row from the edges dataGrid, and determine
    ///  the geometry to be flashed on the map
    /// <param name="netEdge">The edge upon which the barrier resides</param>
    /// <param name="sourceFeature">The source feature corresponding to the network edge</param>
    /// <param name="selectedRow">The row containing the barrier's location range information</param>
    /// </summary>
    private ICurve GetBarrierSubcurve(INetworkEdge netEdge, IFeature sourceFeature, DataGridViewRow selectedRow)
    {
      // value for displaying the entire source feature
      double fromPosition = 0;
      double toPosition = 1;

      // Find the values for displaying only the element portion of the source feature
      double fromElementPosition, toElementPosition;
      netEdge.QueryPositions(out fromElementPosition, out toElementPosition);

      // due to the element possibly being in the against digitized direction,
      //   fromPosition could be greater than toPosition.  If that is the case, swap the values
      if (fromElementPosition > toElementPosition)
      {
        double tmp = fromElementPosition;
        fromElementPosition = toElementPosition;
        toElementPosition = tmp;
      }

      esriNetworkEdgeDirection direction = GetDirectionValue(selectedRow);
      if (direction == esriNetworkEdgeDirection.esriNEDNone) return null;

      // Flash the edge
      if (rbFlashElementPortion.Checked)
      {
        fromPosition = fromElementPosition;
        toPosition = toElementPosition;
      }
      // Flash the barrier portion of the edge
      else if (rbFlashBarrierPortion.Checked)
      {
        double fromBarrierPosition = -1;
        double toBarrierPosition = -1;

        // gather the from and to position values for the barrier
        fromBarrierPosition = Double.Parse(selectedRow.Cells[2].Value.ToString());
        toBarrierPosition = Double.Parse(selectedRow.Cells[3].Value.ToString());

        // for barriers in the against direction, we need to adjust that the element position is
        if (direction == esriNetworkEdgeDirection.esriNEDAgainstDigitized)
        {
          fromBarrierPosition = 1 - fromBarrierPosition;
          toBarrierPosition = 1 - toBarrierPosition;
        }

        // use the positioning along the element of the barrier
        //  to get the position along the original source feature
        fromPosition = fromElementPosition + (fromBarrierPosition * (toElementPosition - fromElementPosition));
        toPosition = fromElementPosition + (toBarrierPosition * (toElementPosition - fromElementPosition));
      }

      if (fromPosition > toPosition)
      {
        double tmp = fromPosition;
        fromPosition = toPosition;
        toPosition = tmp;
      }

      // get the subspan on the polyline that represents the from and to positions we specified
      ICurve displayCurve;
      ICurve sourceCurve = sourceFeature.Shape as ICurve;
      sourceCurve.GetSubcurve(fromPosition, toPosition, true, out displayCurve);
      return displayCurve;
    }

    /// <summary>
    /// Take an EID value as a string from one of the dataGridView controls and find
    ///  the network element that corresponds to the EID
    /// <param name="eidString">The EID value as a string</param>
    /// <param name="datagridviewName">The name of the dataGrid that held the EID</param>
    /// </summary>
    private INetworkElement GetElementByEID(string eidString, string datagridviewName)
    {
      int eid = -1;
      if (!Int32.TryParse(eidString, out eid)) return null;

      INetworkQuery netQuery = m_context.NetworkDataset as INetworkQuery;
      INetworkEdge edge = netQuery.CreateNetworkElement(esriNetworkElementType.esriNETEdge) as INetworkEdge;
      INetworkJunction junction = netQuery.CreateNetworkElement(esriNetworkElementType.esriNETJunction) as INetworkJunction;

      INetworkElement element = null;
      try
      {
        // Populate the network element from the EID
        if (datagridviewName == "dataGridViewEdges")
        {
          netQuery.QueryEdge(eid, esriNetworkEdgeDirection.esriNEDAlongDigitized, edge);
          element = edge as INetworkElement;
        }
        else if (datagridviewName == "dataGridViewJunctions")
        {
          netQuery.QueryJunction(eid, junction);
          element = junction as INetworkElement;
        }
      }
      catch
      {
        // if the query fails, the element will not be displayed
      }

      return element;
    }

    /// <summary>
    /// Take a network element and return its corresponding source feature
    /// <param name="element">The return source feature corresponds to this element</param>
    /// </summary>
    private IFeature GetSourceFeature(INetworkElement element)
    {
      // To draw the network element, we will need its corresponding source feature information
      // Get the sourceID and OID from the element
      int sourceID = element.SourceID;
      int sourceOID = element.OID;

      // Get the source feature from the network source
      INetworkSource netSource = m_context.NetworkDataset.get_SourceByID(sourceID);
      IFeatureClassContainer fClassContainer = m_context.NetworkDataset as IFeatureClassContainer;
      IFeatureClass sourceFClass = fClassContainer.get_ClassByName(netSource.Name);
      return sourceFClass.GetFeature(sourceOID);
    }

    /// <summary>
    /// Flash the feature geometry on the map
    /// <param name="pFeature">The feature being flashed</param>
    /// <param name="pMxDoc">A hook to the application display</param>
    /// <param name="direction">The digitized direction of the barrier with respect to the underlying source feature</param>
    /// </summary>
    private void FlashFeature(IFeature pFeature, esriNetworkEdgeDirection direction)
    {
      IMxDocument pMxDoc = m_app.Document as IMxDocument;

      // Start drawing on screen. 
      pMxDoc.ActiveView.ScreenDisplay.StartDrawing(0, (short)esriScreenCache.esriNoScreenCache);

      // Switch functions based on Geometry type. 
      switch (pFeature.Shape.GeometryType)
      {
        case esriGeometryType.esriGeometryPolyline:
          FlashLine(pMxDoc.ActiveView.ScreenDisplay, pFeature.Shape, direction);
          break;
        case esriGeometryType.esriGeometryPolygon:
          // no network elements can be polygons
          break;
        case esriGeometryType.esriGeometryPoint:
          FlashPoint(pMxDoc.ActiveView.ScreenDisplay, pFeature.Shape);
          break;
        default:
          throw new Exception("Unexpected Geometry Type");
      }

      // Finish drawing on screen. 
      pMxDoc.ActiveView.ScreenDisplay.FinishDrawing();
    }

    /// <summary>
    /// Flash a line feature on the map
    /// <param name="pDisplay">The map screen</param>
    /// <param name="pGeometry">The geometry of the feature to be flashed</param>
    /// <param name="direction">The digitized direction of the barrier with respect to the underlying source feature</param>
    /// </summary>
    private void FlashLine(IScreenDisplay pDisplay, IGeometry pGeometry, esriNetworkEdgeDirection direction)
    {
      // The flash will be on a line symbol with an arrow on it
      ICartographicLineSymbol ipArrowLineSymbol = new CartographicLineSymbolClass();

      // the line color will be red
      IRgbColor ipRgbRedColor = new RgbColorClass();
      ipRgbRedColor.Red = 192;

      // the arrow will be black
      IRgbColor ipRgbBlackColor = new RgbColorClass();
      ipRgbBlackColor.RGB = 0;

      // set up the arrow that will be displayed along the line
      IArrowMarkerSymbol ipArrowMarker = new ArrowMarkerSymbolClass();
      ipArrowMarker.Style = esriArrowMarkerStyle.esriAMSPlain;
      ipArrowMarker.Length = 18;
      ipArrowMarker.Width = 12;
      ipArrowMarker.Color = ipRgbBlackColor;

      // set up the line itself
      ipArrowLineSymbol.Width = 4;
      ipArrowLineSymbol.Color = ipRgbRedColor;

      // Set up the Raster Op-Code to help the flash mechanism
      ((ISymbol)ipArrowMarker).ROP2 = esriRasterOpCode.esriROPNotXOrPen;
      ((ISymbol)ipArrowLineSymbol).ROP2 = esriRasterOpCode.esriROPNotXOrPen;

      // decorate the line with the arrow symbol
      ISimpleLineDecorationElement ipSimpleLineDecorationElement = new SimpleLineDecorationElementClass();
      ipSimpleLineDecorationElement.Rotate = true;
      ipSimpleLineDecorationElement.PositionAsRatio = true;
      ipSimpleLineDecorationElement.MarkerSymbol = ipArrowMarker;
      ipSimpleLineDecorationElement.AddPosition(0.5);
      ILineDecoration ipLineDecoration = new LineDecorationClass();
      ipLineDecoration.AddElement(ipSimpleLineDecorationElement);
      ((ILineProperties)ipArrowLineSymbol).LineDecoration = ipLineDecoration;

      // the arrow is initially set to correspond to the digitized direction of the line
      //  if the barrier direction is against digitized, then we need to flip the arrow direction
      if (direction == esriNetworkEdgeDirection.esriNEDAgainstDigitized)
        ipSimpleLineDecorationElement.FlipAll = true;

      // Flash the line
      //  Two calls are made to Draw.  Since the ROP2 setting is NotXOrPen, the first call
      //  draws the symbol with our new symbology and the second call redraws what was originally 
      //  in the place of the symbol
      pDisplay.SetSymbol(ipArrowLineSymbol as ISymbol);
      pDisplay.DrawPolyline(pGeometry);
      System.Threading.Thread.Sleep(300);
      pDisplay.DrawPolyline(pGeometry);
    }

    /// <summary>
    /// Flash a point feature on the map
    /// <param name="pDisplay">The map screen</param>
    /// <param name="pGeometry">The geometry of the feature to be flashed</param>
    /// </summary>
    private void FlashPoint(IScreenDisplay pDisplay, IGeometry pGeometry)
    {
      // for a point, we only flash a simple circle
      ISimpleMarkerSymbol pMarkerSymbol = new SimpleMarkerSymbolClass();
      pMarkerSymbol.Style = esriSimpleMarkerStyle.esriSMSCircle;

      // Set up the Raster Op-Code to help the flash mechanism
      ISymbol pSymbol = pMarkerSymbol as ISymbol;
      pSymbol.ROP2 = esriRasterOpCode.esriROPNotXOrPen;

      // Flash the point
      //  Two calls are made to Draw.  Since the ROP2 setting is NotXOrPen, the first call
      //  draws the symbol with our new symbology and the second call redraws what was originally 
      //  in the place of the symbol
      pDisplay.SetSymbol(pSymbol);
      pDisplay.DrawPoint(pGeometry);
      System.Threading.Thread.Sleep(300);
      pDisplay.DrawPoint(pGeometry);
    }

    #endregion
  }
}