Vehicle Tracker Extension
Extension.cs
// Copyright 2010 ESRI
// 
// All rights reserved under the copyright laws of the United States
// and applicable international laws, treaties, and conventions.
// 
// You may freely redistribute and use this sample code, with or
// without modification, provided you include the original copyright
// notice and use restrictions.
// 
// See the use restrictions.
// 

using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.Linq;
using System.ComponentModel;

using ESRI.ArcGISExplorer;
using ESRI.ArcGISExplorer.Application;
using ESRI.ArcGISExplorer.Mapping;
using ESRI.ArcGISExplorer.Geometry;
using ESRI.ArcGISExplorer.Data;
using ESRI.ArcGISExplorer.Threading;

namespace VehicleTrackingExtCS
{
  public class Extension : ESRI.ArcGISExplorer.Application.Extension
  {
    ESRI.ArcGISExplorer.Threading.BackgroundWorker _bgWorker;
    System.Windows.Forms.Timer _timer;

    static List<Vehicle> _vehicles;
    Symbol _ambulanceSymbol;
    int _fileCount = 0;

    string _dataPath = String.Empty;
    bool _dataOk = false;

    /// <summary>
    /// Called by Explorer when the application starts up. Work out the path to the Data, 
    /// and hook up the document open event.
    /// </summary>
    public override void OnStartup()
    {
      string sdkPath = Environment.GetEnvironmentVariable("ArcGIS_E3SDK");
      _dataPath = sdkPath + @"\Samples\VehicleTrackingExt\Data";

      if (System.IO.Directory.Exists(_dataPath))
      {
        _dataOk = true;
      }

      if (_dataOk)
      {
        Application.DocumentOpened += new EventHandler(Application_DocumentOpened);
        Application.DocumentClosed += new EventHandler(Application_DocumentClosed);
      }

      //Setup the background worker
      InitializeBackgroundWorker();
    }

    /// <summary>
    /// Called by Explorer when the application shuts down. Stop the Timer, unhook any event handlers.
    /// </summary>
    public override void OnShutdown()
    {
      if (_timer != null)
      {
        _timer.Stop();
        _timer = null;
      }

      Application.DocumentOpened -= new EventHandler(Application_DocumentOpened);
      Application.DocumentClosed -= new EventHandler(Application_DocumentClosed);

    }

    /// <summary>
    /// Initializes the vehicle list and the background worker, listen for graphic clicked events
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    void Application_DocumentOpened(object sender, EventArgs e)
    {
        InitializeBackgroundWorker();
    }

    void InitializeBackgroundWorker()
    {
        Application.ActiveMapDisplay.GraphicClicked += new EventHandler<GraphicMouseEventArgs>(ActiveMapDisplay_GraphicClicked);

        _vehicles = new List<Vehicle>();

        _bgWorker = new ESRI.ArcGISExplorer.Threading.BackgroundWorker();
        _bgWorker.DoWork += new ESRI.ArcGISExplorer.Threading.DoWorkEventHandler(DoWork);
        _bgWorker.RunWorkerCompleted += new ESRI.ArcGISExplorer.Threading.RunWorkerCompletedEventHandler(RunWorkerCompleted);
        if (!_bgWorker.IsBusy)
        {
            // Best practice to always check IsBusy before calling RunWorkerAsync; although in this specific 
            // case as the object has just been created its unlikely to be busy.
            _bgWorker.RunWorkerAsync(_dataPath);
        }
    }

    /// <summary>
    /// Stop the timer and remove the graphics from the map
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    void Application_DocumentClosed(object sender, EventArgs e)
    {
      // Stop the timer (stop any more background work being started).
      if (_timer != null)
      {
        _timer.Stop();
      }
      _timer = null;

      // Unhook event handlers while there is no document.
      Application.ActiveMapDisplay.GraphicClicked -= new EventHandler<GraphicMouseEventArgs>(ActiveMapDisplay_GraphicClicked);
      if (_bgWorker != null)
      {
        _bgWorker.DoWork -= new ESRI.ArcGISExplorer.Threading.DoWorkEventHandler(DoWork);
        _bgWorker.RunWorkerCompleted -= new ESRI.ArcGISExplorer.Threading.RunWorkerCompletedEventHandler(RunWorkerCompleted);
        _bgWorker = null;
      }
    }

