frmVRPSolver.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. // //************************************************************************************* // ArcGIS Network Analyst extension - VRP Solver Demonstration // // This simple code shows how to: // 1) Open a workspace and open a Network DataSet // 2) Create a NAContext and its NASolver // 3) Load Orders, Routes, Depots and Breaks from Feature Classes (or Table) and create Network Locations // 4) Set the Solver parameters // 5) Solve a VRP problem // 6) Read the VRP output to display the Route and Break output information //************************************************************************************ using System; using System.Windows.Forms; using ESRI.ArcGIS.Carto; using ESRI.ArcGIS.esriSystem; using ESRI.ArcGIS.Geodatabase; using ESRI.ArcGIS.Geometry; using ESRI.ArcGIS.NetworkAnalyst; using System.Text; namespace VRP_CSharp { public partial class frmVRPSolver : Form { private INAContext m_NAContext; private System.Collections.Hashtable m_unitTimeList; private System.Collections.Hashtable m_unitDistList; private readonly string OUTPUTCLASSNAME = "Routes"; public frmVRPSolver() { InitializeComponent(); Initialize(); } /// <summary> /// Initialize the solver by calling the ArcGIS Network Analyst extension functions. /// </summary> private void Initialize() { IFeatureWorkspace featureWorkspace = null; INetworkDataset networkDataset = null; try { // Open Geodatabase and network dataset IWorkspace workspace = OpenWorkspace(Application.StartupPath + @"\..\..\..\..\..\Data\SanFrancisco\SanFrancisco.gdb"); networkDataset = OpenNetworkDataset(workspace, "Transportation", "Streets_ND"); featureWorkspace = workspace as IFeatureWorkspace; } catch (Exception ex) { System.Windows.Forms.MessageBox.Show("Unable to open dataset. Error Message: " + ex.Message); this.Close(); return; } // Create NAContext and NASolver m_NAContext = CreateSolverContext(networkDataset); // Get available cost attributes from the network dataset INetworkAttribute networkAttribute; for (int i = 0; i < networkDataset.AttributeCount; i++) { networkAttribute = networkDataset.get_Attribute(i); if (networkAttribute.UsageType == esriNetworkAttributeUsageType.esriNAUTCost) { string unitType = GetAttributeUnitType(networkAttribute.Units); if (unitType == "Time") { comboTimeAttribute.Items.Add(networkAttribute.Name); } else if (unitType == "Distance") { comboDistanceAttribute.Items.Add(networkAttribute.Name); } } } comboTimeAttribute.SelectedIndex = 0; comboDistanceAttribute.SelectedIndex = 0; // Populate time field unit in comboBox m_unitTimeList = new System.Collections.Hashtable(); m_unitTimeList.Add("Seconds", esriNetworkAttributeUnits.esriNAUSeconds); m_unitTimeList.Add("Minutes", esriNetworkAttributeUnits.esriNAUMinutes); foreach (System.Collections.DictionaryEntry timeUnit in m_unitTimeList) { comboTimeUnits.Items.Add(timeUnit.Key.ToString()); } comboTimeUnits.SelectedIndex = 1; // Populate distance field unit in comboBox m_unitDistList = new System.Collections.Hashtable(); m_unitDistList.Add("Miles", esriNetworkAttributeUnits.esriNAUMiles); m_unitDistList.Add("Meters", esriNetworkAttributeUnits.esriNAUMeters); foreach (System.Collections.DictionaryEntry distUnit in m_unitDistList) { comboDistUnits.Items.Add(distUnit.Key.ToString()); } comboDistUnits.SelectedIndex = 0; // Populate time window importance attribute in comboBox comboTWImportance.Items.Add("High"); comboTWImportance.Items.Add("Medium"); comboTWImportance.Items.Add("Low"); comboTWImportance.SelectedIndex = 0; // Load locations IFeatureClass inputFClass = featureWorkspace.OpenFeatureClass("Stores"); LoadNANetworkLocations("Orders", inputFClass as ITable); inputFClass = featureWorkspace.OpenFeatureClass("DistributionCenter"); LoadNANetworkLocations("Depots", inputFClass as ITable); inputFClass = featureWorkspace.OpenFeatureClass("Routes"); LoadNANetworkLocations("Routes", inputFClass as ITable); ITable inputTable = featureWorkspace.OpenTable("Breaks"); LoadNANetworkLocations("Breaks", inputTable); // Create layer for network dataset and add to ArcMap INetworkLayer networkLayer = new NetworkLayerClass(); networkLayer.NetworkDataset = networkDataset; ILayer layer = networkLayer as ILayer; layer.Name = "Network Dataset"; axMapControl.AddLayer(layer, 0); // Create a network analysis layer and add to ArcMap INALayer naLayer = m_NAContext.Solver.CreateLayer(m_NAContext); layer = naLayer as ILayer; layer.Name = m_NAContext.Solver.DisplayName; axMapControl.AddLayer(layer, 0); } /// <summary> /// Call VRP solver and display the results /// </summary> /// <param name="sender">Sender of the event</param> /// <param name="e">Event</param> private void cmdSolve_Click(object sender, EventArgs e) { this.Cursor = Cursors.WaitCursor; listOutput.Items.Clear(); IGPMessages gpMessages = new GPMessagesClass(); try { listOutput.Items.Add("Solving..."); SetSolverSettings(); // Solve m_NAContext.Solver.Solve(m_NAContext, gpMessages, null); // Get the VRP output DisplayOutput(); } catch (Exception ex) { MessageBox.Show(ex.Message); } listOutput.Items.Add(GetGPMessagesAsString(gpMessages)); cmdSolve.Text = "Find VRP Solution"; RefreshMapDisplay(); this.Cursor = Cursors.Default; } /// <summary> /// Gather the error/warning/informative messages from GPMessages /// <summary> /// <param name="gpMessages">GPMessages container</param> /// <returns>string of all GPMessages</returns> public string GetGPMessagesAsString(IGPMessages gpMessages) { // Gather Error/Warning/Informative Messages var messages = new StringBuilder(); if (gpMessages != null) { for (int i = 0; i < gpMessages.Count; i++) { IGPMessage gpMessage = gpMessages.GetMessage(i); string message = gpMessage.Description; switch (gpMessages.GetMessage(i).Type) { case esriGPMessageType.esriGPMessageTypeError: messages.AppendLine("Error " + gpMessage.ErrorCode + ": " + message); break; case esriGPMessageType.esriGPMessageTypeWarning: messages.AppendLine("Warning: " + message); break; default: messages.AppendLine("Information: " + message); break; } } } return messages.ToString(); } /// <summary> /// Refresh the map display /// <summary> public void RefreshMapDisplay() { // Zoom to the extent of the service areas IGeoDataset geoDataset = m_NAContext.NAClasses.get_ItemByName(OUTPUTCLASSNAME) as IGeoDataset; IEnvelope envelope = geoDataset.Extent; if (!envelope.IsEmpty) { envelope.Expand(1.1, 1.1, true); axMapControl.Extent = envelope; m_NAContext.Solver.UpdateLayer(axMapControl.get_Layer(0) as INALayer); } axMapControl.Refresh(); } /// <summary> /// Get the VRP route output /// </summary> public void DisplayOutput() { // Display route information ITable naTable = m_NAContext.NAClasses.get_ItemByName(OUTPUTCLASSNAME) as ITable; if (naTable.RowCount(null) > 0) { listOutput.Items.Add("Route Name,\tOrder Count,\tTotal Cost,\tTotal Time,\tTotal Distance,\tStart Time,\tEnd Time:"); string routeName; long orderCount; double totalCost; double totalTime; double totalDistance; string routeStart; string routeEnd; ICursor naCursor = naTable.Search(null, false); IRow naRow = naCursor.NextRow(); // Display route details while (naRow != null) { routeName = naRow.get_Value(naTable.FindField("Name")).ToString(); orderCount = long.Parse(naRow.get_Value(naTable.FindField("OrderCount")).ToString()); totalCost = double.Parse(naRow.get_Value(naTable.FindField("TotalCost")).ToString()); totalTime = double.Parse(naRow.get_Value(naTable.FindField("TotalTime")).ToString()); totalDistance = double.Parse(naRow.get_Value(naTable.FindField("TotalDistance")).ToString()); routeStart = Convert.ToDateTime(naRow.get_Value(naTable.FindField("StartTime")).ToString()).ToString("T"); routeEnd = Convert.ToDateTime(naRow.get_Value(naTable.FindField("EndTime")).ToString()).ToString("T"); listOutput.Items.Add(routeName + ",\t\t" + orderCount.ToString() + ",\t\t" + totalCost.ToString("#0.00") + ",\t\t" + totalTime.ToString("#0.00") + ",\t\t" + totalDistance.ToString("#0.00") + ",\t\t" + routeStart + ",\t\t" + routeEnd); naRow = naCursor.NextRow(); } } listOutput.Items.Add(""); // Display lunch break information ITable naBreakTable = m_NAContext.NAClasses.get_ItemByName("Breaks") as ITable; if (naBreakTable.RowCount(null) > 0) { listOutput.Items.Add("Route Name,\tBreak Start Time,\tBreak End Time:"); ICursor naCursor = naBreakTable.Search(null, false); IRow naRow = naCursor.NextRow(); string routeName; string startTime; string endTime; // Display lunch details for each route while (naRow != null) { routeName = naRow.get_Value(naBreakTable.FindField("RouteName")).ToString(); startTime = Convert.ToDateTime(naRow.get_Value(naBreakTable.FindField("ArriveTime")).ToString()).ToString("T"); endTime = Convert.ToDateTime(naRow.get_Value(naBreakTable.FindField("DepartTime")).ToString()).ToString("T"); listOutput.Items.Add(routeName + ",\t\t" + startTime + ",\t\t" + endTime); naRow = naCursor.NextRow(); } } listOutput.Refresh(); } #region Geodatabase functions: open workspace and network dataset /// <summary> /// Geodatabase function: open workspace /// </summary> /// <param name="strGDBName">Input file name</param> /// <returns>Workspace</returns> public IWorkspace OpenWorkspace(string strGDBName) { // As Workspace Factories are Singleton objects, they must be instantiated with the Activator var workspaceFactory = System.Activator.CreateInstance(System.Type.GetTypeFromProgID("esriDataSourcesGDB.FileGDBWorkspaceFactory")) as ESRI.ArcGIS.Geodatabase.IWorkspaceFactory; if (!System.IO.Directory.Exists(strGDBName)) { MessageBox.Show("The workspace: " + strGDBName + " does not exist", "Workspace Error"); return null; } IWorkspace workspace = null; try { workspace = workspaceFactory.OpenFromFile(strGDBName, 0); } catch (Exception ex) { MessageBox.Show("Opening workspace failed: " + ex.Message, "Workspace Error"); } return workspace; } /// <summary> /// Geodatabase function: open network dataset /// </summary> /// <param name="workspace">Input workspace</param> /// <param name="strNDSName">Dataset name</param> /// <returns>Network dataset</returns> public INetworkDataset OpenNetworkDataset(IWorkspace workspace, string featureDatasetName, string strNDSName) { // Obtain the dataset container from the workspace var featureWorkspace = workspace as IFeatureWorkspace; ESRI.ArcGIS.Geodatabase.IFeatureDataset featureDataset = featureWorkspace.OpenFeatureDataset(featureDatasetName); var featureDatasetExtensionContainer = featureDataset as ESRI.ArcGIS.Geodatabase.IFeatureDatasetExtensionContainer; ESRI.ArcGIS.Geodatabase.IFeatureDatasetExtension featureDatasetExtension = featureDatasetExtensionContainer.FindExtension(ESRI.ArcGIS.Geodatabase.esriDatasetType.esriDTNetworkDataset); var datasetContainer3 = featureDatasetExtension as ESRI.ArcGIS.Geodatabase.IDatasetContainer3; // Use the container to open the network dataset. ESRI.ArcGIS.Geodatabase.IDataset dataset = datasetContainer3.get_DatasetByName(ESRI.ArcGIS.Geodatabase.esriDatasetType.esriDTNetworkDataset, strNDSName); return dataset as ESRI.ArcGIS.Geodatabase.INetworkDataset; } /// <summary> /// Geodatabase function: get network dataset /// </summary> /// <param name="networkDataset">Input network dataset</param> /// <returns>DE network dataset</returns> public IDENetworkDataset GetDENetworkDataset(INetworkDataset networkDataset) { // Cast from the network dataset to the DatasetComponent IDatasetComponent dsComponent = networkDataset as IDatasetComponent; // Get the data element return dsComponent.DataElement as IDENetworkDataset; } #endregion #region Network analyst functions /// <summary> /// Create NASolver and NAContext /// </summary> /// <param name="networkDataset">Input network dataset</param> /// <returns>NAContext</returns> public INAContext CreateSolverContext(INetworkDataset networkDataset) { // Get the data element IDENetworkDataset deNDS = GetDENetworkDataset(networkDataset); INASolver naSolver = new NAVRPSolver(); INAContextEdit contextEdit = naSolver.CreateContext(deNDS, naSolver.Name) as INAContextEdit; // Bind a context using the network dataset contextEdit.Bind(networkDataset, new GPMessagesClass()); return contextEdit as INAContext; } /// <summary> /// Load the input table and create field map to map fields from input table to NAClass /// </summary> /// <param name="strNAClassName">NAClass name</param> /// <param name="inputTable">Input table</param> public void LoadNANetworkLocations(string strNAClassName, ITable inputTable) { INamedSet classes = m_NAContext.NAClasses; INAClass naClass = classes.get_ItemByName(strNAClassName) as INAClass; // Delete existing rows from the specified NAClass naClass.DeleteAllRows(); // Create a NAClassLoader and set the snap tolerance (meters unit) INAClassLoader loader = new NAClassLoader(); loader.Locator = m_NAContext.Locator; loader.Locator.SnapTolerance = 100; loader.NAClass = naClass; // Create field map to automatically map fields from input table to NAclass INAClassFieldMap fieldMap = new NAClassFieldMapClass(); fieldMap.CreateMapping(naClass.ClassDefinition, inputTable.Fields); loader.FieldMap = fieldMap; // Avoid loading network locations onto non-traversable portions of elements INALocator3 locator = m_NAContext.Locator as INALocator3; locator.ExcludeRestrictedElements = true; locator.CacheRestrictedElements(m_NAContext); // Load input table int rowsIn = 0; int rowsLocated = 0; loader.Load(inputTable.Search(null, true), null, ref rowsIn, ref rowsLocated); // Message all of the network analysis agents that the analysis context has changed. INAContextEdit naContextEdit = m_NAContext as INAContextEdit; naContextEdit.ContextChanged(); } /// <summary> /// Set solver settings /// </summary> public void SetSolverSettings() { // Set VRP solver specific settings INASolver solver = m_NAContext.Solver; INAVRPSolver vrpSolver = solver as INAVRPSolver; // Both orders and routes have capacity count of 2 in the input shape files. User can modify the input data and update this value accordingly. vrpSolver.CapacityCount = 2; // Read the time and distance unit from comboBox vrpSolver.DistanceFieldUnits = (esriNetworkAttributeUnits)m_unitDistList[comboDistUnits.Items[comboDistUnits.SelectedIndex].ToString()]; vrpSolver.TimeFieldUnits = (esriNetworkAttributeUnits)m_unitTimeList[comboTimeUnits.Items[comboTimeUnits.SelectedIndex].ToString()]; // The value of time window violation penalty factor can be adjusted in terms of the user's preference. string importance = comboTWImportance.Items[comboTWImportance.SelectedIndex].ToString(); if (importance == "Low") vrpSolver.TimeWindowViolationPenaltyFactor = 0; else if (importance == "Medium") vrpSolver.TimeWindowViolationPenaltyFactor = 1; else if (importance == "High") vrpSolver.TimeWindowViolationPenaltyFactor = 10; // Set output line type vrpSolver.OutputLines = esriNAOutputLineType.esriNAOutputLineStraight; // Set generic solver settings // Set the impedance attribute INASolverSettings solverSettings = solver as INASolverSettings; solverSettings.ImpedanceAttributeName = comboTimeAttribute.Text; // Set the accumulated attribute IStringArray accumulatedAttributes = solverSettings.AccumulateAttributeNames; accumulatedAttributes.RemoveAll(); accumulatedAttributes.Insert(0, comboDistanceAttribute.Text); solverSettings.AccumulateAttributeNames = accumulatedAttributes; // Set the oneway restriction if necessary IStringArray restrictions = solverSettings.RestrictionAttributeNames; restrictions.RemoveAll(); if (checkUseRestriction.Checked) restrictions.Add("oneway"); solverSettings.RestrictionAttributeNames = restrictions; // Restrict UTurns solverSettings.RestrictUTurns = esriNetworkForwardStarBacktrack.esriNFSBNoBacktrack; // Set the hierarchy attribute solverSettings.UseHierarchy = checkUseHierarchy.Checked; if (solverSettings.UseHierarchy) solverSettings.HierarchyAttributeName = "HierarchyMultiNet"; // Do not forget to update the context after you set your impedance solver.UpdateContext(m_NAContext, GetDENetworkDataset(m_NAContext.NetworkDataset), new GPMessagesClass()); } #endregion /// <summary> /// Check whether the attribute unit is time or distance unit. /// </summary> /// <param name="units">Input network attribute units</param> /// <returns>Unit type</returns> private string GetAttributeUnitType(esriNetworkAttributeUnits units) { string unitType = ""; switch (units) { case esriNetworkAttributeUnits.esriNAUDays: case esriNetworkAttributeUnits.esriNAUHours: case esriNetworkAttributeUnits.esriNAUMinutes: case esriNetworkAttributeUnits.esriNAUSeconds: unitType = "Time"; break; case esriNetworkAttributeUnits.esriNAUYards: case esriNetworkAttributeUnits.esriNAUMillimeters: case esriNetworkAttributeUnits.esriNAUMiles: case esriNetworkAttributeUnits.esriNAUMeters: case esriNetworkAttributeUnits.esriNAUKilometers: case esriNetworkAttributeUnits.esriNAUInches: case esriNetworkAttributeUnits.esriNAUFeet: case esriNetworkAttributeUnits.esriNAUDecimeters: case esriNetworkAttributeUnits.esriNAUNauticalMiles: case esriNetworkAttributeUnits.esriNAUCentimeters: unitType = "Distance"; break; default: listOutput.Items.Add("Failed to find Network Attribute Units."); break; } return unitType; } } }