RSSWeatherLayerClass.cs
RSS Weather layer
RSSWeatherLayerClass.cs
// Copyright 2006 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;
using System.Data;
using System.Runtime.InteropServices;
using System.Xml;
using System.Threading;
using System.Timers;
using System.Text.RegularExpressions;
using System.IO;
using System.Drawing;
using System.Drawing.Imaging;
using System.Windows.Forms;
using System.ComponentModel;
using Microsoft.Win32;
using ESRI.ArcGIS.Carto;
using ESRI.ArcGIS.Geodatabase;
using ESRI.ArcGIS.esriSystem;
using ESRI.ArcGIS.Geometry;
using ESRI.ArcGIS.Display;
using ESRI.ArcGIS.DataSourcesFile;

namespace RSSWeatherLayer
{
  #region WeatherItemEventArgs class members
  public sealed class WeatherItemEventArgs : EventArgs
  {
    private int m_id;
    private long m_zipCode;
    private double m_x;
    private double m_y;
    private int m_iconWidth;
    private int m_iconHeight;
    public WeatherItemEventArgs(int id, long zipCode, double X, double Y, int iconWidth, int iconHeight)
    {
      m_id = id;
      m_zipCode = zipCode;
      m_x = X;
      m_y = Y;
      m_iconWidth = iconWidth;
      m_iconHeight = iconHeight;
    }

    public int ID
    {
      get { return m_id; }
    }
    public long ZipCode
    {
      get { return m_zipCode; }
    }
    public double mapY
    {
      get { return m_y; }
    }
    public double mapX
    {
      get { return m_x; }
    }
    public int IconWidth
    {
      get { return m_iconWidth; }
    }
    public int IconHeight
    {
      get { return m_iconHeight; }
    }
  }
  #endregion

  //declare delegates for the event handling
  public delegate void WeatherItemAdded(object sender, WeatherItemEventArgs args);
  public delegate void WeatherItemsUpdated(object sender, EventArgs args);


  /// <summary>
  /// RSSWeatherLayerClass is a custom layer for ArcMap/MapControl. It inherits CustomLayerBase
  /// which implements the relevant interfaces required by the Map.
  /// This sample is a comprehensive sample of a real life scenario for creating a new layer in 
  /// order to consume a web service and display the information in a map.
  /// In this sample you can find implementation of simple editing capabilities, selection by 
  /// attribute and by location, persistence and identify.
  /// </summary>
  [Guid("3460FB55-4326-4d28-9F96-D62211B0C754")]
  [ClassInterface(ClassInterfaceType.None)]
  [ComVisible(true)]
  [ProgId("RSSWeatherLayer.RSSWeatherLayerClass")]
  public sealed class RSSWeatherLayerClass : CustomLayerBase, IIdentify
  {
    #region class members
    private System.Timers.Timer      m_timer                  = null;
    private Thread                  m_updateThread          = null;
    private string                  m_iconFolder            = string.Empty;
    private DataTable                m_symbolTable            = null;
    private DataTable               m_locations              = null;
    private ISpatialReference        m_layerSpatialRef        = null; //DataFrame's SR
    private ISymbol                  m_selectionSymbol        = null;
    private IDisplay                m_display               = null;
    private bool                    m_bOnce                 = true;
    private string                  m_dataFolder            = string.Empty;

    private IPoint                  m_point                 = null;
    private IPoint                  m_llPnt                 = null;
    private IPoint                  m_urPnt                 = null;
    private IEnvelope               m_env                   = null;


    //weather items events
    public event WeatherItemAdded OnWeatherItemAdded;
    public event WeatherItemsUpdated OnWeatherItemsUpdated;
    #endregion

    #region Constructor
    /// <summary>
    /// The class has only default CTor.
    /// </summary>
    public RSSWeatherLayerClass() : base()
    {
      try
      {
        //setthe layer's name
        base.m_sName = "RSS Weather Layer";
        //ask the Map to create a separate cache for the layer
        base.m_IsCached = true;

        //get the directory for the layer's cache. If it does not exist, create it.
        m_dataFolder = System.IO.Path.Combine(System.Environment.CurrentDirectory, "Data");
        if (!System.IO.Directory.Exists(m_dataFolder))
        {
          System.IO.Directory.CreateDirectory(m_dataFolder);
        }
        m_iconFolder = m_dataFolder;

        //instantiate the timer for the weather update
        m_timer = new System.Timers.Timer(1000);
        m_timer.Enabled = false;
        m_timer.Elapsed += new ElapsedEventHandler(OnUpdateTimer);

        //initialize the layer's tables (main table as well as the symbols table)
        InitializeTables();

        //get the location list from a featureclass (US major cities) and synchronize it with the 
        //cached information in case it exists.
        if (null == m_locations)
          InitializeLocations();

        //initialize the selection symbol used to highlight selected weather items
        InitializeSelectionSymbol();

        m_point = new PointClass();
        m_llPnt = new PointClass();
        m_urPnt = new PointClass();
        m_env = new EnvelopeClass();

        //connect to the RSS service
        Connect();
      }
      catch (Exception ex)
      {
        System.Diagnostics.Trace.WriteLine(ex.Message);
      }
    }

    

    ~RSSWeatherLayerClass()
    {
      Disconnect();
    }
    #endregion
    
    #region Overriden methods

