-----------------------------------

Acquista i software ArcGIS tramite Studio A&T srl, rivenditore autorizzato dei prodotti Esri.

I migliori software GIS, il miglior supporto tecnico!

I migliori software GIS, il miglior supporto tecnico!
Azienda operante nel settore GIS dal 2001, specializzata nell’utilizzo della tecnologia ArcGIS e aderente ai programmi Esri Italia Business Network ed Esri Partner Network

-----------------------------------



venerdì 31 maggio 2013

Time Evaluator

L'extension Network Analyst disponibile sia in ambiente Desktop che Server  permette la gestione e le analisi spaziali basate su reti come routing, fleet routing, travel directions, closest facility, service area, e location-allocation.
Se però volessimo gestire la schedulazione dell'orario dei bus nella nostra analisi - in modo che, se una route arriva ad uno stop del bus prima del prossimo orario schedulato di partenza, essa debba attendere - occorrerebbe sviluppare un custom evaluator. Infatti, per gestire la schedulazione dell'orario dei bus, non è prevista una soluzione out-of-box ma lo sarà nelle successive release.
Fortunatamente tuttavia è disponibile, sin dalla versione 10.0, l'interfaccia ITimeAwareEvaluator che ci permette -  sviluppando un custom time-aware network attribute evaluator - di gestire anche questa problematica in modo molto semplice. Chiaramente potremo poi esporre la stessa funzionalità lato server tramite ArcGIS Server registrando opportunamente la dll.


Creiamo un progetto in Visual Studio per creare una class library (.dll).
La nostra classe dovrà implementare le seguenti interfacce INetworkEvaluator2, INetworkEvaluatorSetup e, come già detto, ITimeAwareEvaluator.

Se volessimo impostare anche delle proprietà, tramite ArcCatalog, del custom evaluator potremmo anche creare una classe che implementa l'interfaccia IEvaluatorEditor e crearci la nostra interfaccia utente ad esempio tramite una Windows Form. L'associazione con l'evaluator avviene impostando il CLSID dell'evaluator nella proprietà EvaluatorCLSID e tramite la proprietà EditEvaluators, utilizzata da ArcCatalog per impostare un  riferimento ai suoi object EditEvaluator su ogni EvaluatorEditor  registrato e consente così di accedere allo stato corrente della finestra di dialogo degli Evaluator e listare quelli correnti e selezionati per poi leggere ed impostare la proprietà Data da utilizzare nell'evaluator.

// -----------------------------------------------------------------------
// <copyright file="DepartureTimeEvaluatorEditor.cs" company="Studio A&T s.r.l.">
// Copyright (c) Studio A&T s.r.l. All rights reserved.
// </copyright>
// <author>Nicogis</author>
// -----------------------------------------------------------------------
namespace Studioat.ArcGIS.DepartureTimeEvaluator
{
 using System;
 using System.Collections.Generic;
 using System.Runtime.InteropServices;
 using ESRI.ArcGIS.ADF.CATIDs;
 using ESRI.ArcGIS.CatalogUI;
 using ESRI.ArcGIS.esriSystem;
 using ESRI.ArcGIS.Geodatabase;
 
 
 [ClassInterface(ClassInterfaceType.None)]
 [Guid("7409A80D-13BB-46EB-8658-B1757CEB0CCA")]
 public class DepartureTimeEvaluatorEditor : IEvaluatorEditor
 {
  #region Component Category Registration
 
  [ComRegisterFunction()]
  [ComVisible(false)]
  static void RegisterFunction(Type registerType)
  {
   string regKey = string.Format("HKEY_CLASSES_ROOT\\CLSID\\{{{0}}}", registerType.GUID);
   NetworkEvaluatorEditor.Register(regKey);
  }
 
  [ComUnregisterFunction()]
  [ComVisible(false)]
  static void UnregisterFunction(Type registerType)
  {
   string regKey = string.Format("HKEY_CLASSES_ROOT\\CLSID\\{{{0}}}", registerType.GUID);
   NetworkEvaluatorEditor.Unregister(regKey);
  }
 
  #endregion
 
  #region IEvaluatorEditor Members
 