    /// <summary>
    /// Re-execute the background worker to poll the GeoRSS feed.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    void _timer_Tick(object sender, EventArgs e)
    {
      if ((_bgWorker != null) && (!_bgWorker.IsBusy))
      {
        _bgWorker.RunWorkerAsync(_dataPath);
      }
    }

    /// <summary>
    /// Called on the worker thread. Queries a GeoRSS feed to get a list of emergency vehicles
    /// and their locations and updates a list of known vehicles from previous queries
    /// on the feed.
    /// NOTE: In order to provide reliable sample code, this reads files saved from a GeoRSS feed,
    /// not a live GeoRSS feed.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    void DoWork(object sender, ESRI.ArcGISExplorer.Threading.DoWorkEventArgs e)
    {
      // Pull vehicle records from the GeoRSS feed / files.
      DataSet ds = new DataSet();
      string path = (string) e.Argument;

      _fileCount += 1;
      if (_fileCount > 20)
      {
        _fileCount = 1;
      }

      string filePath = path + "\\GetAmbulances" + _fileCount.ToString() + @".xml";
      _dataOk = System.IO.File.Exists(filePath);
      if (_dataOk)
      {
        ds.ReadXml(filePath, XmlReadMode.Auto);
        DataTable table = ds.Tables[0];
        ProcessVehicles(table);
      }
    }

    /// <summary>
    /// Called on the UI thread. Update the vehicle locations on the map, add and delete
    /// any new or removed vehicles.
    /// The first time this method is called for the document, initializes a timer to update
    /// the vehicles at a regular interval.
    /// </summary>
    void RunWorkerCompleted(object sender, ESRI.ArcGISExplorer.Threading.RunWorkerCompletedEventArgs e)
    {
      //Don't update the graphic display if the user is in the process of navigating
      if (ESRI.ArcGISExplorer.Application.Application.ActiveMapDisplay.IsNavigating) return;

      if (e.Error != null)
      {
        // There was an exception thrown within the DoWork event.
        // May wish to investigate here.
        System.Diagnostics.Debug.WriteLine(e.Error.ToString());
      }

      
      if (_vehicles != null)
      {
          //Put the updating of vehicles inside a SuspendDrawing block for better performance
          MapDisplay mapDisp = ESRI.ArcGISExplorer.Application.Application.ActiveMapDisplay;
          using (mapDisp.SuspendDisplay())
          {
              GraphicCollection graphics = Application.ActiveMapDisplay.Graphics;
              foreach (Vehicle vehicle in _vehicles)
              {
                  if (vehicle.GraphicUpdateStatus == UpdateStatus.Delete)
                  {
                      graphics.Remove(vehicle.MapGraphic);
                  }
              }

              GetVehicleGraphics();

              foreach (Vehicle vehicle in _vehicles)
              {
                  if (!graphics.Contains(vehicle.MapGraphic))
                  {
                      graphics.Add(vehicle.MapGraphic);
                  }
              }
          }
      }

      // If this is the first time the worker has completed, then start the timer to start 
      // generating more updates.
      if (_timer == null)
      {
        _timer = new System.Windows.Forms.Timer();
        _timer.Interval = (5000);
        _timer.Tick += new EventHandler(_timer_Tick);
        _timer.Start();
      }
    }