    /// <summary>
    /// Draws the layer to the specified display for the given draw phase. 
    /// </summary>
    /// <param name="drawPhase"></param>
    /// <param name="Display"></param>
    /// <param name="trackCancel"></param>
    /// <remarks>the draw method is set as an abstruct method and therefor must be overridden</remarks>
    public override void Draw(esriDrawPhase drawPhase, IDisplay Display, ITrackCancel trackCancel)
    {
      if(drawPhase != esriDrawPhase.esriDPGeography) return;
      if(Display == null) return;
      if(m_table == null || m_symbolTable == null) return;

      if(m_bOnce)
      {
        m_display = Display;
        m_bOnce = false;
      } 

      IPoint point = new PointClass();
      point.SpatialReference = m_spatialRef;
      IEnvelope envelope = Display.DisplayTransformation.FittedBounds as IEnvelope;  
      
      double lat, lon;
      int iconCode;
      bool selected;
      ISymbol symbol = null;

      //loop through the rows. Draw each row that has a shape
      foreach (DataRow row in m_table.Rows)
      {
        //get the Lat/Lon of the item
        lat = Convert.ToDouble(row[3]);
        lon = Convert.ToDouble(row[4]);
        //get the icon ID
        iconCode = Convert.ToInt32(row[8]);

        //get the selection state of the item
        selected = Convert.ToBoolean(row[13]);
        
        if(lon >= envelope.XMin && lon <= envelope.XMax && lat >= envelope.YMin && lat <= envelope.YMax) 
        {  
          //search for the symbol in the symbology table
          symbol = GetSymbol(iconCode, row);
          if(null == symbol)
            continue;

          point.X = lon;
          point.Y = lat;

          //reproject the point to the DataFrame's spatial reference
          if(null != m_layerSpatialRef)
            point.Project(m_layerSpatialRef);

          Display.SetSymbol(symbol);
          Display.DrawPoint(point);

          if(selected)
          {
            Display.SetSymbol(m_selectionSymbol);
            Display.DrawPoint(point);
          }
        }
      }
    }
    
    /// <summary>
    /// The spatial reference of the underlying data.
    /// </summary>
    public override ISpatialReference SpatialReference
    {
      get
      {
        if(null == m_spatialRef)
        {
          m_spatialRef = CreateGeographicSpatialReference();
        }
        return m_spatialRef;
      }
    }

    /// <summary>
    /// The ID of the object. 
    /// </summary>
    public override ESRI.ArcGIS.esriSystem.UID ID
    {
      get
      {
        UID uid = new UIDClass();
        uid.Value = "RSSWeatherLayer.RSSWeatherLayerClass";

        return uid;
      }
    }
    
    /// <summary>
    /// The default area of interest for the layer. Returns the spatial-referenced extent of the layer. 
    /// </summary>
    public override IEnvelope AreaOfInterest
    {
      get
      {
        return this.Extent;
      }
    }

    /// <summary>
    /// The layer's extent which is a union of the extents of all the items of the layer 
    /// </summary>
    /// <remarks>In case where the DataFram's spatial reference is different than the underlying
    /// data's spatial reference the envelope must be projected</remarks>
    public override IEnvelope Extent
    {
      get
      {
        m_extent = GetLayerExtent();
        if(null == m_extent)
          return null;

        IEnvelope env = ((IClone)m_extent).Clone() as IEnvelope;
        if(null != m_layerSpatialRef)
          env.Project(m_layerSpatialRef);
        
        return env;
      }
    }

    /// <summary>
    /// Map tip text at the specified mouse location.
    /// </summary>
    /// <param name="X"></param>
    /// <param name="Y"></param>
    /// <param name="Tolerance"></param>
    /// <returns></returns>
    public override string get_TipText(double X, double Y, double Tolerance)
    {
      IEnvelope envelope = new EnvelopeClass();
      envelope.PutCoords(X - Tolerance, Y - Tolerance,X + Tolerance, Y + Tolerance);
      
      //reproject the envelope to the datasource doordinate system
      if(null != m_layerSpatialRef)
      {
        envelope.SpatialReference = m_layerSpatialRef;
        envelope.Project(m_spatialRef);
      }

      double xmin, ymin, xmax, ymax;
      envelope.QueryCoords(out xmin, out ymin, out xmax, out ymax);  
      
      //select all the records within the given extent
      string qry = "LON >= " + xmin.ToString() + " AND LON <= " + xmax.ToString() + " AND Lat >= " + ymin.ToString() + " AND LAT <= " + ymax.ToString();
      DataRow[] rows = m_table.Select(qry);
      if(0 == rows.Length)
        return string.Empty;

      DataRow r = rows[0];
      string zipCode = Convert.ToString(r[1]);
      string cityName = Convert.ToString(r[2]);
      string temperature = Convert.ToString(r[5]);

      return cityName + ", " + zipCode + ", " + temperature + "F";
    }

    #endregion

    #region public methods

    /// <summary>
    /// connects to RSS weather service
    /// </summary>
    public void Connect()
    {
      //enable the update timer
      m_timer.Enabled = true;
    }

    /// <summary>
    /// disconnects from RSS weather service
    /// </summary>
    public void Disconnect()
    {
      //disable the update timer
      m_timer.Enabled = false;

      try
      {
        //abort the update thread in case that it is alive
        if(m_updateThread.IsAlive)
          m_updateThread.Abort();
      }
      catch
      {
        System.Diagnostics.Trace.WriteLine("WeatherLayer update thread has been terminated");  
      }
    }

    /// <summary>
    /// select a weather item by its zipCode
    /// </summary>
    /// <param name="zipCode"></param>
    /// <param name="newSelection"></param>
    public void Select(long zipCode, bool newSelection)
    {
      if(null == m_table)
        return;

      if(newSelection)
      {
        UnselectAll();
      }
      
      DataRow[] rows = m_table.Select("ZIPCODE = " + zipCode.ToString());
      if(rows.Length == 0)
        return;

      DataRow rec = rows[0];
      lock(m_table)
      {
        //13 is the selection column ID
        rec[13] = true;
        rec.AcceptChanges();
      }
    }

    /// <summary>
    /// unselect all weather items
    /// </summary>
    public void UnselectAll()
    {
      if(null == m_table)
        return;

      //unselect all the currently selected items
      lock(m_table)
      {
        foreach(DataRow r in m_table.Rows)
        {
          //13 is the selection column ID
          r[13] = false;
        }
        m_table.AcceptChanges();
      }
    }