  public bool ContextSupportsEditDescriptors
  {
   // The descriptor text is the single line of text in the Evaluators dialog that appears under the Value column.
   //  This property indicates whether the descriptor text can be directly edited in the dialog by the user.
   //  Since this evaluator editor does not make use of descriptors, it returns false
   get { return false; }
  }
 
 
  public void EditDescriptors(string value)
  {
   // This evaluator editor does not make use of descriptors.
  }
 
  public bool ContextSupportsEditProperties
  {
   // This property indicates whether the ArcCatalog user is able to bring up a dialog by 
   //  clicking the Properties button (or pressing F12) in order to specify settings for the evaluator.
   get { return true; }
  }
 
  private IEditEvaluators m_EditEvaluators;
  public IEditEvaluators EditEvaluators
  {
   // This property is used by ArcCatalog to set a reference to its EditEvaluators object 
   //  on each registered EvaluatorEditor. This allows each EvaluatorEditor to access the 
   //  current state of ArcCatalog's Evaluators dialog, such as how many evaluators are listed 
   //  and which evaluators are currently selected.
   get { return m_EditEvaluators; }
   set { m_EditEvaluators = value; }
  }
 
  public bool EditProperties(int parentWindow)
  {
   // Get an array of the currently selected evaluators to pass to the properties form.
   var evaluators = new List<INetworkEvaluator>();
   for (int i = 0; i < m_EditEvaluators.Count; i++)
   {
    if (m_EditEvaluators.get_IsSelected(i))
     evaluators.Add(m_EditEvaluators.get_EditEvaluator(i));
   }
 
   PropertiesDialog form = new PropertiesDialog(evaluators);
   form.ShowDialog();
   return false;
  }
 
  public UID EvaluatorCLSID
  {
   get
   {
    // This property returns the GUID of this EvaluatorEditor's associated INetworkEvaluator.
    UID uid = new UIDClass();
    uid.Value = "{13C81137-7DE4-40B4-9A73-71271CEE44BE}";
    return uid;
   }
  }
 
  public void SetDefaultProperties(int index)
  {
   // This method is called when the ArcCatalog user selects this evaluator 
   //  under the Type column of the Evaluators dialog. This method can be used to 
   //  initialize any dialogs that the evaluator editor uses.
  }
 
  public int ValueChoice
  {
   // This evaluator editor does not support value choices.
   set { }
  }
 
  public int ValueChoiceCount
  {
   // This evaluator editor has no value choices.
   get { return 0; }
  }
 
  public string get_Descriptor(int index)
  {
   // This evaluator editor does not make use of descriptors.
   return string.Empty;
  }
 
  public string get_FullDescription(int index)
  {
   // This property is the text representation of all of the settings made on this evaluator.
   // This evaluator editor does not make any settings changes, so it returns an empty string.
   return string.Empty;
  }
 
  public string get_ValueChoiceDescriptor(int choice)
  {
   // This evaluator editor does not make use of value choices.
   return string.Empty;
  }
 
  #endregion
 }
}
 
 
Mentre la form imposta e legge la property Data dell'evaluator:


namespace Studioat.ArcGIS.DepartureTimeEvaluator
{
    public partial class PropertiesDialog : Form
    {
            List<INetworkEvaluator> m_evaluators;
            public PropertiesDialog(List<INetworkEvaluator> evaluators)
            {
                m_evaluators = evaluators;
 
                InitializeComponent();
 
                foreach (var evaluator in m_evaluators)
                {
                    var evaluatorSetup = evaluator as INetworkEvaluatorSetup;
                    IPropertySet propSet = evaluatorSetup.Data;
                    if (propSet != null)
                    {
                        txtFilePath.Text = propSet.GetProperty(DepartureTimeEvaluator.PROPNAME_SCHEDULE_FILEGDB_PATH).ToString();
                    }
                }
            }
 
            private void btnCancel_Click(object sender, EventArgs e)
            {
                this.Close();
            }
 
            private void btnOK_Click(object sender, EventArgs e)
            {
                string scheduleFilePath = txtFilePath.Text;
                if (!File.Exists(scheduleFilePath))
                {
                    MessageBox.Show("Schedule file gdb does not exist");
                    return;
                }
 
                
                foreach (var evaluator in m_evaluators)
                {
                    var evaluatorSetup = evaluator as INetworkEvaluatorSetup;
                    IPropertySet propSet = evaluatorSetup.Data;
                    if (propSet == null)
                        propSet = new PropertySetClass();
 
                    propSet.SetProperty(DepartureTimeEvaluator.PROPNAME_SCHEDULE_FILEGDB_PATH, scheduleFilePath);
 
                    evaluatorSetup.Data = propSet;
                }
 
                this.Close();
            }
    }
}




