ArcObjects Library Reference  

RSSWeatherLayerClass

About the RSS weather layer Sample

[C#]

RSSWeatherLayerClass.cs

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\ArcGIS_SXS_SDK");
      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
  }
}

[Visual Basic .NET]

RSSWeatherLayerClass.vb

Imports Microsoft.VisualBasic
Imports System
Imports System.Collections
Imports System.Data
Imports System.Runtime.InteropServices
Imports System.Xml
Imports System.Threading
Imports System.Timers
Imports System.Text.RegularExpressions
Imports System.IO
Imports System.Drawing
Imports System.Drawing.Imaging
Imports System.Windows.Forms
Imports System.ComponentModel
Imports Microsoft.Win32
Imports ESRI.ArcGIS.ADF.BaseClasses
Imports ESRI.ArcGIS.Carto
Imports ESRI.ArcGIS.Geodatabase
Imports ESRI.ArcGIS.esriSystem
Imports ESRI.ArcGIS.Geometry
Imports ESRI.ArcGIS.Display
Imports ESRI.ArcGIS.DataSourcesFile

  #Region "WeatherItemEventArgs class members"
  Public NotInheritable Class WeatherItemEventArgs : Inherits EventArgs
		Private m_id As Integer
		Private m_zipCode As Long
		Private m_x As Double
		Private m_y As Double
		Private m_iconWidth As Integer
		Private m_iconHeight As Integer

	Public Sub New(ByVal newID As Integer, ByVal newZipCode As Long, ByVal X As Double, ByVal Y As Double, ByVal newIconWidth As Integer, ByVal newIconHeight As Integer)
			m_id = newID
			m_zipCode = newZipCode
			m_x = X
			m_y = Y
			m_iconWidth = newIconWidth
			m_iconHeight = newIconHeight
	End Sub

		Public ReadOnly Property ID() As Integer
			Get
				Return m_id
			End Get
		End Property
		Public ReadOnly Property ZipCode() As Long
			Get
				Return m_zipCode
			End Get
		End Property
		Public ReadOnly Property mapY() As Double
			Get
				Return m_y
			End Get
		End Property
		Public ReadOnly Property mapX() As Double
			Get
				Return m_x
			End Get
		End Property
		Public ReadOnly Property IconWidth() As Integer
			Get
				Return m_iconWidth
			End Get
		End Property
		Public ReadOnly Property IconHeight() As Integer
			Get
				Return m_iconHeight
			End Get
		End Property
  End Class
  #End Region

	'declare delegates for the event handling
	Public Delegate Sub WeatherItemAdded(ByVal sender As Object, ByVal args As WeatherItemEventArgs)
	Public Delegate Sub WeatherItemsUpdated(ByVal sender As Object, ByVal args As EventArgs)


  ''' <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("RSSWeatherLayerClass")> _
  Public NotInheritable Class RSSWeatherLayerClass : Inherits BaseDynamicLayer : Implements IIdentify
	#Region "class members"

	Private m_timer As System.Timers.Timer = Nothing
	Private m_updateThread As Thread = Nothing
	Private m_iconFolder As String = String.Empty
	Private m_table As DataTable = Nothing
	Private m_symbolTable As DataTable = Nothing
	Private m_locations As DataTable = Nothing
	Private m_selectionSymbol As ISymbol = Nothing
	Private m_display As IDisplay = Nothing
	Private m_dataFolder As String = String.Empty
	Private m_layerSRFactoryCode As Integer = 0
	Private m_symbolSize As Integer = 32

	Private m_point As IPoint = Nothing
	Private m_llPnt As IPoint = Nothing
	Private m_urPnt As IPoint = Nothing
	Private m_env As IEnvelope = Nothing

	' dynamic display members
	Private m_dynamicGlyphFactory As IDynamicGlyphFactory2 = Nothing
	Private m_dynamicSymbolProperties As IDynamicSymbolProperties2 = Nothing
	Private m_dynamicCompoundMarker As IDynamicCompoundMarker2 = Nothing
	Private m_textGlyph As IDynamicGlyph = Nothing
	Private m_selectionGlyph As IDynamicGlyph = Nothing
	Private m_bDDOnce As Boolean = True

	'weather items events
	Public Event OnWeatherItemAdded As WeatherItemAdded
	Public Event OnWeatherItemsUpdated As WeatherItemsUpdated
	#End Region

	#Region "Constructor"
	''' <summary>
	''' The class has only default CTor.
	''' </summary>
	Public Sub New()
		MyBase.New()
	  Try
			'set the layer's name
			MyBase.m_sName = "RSS Weather Layer"
			'ask the Map to create a separate cache for the layer
			MyBase.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 (Not System.IO.Directory.Exists(m_dataFolder)) Then
				System.IO.Directory.CreateDirectory(m_dataFolder)
			End If
			m_iconFolder = m_dataFolder

			'instantiate the timer for the weather update
			m_timer = New System.Timers.Timer(1000)
			m_timer.Enabled = False
			AddHandler m_timer.Elapsed, AddressOf 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 Nothing Is m_locations Then
				InitializeLocations()
			End If

			'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 ex As Exception
				System.Diagnostics.Trace.WriteLine(ex.Message)
			End Try
	End Sub

	Protected Overrides Sub Finalize()
	  Disconnect()
	End Sub
	#End Region