    /// <summary>
    /// Run the update thread
    /// </summary>
    /// <remarks>calling this method to frequently might end up in blockage of RSS service.
    /// The service will interpret the excessive calls as an offence and thus would block the service for a while.</remarks>
    public void Refresh()
    {
      try
      {
        m_updateThread = new Thread(new ThreadStart(ThreadProc));

        //run the update thread
        m_updateThread.Start();
      }
      catch(Exception ex)
      {
        System.Diagnostics.Trace.WriteLine(ex.Message);
      }
    }

    /// <summary>
    /// add a new item given only a zipcode (will use the default location given by the service)
    /// should the item exists, it will get updated
    /// </summary>
    /// <param name="zipCode"></param>
    /// <returns></returns>
    public bool AddItem(long zipCode)
    {
      return AddItem(zipCode, 0.0 ,0.0);
    }

    /// <summary>
    /// adds a new item given a zipcode and a coordinate.
    /// Should the item already exists, it will get updated and will move to the new coordinate.
    /// </summary>
    /// <param name="zipCode"></param>
    /// <param name="lat"></param>
    /// <param name="lon"></param>
    /// <returns></returns>
    public bool AddItem(long zipCode, double lat, double lon)
    {
      if(null == m_table)
        return false;

      DataRow r = m_table.Rows.Find(zipCode);
      if(null != r) //if the record with this zipCode already exists
      {
        //in case that the record exists and the input coordinates are not valid 
        if(lat == 0.0 && lon == 0.0)
          return false;
        else //update the location according to the new coordinate
        {
          r[3] = lat;
          r[4] = lon;
          lock(m_table)
          {
            r.AcceptChanges();
          }
        }
      }
      else
      {
        //add new zip code to the locations list
        DataRow rec = m_locations.NewRow();
        rec[1] = zipCode;
        lock(m_locations)
        {
          m_locations.Rows.Add(rec);
        }

        //need to connect to the service and get the info
        AddWeatherItem(zipCode, lat, lon);
      }

      return true;
    }

    /// <summary>
    /// delete an item from the dataset
    /// </summary>
    /// <param name="zipCode"></param>
    /// <returns></returns>
    public bool DeleteItem(long zipCode)
    {
      if(null == m_table)
        return false;

      try
      {
        DataRow r = m_table.Rows.Find(zipCode);
        if(null != r) //if the record with this zipCode already exists
        {
          lock(m_table)
          {
            r.Delete();
          }
          return true;
        }
        return false;
      }
      catch(Exception ex)
      {
        System.Diagnostics.Trace.WriteLine(ex.Message);
        return false;
      }
    }

    /// <summary>
    /// get a weather item given a city name.
    /// </summary>
    /// <param name="cityName"></param>
    /// <returns></returns>
    /// <remarks>a city might have more than one zipCode and therefore this method will
    /// return the first zipcOde found for the specified city name.</remarks>
    public IPropertySet GetWeatherItem(string cityName)
    {
      if(null == m_table)
        return null;

      DataRow[] rows = m_table.Select("CITYNAME = '" + cityName + "'");
      if(rows.Length == 0)
        return null;

      long zipCode = Convert.ToInt64(rows[0][1]);
      return GetWeatherItem(zipCode);
    }

    /// <summary>
    /// This method searches for the record of the given zipcode and retunes the information as a PropertySet.
    /// </summary>
    /// <param name="zipCode"></param>
    /// <returns>a PropertySet encapsulating the weather information for the given weather item.</returns>
    public IPropertySet GetWeatherItem(long zipCode)
    {
      DataRow r = m_table.Rows.Find(zipCode);
      if(null == r)
        return null;

      IPropertySet propSet = new PropertySetClass();
      propSet.SetProperty(  "ID",            r[0]);
      propSet.SetProperty(  "ZIPCODE",      r[1]);
      propSet.SetProperty(  "CITYNAME",      r[2]);
      propSet.SetProperty(  "LAT",          r[3]);
      propSet.SetProperty(  "LON",          r[4]);
      propSet.SetProperty(  "TEMPERATURE",  r[5]);
      propSet.SetProperty(  "CONDITION",    r[6]);
      propSet.SetProperty(  "ICONNAME",      r[7]);
      propSet.SetProperty(  "ICONID",        r[8]);
      propSet.SetProperty(  "DAY",          r[9]);
      propSet.SetProperty(  "DATE",          r[10]);
      propSet.SetProperty(  "LOW",          r[11]);
      propSet.SetProperty(  "HIGH",          r[12]);
      propSet.SetProperty(  "UPDATEDATE",    r[14]);

      return propSet;
    }

    /// <summary>
    /// get a list of all citynames currently in the dataset.
    /// </summary>
    /// <returns></returns>
    /// <remarks>Please note that since the unique ID is zipCode, it is possible
    /// to have a city name appearing more than once.</remarks>
    public string[] GetCityNames()
    {
      if(null == m_table || 0 == m_table.Rows.Count)
        return null;

      string[] cityNames = new string[m_table.Rows.Count];
      for(int i=0; i<m_table.Rows.Count; i++)
      {
        //column #2 stores the cityName
        cityNames[i] = Convert.ToString(m_table.Rows[i][2]);
      }

      return cityNames;
    }

    /// <summary>
    /// Zoom to a weather item according to its city name
    /// </summary>
    /// <param name="cityName"></param>
    public void ZoomTo(string cityName)
    {
      if(null == m_table)
        return;

      DataRow[] rows = m_table.Select("CITYNAME = '" + cityName + "'");
      if(rows.Length == 0)
        return;

      long zipCode = Convert.ToInt64(rows[0][1]);
      ZoomTo(zipCode);
    }