In questo caso specifico una proprietà per questo tipo di evaluator è la tabella che contiene gli orari dei singoli tratti delle linee bus, relazionata (uno a molti) con i singoli tratti delle linee bus.
Possiamo dare un nome ben preciso alla tabella e decidere che si deve trovare nello stesso workspace del nostro network dataset e così non abbiamo bisogno di proprietà particolari da far selezionare all'utente o dare la possibilità all'utente di selezionare una tabella, con determinate caratteristiche, in un qualsiasi workspace e così abiliteremo l'editor come visto sopra (proprietà ContextSupportsEditProperties in IEvaluatorEditor).




Vediamo nel dettaglio l'implementazione delle interfacce INetworkEvaluator2 e INetworkEvaluatorSetup:

    [ClassInterface(ClassInterfaceType.None)]
    [Guid("13C81137-7DE4-40B4-9A73-71271CEE44BE")] 
    public class DepartureTimeEvaluator : INetworkEvaluator2INetworkEvaluatorSetupITimeAwareEvaluator
    {
 
        #region Member Variables
 
        public static readonly string tableStopToStopTime = "StopToStopTime";
 
        public static readonly TimeSpan midnightTransit = new TimeSpan(24, 0, 0);
        public static readonly TimeSpan midnight = new TimeSpan(0, 0, 0);
 
        private IFeatureWorkspace featureWorkspace = null;
        private List<StopToStopTime> stopToStopTime = null;
 
        private Dictionary<intList<StopToStopTime>> track = null;
 
        private int? idEdgeStart = null;
        private TimeSpan timeSpanStart = TimeSpan.Zero;
 
        #endregion
 
        #region INetworkEvaluator Members
 
        public bool CacheAttribute
        {
            //  CacheAttribute returns whether or not we want the network dataset to cache our evaluated attribute 
            //  values during the network dataset build. Since this is a dynamic evaluator, we will return false, 
            //  so that our attribute values are dynamically queried at runtime.
            get { return false; }
        }
 
        public string DisplayName
        {
            get { return "BusTimeEvaluator"; }
        }
 
        public string Name
        {
            get { return "Studioat.ArcGIS.DepartureTimeEvaluator.DepartureTimeEvaluator"; }
        }
 
        #endregion
 
        #region INetworkEvaluatorSetup Members
 
        public UID CLSID
        {
            get
            {
                // Create and return the GUID for this custom evaluator
                UID uid = new UIDClass();
                uid.Value = "{13C81137-7DE4-40B4-9A73-71271CEE44BE}";
                return uid;
            }
        }
 
 
        public IPropertySet Data
        {
            // The Data property is intended to make use of property sets to get/set the custom 
            //  evaluator's properties using only one call to the evaluator object.
            get;
            set;
        }
 
        public bool DataHasEdits
        {
            // Since this custom evaluator does not make any data edits, return false.
            get { return false; }
        }
 
        public void Initialize(INetworkDataset networkDataset, IDENetworkDataset dataElement, INetworkSource source, IEvaluatedNetworkAttribute attribute)
        {
            try
            {
                
                IDataset dataset = networkDataset as IDataset;
                this.featureWorkspace = dataset.Workspace as IFeatureWorkspace;
 
                //ESRI.ArcGIS.Geodatabase.INetworkSource netSource = networkDataset.get_SourceByID(this.sourceID);
 
                //// Get the network source as a feature class.
                //IFeatureClassContainer featureClassContainer = networkDataset as IFeatureClassContainer;
                //this.featureClassStop = featureClassContainer.get_ClassByName(netSource.Name);
                
                CacheSchedules();
 
            }
            catch (Exception e)
            {
                MessageBox.Show("Evaluator initialization failure for " + DisplayName + ". Error message: " + e.Message);
            }
        }
 
        public object QueryValue(INetworkElement element, IRow row)
        {
            // If this evaluator is queried without specifying a time, then the element has no added costs.
            return 0;
        }
 
        public bool SupportsDefault(esriNetworkElementType elementType, IEvaluatedNetworkAttribute attribute)
        {
            // This custom evaluator can not be used for assigning default attribute values.
            return false;
        }
 
        public bool SupportsSource(INetworkSource source, IEvaluatedNetworkAttribute attribute)
        {
            // This custom evaluator supports added costs only for turns.
            bool isEdgeSource = (source.ElementType == esriNetworkElementType.esriNETEdge);
            bool isCostAttribute = (attribute.UsageType == esriNetworkAttributeUsageType.esriNAUTCost);
 
            return (isEdgeSource && isCostAttribute);
        }
 
        public bool ValidateDefault(esriNetworkElementType elementType, IEvaluatedNetworkAttribute attribute, ref int errorCode, ref string errorDescription, ref string errorAppendInfo)
        {
            if (SupportsDefault(elementType, attribute))
            {
                errorCode = 0;
                errorDescription = errorAppendInfo = string.Empty;
                return true;
            }
            else
            {
                errorCode = -1;
                errorDescription = errorAppendInfo = string.Empty;
                return false;
            }
        }
 
        public bool ValidateSource(IDatasetContainer2 datasetContainer, INetworkSource networkSource, IEvaluatedNetworkAttribute attribute, ref int errorCode, ref string errorDescription, ref string errorAppendInfo)
        {
            if (SupportsSource(networkSource, attribute))
            {
                errorCode = 0;
                errorDescription = errorAppendInfo = string.Empty;
                return true;
            }
            else
            {
                errorCode = -1;
                errorDescription = errorAppendInfo = string.Empty;
                return false;
            }
        }
 
        #endregion
 
        #region INetworkEvaluator2 Members
 
        public void Refresh()
        {
            // Refresh is called for each evaluator on each solve.
            try
            {
                this.track = new Dictionary<intList<StopToStopTime>>();
                this.idEdgeStart = null;
                this.CacheSchedules();
                LogMessage("Solve called (Refresh)");
            }
            catch (Exception e)
            {
                MessageBox.Show("Evaluator refresh failure for " + DisplayName + ". Error message: " + e.Message);
            }
        }
 
        public IStringArray RequiredFieldNames
        {
            // This custom evaluator does not require any field names.
            get { return null; }
        }
 
        #endregion

Per la proprietà CacheAttribute restituiamo false perché in questo caso l'evaluator è dinamico nel senso che l'attributo è valutato a run time e quando viene fatto un build  della network non occorre memorizzare i valori degli attributi dell'evaluator.
Il DisplayName è il nome dell'evaluator che viene visto in ArcCatalog mentre Name è il nome univoco dell'evaluator.


 
 
CLSID è la GUID del nostro custom evaluator.

Data è il propertySet che memorizza eventuali impostazione del nostro custom evaluator, che possono essere impostare utilizzando IEvaluatorEditor.

DataHasEdits per notificare se sono state fatte modifiche a Data e quindi per poter reinizializzare l'evaluator.

Initialize metodo che inizializza l'evaluator ad essere utilizzato per l'interrogazione degli attributi. Nel nostro caso il metodo viene eseguito sulla prima chiamata nella sessione che richiede che l'evaluator determini il valore dell'attributo.

QueryValue metodo che non viene utilizzato. Se l'evaluator è interrogato senza impostare lo Start Time per gli elementi non aggiungiamo costi e quindi impostiamo a 0.

SupportDefault impostiamo a false così da non mostrare in ArcCatalog la possibilità di sceglierlo come evaluator di default


SupportSource se l'evaluator può essere utilizzato per assegnare valori degli attributi di date risorse. Nel nostro caso verifichiamo che sia un attributo di tipo costo ed applicato ad elementi di tipo edge.

ValidateDefault e ValidateSource indica se l'evaluator è in uno stato valido per essere utilizzato come Default evaluator o evaluator per la data source e attributo della network.

Refresh il metodo è chiamato ogni volta che eseguiamo un solve. Nel nostro caso potremmo rileggere (se serve) la schedulazione degli orari dei bus.

RequiredFieldNames per minimizzare i dati restituiti dalla query è possibile impostare solo i campi che servono all'evaluator od impostare null se sono necessari tutti i campi. Ma nel nostro caso restituimo null perché l'evaluator non richiede campi dato che il metodo query non viene utilizzato.

Ma il core della funzionalità è implementato dall'interfaccia ITimeAwareEvaluator con il metodo QueryValueAtTime :

Tramite element ci facciamo restituire l'objectId dell'elemento corrente interrogato e con questo filtriamo la tabella delle schedulazioni per farci restituire tutti gli orari di partenza dell'edge corrente. A questo punto selezioniamo l'orario più vicino e superiore a quello corrente (queryTime), ne calcoliamo la differenza (tempo di attesa) e sommiamo il tempo per percorrere il tratto corrente (orario di partenza - orario di arrivo):

Impedance edge = wait time + edge time


#region ITimeAwareEvaluator
        
        public object QueryValueAtTime(INetworkElement element, DateTime queryTime, esriNetworkTimeUsage timeUsage)
        {
            // Note that all query times are local times in the time zone of the network element.
 
            // This element has added costs if its associated ObjectID is currently stored within 
            //  the network source's hashtable. The added cost is equal to the amount of wait time 
            //  from the query time to the next scheduled departure.
 
            int oid = element.OID;
 
            var stopToStopTime = this.stopToStopTime.Where(f => f.OIDStopToStop == oid).OrderBy(c => c.StartTime);
 
            TimeSpan queryTimeOfDay = queryTime.TimeOfDay;
 
            if (this.idEdgeStart.HasValue)
            {
                if ((this.idEdgeStart == oid) && (this.timeSpanStart == queryTimeOfDay))
                {
                    LogMessage("----------------------------------Start Path ---------------------------------------------");
                }
            }
            else
            {
                this.idEdgeStart = oid;
                this.timeSpanStart = queryTimeOfDay;
                LogMessage("------------------------------------------------------------------------------------------");
                LogMessage("----------------------------------Start Path ---------------------------------------------");
            }
 
 
            LogMessage("TimeOfDay: " + queryTimeOfDay.ToString() + "queryTime:" + queryTime.ToString());
            LogMessage("OID: " + oid + " - SourceOID: " + element.SourceID + " - TimeUsage:" + ((int)timeUsage == 1? "before" : "after"));
 
 
            //check if span multiple date and queryTimeOfDay is between midnight and max starttime next day
            if (stopToStopTime.Where(t => t.StartTime >= midnightTransit).Count() > 0)
            {
                TimeSpan maxTimeNextDay = stopToStopTime.Where(t => t.StartTime >= midnightTransit).Max(k => k.StartTime);
 
                if ((maxTimeNextDay.Subtract(midnightTransit) >= queryTimeOfDay) && (queryTimeOfDay >= midnight))
                {
                    queryTimeOfDay = queryTimeOfDay.Add(midnightTransit);
                }
            }
 
            var schedule = stopToStopTime.Where(t => t.StartTime >= queryTimeOfDay);
 
            double seconds;
            StopToStopTime s;
            if (schedule.Count() == 0)
            {
                LogMessage("schedulate next day");
                s = stopToStopTime.First();
 
                double midnightWait = 0.0;
                
                if (queryTimeOfDay.Ticks != 0) // it isn't midnight
                {
                    midnightWait = midnightTransit.Subtract(queryTimeOfDay).TotalSeconds;
                }
 
                seconds = midnightWait + s.StartTime.TotalSeconds + s.Seconds; //(until midnight + from first start) wait time + edge time
            }
            else
            {
                s = schedule.First();
                TimeSpan schedulateTime = s.StartTime;
                seconds = schedulateTime.Subtract(queryTimeOfDay).TotalSeconds + s.Seconds; //wait time + edge time 
            }
 
            
            LogMessage("Seconds:" + seconds);
            LogMessage("------------------------------------------------------------------------------------------");
            return seconds;
        }
 
        #endregion
 
        private void CacheSchedules()
        {
            
            ITable tableStopToStopTime = this.featureWorkspace.OpenTable(DepartureTimeEvaluator.tableStopToStopTime);
            this.stopToStopTime = new List<StopToStopTime>();
 
            using (ComReleaser comReleaser = new ComReleaser())
            {
                ICursor cursor = tableStopToStopTime.Search(nulltrue);
                int idxFieldArrivalTime = tableStopToStopTime.FindField("ArrivalTime");
                int idxFieldDepartureTime = tableStopToStopTime.FindField("DepartureTime");
                int idxFieldOIDStopToStop = tableStopToStopTime.FindField("OIDStopToStop");
 
                comReleaser.ManageLifetime(cursor);
                IRow row = cursor.NextRow();
 
                while (row != null)
                {
 
                    string[] start = Convert.ToString(row.get_Value(idxFieldDepartureTime)).Split(new char[] { ':' });
                    TimeSpan departureTime = new TimeSpan(Convert.ToInt32(start[0]), Convert.ToInt32(start[1]), Convert.ToInt32(start[2]));
                    string[] end = Convert.ToString(row.get_Value(idxFieldArrivalTime)).Split(new char[] { ':' });
                    TimeSpan arrivalTime = new TimeSpan(Convert.ToInt32(end[0]), Convert.ToInt32(end[1]), Convert.ToInt32(end[2]));
 
                    int OIDStopToStop = Convert.ToInt32(row.get_Value(idxFieldOIDStopToStop));
 
                    double seconds = arrivalTime.Subtract(departureTime).TotalSeconds;
 
                    stopToStopTime.Add(new StopToStopTime(OIDStopToStop, departureTime, arrivalTime, seconds));
                    row = cursor.NextRow();
                }
            }
        }


Una volta creata la dll occorre registrarla con ESRIRegAsm:

"%COMMONPROGRAMFILES(x86)%\ArcGIS\bin\ESRIRegAsm" /p:desktop Studioat.ArcGIS.DepartureTimeEvaluator.dll


Mentre se vogliamo utilizzarla anche in ArcGIS Server utilizziamo il comando:

"%COMMONPROGRAMFILES%\ArcGIS\bin\ESRIRegAsm" Studioat.ArcGIS.DepartureTimeEvaluator.dll /p:server

In questo ultimo caso occorre compilare la dll in Platform target con Any CPU



Ma veniamo ora ad un esempio:


Una volta impostato l'evaluator agli elementi edge dell'attributo di tipo costo creiamo un nuovo layer di analisi Route.
In Route Properties selezioniamo la checkbox Use Start Time ed impostiamo un orario di partenza (ad esempio 8.00) mentre chiaramente in Impedance è impostato il costo sul quale è impostato il nostro evaluator ed indichiamo due stop.





A questo punto eseguiamo con il Solve per determinare il percorso che minimizza la impedance:



Riepilogando:
- partenza alle 8.00
- partenza più vicina e uguale o superiore alle 8:00 per il tratto 2 è alle 8:10:50 e arrivo alle 8:12:30
- partenza più vicina e uguale o superiore alle 8:12:30 per il tratto 3 è alle 8:12:30 e arrivo alle 8:34:30

Quindi l'end time è 8:34:30.

Ma se dovessimo modificare un orario, ad esempio quello del tratto 11 alla data  di arrivo 8:33:20 il percorso come possiamo vedere cambia. Possiamo vederlo dinamicamente perché nell'evaluator per il metodo Refresh rifacciamo leggere la schedulazione che consente così all'evaluator di rilevare i dati in tabella aggiornati.


Riepilogando:
- partenza alle 8.00
- partenza più vicina e uguale o superiore alle 8:00 per il tratto 2 è alle 8:10:50 e arrivo alle 8:12:30
- partenza più vicina e uguale o superiore alle 8:12:30 per il tratto 9 è alle 8:20:20 e arrivo alle 8:30:50
- partenza più vicina e uguale o superiore alle 8:30:50 per il tratto 10 è alle 8:30:50 e arrivo alle 8:32:30
- partenza più vicina e uguale o superiore alle 8:32:30 per il tratto 11 è alle 8:32:30 e arrivo alle 8:33:20

Quindi l'end time è 8:33:20 e quindi inferiore al caso precedente. Ecco perché il solver ha scelto questo percorso.

Infine per poter andare in debug con il custom evaluator possiamo, come avviene, per le altre personalizzazioni in ArcGis Desktop impostare l'exe di ArcMap  in Start external program nella sezione Debug delle proprietà del progetto.



Qui potete vedere un sample del custom evaluator esposto via ArcGIS for Server con la stessa logica descritta nel documento PDF di questo link.