    /// <summary>
    /// Creates Vehicle objects from the GeoRSS feed records, adds them if they are new and
    /// marks them as updated if they have changed
    /// </summary>
    /// <param name="table"></param>
    /// <param name="vehicleType"></param>
    private void ProcessVehicles(DataTable table)
    {
      foreach (DataRow row in table.Rows)
      {
        Vehicle newVehicle = new Vehicle()
        {
          X = Convert.ToDouble(row.ItemArray.GetValue(1).ToString()),
          Y = Convert.ToDouble(row.ItemArray.GetValue(2).ToString()),
          Status = row.ItemArray.GetValue(5).ToString(),
          ID = row.ItemArray.GetValue(0).ToString(),
          Speed = row.ItemArray.GetValue(3).ToString(),
          Bearing = row.ItemArray.GetValue(4).ToString(),
          Name = row.ItemArray.GetValue(6).ToString(),
          GraphicUpdateStatus = UpdateStatus.None
        };

        // verify the vehicle exists 
        Vehicle existing = FindVehicle(row.ItemArray.GetValue(0).ToString());
        if (existing == null)
        {
          _vehicles.Add(newVehicle);
        }
        else if (existing.X != newVehicle.X || existing.Y != newVehicle.Y)
        {
          existing.X = newVehicle.X;
          existing.Y = newVehicle.Y;
          existing.GraphicUpdateStatus = UpdateStatus.Update;
        }
        else
        {
          existing.GraphicUpdateStatus = UpdateStatus.None;
        }
      }
    }

    /// <summary>
    /// Creates graphics for the new vehicles from the feed and updates those that
    /// have been updated since the last poll
    /// </summary>
    private void GetVehicleGraphics()
    {
      if (_ambulanceSymbol == null)
      {
        _ambulanceSymbol = Symbol.Marker.Health.Ambulance;
      }

      foreach (Vehicle vehicle in _vehicles)
      {
        //Create a point for the new location of the vehicle
        ESRI.ArcGISExplorer.Geometry.Point xoPoint = new ESRI.ArcGISExplorer.Geometry.Point(vehicle.X, vehicle.Y);

        if (vehicle.MapGraphic == null)
        {
          Graphic vehicleGraphic = new Graphic(xoPoint, _ambulanceSymbol, vehicle.ID);
          vehicleGraphic.Placement3D = Placement3D.AttachToSurface;
          vehicle.MapGraphic = vehicleGraphic;
        }
        else if (vehicle.GraphicUpdateStatus == UpdateStatus.Update)
        {
          vehicle.MapGraphic.MoveTo(xoPoint.X, xoPoint.Y);
          vehicle.GraphicUpdateStatus = UpdateStatus.None;
        }
      }
    }

    /// <summary>
    /// Finds a vehicle in the collection based on its ID or graphic
    /// </summary>
    /// <param name="ID"></param>
    /// <returns></returns>
    private static Vehicle FindVehicle(string ID)
    {
      foreach (Vehicle vehicle in _vehicles)
      {
        if (vehicle.ID == ID)
        {
          return vehicle;
        }
      }

      return null;
    }

    /// <summary>
    /// Finds a vehicle with the specified graphic
    /// </summary>
    /// <param name="g"></param>
    /// <returns></returns>
    private static Vehicle FindVehicle(Graphic g)
    {
      foreach (Vehicle vehicle in _vehicles)
      {
        if (vehicle.MapGraphic == g)
        {
          return vehicle;
        }
      }

      return null;
    }

    /// <summary>
    /// Event handler when a graphic is clicked on the map: open its infopopup window
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    void ActiveMapDisplay_GraphicClicked(object sender, GraphicMouseEventArgs e)
    {
      Vehicle vehicle = FindVehicle(e.Graphics[0]); // Use first graphic clicked.
      if (vehicle != null)
      {
        ShowPopup(vehicle);
      }
    }

    /// <summary>
    /// Display the popup for the specified vehicle
    /// </summary>
    /// <param name="vehicle"></param>
    public static void ShowPopup(Vehicle vehicle)
    {
      Graphic g = vehicle.MapGraphic;
      string content = "<b>" + vehicle.ID + "</b>" + "<br><br>Driver: " + vehicle.Name + "<br>" +
                  "Speed: " + vehicle.Speed + "<br>" + "Bearing: " + vehicle.Bearing + "<br>" +
                  "Status: " + vehicle.Status;

      Application.ActiveMapDisplay.HidePopups(true);
      Popup popUp = new Popup(Application.ActiveMapDisplay, null, content, "Vehicle Information", (ESRI.ArcGISExplorer.Geometry.Point)g.Geometry);
      popUp.Activate();

    }
  }
}