    /// <summary>
    /// Zoom to weather item according to its zipcode
    /// </summary>
    /// <param name="zipCode"></param>
    public void ZoomTo(long zipCode)
    {
      if(null == m_table || null == m_symbolTable )
        return;

      if(null == m_display)
        return;

      //get the record for the requested zipCode
      DataRow r = m_table.Rows.Find(zipCode);
      if(null == r)
        return;

      //get the coordinate of the zipCode
      double lat = Convert.ToDouble(r[3]);
      double lon = Convert.ToDouble(r[4]);

      IPoint point = new PointClass();
      point.X = lon;
      point.Y = lat;
      point.SpatialReference = m_spatialRef;

      if(null != m_layerSpatialRef)
        point.Project(m_layerSpatialRef);

      int iconCode = Convert.ToInt32(r[8]);
      //find the appropreate symbol record
      DataRow rec = m_symbolTable.Rows.Find(iconCode);
      if(rec == null)
        return;

      //get the icon's dimensions
      int iconWidth = Convert.ToInt32(rec[3]);
      int iconHeight = Convert.ToInt32(rec[4]);

      IDisplayTransformation displayTransformation = ((IScreenDisplay)m_display).DisplayTransformation;

      //Convert the icon coordinate into screen coordinate
      int windowX, windowY;
      displayTransformation.FromMapPoint(point,out windowX, out windowY);
      
      //get the upper left coord
      int ulx, uly;
      ulx = windowX - iconWidth/2;
      uly = windowY - iconHeight/2;
      IPoint ulPnt = displayTransformation.ToMapPoint(ulx, uly);

      //get the lower right coord
      int lrx,lry;
      lrx = windowX + iconWidth/2;
      lry = windowY + iconHeight/2;
      IPoint lrPnt = displayTransformation.ToMapPoint(lrx, lry);
      
      //construct the new extent
      IEnvelope envelope = new EnvelopeClass();
      envelope.PutCoords(ulPnt.X, lrPnt.Y, lrPnt.X, ulPnt.Y);
      envelope.Expand(2,2,false);
      
      //set the new extent and refresh the display
      displayTransformation.VisibleBounds = envelope;
      ((IScreenDisplay)m_display).Invalidate(null, true, (short)esriScreenCache.esriAllScreenCaches);
      ((IScreenDisplay)m_display).UpdateWindow();
    }

    private void SetSymbolSize(int newSize)
    {
      if (newSize <= 0)
      {
        MessageBox.Show("Size is not allowed.");
        return;
      }

      if (null == m_symbolTable || 0 == m_symbolTable.Rows.Count)
        return;

      foreach (DataRow r in m_symbolTable.Rows)
      {
        IPictureMarkerSymbol pictureMarkerSymbol = r[2] as IPictureMarkerSymbol;
        if (null == pictureMarkerSymbol)
          continue;
        
        pictureMarkerSymbol.Size = newSize;
        r[2] = pictureMarkerSymbol;
        r.AcceptChanges(); 
      }

      ((IScreenDisplay)m_display).Invalidate(null, true, (short)esriScreenCache.esriAllScreenCaches);
      ((IScreenDisplay)m_display).UpdateWindow();
    }

    private int GetSymbolSize()
    {
      if (null == m_symbolTable || 0 == m_symbolTable.Rows.Count)
        return -1;

      DataRow r = m_symbolTable.Rows[0];

      ISymbol symbol = (ISymbol)r[2];
      if (null == symbol)
        return -1;

      IPictureMarkerSymbol pictureMarkerSymbol = (IPictureMarkerSymbol)symbol;
      return Convert.ToInt32(pictureMarkerSymbol.Size);
    }

    public int SymbolSize
    {
      set { SetSymbolSize(value); }
      get { return GetSymbolSize();  }
    }

    #endregion

    #region private utility methods

    /// <summary>
    /// create a WGS1984 geographic coordinate system.
    /// In this case, the underlying data provided by the service is in WGS1984.
    /// </summary>
    /// <returns></returns>
    private ISpatialReference CreateGeographicSpatialReference()
    {
      ISpatialReferenceFactory spatialRefFatcory = new SpatialReferenceEnvironmentClass();
      IGeographicCoordinateSystem geoCoordSys;
      geoCoordSys = spatialRefFatcory.CreateGeographicCoordinateSystem((int)esriSRGeoCSType.esriSRGeoCS_WGS1984);
      geoCoordSys.SetFalseOriginAndUnits(-180.0, -180.0, 5000000.0);
      geoCoordSys.SetZFalseOriginAndUnits(0.0, 100000.0);
      geoCoordSys.SetMFalseOriginAndUnits(0.0, 100000.0);

      return geoCoordSys as ISpatialReference;
    }

    /// <summary>
    /// get the overall extent of the items in the layer
    /// </summary>
    /// <returns></returns>
    private IEnvelope GetLayerExtent()
    {
      //in case that it does not exists set the layre's spatial reference
      if(null == base.m_spatialRef)
      {
        base.m_spatialRef = CreateGeographicSpatialReference();    
      }

      //iterate through all the items in the layers DB and get the bounding envelope
      IEnvelope env = new EnvelopeClass();
      env.SpatialReference = base.m_spatialRef;
      IPoint point = new PointClass();
      point.SpatialReference = m_spatialRef;
      foreach(DataRow r in m_table.Rows)
      {
        point.Y = Convert.ToDouble(r[3]);
        point.X = Convert.ToDouble(r[4]);

        env.Union(point.Envelope);
      }

      //return the layer's extent in the data underlying coordinate system
      return env;
    }