#Region "Overridden 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 Overrides Sub Draw(ByVal drawPhase As esriDrawPhase, ByVal Display As IDisplay, ByVal trackCancel As ITrackCancel)
        If drawPhase <> esriDrawPhase.esriDPGeography Then
            Return
        End If
        If Display Is Nothing Then
            Return
        End If
        If m_table Is Nothing OrElse m_symbolTable Is Nothing Then
            Return
        End If

        m_display = Display

        Dim envelope As IEnvelope = TryCast(Display.DisplayTransformation.FittedBounds, IEnvelope)

        Dim lat, lon As Double
        Dim iconCode As Integer
        Dim selected As Boolean
        Dim symbol As ISymbol = Nothing

        'loop through the rows. Draw each row that has a shape
        For Each row As DataRow 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 AndAlso lon <= envelope.XMax AndAlso lat >= envelope.YMin AndAlso lat <= envelope.YMax Then
                'search for the symbol in the symbology table
                symbol = GetSymbol(iconCode, row)
                If Nothing Is symbol Then
                    Continue For
                End If

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

                'reproject the point to the DataFrame's spatial reference
                If Not Nothing Is m_mapSpatialRef AndAlso m_mapSpatialRef.FactoryCode <> m_layerSRFactoryCode Then
                    m_point.Project(m_mapSpatialRef)
                End If

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

                If selected Then
                    Display.SetSymbol(m_selectionSymbol)
                    Display.DrawPoint(m_point)
                End If
            End If
        Next row
    End Sub

    ''' <summary>
    ''' Draw the layer while in dynamic mode
    ''' </summary>
    ''' <param name="DynamicDrawPhase"></param>
    ''' <param name="Display"></param>
    ''' <param name="DynamicDisplay"></param>
    Public Overrides Sub DrawDynamicLayer(ByVal DynamicDrawPhase As esriDynamicDrawPhase, ByVal Display As IDisplay, ByVal DynamicDisplay As IDynamicDisplay)
        If DynamicDrawPhase <> esriDynamicDrawPhase.esriDDPCompiled Then
            Return
        End If

        If (Not m_bValid) OrElse (Not m_visible) Then
            Return
        End If

        If m_bDDOnce Then
            m_dynamicGlyphFactory = TryCast(DynamicDisplay.DynamicGlyphFactory, IDynamicGlyphFactory2)
            m_dynamicSymbolProperties = TryCast(DynamicDisplay, IDynamicSymbolProperties2)
            m_dynamicCompoundMarker = TryCast(DynamicDisplay, IDynamicCompoundMarker2)

            m_textGlyph = m_dynamicGlyphFactory.DynamicGlyph(1, esriDynamicGlyphType.esriDGlyphText, 1)

            ' create glyph for the selection symbol
            If m_selectionSymbol Is Nothing Then
                InitializeSelectionSymbol()
            End If

            m_selectionGlyph = m_dynamicGlyphFactory.CreateDynamicGlyph(m_selectionSymbol)

            m_bDDOnce = False
        End If

        m_display = Display

        Dim lat, lon As Double
        Dim iconCode As Integer
        Dim iconWidth As Integer = 0
        Dim selected As Boolean
        Dim dynamicGlyph As IDynamicGlyph = Nothing
        Dim symbolSized As Single
        Dim citiName As String = String.Empty
        Dim temperature As String = String.Empty

        'loop through the rows. Draw each row that has a shape
        For Each row As DataRow 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, iconWidth)
            If Nothing Is dynamicGlyph Then
                Continue For
            End If

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

            'reproject the point to the DataFrame's spatial reference
            If Not Nothing Is m_spatialRef AndAlso m_mapSpatialRef.FactoryCode <> m_layerSRFactoryCode Then
                m_point.Project(m_mapSpatialRef)
            End If

            symbolSized = 1.35F * CSng(m_symbolSize / CDbl(iconWidth))

            ' draw the weather item

            ' 1. set the whether symbol properties
            m_dynamicSymbolProperties.DynamicGlyph(esriDynamicSymbolType.esriDSymbolMarker) = dynamicGlyph
            m_dynamicSymbolProperties.RotationAlignment(esriDynamicSymbolType.esriDSymbolMarker) = esriDynamicSymbolRotationAlignment.esriDSRAScreen
            m_dynamicSymbolProperties.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.Smooth(esriDynamicSymbolType.esriDSymbolMarker) = False

            ' 2. set the text properties
            m_dynamicSymbolProperties.DynamicGlyph(esriDynamicSymbolType.esriDSymbolText) = m_textGlyph
            m_dynamicSymbolProperties.RotationAlignment(esriDynamicSymbolType.esriDSymbolMarker) = esriDynamicSymbolRotationAlignment.esriDSRAScreen
            m_dynamicSymbolProperties.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.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 Then ' draw the selected symbol
                m_dynamicSymbolProperties.SetColor(esriDynamicSymbolType.esriDSymbolMarker, 0.0F, 1.0F, 1.0F, 1.0F)
                m_dynamicSymbolProperties.DynamicGlyph(esriDynamicSymbolType.esriDSymbolMarker) = m_selectionGlyph
                DynamicDisplay.DrawMarker(m_point)
            End If
        Next row

        MyBase.m_bIsCompiledDirty = False
    End Sub

    ''' <summary>
    ''' The spatial reference of the underlying data.
    ''' </summary>
    Public Overrides ReadOnly Property SpatialReference() As ISpatialReference
        Get
            If Nothing Is m_spatialRef Then
                m_spatialRef = CreateGeographicSpatialReference()
            End If
            Return m_spatialRef
        End Get
    End Property

    ''' <summary>
    ''' The ID of the object. 
    ''' </summary>
    Public Overrides ReadOnly Property ID() As ESRI.ArcGIS.esriSystem.UID
        Get
            Dim uid As UID = New UIDClass()
            uid.Value = "RSSWeatherLayerClass"

            Return uid
        End Get
    End Property

    ''' <summary>
    ''' The default area of interest for the layer. Returns the spatial-referenced extent of the layer. 
    ''' </summary>
    Public Overrides ReadOnly Property AreaOfInterest() As IEnvelope
        Get
            Return Me.Extent
        End Get
    End Property

    ''' <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 Overrides ReadOnly Property Extent() As IEnvelope
        Get
            m_extent = GetLayerExtent()
            If Nothing Is m_extent Then
                Return Nothing
            End If

            Dim env As IEnvelope = TryCast((CType(m_extent, IClone)).Clone(), IEnvelope)
            If Not Nothing Is m_mapSpatialRef AndAlso m_mapSpatialRef.FactoryCode <> m_layerSRFactoryCode Then
                env.Project(m_mapSpatialRef)
            End If

            Return env
        End Get
    End Property

    ''' <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 Overrides Function get_TipText(ByVal X As Double, ByVal Y As Double, ByVal Tolerance As Double) As String
        Dim envelope As IEnvelope = New EnvelopeClass()
        envelope.PutCoords(X - Tolerance, Y - Tolerance, X + Tolerance, Y + Tolerance)

        'reproject the envelope to the datasource coordinate system
        If Not Nothing Is m_mapSpatialRef AndAlso m_mapSpatialRef.FactoryCode <> m_layerSRFactoryCode Then
            envelope.SpatialReference = m_spatialRef
            envelope.Project(m_mapSpatialRef)
        End If

        Dim xmin, ymin, xmax, ymax As Double
        envelope.QueryCoords(xmin, ymin, xmax, ymax)

        'select all the records within the given extent
        Dim qry As String = "LON >= " & xmin.ToString() & " AND LON <= " & xmax.ToString() & " AND Lat >= " & ymin.ToString() & " AND LAT <= " & ymax.ToString()
        Dim rows As DataRow() = m_table.Select(qry)
        If 0 = rows.Length Then
            Return String.Empty
        End If

        Dim r As DataRow = rows(0)
        Dim zipCode As String = Convert.ToString(r(1))
        Dim cityName As String = Convert.ToString(r(2))
        Dim temperature As String = Convert.ToString(r(5))

        Return cityName & ", " & zipCode & ", " & temperature & "F"
    End Function

#End Region

	#Region "public methods"

	''' <summary>
	''' connects to RSS weather service
	''' </summary>
		Public Sub Connect()
	  'enable the update timer
			m_timer.Enabled = True

			MyBase.m_bIsCompiledDirty = True
		End Sub

	''' <summary>
	''' disconnects from RSS weather service
	''' </summary>
		Public Sub Disconnect()
	  'disable the update timer
	  m_timer.Enabled = False

			Try
		'abort the update thread in case that it is alive
				If m_updateThread.IsAlive Then
					m_updateThread.Abort()
				End If
			Catch
				System.Diagnostics.Trace.WriteLine("WeatherLayer update thread has been terminated")
			End Try
		End Sub

	''' <summary>
	''' select a weather item by its zipCode
	''' </summary>
	''' <param name="zipCode"></param>
	''' <param name="newSelection"></param>
		Public Sub [Select](ByVal zipCode As Long, ByVal newSelection As Boolean)
			If Nothing Is m_table Then
				Return
			End If

			If newSelection Then
				UnselectAll()
			End If

			Dim rows As DataRow() = m_table.Select("ZIPCODE = " & zipCode.ToString())
			If rows.Length = 0 Then
				Return
			End If

			Dim rec As DataRow = rows(0)
			SyncLock m_table
				'13 is the selection column ID
				rec(13) = True
				rec.AcceptChanges()
			End SyncLock
			MyBase.m_bIsCompiledDirty = True
		End Sub

	''' <summary>
	''' unselect all weather items
	''' </summary>
		Public Sub UnselectAll()
			If Nothing Is m_table Then
				Return
			End If

			'unselect all the currently selected items
			SyncLock m_table
				For Each r As DataRow In m_table.Rows
					'13 is the selection column ID
					r(13) = False
				Next r
				m_table.AcceptChanges()
			End SyncLock

			MyBase.m_bIsCompiledDirty = True
		End Sub

	''' <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 Sub Refresh()
			Try
				m_updateThread = New Thread(AddressOf ThreadProc)

				'run the update thread
				m_updateThread.Start()
			Catch ex As Exception
				System.Diagnostics.Trace.WriteLine(ex.Message)
			End Try
		End Sub

	''' <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 Function AddItem(ByVal zipCode As Long) As Boolean
			Return AddItem(zipCode, 0.0,0.0)
		End Function

	''' <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 Function AddItem(ByVal zipCode As Long, ByVal lat As Double, ByVal lon As Double) As Boolean
			If Nothing Is m_table Then
				Return False
			End If

			Dim r As DataRow = m_table.Rows.Find(zipCode)
			If Not Nothing Is r Then '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 AndAlso lon = 0.0 Then
					Return False
				Else 'update the location according to the new coordinate
					SyncLock m_table
						r(3) = lat
						r(4) = lon

						r.AcceptChanges()
					End SyncLock
				End If
			Else
				'add new zip code to the locations list
				Dim rec As DataRow = m_locations.NewRow()
				SyncLock m_locations
					rec(1) = zipCode
					m_locations.Rows.Add(rec)
				End SyncLock

				'need to connect to the service and get the info
				AddWeatherItem(zipCode, lat, lon)
			End If

			Return True
		End Function

	''' <summary>
	''' delete an item from the dataset
	''' </summary>
	''' <param name="zipCode"></param>
	''' <returns></returns>
		Public Function DeleteItem(ByVal zipCode As Long) As Boolean
			If Nothing Is m_table Then
				Return False
			End If

			Try
				Dim r As DataRow = m_table.Rows.Find(zipCode)
				If Not Nothing Is r Then 'if the record with this zipCode already exists
					SyncLock m_table
						r.Delete()
					End SyncLock
					MyBase.m_bIsCompiledDirty = True
					Return True
				End If
				MyBase.m_bIsCompiledDirty = True
				Return False
			Catch ex As Exception
				System.Diagnostics.Trace.WriteLine(ex.Message)
				Return False
			End Try
		End Function

	''' <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 Function GetWeatherItem(ByVal cityName As String) As IPropertySet
			If Nothing Is m_table Then
				Return Nothing
			End If

			Dim rows As DataRow() = m_table.Select("CITYNAME = '" & cityName & "'")
			If rows.Length = 0 Then
				Return Nothing
			End If

			Dim zipCode As Long = Convert.ToInt64(rows(0)(1))
			Return GetWeatherItem(zipCode)
		End Function

	''' <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 Function GetWeatherItem(ByVal zipCode As Long) As IPropertySet
			Dim r As DataRow = m_table.Rows.Find(zipCode)
			If Nothing Is r Then
				Return Nothing
			End If

			Dim propSet As IPropertySet = 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
		End Function

	''' <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 Function GetCityNames() As String()
			If Nothing Is m_table OrElse 0 = m_table.Rows.Count Then
				Return Nothing
			End If

			Dim cityNames As String() = New String(m_table.Rows.Count - 1){}
			Dim i As Integer=0
			Do While i<m_table.Rows.Count
				'column #2 stores the cityName
				cityNames(i) = Convert.ToString(m_table.Rows(i)(2))
				i += 1
			Loop

			Return cityNames
		End Function

	''' <summary>
	''' Zoom to a weather item according to its city name
	''' </summary>
	''' <param name="cityName"></param>
	Public Sub ZoomTo(ByVal cityName As String)
	  If Nothing Is m_table Then
			Return
	  End If

	  Dim rows As DataRow() = m_table.Select("CITYNAME = '" & cityName & "'")
	  If rows.Length = 0 Then
			Return
	  End If

	  Dim zipCode As Long = Convert.ToInt64(rows(0)(1))
	  ZoomTo(zipCode)
	End Sub

	''' <summary>
	''' Zoom to weather item according to its zipcode
	''' </summary>
	''' <param name="zipCode"></param>
	Public Sub ZoomTo(ByVal zipCode As Long)
	  If Nothing Is m_table OrElse Nothing Is m_symbolTable Then
			Return
	  End If

	  If Nothing Is m_display Then
			Return
	  End If

	  'get the record for the requested zipCode
	  Dim r As DataRow = m_table.Rows.Find(zipCode)
	  If Nothing Is r Then
			Return
	  End If

	  'get the coordinate of the zipCode
	  Dim lat As Double = Convert.ToDouble(r(3))
	  Dim lon As Double = Convert.ToDouble(r(4))

	  Dim point As IPoint = New PointClass()
	  point.X = lon
	  point.Y = lat
	  point.SpatialReference = m_spatialRef

		If Not Nothing Is m_mapSpatialRef AndAlso m_mapSpatialRef.FactoryCode <> m_layerSRFactoryCode Then
			point.Project(m_mapSpatialRef)
		End If

	  Dim iconCode As Integer = Convert.ToInt32(r(8))
        'find the appropriate symbol record
	  Dim rec As DataRow = m_symbolTable.Rows.Find(iconCode)
	  If rec Is Nothing Then
			Return
	  End If

	  'get the icon's dimensions
	  Dim iconWidth As Integer = Convert.ToInt32(rec(3))
	  Dim iconHeight As Integer = Convert.ToInt32(rec(4))

	  Dim displayTransformation As IDisplayTransformation = (CType(m_display, IScreenDisplay)).DisplayTransformation

	  'Convert the icon coordinate into screen coordinate
	  Dim windowX, windowY As Integer
	  displayTransformation.FromMapPoint(point,windowX, windowY)

	  'get the upper left coord
	  Dim ulx, uly As Integer
		ulx = Convert.ToInt32(windowX - iconWidth / 2)
		uly = Convert.ToInt32(windowY - iconHeight / 2)
	  Dim ulPnt As IPoint = displayTransformation.ToMapPoint(ulx, uly)

	  'get the lower right coord
	  Dim lrx, lry As Integer
		lrx = Convert.ToInt32(windowX + iconWidth / 2)
		lry = Convert.ToInt32(windowY + iconHeight / 2)
	  Dim lrPnt As IPoint = displayTransformation.ToMapPoint(lrx, lry)

	  'construct the new extent
	  Dim envelope As IEnvelope = 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

		MyBase.m_bIsCompiledDirty = True

	  CType(m_display, IScreenDisplay).Invalidate(Nothing, True, CShort(esriScreenCache.esriAllScreenCaches))
	  CType(m_display, IScreenDisplay).UpdateWindow()
	End Sub

	Private Sub SetSymbolSize(ByVal newSize As Integer)
	  If newSize <= 0 Then
			MessageBox.Show("Size is not allowed.")
			Return
	  End If

	  m_symbolSize = newSize

	  If Nothing Is m_symbolTable OrElse 0 = m_symbolTable.Rows.Count Then
			Return
	  End If

	  Dim pictureMarkerSymbol As IPictureMarkerSymbol = Nothing

	  SyncLock m_symbolTable
		For Each r As DataRow In m_symbolTable.Rows
		  pictureMarkerSymbol = TryCast(r(2), IPictureMarkerSymbol)
		  If Nothing Is pictureMarkerSymbol Then
				Continue For
		  End If

		  pictureMarkerSymbol.Size = newSize
		  r(2) = pictureMarkerSymbol
		  r.AcceptChanges()
		Next r
	  End SyncLock

		MyBase.m_bIsCompiledDirty = True

	  CType(m_display, IScreenDisplay).Invalidate(Nothing, True, CShort(esriScreenCache.esriAllScreenCaches))
	  CType(m_display, IScreenDisplay).UpdateWindow()
	End Sub

	Public Property SymbolSize() As Integer
	  Set
		  SetSymbolSize(Value)
	  End Set
	  Get
		  Return m_symbolSize
	  End Get
	End Property

		#End Region

		#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 Function CreateGeographicSpatialReference() As ISpatialReference
			Dim spatialRefFatcory As ISpatialReferenceFactory = New SpatialReferenceEnvironmentClass()
			Dim geoCoordSys As IGeographicCoordinateSystem
			geoCoordSys = spatialRefFatcory.CreateGeographicCoordinateSystem(CInt(esriSRGeoCSType.esriSRGeoCS_WGS1984))
			geoCoordSys.SetFalseOriginAndUnits(-180.0, -180.0, 5000000.0)
			geoCoordSys.SetZFalseOriginAndUnits(0.0, 100000.0)
			geoCoordSys.SetMFalseOriginAndUnits(0.0, 100000.0)

			Return TryCast(geoCoordSys, ISpatialReference)
		End Function

	''' <summary>
	''' get the overall extent of the items in the layer
	''' </summary>
	''' <returns></returns>
		Private Function GetLayerExtent() As IEnvelope
	  'iterate through all the items in the layers DB and get the bounding envelope
			Dim env As IEnvelope = New EnvelopeClass()
			env.SpatialReference = m_spatialRef
			Dim point As IPoint = New PointClass()
			point.SpatialReference = m_spatialRef
			Dim symbolCode As Integer = 0
			Dim newSymbolSize As Double = 0
			For Each r As DataRow In m_table.Rows
				If TypeOf r(3) Is DBNull OrElse TypeOf r(4) Is DBNull Then
					Continue For
				End If

				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 Not m_display Is Nothing Then
					symbolCode = Convert.ToInt32(r(8))
					newSymbolSize = Math.Max(GetSymbolSize(m_display, symbolCode), newSymbolSize)
				End If


				env.Union(point.Envelope)
			Next r

	  ' Expand the envelope in order to include the size of the symbol
			env.Expand(newSymbolSize, newSymbolSize, False)

	  'return the layer's extent in the data underlying coordinate system
			Return env
		End Function

	''' <summary>
	''' initialize the main table used by the layer as well as the symbols table.
    ''' The base class calls new on the table and adds a default ID field.
	''' </summary>
		Private Sub InitializeTables()
			Dim path As String = System.IO.Path.Combine(m_dataFolder, "Weather.xml")
			'In case that there is no existing cache on the local machine, create the table.
			If (Not System.IO.File.Exists(path)) Then

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

				m_table.Columns.Add("ID", GetType(Long)) '0
				m_table.Columns.Add("ZIPCODE", GetType(Long)) '1
				m_table.Columns.Add("CITYNAME", GetType(String)) '2
				m_table.Columns.Add("LAT", GetType(Double)) '3
				m_table.Columns.Add("LON", GetType(Double)) '4
				m_table.Columns.Add("TEMP", GetType(Integer)) '5
				m_table.Columns.Add("CONDITION", GetType(String)) '6
				m_table.Columns.Add("ICONNAME", GetType(String)) '7
				m_table.Columns.Add("ICONID", GetType(Integer)) '8
				m_table.Columns.Add("DAY", GetType(String)) '9
				m_table.Columns.Add("DATE", GetType(String)) '10
				m_table.Columns.Add("LOW", GetType(String)) '11
				m_table.Columns.Add("HIGH", GetType(String)) '12
				m_table.Columns.Add(" SELECTED", GetType(Boolean)) '13
				m_table.Columns.Add("UPDATEDATE", GetType(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.
				Dim ds As DataSet = New DataSet()
				ds.ReadXml(path)

				m_table = ds.Tables("RECORDS")

			If Nothing Is m_table Then
				Throw New Exception("Cannot find 'RECORDS' table")
			End If

			If 15 <> m_table.Columns.Count Then
				Throw New Exception("Table 'RECORDS' does not have all required columns")
			End If

			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
				For Each r As DataRow In m_table.Rows
					Try
			'in case that the locations table does not exists, create and initialize it
						If Nothing Is m_locations Then
							InitializeLocations()
						End If

			'get the zipcode for the record
						Dim zip As String = Convert.ToString(r(1))

					'make sure that there is no existing record with that zipCode already in the 
					'locations table.
					Dim rows As DataRow() = m_locations.Select("ZIPCODE = " & zip)
						If 0 = rows.Length Then
							Dim rec As DataRow = 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
			  SyncLock m_locations
								m_locations.Rows.Add(rec)
			  End SyncLock
						End If
					Catch ex As Exception
						System.Diagnostics.Trace.WriteLine(ex.Message)
					End Try
				Next r

            'dispose the DS
			ds.Tables.Remove(m_table)
			ds.Dispose()
			GC.Collect()
	  End If

			'initialize the symbol map table
			m_symbolTable = New DataTable("Symbology")

			'add the columns to the table
			m_symbolTable.Columns.Add("ID", GetType(Integer)) '0
			m_symbolTable.Columns.Add("ICONID", GetType(Integer)) '1
			m_symbolTable.Columns.Add("SYMBOL", GetType(ISymbol)) '2
			m_symbolTable.Columns.Add("SYMBOLWIDTH", GetType(Integer)) '3
			m_symbolTable.Columns.Add("SYMBOLHEIGHT", GetType(Integer)) '4
			m_symbolTable.Columns.Add("DYNAMICGLYPH", GetType(IDynamicGlyph)) '5
			m_symbolTable.Columns.Add("BITMAP", GetType(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")}
		End Sub

	''' <summary>
	''' Initialize the location table. Gets the location from a featureclass
	''' </summary>
	Private Sub InitializeLocations()
        'create a new instance of the location table
		m_locations = New DataTable()

		'add fields to the table
		m_locations.Columns.Add("ID", GetType(Integer))
		m_locations.Columns.Add("ZIPCODE", GetType(Long))
		m_locations.Columns.Add("CITYNAME", GetType(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
		Dim t As Thread = New Thread(AddressOf PopulateLocationsTableProc)
		t.Start()

		System.Threading.Thread.Sleep(1000)
	End Sub

	''' <summary>
	''' Load the information from the MajorCities featureclass to the locations table
	''' </summary>
	Private Sub PopulateLocationsTableProc()
	  'get the ArcGIS path from the registry
        Dim key As RegistryKey = Registry.LocalMachine.OpenSubKey("SOFTWARE\ESRI\ArcObjectsSdk10.0")
	  Dim path As String = Convert.ToString(key.GetValue("InstallDir"))

        If (Not System.IO.File.Exists(System.IO.Path.Combine(path, "Samples\Data\USZipCodeData\ZipCode_Boundaries_US_Major_Cities.shp"))) Then
            MessageBox.Show("Cannot find file ZipCode_Boundaries_US_Major_Cities.shp!")
            Return
        End If

	  'open the featureclass
		Dim wf As IWorkspaceFactory = New ShapefileWorkspaceFactoryClass()
        Dim ws As IWorkspace = wf.OpenFromFile(System.IO.Path.Combine(path, "Samples\Data\USZipCodeData"), 0)
	  Dim fw As IFeatureWorkspace = TryCast(ws, IFeatureWorkspace)
	  Dim featureClass As IFeatureClass = fw.OpenFeatureClass("ZipCode_Boundaries_US_Major_Cities")
	  'map the name and zip fields
	  Dim zipIndex As Integer = featureClass.FindField("ZIP")
	  Dim nameIndex As Integer = featureClass.FindField("NAME")
	  Dim cityName As String
	  Dim zip As Long

	  Try
			'iterate through the features and add the information to the table
			Dim fCursor As IFeatureCursor = Nothing
			fCursor = featureClass.Search(Nothing, True)
			Dim feature As IFeature = fCursor.NextFeature()
			Dim index As Integer = 0

			Do While Not Nothing Is feature
				Dim obj As Object = feature.Value(nameIndex)
				If obj Is Nothing Then
					Continue Do
				End If
				cityName = Convert.ToString(obj)

				obj = feature.Value(zipIndex)
				If obj Is Nothing Then
					Continue Do
				End If
				zip = Long.Parse(Convert.ToString(obj))
				If zip <= 0 Then
					Continue Do
				End If

				'add the current location to the location table
				Dim r As DataRow = m_locations.Rows.Find(zip)
				If Nothing Is r Then
					r = m_locations.NewRow()
					r(1) = zip
					r(2) = cityName
					SyncLock m_locations
						m_locations.Rows.Add(r)
					End SyncLock
				End If

				feature = fCursor.NextFeature()

				index += 1
			Loop

		'release the feature cursor
		Marshal.ReleaseComObject(fCursor)
	  Catch ex As Exception
			System.Diagnostics.Trace.WriteLine(ex.Message)
	  End Try
	End Sub

		''' <summary>
		''' Initialize the symbol that would use to highlight selected items
		''' </summary>
	Private Sub InitializeSelectionSymbol()
		'use a character marker symbol:
		Dim chMrkSym As ICharacterMarkerSymbol
		chMrkSym = New CharacterMarkerSymbolClass()

	  'Set the selection color (yellow)
		Dim color As IRgbColor
		color = New RgbColorClass()
		color.Red = 0
		color.Green = 255
		color.Blue = 255

	  'set the font
		Dim aFont As stdole.IFont
		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 = TryCast(color, IColor)
		chMrkSym.Font = TryCast(aFont, stdole.IFontDisp)
	  chMrkSym.Size = m_symbolSize

		m_selectionSymbol = TryCast(chMrkSym, ISymbol)
	End Sub
	''' <summary>
	''' run the thread that does the update of the weather data
	''' </summary>
	''' <param name="sender"></param>
	''' <param name="e"></param>
	Private Sub OnUpdateTimer(ByVal sender As Object, ByVal e As ElapsedEventArgs)
		m_timer.Interval = 2700000 '(45 minutes)
		m_updateThread = New Thread(AddressOf ThreadProc)

		'run the update thread
		m_updateThread.Start()
	End Sub

	''' <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 Sub ThreadProc()
		Try

			Dim lZipCode As Long
			'iterate through all the records in the main table and update it against 
			'the information from the website.
			For Each r As DataRow 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)
			Next r

			'serialize the tables onto the local machine
			Dim ds As DataSet = 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()

			MyBase.m_bIsCompiledDirty = True

			'fire an event to notify update of the weatheritems 
			If Not OnWeatherItemsUpdatedEvent Is Nothing Then
				RaiseEvent OnWeatherItemsUpdated(Me, New EventArgs())
			End If
		Catch ex As Exception
			System.Diagnostics.Trace.WriteLine(ex.Message)
		End Try
	End Sub

		''' <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 Function DownloadIcon(ByVal iconPath As String, <System.Runtime.InteropServices.Out()> ByRef width As Integer, <System.Runtime.InteropServices.Out()> ByRef height As Integer) As Bitmap
		'if the icon does not exist on the local machine, get it from RSS site
		Dim iconFileName As String = System.IO.Path.Combine(m_iconFolder, System.IO.Path.GetFileNameWithoutExtension(iconPath) & ".bmp")
		width = 0
		height = 0
		Dim bitmap As Bitmap = Nothing
		If (Not File.Exists(iconFileName)) Then
			Using webClient As System.Net.WebClient = New System.Net.WebClient()
				'open a readable stream to download the bitmap
				Using stream As System.IO.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
				End Using
			End Using
		Else
			'get the bitmap's dimensions
			bitmap = New Bitmap(iconFileName)
			width = bitmap.Width
			height = bitmap.Height
		End If

		Return bitmap
	End Function

	''' <summary>
	''' get the specified symbol from the symbols table.
	''' </summary>
	''' <param name="iconCode"></param>
	''' <param name="dbr"></param>
	''' <returns></returns>
	Private Function GetSymbol(ByVal iconCode As Integer, ByVal dbr As DataRow) As ISymbol
		Dim symbol As ISymbol = Nothing
		Dim iconPath As String
		Dim iconWidth, iconHeight As Integer
		Dim bitmap As Bitmap = Nothing

		'search for an existing symbol in the table
		Dim r As DataRow = m_symbolTable.Rows.Find(iconCode)
		If r Is Nothing Then '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, iconWidth, iconHeight, bitmap)
			If Nothing Is symbol Then
				Return Nothing
			End If

			'update the symbol table
			SyncLock m_symbolTable
				r(2) = symbol
				r(3) = iconWidth
				r(4) = iconHeight
				r(6) = bitmap
				m_symbolTable.Rows.Add(r)
			End SyncLock
		Else
			If TypeOf r(2) Is DBNull Then	'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, iconWidth, iconHeight, bitmap)
				If Nothing Is symbol Then
					Return Nothing
				End If

				'update the symbol table
				SyncLock m_symbolTable
					r(2) = symbol
					r(6) = bitmap
					r.AcceptChanges()
				End SyncLock
			Else 'the record exists in the table and the symbol has been initialized
				'get the symbol
				symbol = TryCast(r(2), ISymbol)
			End If
		End If

		'return the requested symbol
		Return symbol
	End Function

	Private Function GetDynamicGlyph(ByVal dynamicGlyphFactory As IDynamicGlyphFactory2, ByVal iconCode As Integer, ByVal dbr As DataRow, <System.Runtime.InteropServices.Out()> ByRef originalIconSize As Integer) As IDynamicGlyph
		originalIconSize = 0

		If dynamicGlyphFactory Is Nothing Then
			Return Nothing
		End If

		Dim iconPath As String
		Dim iconWidth, iconHeight As Integer
		Dim bitmap As Bitmap = Nothing
		Dim dynamicGlyph As IDynamicGlyph = Nothing

		'search for an existing symbol in the table
		Dim r As DataRow = m_symbolTable.Rows.Find(iconCode)
		If r Is Nothing Then
			iconPath = Convert.ToString(dbr(7))
			bitmap = DownloadIcon(iconPath, iconWidth, iconHeight)
			If Not bitmap Is Nothing Then
				originalIconSize = iconWidth

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


				'update the symbol table
				r = m_symbolTable.NewRow()
				SyncLock m_symbolTable
					r(1) = iconCode
					r(3) = iconWidth
					r(4) = iconHeight
					r(5) = dynamicGlyph
					r(6) = bitmap
					m_symbolTable.Rows.Add(r)
				End SyncLock
			End If
		Else
			If TypeOf r(5) Is DBNull Then
				If TypeOf r(6) Is DBNull Then
					iconPath = Convert.ToString(dbr(7))
					bitmap = DownloadIcon(iconPath, iconWidth, iconHeight)
					If bitmap Is Nothing Then
						Return Nothing
					End If

					originalIconSize = iconWidth

					SyncLock m_symbolTable
						r(3) = iconWidth
						r(4) = iconHeight
						r(6) = bitmap
					End SyncLock
				Else
					originalIconSize = Convert.ToInt32(r(3))
					bitmap = CType(r(6), Bitmap)
				End If
                dynamicGlyph = dynamicGlyphFactory.CreateDynamicGlyphFromBitmap(esriDynamicGlyphType.esriDGlyphMarker, bitmap.GetHbitmap().ToInt32(), False, CType(ESRI.ArcGIS.ADF.Connection.Local.Converter.ToRGBColor(Color.FromArgb(255, 255, 255)), IColor))

				SyncLock m_symbolTable
					r(5) = dynamicGlyph
				End SyncLock
			Else
				originalIconSize = Convert.ToInt32(r(3))
				dynamicGlyph = CType(r(5), IDynamicGlyph)
			End If
		End If

		Return dynamicGlyph
	End Function


		''' <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 Function InitializeSymbol(ByVal iconPath As String, <System.Runtime.InteropServices.Out()> ByRef iconWidth As Integer, <System.Runtime.InteropServices.Out()> ByRef iconHeight As Integer, <System.Runtime.InteropServices.Out()> ByRef bitmap As Bitmap) As ISymbol
		iconWidth = 0
		iconHeight = 0
		bitmap = Nothing
		Try
			'make sure that the icon exit on dist or else download it
			bitmap = DownloadIcon(iconPath, iconWidth, iconHeight)
			Dim iconFileName As String = System.IO.Path.Combine(m_iconFolder, System.IO.Path.GetFileNameWithoutExtension(iconPath) & ".bmp")
			If (Not System.IO.File.Exists(iconFileName)) Then
				Return Nothing
			End If

			'initialize the transparent color
			Dim rgbColor As IRgbColor = New RgbColorClass()
			rgbColor.Red = 255
			rgbColor.Blue = 255
			rgbColor.Green = 255

			'instantiate the marker symbol and set its properties
			Dim pictureMarkerSymbol As IPictureMarkerSymbol = 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 = TryCast(rgbColor, IColor)

			'return the symbol
			Return CType(pictureMarkerSymbol, ISymbol)
		Catch
			Return Nothing
		End Try
	End Function

	''' <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 Sub AddWeatherItem(ByVal zipCode As Long, ByVal Lat As Double, ByVal Lon As Double)
			Try
				Dim cityName As String
				Dim newLat, newLon As Double
				Dim temp As Integer
				Dim condition As String
				Dim desc As String
				Dim iconPath As String
				Dim day As String
				Dim [date] As String
				Dim low As Integer
				Dim high As Integer
				Dim iconCode As Integer
				Dim iconWidth As Integer = 52 'default values
				Dim iconHeight As Integer = 52
				Dim bitmap As Bitmap = Nothing

				Dim dbr As DataRow = m_table.Rows.Find(zipCode)
				If Not dbr Is Nothing Then
					' get the date 
					Dim updateDate As DateTime = Convert.ToDateTime(dbr(14))
					Dim ts As TimeSpan = DateTime.Now.Subtract(updateDate)

					' if the item had been updated in the past 15 minutes, simply bail out.
					If ts.TotalMinutes < 15 Then
						Return
					End If
				End If

			'the base URL for the service
				Dim url As String = "http://xml.weather.yahoo.com/forecastrss?p="
			'the RegEx used to extract the icon path from the HTML tag
				Dim regxQry As String = "(http://(\"")?(.*?\.gif))"
				Dim reader As XmlTextReader = Nothing
				Dim doc As XmlDocument
				Dim node As XmlNode

				Try	
				'make the request and get the result back into XmlReader
					reader = New XmlTextReader(url & zipCode.ToString())
				Catch ex As Exception
					System.Diagnostics.Trace.WriteLine(ex.Message)
					Return
				End Try

			'load the XmlReader to an xml doc
				doc = New XmlDocument()
				doc.Load(reader)

			'set an XmlNamespaceManager since we have to make explicit namespace searches
			Dim xmlnsManager As XmlNamespaceManager = 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 Nothing Is node Then
				Return
			End If

			'get the cityname
			cityName = doc.DocumentElement.SelectSingleNode("/rss/channel/yweather:location/@city", xmlnsManager).InnerXml
			If Lat = 0.0 AndAlso Lon = 0.0 Then
                'in case that the caller did not specify a coordinate, get the default coordinate from the service
				newLat = Convert.ToDouble(doc.DocumentElement.SelectSingleNode("/rss/channel/item/geo:lat", xmlnsManager).InnerXml)
				newLon = Convert.ToDouble(doc.DocumentElement.SelectSingleNode("/rss/channel/item/geo:long", xmlnsManager).InnerXml)
			Else
				newLat = Lat
				newLon = Lon
			End If

			'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
				Dim m As Match = Regex.Match(desc,regxQry)
				If m.Success Then
					iconPath = m.Value

					'add the icon ID to the symbology table
					Dim tr As DataRow = m_symbolTable.Rows.Find(iconCode)
					If Nothing Is tr Then
						'get the icon from the website
						bitmap = DownloadIcon(iconPath, iconWidth, 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.
						SyncLock m_symbolTable
							m_symbolTable.Rows.Add(tr)
						End SyncLock
					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))
					End If
				Else
					iconPath = ""
				End If

			'test whether the record already exists in the layer's table.
            If Nothing Is dbr Then 'in case that the record does not exist
                'create a new record
                dbr = m_table.NewRow()

                If (Not m_table.Columns(0).AutoIncrement) Then
                    dbr(0) = Convert.ToInt32(DateTime.Now.Millisecond)
                End If

                'add the item to the table
                SyncLock m_table
                    dbr(1) = zipCode
                    dbr(2) = cityName
                    dbr(3) = newLat
                    dbr(4) = newLon
                    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)
                End SyncLock
            Else 'in case that the record exists, just update it
                'update the record
                SyncLock 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()
                End SyncLock
            End If

				MyBase.m_bIsCompiledDirty = True

				'fire an event to notify the user that the item has been updated
		If Not OnWeatherItemAddedEvent Is Nothing Then
		  Dim weatherItemEventArgs As WeatherItemEventArgs = New WeatherItemEventArgs(Convert.ToInt32(dbr(0)), zipCode, newLon, newLat, iconWidth, iconHeight)
		  RaiseEvent OnWeatherItemAdded(Me, weatherItemEventArgs)
		End If

		Catch ex As Exception
			System.Diagnostics.Trace.WriteLine("AddWeatherItem: " & ex.Message)
		End Try
	End Sub
	#End Region

	#Region "IIdentify Members"

	''' <summary>
	''' Identifying all the weather items falling within the given envelope
	''' </summary>
	''' <param name="pGeom"></param>
	''' <returns></returns>
	Public Function Identify(ByVal pGeom As IGeometry) As IArray Implements IIdentify.Identify
	  Dim intersectEnv As IEnvelope = New EnvelopeClass()
	  Dim inEnv As IEnvelope
	  Dim array As IArray = New ArrayClass()

        'get the envelope from the geometry 
	  If pGeom.GeometryType = esriGeometryType.esriGeometryEnvelope Then
			inEnv = pGeom.Envelope
	  Else
			inEnv = TryCast(pGeom, IEnvelope)
	  End If

	  'reproject the envelope to the source coordsys
	  'this would allow to search directly on the Lat/Lon columns
			If Not Nothing Is m_spatialRef AndAlso m_mapSpatialRef.FactoryCode <> m_layerSRFactoryCode AndAlso Not Nothing Is inEnv.SpatialReference Then
				inEnv.Project(MyBase.m_spatialRef)
			End If

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

	  Dim xmin, ymin, xmax, ymax As Double
	  inEnv.QueryCoords(xmin, ymin, xmax, ymax)

	  'select all the records within the given extent
	  Dim qry As String = "LON >= " & xmin.ToString() & " AND LON <= " & xmax.ToString() & " AND Lat >= " & ymin.ToString() & " AND LAT <= " & ymax.ToString()
	  Dim rows As DataRow() = m_table.Select(qry)
	  If 0 = rows.Length Then
			Return array
	  End If

	  Dim zipCode As Long
	  Dim propSet As IPropertySet = Nothing
	  Dim idObj As IIdentifyObj = Nothing
	  Dim idObject As IIdentifyObject = Nothing
	  Dim bIdentify As Boolean = False

	  For Each r As DataRow 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 = Me.GetWeatherItem(zipCode)
		If Not Nothing Is propSet Then
		  'instantiate the identify object and add it to the array
		  idObj = New RSSWeatherIdentifyObject()
		  'test whether the layer can be identified
		  bIdentify = idObj.CanIdentify(CType(Me, ILayer))
		  If bIdentify Then
				idObject = TryCast(idObj, IIdentifyObject)
				idObject.PropertySet = propSet
				array.Add(idObj)
		  End If
		End If
	  Next r

	  'return the array with the identify objects
	  Return array
	End Function

		Private Function GetSymbolSize(ByVal display As IDisplay, ByVal symbolCode As Integer) As Double
			If display Is Nothing Then
				Return 0
			End If

			Dim newSymbolSize As Double = 0
			Dim symbolSizePixels As Double = 0
			Dim r As DataRow = m_symbolTable.Rows.Find(symbolCode)
			If Not r Is Nothing Then
				symbolSizePixels = Convert.ToDouble(m_symbolSize)

            ' convert the symbol size from pixels to map units
				Dim transform As ITransformation = TryCast(display.DisplayTransformation, ITransformation)
				If transform Is Nothing Then
					Return 0
				End If

				Dim symbolDimensions As Double() = New Double(1){}
				symbolDimensions(0) = CDbl(symbolSizePixels)
				symbolDimensions(1) = CDbl(symbolSizePixels)

				Dim symbolDimensionsMap As Double() = New Double(1){}

				transform.TransformMeasuresFF(esriTransformDirection.esriTransformReverse, 1, symbolDimensionsMap(0), symbolDimensions(0))
				newSymbolSize = symbolDimensionsMap(0)
			End If

			Return newSymbolSize
		End Function

	#End Region
  End Class