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

using System;
using System.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.ADF.BaseClasses;
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 : BaseDynamicLayer, 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_table                     = null;
    private DataTable                    m_symbolTable                = null;
    private DataTable                   m_locations                  = null;
    private ISymbol                      m_selectionSymbol            = null;
    private IDisplay                    m_display                   = null;
    private string                      m_dataFolder                = string.Empty;
    private int                          m_layerSRFactoryCode        = 0;
    private int                          m_symbolSize                = 32;

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

    // dynamic display members
    private IDynamicGlyphFactory2        m_dynamicGlyphFactory        = null;
    private IDynamicSymbolProperties2    m_dynamicSymbolProperties    = null;
    private IDynamicCompoundMarker2     m_dynamicCompoundMarker     = null;
    private IDynamicGlyph               m_textGlyph                 = null;
    private IDynamicGlyph               m_selectionGlyph            = null;
    private bool                        m_bDDOnce                    = true;

    //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
      {
        //set the layer's name
        base.m_sName = "RSS Weather Layer";
        //ask the Map to create a separate cache for the layer
        base.m_IsCached = false;

        // the underlying data for this layer is always in WGS1984 geographical coordinate system
        m_spatialRef = CreateGeographicSpatialReference();
        m_layerSRFactoryCode = m_spatialRef.FactoryCode;

        //get the directory for the layer's cache. If it does not exist, create it.
        m_dataFolder = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "RSSWeatherLayer");
        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 abstract method and therefore 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;

      m_display = Display;
      
      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;

          m_point.X = lon;
          m_point.Y = lat;
          m_point.SpatialReference = m_spatialRef;

          //reproject the point to the DataFrame's spatial reference
          if (null != m_mapSpatialRef && m_mapSpatialRef.FactoryCode != m_layerSRFactoryCode)
            m_point.Project(m_mapSpatialRef);

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

          if(selected)
          {
            Display.SetSymbol(m_selectionSymbol);
            Display.DrawPoint(m_point);
          }
        }
      }
    }

    /// <summary>
    /// Draw the layer while in dynamic mode
    /// </summary>
    /// <param name="DynamicDrawPhase"></param>
    /// <param name="Display"></param>
    /// <param name="DynamicDisplay"></param>
    public override void DrawDynamicLayer(esriDynamicDrawPhase DynamicDrawPhase, IDisplay Display, IDynamicDisplay DynamicDisplay)
    {
      if (DynamicDrawPhase != esriDynamicDrawPhase.esriDDPCompiled)
        return;

      if (!m_bValid || !m_visible)
        return;


      if (m_bDDOnce)
      {
        m_dynamicGlyphFactory = DynamicDisplay.DynamicGlyphFactory as IDynamicGlyphFactory2;
        m_dynamicSymbolProperties = DynamicDisplay as IDynamicSymbolProperties2;
        m_dynamicCompoundMarker = DynamicDisplay as IDynamicCompoundMarker2;

        m_textGlyph = m_dynamicGlyphFactory.get_DynamicGlyph(1, esriDynamicGlyphType.esriDGlyphText, 1);

        // create glyph for the selection symbol
        if (m_selectionSymbol == null)
          InitializeSelectionSymbol();

        m_selectionGlyph = m_dynamicGlyphFactory.CreateDynamicGlyph(m_selectionSymbol);

        m_bDDOnce = false;
      }

      m_display = Display;


      double lat, lon;
      int iconCode;
      int iconWidth = 0;
      bool selected;
      IDynamicGlyph dynamicGlyph = null;
      float symbolSized;
      string citiName = string.Empty;
      string temperature = string.Empty;

      //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 citiname and temperature
        citiName = Convert.ToString(row[2]);
        temperature = string.Format("{0} F", row[5]);

        //get the selection state of the item
        selected = Convert.ToBoolean(row[13]);

        //search for the symbol in the symbology table
        dynamicGlyph = GetDynamicGlyph(m_dynamicGlyphFactory, iconCode, row, out iconWidth);
        if (null == dynamicGlyph)
          continue;

        m_point.X = lon;
        m_point.Y = lat;
        m_point.SpatialReference = m_spatialRef;

        //reproject the point to the DataFrame's spatial reference
        if (null != m_spatialRef && m_mapSpatialRef.FactoryCode != m_layerSRFactoryCode)
          m_point.Project(m_mapSpatialRef);

        symbolSized = 1.35f * (float)(m_symbolSize / (double)iconWidth);

        // draw the weather item

        // 1. set the whether symbol properties
        m_dynamicSymbolProperties.set_DynamicGlyph(esriDynamicSymbolType.esriDSymbolMarker, dynamicGlyph);
        m_dynamicSymbolProperties.set_RotationAlignment(esriDynamicSymbolType.esriDSymbolMarker, esriDynamicSymbolRotationAlignment.esriDSRAScreen);
        m_dynamicSymbolProperties.set_Heading(esriDynamicSymbolType.esriDSymbolMarker, 0.0f);
        m_dynamicSymbolProperties.SetColor(esriDynamicSymbolType.esriDSymbolMarker, 1.0f, 1.0f, 1.0f, 1.0f);
        m_dynamicSymbolProperties.SetScale(esriDynamicSymbolType.esriDSymbolMarker, symbolSized, symbolSized);
        m_dynamicSymbolProperties.set_Smooth(esriDynamicSymbolType.esriDSymbolMarker, false);

        // 2. set the text properties
        m_dynamicSymbolProperties.set_DynamicGlyph(esriDynamicSymbolType.esriDSymbolText, m_textGlyph);
        m_dynamicSymbolProperties.set_RotationAlignment(esriDynamicSymbolType.esriDSymbolMarker, esriDynamicSymbolRotationAlignment.esriDSRAScreen);
        m_dynamicSymbolProperties.set_Heading(esriDynamicSymbolType.esriDSymbolText, 0.0f);
        m_dynamicSymbolProperties.SetColor(esriDynamicSymbolType.esriDSymbolText, 0.0f, 0.85f, 0.0f, 1.0f);
        m_dynamicSymbolProperties.SetScale(esriDynamicSymbolType.esriDSymbolText, 1.0f, 1.0f);
        m_dynamicSymbolProperties.set_Smooth(esriDynamicSymbolType.esriDSymbolText, false);
        m_dynamicSymbolProperties.TextBoxUseDynamicFillSymbol = false;
        m_dynamicSymbolProperties.TextBoxHorizontalAlignment = esriTextHorizontalAlignment.esriTHACenter;
        m_dynamicSymbolProperties.TextRightToLeft = false;

        // draw both the icon and the text as a compound marker
        m_dynamicCompoundMarker.DrawCompoundMarker2(m_point, temperature, citiName);

        if (selected) // draw the selected symbol
        {
          m_dynamicSymbolProperties.SetColor(esriDynamicSymbolType.esriDSymbolMarker, 0.0f, 1.0f, 1.0f, 1.0f);
          m_dynamicSymbolProperties.set_DynamicGlyph(esriDynamicSymbolType.esriDSymbolMarker, m_selectionGlyph);
          DynamicDisplay.DrawMarker(m_point);
        }
      }

      base.m_bIsCompiledDirty = false;
    }
    
    /// <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_mapSpatialRef && m_mapSpatialRef.FactoryCode != m_layerSRFactoryCode)
          env.Project(m_mapSpatialRef);
        
        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_mapSpatialRef && m_mapSpatialRef.FactoryCode != m_layerSRFactoryCode)
      {
        envelope.SpatialReference = m_spatialRef;
        envelope.Project(m_mapSpatialRef);
      }

      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;

      base.m_bIsCompiledDirty = 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();
      }
      base.m_bIsCompiledDirty = true;
    }

    /// <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();
      }

      base.m_bIsCompiledDirty = true;
    }

    /// <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
        {
          lock(m_table)
          {
            r[3] = lat;
            r[4] = lon;

            r.AcceptChanges();
          }
        }
      }
      else
      {
        //add new zip code to the locations list
        DataRow rec = m_locations.NewRow();
        lock(m_locations)
        {
          rec[1] = zipCode;
          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();
          }
          base.m_bIsCompiledDirty = true;
          return true;
        }
        base.m_bIsCompiledDirty = 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_mapSpatialRef && m_mapSpatialRef.FactoryCode != m_layerSRFactoryCode)
        point.Project(m_mapSpatialRef);

      int iconCode = Convert.ToInt32(r[8]);
      //find the appropriate 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;

      base.m_bIsCompiledDirty = true;

      ((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;
      }

      m_symbolSize = newSize;

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

      IPictureMarkerSymbol pictureMarkerSymbol = null;

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

          pictureMarkerSymbol.Size = newSize;
          r[2] = pictureMarkerSymbol;
          r.AcceptChanges();
        }
      }

      base.m_bIsCompiledDirty = true;

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

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

    #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()
    {
      //iterate through all the items in the layers DB and get the bounding envelope
      IEnvelope env = new EnvelopeClass();
      env.SpatialReference = m_spatialRef;
      IPoint point = new PointClass();
      point.SpatialReference = m_spatialRef;
      int symbolCode = 0;
      double symbolSize = 0;
      foreach(DataRow r in m_table.Rows)
      {
        if (r[3] is DBNull || r[4] is DBNull)
          continue;

        point.Y = Convert.ToDouble(r[3]);
        point.X = Convert.ToDouble(r[4]);

        // need to get the symbol size in meters in order to add it to the total layer extent
        if (m_display != null)
        {
          symbolCode = Convert.ToInt32(r[8]);
          symbolSize = Math.Max(GetSymbolSize(m_display, symbolCode), symbolSize);
        }


        env.Union(point.Envelope);
      }

      // Expand the envelope in order to include the size of the symbol
      env.Expand(symbolSize, symbolSize, false);

      //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))
      {

        //create the table the table  in addition to the default 'ID' and 'Geometry'  
        m_table = new DataTable("RECORDS");

        m_table.Columns.Add(  "ID",         typeof(long));     //0
        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[1].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
      m_symbolTable.Columns.Add( "DYNAMICGLYPH",  typeof(IDynamicGlyph)); //5
      m_symbolTable.Columns.Add( "BITMAP",        typeof(Bitmap));        //6
  
      //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;
      m_symbolTable.Columns[1].Unique = true;

      //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()
    {
      //create 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\ArcObjectsSdk10.1");
      string path = Convert.ToString(key.GetValue("InstallDir"));

      if (!System.IO.File.Exists(System.IO.Path.Combine(path, @"Samples\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, @"Samples\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 = m_symbolSize;
      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 = m_symbolSize;
      
      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 while waiting
    /// for response. Therefore, 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();

        base.m_bIsCompiledDirty = true;

        //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 returns its size
    /// </summary>
    /// <param name="iconPath"></param>
    /// <param name="width"></param>
    /// <param name="height"></param>
    private Bitmap 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 bitmap = null;
      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))
          {
            bitmap = new Bitmap(stream, true);

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

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

      return bitmap;
    }

    /// <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;
      Bitmap bitmap = null;

      //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, out bitmap);
        if(null == symbol)
          return null;

        //update the symbol table
        lock (m_symbolTable)
        {
          r[2] = symbol;
          r[3] = iconWidth;
          r[4] = iconHeight;
          r[6] = bitmap;
          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, out bitmap);
          if(null == symbol)
            return null;

          //update the symbol table
          lock(m_symbolTable)
          {
            r[2] = symbol;
            r[6] = bitmap;
            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;
    }

    private IDynamicGlyph GetDynamicGlyph(IDynamicGlyphFactory2 dynamicGlyphFactory, int iconCode, DataRow dbr, out int originalIconSize)
    {
      originalIconSize = 0;

      if (dynamicGlyphFactory == null)
        return null;
      
      string iconPath;
      int iconWidth, iconHeight;
      Bitmap bitmap = null;
      IDynamicGlyph dynamicGlyph = null;

      //search for an existing symbol in the table
      DataRow r = m_symbolTable.Rows.Find(iconCode);
      if (r == null)
      {
        iconPath = Convert.ToString(dbr[7]);
        bitmap = DownloadIcon(iconPath, out iconWidth, out iconHeight);
        if (bitmap != null)
        {
          originalIconSize = iconWidth;

          dynamicGlyph = dynamicGlyphFactory.CreateDynamicGlyphFromBitmap(esriDynamicGlyphType.esriDGlyphMarker,
                                             bitmap.GetHbitmap().ToInt32(),
                                             false,
                                             (IColor)ESRI.ArcGIS.ADF.Connection.Local.Converter.ToRGBColor(Color.FromArgb(255, 255, 255))
                                             );

          
          //update the symbol table
          r = m_symbolTable.NewRow();
          lock (m_symbolTable)
          {
            r[1] = iconCode;
            r[3] = iconWidth;
            r[4] = iconHeight;
            r[5] = dynamicGlyph;
            r[6] = bitmap;
            m_symbolTable.Rows.Add(r);
          }
        }
      }
      else
      {
        if (r[5] is DBNull)
        {
          if (r[6] is DBNull)
          {
            iconPath = Convert.ToString(dbr[7]);
            bitmap = DownloadIcon(iconPath, out iconWidth, out iconHeight);
            if (bitmap == null)
              return null;

            originalIconSize = iconWidth;

            lock (m_symbolTable)
            {
              r[3] = iconWidth;
              r[4] = iconHeight;
              r[6] = bitmap;
            }
          }
          else
          {
            originalIconSize = Convert.ToInt32(r[3]);
            bitmap = (Bitmap)r[6];
          }
          dynamicGlyph = dynamicGlyphFactory.CreateDynamicGlyphFromBitmap(esriDynamicGlyphType.esriDGlyphMarker,
                                             bitmap.GetHbitmap().ToInt32(),
                                             false,
                                             (IColor)ESRI.ArcGIS.ADF.Connection.Local.Converter.ToRGBColor(Color.FromArgb(255, 255, 255))
                                             );

          lock (m_symbolTable)
          {
            r[5] = dynamicGlyph;
          }
        }
        else
        {
          originalIconSize = Convert.ToInt32(r[3]);
          dynamicGlyph = (IDynamicGlyph)r[5];
        }
      }

      return dynamicGlyph;
    }


    /// <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, out Bitmap bitmap)
    {
      iconWidth = iconHeight = 0;
      bitmap = null;
      try
      { 
        //make sure that the icon exit on dist or else download it
        bitmap = 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 = m_symbolSize;
        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;
        Bitmap bitmap = null;

        DataRow dbr = m_table.Rows.Find(zipCode);
        if (dbr != null)
        {
          // get the date 
          DateTime updateDate = Convert.ToDateTime(dbr[14]);
          TimeSpan ts = DateTime.Now - updateDate;
          
          // if the item had been updated in the past 15 minutes, simply bail out.
          if (ts.TotalMinutes < 15)
            return;
        }

        //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 case 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
            bitmap = DownloadIcon(iconPath, out iconWidth, out iconHeight);

            //create a new record
            tr = m_symbolTable.NewRow();
            tr[1] = iconCode;
            tr[3] = iconWidth;
            tr[4] = iconHeight;
            tr[6] = bitmap;
            
            //update the symbol table. The initialization of the symbol cannot take place in here, since
            //this code gets executed on a background thread.
            lock(m_symbolTable)
            {
              m_symbolTable.Rows.Add(tr);
            }
          }
          else //get the icon's dimensions from the table
          {
            //get the icon's dimensions 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.
        if(null == dbr) //in case that the record does not exist
        {
          //create a new record
          dbr = m_table.NewRow();

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

          //add the item to the table
          lock (m_table)
          {
            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;
          
            m_table.Rows.Add(dbr);
          }
        }
        else //in case that the record exists, just update it
        {
          //update the record
          lock (m_table)
          {
            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;

            dbr.AcceptChanges();
          }
        }

        base.m_bIsCompiledDirty = true;

        //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, lon, lat, 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 geometry 
      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_spatialRef && m_mapSpatialRef.FactoryCode != m_layerSRFactoryCode && null != inEnv.SpatialReference)
        inEnv.Project(base.m_spatialRef);

      //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;
    }

    private double GetSymbolSize(IDisplay display, int symbolCode)
    {
      if (display == null)
        return 0;

      double symbolSize = 0;
      double symbolSizePixels = 0;
      DataRow r = m_symbolTable.Rows.Find(symbolCode);
      if (r != null)
      {
        symbolSizePixels = Convert.ToDouble(m_symbolSize);

        // convert the symbol size from pixels to map units
        ITransformation transform = display.DisplayTransformation as ITransformation;
        if (transform == null)
          return 0;

        double[] symbolDimensions = new double[2];
        symbolDimensions[0] = (double)symbolSizePixels;
        symbolDimensions[1] = (double)symbolSizePixels;

        double[] symbolDimensionsMap = new double[2];

        transform.TransformMeasuresFF(esriTransformDirection.esriTransformReverse, 1, ref symbolDimensionsMap[0], ref symbolDimensions[0]);
        symbolSize = symbolDimensionsMap[0];
      }

      return symbolSize;
    }

    #endregion
  }
}