    /// <summary>
    /// initialize the main table used by the layer as well as the symbols table.
    /// The base class calles new on the table and adds a default ID field.
    /// </summary>
    private void InitializeTables()
    {
      string path =  System.IO.Path.Combine(m_dataFolder, "Weather.xml");
      //In case that there is no existing cache on the local machine, create the table.
      if(!System.IO.File.Exists(path))
      {

        //add columns to the table  in addition to the default 'ID' and 'Geometry'  
        m_table.Columns.Add(  "ZIPCODE",    typeof(long));     //1
        m_table.Columns.Add(  "CITYNAME",    typeof(string));   //2
        m_table.Columns.Add(  "LAT",        typeof(double));   //3
        m_table.Columns.Add(  "LON",        typeof(double));   //4
        m_table.Columns.Add(  "TEMP",        typeof(int));       //5  
        m_table.Columns.Add(  "CONDITION",  typeof(string));   //6
        m_table.Columns.Add(  "ICONNAME",    typeof(string));   //7  
        m_table.Columns.Add(  "ICONID",      typeof(int));       //8 
        m_table.Columns.Add(  "DAY",        typeof(string));   //9  
        m_table.Columns.Add(  "DATE",        typeof(string));   //10
        m_table.Columns.Add(  "LOW",        typeof(string));   //11
        m_table.Columns.Add(  "HIGH",        typeof(string));   //12
        m_table.Columns.Add(  "SELECTED",    typeof(bool));     //13
        m_table.Columns.Add(  "UPDATEDATE",  typeof(DateTime)); //14  
        
  
        //set the ID column to be auto increment
        m_table.Columns[0].AutoIncrement = true;
        m_table.Columns[0].ReadOnly = true;
        
        //the zipCode column must be the unique and nut allow null
        m_table.Columns[2].Unique = true;
        
        // set the ZIPCODE primary key for the table
        m_table.PrimaryKey = new DataColumn[] {m_table.Columns["ZIPCODE"]};

      }
      else //in case that the local cache exists, simply load the tables from the cache.
      {
        DataSet ds = new DataSet();
        ds.ReadXml(path);
          
        m_table = ds.Tables["RECORDS"];

        if (null == m_table)
          throw new Exception("Cannot find 'RECORDS' table");

        if (15 != m_table.Columns.Count)
          throw new Exception("Table 'RECORDS' does not have all required columns");

        m_table.Columns[0].ReadOnly = true;

        // set the ZIPCODE primary key for the table
        m_table.PrimaryKey = new DataColumn[] {m_table.Columns["ZIPCODE"]};
        
        //synchronize the locations table
        foreach(DataRow r in m_table.Rows)
        {
          try
          {
            //in case that the locations table does not exists, create and initialize it
            if(null == m_locations)
              InitializeLocations();

            //get the zipcode for the record
            string zip = Convert.ToString(r[1]);

            //make sure that there is no existing record with that zipCode already in the 
            //locations table.
            DataRow[] rows = m_locations.Select("ZIPCODE = " + zip);
            if(0 == rows.Length)
            {
              DataRow rec = m_locations.NewRow();
              rec[1] = Convert.ToInt64(r[1]);  //zip code 
              rec[2] = Convert.ToString(r[2]); //city name

              //add the new record to the locations table
              lock(m_locations)
              {
                m_locations.Rows.Add(rec);
              }
            }
          }
          catch(Exception ex)
          {
            System.Diagnostics.Trace.WriteLine(ex.Message);
          }
        }

        //displose the DS
        ds.Tables.Remove(m_table);
        ds.Dispose();
        GC.Collect();
      }

      //initialize the symbol map table
      m_symbolTable = new DataTable("Symbology");

      //add the columns to the table
      m_symbolTable.Columns.Add(  "ID",            typeof(int));      //0
      m_symbolTable.Columns.Add( "ICONID",        typeof(int));      //1
      m_symbolTable.Columns.Add( "SYMBOL",        typeof(ISymbol));  //2
      m_symbolTable.Columns.Add( "SYMBOLWIDTH",    typeof(int));      //3
      m_symbolTable.Columns.Add( "SYMBOLHEIGHT",  typeof(int));      //4
        
      //set the ID column to be auto increment
      m_symbolTable.Columns[0].AutoIncrement = true;
      m_symbolTable.Columns[0].ReadOnly = true;

      m_symbolTable.Columns[1].AllowDBNull = false;

      //set ICONID as the primary key for the table
      m_symbolTable.PrimaryKey = new DataColumn[] {m_symbolTable.Columns["ICONID"]};
    }

    /// <summary>
    /// Initialize the location table. Gets the location from a featureclass
    /// </summary>
    private void InitializeLocations()
    {
      //ceate a new instance of the location table
      m_locations = new DataTable();

      //add fields to the table
      m_locations.Columns.Add( "ID",        typeof(int));
      m_locations.Columns.Add( "ZIPCODE",    typeof(long));
      m_locations.Columns.Add( "CITYNAME",  typeof(string));

      m_locations.Columns[0].AutoIncrement = true;
      m_locations.Columns[0].ReadOnly = true;

      //set ZIPCODE as the primary key for the table
      m_locations.PrimaryKey = new DataColumn[] {m_locations.Columns["ZIPCODE"]};

      //spawn a thread to populate the locations table
      Thread t = new Thread(new ThreadStart(PopulateLocationsTableProc));
      t.Start();

      System.Threading.Thread.Sleep(1000);
    }

    /// <summary>
    /// Load the information from the MajorCities featureclass to the locations table
    /// </summary>
    private void PopulateLocationsTableProc()
    {
      //get the ArcGIS path from the registry
      RegistryKey key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\ESRI\ArcGIS");
      string path = Convert.ToString(key.GetValue("InstallDir"));

      if (!System.IO.File.Exists(System.IO.Path.Combine(path, @"DeveloperKit\SamplesNET\Data\USZipCodeData\ZipCode_Boundaries_US_Major_Cities.shp")))
      {
        MessageBox.Show("Cannot find file ZipCode_Boundaries_US_Major_Cities.shp!");
        return;
      }

      //open the featureclass
      IWorkspaceFactory wf = new ShapefileWorkspaceFactoryClass() as IWorkspaceFactory;
      IWorkspace ws = wf.OpenFromFile(System.IO.Path.Combine(path, @"DeveloperKit\SamplesNET\Data\USZipCodeData"), 0);
      IFeatureWorkspace fw = ws as IFeatureWorkspace;
      IFeatureClass featureClass = fw.OpenFeatureClass("ZipCode_Boundaries_US_Major_Cities");
      //map the name and zip fields
      int zipIndex = featureClass.FindField("ZIP");
      int nameIndex = featureClass.FindField("NAME");
      string cityName;
      long zip;  
      
      try
      {
        //iterate through the features and add the information to the table
        IFeatureCursor fCursor = null;
        fCursor = featureClass.Search(null, true);
        IFeature feature = fCursor.NextFeature();
        int index = 0;

        while(null != feature)
        {
          object obj = feature.get_Value(nameIndex);
          if (obj == null)
            continue;
          cityName = Convert.ToString(obj);

          obj = feature.get_Value(zipIndex);
          if (obj == null)
            continue;
          zip = long.Parse(Convert.ToString(obj));
          if(zip <= 0)
            continue;
          
          //add the current location to the location table
          DataRow r = m_locations.Rows.Find(zip);
          if(null == r)
          {
            r = m_locations.NewRow();
            r[1] = zip;
            r[2] = cityName;
            lock(m_locations)
            {
              m_locations.Rows.Add(r);
            }
          }
        
          feature = fCursor.NextFeature();

          index++;
        }

        //release the feature cursor
        Marshal.ReleaseComObject(fCursor);
      }
      catch(Exception ex)
      {
        System.Diagnostics.Trace.WriteLine(ex.Message);
      }
    }
    
    /// <summary>
    /// Initialize the symbol that would use to highlight selected items
    /// </summary>
    private void InitializeSelectionSymbol()
    {
      //use a character marker symbol:
      ICharacterMarkerSymbol chMrkSym;
      chMrkSym = new CharacterMarkerSymbolClass();
      
      //Set the selection color (yellow)
      IRgbColor color;
      color = new RgbColorClass();
      color.Red = 0;
      color.Green = 255;
      color.Blue = 255;

      //set the font
      stdole.IFont aFont;
      aFont = new stdole.StdFontClass();
      aFont.Name = "ESRI Default Marker";
      aFont.Size = 31;
      aFont.Bold = true;      

      //char #41 is just a rectangle
      chMrkSym.CharacterIndex = 41;
      chMrkSym.Color = color as IColor;
      chMrkSym.Font = aFont as stdole.IFontDisp;
      chMrkSym.Size = 31;
      
      m_selectionSymbol = chMrkSym as ISymbol;
    }
    /// <summary>
    /// run the thread that does the update of the weather data
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void OnUpdateTimer(object sender, ElapsedEventArgs e)
    {
      m_timer.Interval = 2700000; //(45 minutes)
      m_updateThread = new Thread(new ThreadStart(ThreadProc));

      //run the update thread
      m_updateThread.Start();
    }

    /// <summary>
    /// the main update thread for the layer.
    /// </summary>
    /// <remarks>Since the layer gets the weather information from a web service which might
    /// take a while to respond, it is not logical to let the application hang whie waiting
    /// for response. Therefor, running the request on a different thread frees the application to 
    /// continue working while waiting for a response. 
    /// Please note that in this case, synchronization of shared resources must be addressed,
    /// otherwise you might end up getting unexpected results.</remarks>
    private void ThreadProc()
    {
      try
      {  

        long lZipCode;
        //iterate through all the records in the main table and update it against 
        //the information from the website.
        foreach(DataRow r in m_locations.Rows)
        {
          //put the thread to sleep in order not to overwhelm yahoo web site might
          System.Threading.Thread.Sleep(200);

          //get the zip code of the record (column #1)
          lZipCode = Convert.ToInt32(r[1]);

          //make the request and update the item
          AddWeatherItem(lZipCode, 0.0, 0.0);
        }

        //serialize the tables onto the local machine
        DataSet ds = new DataSet();
        ds.Tables.Add(m_table);
        ds.WriteXml(System.IO.Path.Combine(m_dataFolder, "Weather.xml"));
        ds.Tables.Remove(m_table);
        ds.Dispose();
        GC.Collect();

        //fire an event to notify update of the weatheritems 
        if(OnWeatherItemsUpdated != null)
          OnWeatherItemsUpdated(this, new EventArgs());
      }
      catch(Exception ex)
      {
        System.Diagnostics.Trace.WriteLine(ex.Message);
      }
    }

    /// <summary>
    /// given a bitmap url, saves it on the local machine and returnes its size
    /// </summary>
    /// <param name="iconPath"></param>
    /// <param name="width"></param>
    /// <param name="height"></param>
    private void DownloadIcon(string iconPath, out int width, out int height)
    {
      //if the icon does not exist on the local machine, get it from RSS site
      string iconFileName = System.IO.Path.Combine(m_iconFolder, System.IO.Path.GetFileNameWithoutExtension(iconPath) + ".bmp");
      width = 0;
      height = 0;
      Bitmap b;
      if(!File.Exists(iconFileName))
      {
        using (System.Net.WebClient webClient = new System.Net.WebClient())
        {
          //open a readable stream to download the bitmap
          using (System.IO.Stream stream = webClient.OpenRead(iconPath))
          {
            b = new Bitmap(stream, true);

            //save the image as a bitmap in the icons folder
            b.Save(iconFileName, ImageFormat.Bmp);

            //get the bitmap's dimensions
            width = b.Width;
            height = b.Height;
          }
        }
      }
      else
      {
        //get the bitmap's dimensions
        {
          b= new Bitmap(iconFileName);
          width = b.Width;
          height = b.Height;
        }
      }
    }

    /// <summary>
    /// get the specified symbol from the symbols table.
    /// </summary>
    /// <param name="iconCode"></param>
    /// <param name="dbr"></param>
    /// <returns></returns>
    private ISymbol GetSymbol(int iconCode, DataRow dbr)
    {
      ISymbol symbol = null;
      string iconPath;
      int iconWidth, iconHeight;

      //search for an existing symbol in the table
      DataRow r = m_symbolTable.Rows.Find(iconCode);
      if(r == null) //in case that the symbol does not exist in the table, create a new entry
      {
        r = m_symbolTable.NewRow();
        r[1] = iconCode;
        
        iconPath = Convert.ToString(dbr[7]);
        //Initialize the picture marker symbol
        symbol = InitializeSymbol(iconPath, out iconWidth, out iconHeight);
        if(null == symbol)
          return null;

        //update the symbol table
        r[2] = symbol;
        r[3] = iconWidth;
        r[4] = iconHeight;
        lock(m_symbolTable)
        {
          m_symbolTable.Rows.Add(r);
        }
      }
      else
      {
        if (r[2] is DBNull) //in case that the record exists but the symbol hasn't been initialized
        {
          iconPath = Convert.ToString(dbr[7]);
          //Initialize the picture marker symbol
          symbol = InitializeSymbol(iconPath, out iconWidth, out iconHeight);
          if(null == symbol)
            return null;

          //update the symbol table
          r[2] = symbol;
          lock(m_symbolTable)
          {
            r.AcceptChanges();
          }
        }
        else //the record exists in the table and the symbol has been initialized
          //get the symbol
          symbol = r[2] as ISymbol;
      }
      
      //return the requested symbol
      return symbol;
    }

    /// <summary>
    /// Initialize a character marker symbol for a given bitmap path
    /// </summary>
    /// <param name="iconPath"></param>
    /// <param name="iconWidth"></param>
    /// <param name="iconHeight"></param>
    /// <returns></returns>
    private ISymbol InitializeSymbol(string iconPath, out int iconWidth, out int iconHeight)
    {
      iconWidth = iconHeight = 0;
      try
      { 
        //make sure that the icon exit on dist or else download it
        DownloadIcon(iconPath, out iconWidth, out iconHeight);
        string iconFileName = System.IO.Path.Combine(m_iconFolder, System.IO.Path.GetFileNameWithoutExtension(iconPath) + ".bmp");
        if(!System.IO.File.Exists(iconFileName))
          return null;
        
        //initialize the transparent color
        IRgbColor rgbColor = new RgbColorClass();
        rgbColor.Red = 255;
        rgbColor.Blue = 255;
        rgbColor.Green = 255;
        
        //instantiate the marker symbol and set its properties
        IPictureMarkerSymbol pictureMarkerSymbol = new PictureMarkerSymbolClass();
        pictureMarkerSymbol.CreateMarkerSymbolFromFile(ESRI.ArcGIS.Display.esriIPictureType.esriIPictureBitmap, iconFileName);
        pictureMarkerSymbol.Angle = 0;
        pictureMarkerSymbol.Size = 28;
        pictureMarkerSymbol.XOffset = 0;
        pictureMarkerSymbol.YOffset = 0;
        pictureMarkerSymbol.BitmapTransparencyColor = rgbColor as IColor;

        //return the symbol
        return (ISymbol)pictureMarkerSymbol;
      }
      catch
      {
        return null;
      }
    }

    /// <summary>
    /// Makes a request against RSS Weather service and add update the layer's table
    /// </summary>
    /// <param name="zipCode"></param>
    /// <param name="Lat"></param>
    /// <param name="Lon"></param>
    private void AddWeatherItem(long zipCode, double Lat, double Lon)
    {
      try
      {  
        string cityName;
        double lat, lon;
        int temp;
        string condition;
        string desc;
        string iconPath;
        string day;
        string date;
        int low;
        int high;
        int iconCode;
        int iconWidth = 52; //default values
        int iconHeight = 52;

        //the base URL for the service
        string url = "http://xml.weather.yahoo.com/forecastrss?p=";
        //the RegEx used to extract the icon path from the HTML tag
        string regxQry = "(http://(\\\")?(.*?\\.gif))";
        XmlTextReader reader = null;
        XmlDocument doc;
        XmlNode node;
        
        try
        {
          //make the request and get the result back into XmlReader
          reader = new XmlTextReader(url + zipCode.ToString());
        }
        catch(Exception ex)
        {
          System.Diagnostics.Trace.WriteLine(ex.Message);
          return;
        }

        //load the XmlReader to an xml doc
        doc = new XmlDocument();
        doc.Load(reader);

        //set an XmlNamespaceManager since we have to make explicit namespace searches
        XmlNamespaceManager xmlnsManager = new System.Xml.XmlNamespaceManager(doc.NameTable);
        //Add the namespaces used in the xml doc to the XmlNamespaceManager.
        xmlnsManager.AddNamespace("yweather", "http://xml.weather.yahoo.com/ns/rss/1.0");
        xmlnsManager.AddNamespace("geo", "http://www.w3.org/2003/01/geo/wgs84_pos#");

        //make sure that the node exists
        node = doc.DocumentElement.SelectSingleNode("/rss/channel/yweather:location/@city", xmlnsManager);
        if(null == node)
          return;

        //get the cityname
        cityName = doc.DocumentElement.SelectSingleNode("/rss/channel/yweather:location/@city", xmlnsManager).InnerXml;
        if(Lat == 0.0 && Lon == 0.0)
        {
          //in cae that the caller did not specify a coordinate, get the default coordinate from the service
          lat = Convert.ToDouble(doc.DocumentElement.SelectSingleNode("/rss/channel/item/geo:lat", xmlnsManager).InnerXml);
          lon = Convert.ToDouble(doc.DocumentElement.SelectSingleNode("/rss/channel/item/geo:long", xmlnsManager).InnerXml);
        }
        else
        {
          lat = Lat;
          lon = Lon;
        }

        //extract the rest of the information from the RSS response
        condition = doc.DocumentElement.SelectSingleNode("/rss/channel/item/yweather:condition/@text", xmlnsManager).InnerXml;
        iconCode = Convert.ToInt32(doc.DocumentElement.SelectSingleNode("/rss/channel/item/yweather:condition/@code", xmlnsManager).InnerXml);
        temp = Convert.ToInt32(doc.DocumentElement.SelectSingleNode("/rss/channel/item/yweather:condition/@temp", xmlnsManager).InnerXml);
        desc = doc.DocumentElement.SelectSingleNode("/rss/channel/item/description").InnerXml;
        day = doc.DocumentElement.SelectSingleNode("/rss/channel/item/yweather:forecast/@day", xmlnsManager).InnerXml;
        date = doc.DocumentElement.SelectSingleNode("/rss/channel/item/yweather:forecast/@date", xmlnsManager).InnerXml;
        low = Convert.ToInt32(doc.DocumentElement.SelectSingleNode("/rss/channel/item/yweather:forecast/@low", xmlnsManager).InnerXml);
        high = Convert.ToInt32(doc.DocumentElement.SelectSingleNode("/rss/channel/item/yweather:forecast/@high", xmlnsManager).InnerXml);


        //use regex in order to extract the icon name from the html script
        Match m = Regex.Match(desc,regxQry);
        if(m.Success)
        {
          iconPath = m.Value;

          //add the icon ID to the symbology table
          DataRow tr = m_symbolTable.Rows.Find(iconCode);
          if(null == tr)
          {
            //get the icon from the website
            DownloadIcon(iconPath, out iconWidth, out iconHeight);

            //ceate a new record
            tr = m_symbolTable.NewRow();
            tr[1] = iconCode;
            tr[3] = iconWidth;
            tr[4] = iconHeight;

            //update the symbol table. The initialization of the symbol cannot take place in here, since
            //this code gets executed on a backround thread.
            lock(m_symbolTable)
            {
              m_symbolTable.Rows.Add(tr);
            }
          }
          else //get the icon's dimensions from the table
          {
            //get the icon's dimentions from the table
            iconWidth = Convert.ToInt32(tr[3]);
            iconHeight = Convert.ToInt32(tr[4]);
          }
        }
        else
        {
          iconPath = "";
        }

        //test whether the record already exists in the layer's table.
        DataRow dbr = m_table.Rows.Find(zipCode);
        if(null == dbr) //in case that the recored does not exist
        {
          //create a new record
          dbr = m_table.NewRow();

          if (!m_table.Columns[0].AutoIncrement)
            dbr[0] = Convert.ToInt32(DateTime.Now.Millisecond);

          dbr[1] = zipCode;
          dbr[2] = cityName;
          dbr[3] = lat;
          dbr[4] = lon;
          dbr[5] = temp;
          dbr[6] = condition;
          dbr[7] = iconPath;
          dbr[8] = iconCode;
          dbr[9] = day;
          dbr[10] = date;
          dbr[11] = low;
          dbr[12] = high;
          dbr[13] = false;
          dbr[14] = DateTime.Now;

          //add the item to the table
          lock(m_table)
          {
            m_table.Rows.Add(dbr);
          }
        }
        else //in case that the record exists, just update it
        {
          dbr[5] = temp;
          dbr[6] = condition;
          dbr[7] = iconPath;
          dbr[8] = iconCode;
          dbr[9] = day;
          dbr[10] = date;
          dbr[11] = low;
          dbr[12] = high;
          dbr[14] = DateTime.Now;

          //update the record
          lock(m_table)
          {
            dbr.AcceptChanges();
          }
        }

        //fire an event to notify the user that the item has been updated
        if (OnWeatherItemAdded != null)
        {
          WeatherItemEventArgs weatherItemEventArgs = new WeatherItemEventArgs(Convert.ToInt32(dbr[0]), zipCode, lat, lon, iconWidth, iconHeight);
          OnWeatherItemAdded(this, weatherItemEventArgs);
        }
      }
      catch(Exception ex)
      {
        System.Diagnostics.Trace.WriteLine("AddWeatherItem: " + ex.Message);
      }
    }
    #endregion

    #region IIdentify Members

    /// <summary>
    /// Identifying all the weather items falling within the given envelope
    /// </summary>
    /// <param name="pGeom"></param>
    /// <returns></returns>
    public IArray Identify(IGeometry pGeom)
    {
      IEnvelope intersectEnv = new EnvelopeClass();
      IEnvelope inEnv;
      IArray array = new ArrayClass();

      //get the envelope from the ggeometry 
      if(pGeom.GeometryType == esriGeometryType.esriGeometryEnvelope)
        inEnv = pGeom.Envelope;
      else
        inEnv = pGeom as IEnvelope;

      //reproject the envelope to the source coordsys
      //this would allow to search directly on the Lat/Lon columns
      if(null != m_layerSpatialRef && null != inEnv.SpatialReference)
        inEnv.Project(m_spatialRef);

      //Test intersection with the layer's extent
      //inEnv.QueryEnvelope(intersectEnv);
      //intersectEnv.Intersect(m_extent);
      //if(intersectEnv.IsEmpty)
      //  return array;

      //expand the envelope so that it'll cover the symbol
      inEnv.Expand(4,4,true);

      double xmin, ymin, xmax, ymax;
      inEnv.QueryCoords(out xmin, out ymin, out xmax, out ymax);  
      
      //select all the records within the given extent
      string qry = "LON >= " + xmin.ToString() + " AND LON <= " + xmax.ToString() + " AND Lat >= " + ymin.ToString() + " AND LAT <= " + ymax.ToString();
      DataRow[] rows = m_table.Select(qry);
      if(0 == rows.Length)
        return array;
      
      long zipCode;
      IPropertySet      propSet    = null;
      IIdentifyObj      idObj      = null;
      IIdentifyObject    idObject  = null;
      bool              bIdentify  = false;

      foreach(DataRow r in rows)
      {
        //get the zipCode
        zipCode = Convert.ToInt64(r["ZIPCODE"]); 
 
        //get the properties of the given item in order to pass it to the identify object
        propSet = this.GetWeatherItem(zipCode);
        if(null != propSet)
        {
          //instantiate the identify object and add it to the array
          idObj = new RSSWeatherIdentifyObject();
          //test whether the layer can be identified
          bIdentify = idObj.CanIdentify((ILayer)this);
          if(bIdentify)
          {
            idObject = idObj as IIdentifyObject;
            idObject.PropertySet = propSet;
            array.Add(idObj);
          }
        }
      }

      //return the array with the identify objects
      return array;
    }

    #endregion
  }
}