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

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.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 id As Integer, ByVal zipCode As Long, ByVal X As Double, ByVal Y As Double, ByVal iconWidth As Integer, ByVal iconHeight As Integer)
    m_id = id
    m_zipCode = zipCode
    m_x = X
    m_y = Y
    m_iconWidth = iconWidth
    m_iconHeight = iconHeight
  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("RSSWeatherLayer.RSSWeatherLayerClass")> _
  Public NotInheritable Class RSSWeatherLayerClass : Inherits CustomLayerBase : 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_symbolTable As DataTable = Nothing
  Private m_locations As DataTable = Nothing
  Private m_layerSpatialRef As ISpatialReference = Nothing 'DataFrame's SR
  Private m_selectionSymbol As ISymbol = Nothing
  Private m_display As IDisplay = Nothing
  Private m_bOnce As Boolean = True
  Private m_dataFolder As String = String.Empty

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


    '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
    'setthe layer's name
    MyBase.m_sName = "RSS Weather Layer"
    'ask the Map to create a separate cache for the layer
    MyBase.m_IsCached = True

    'get the directory for the layer's cache. If it does not exist, create it.
    m_dataFolder = System.IO.Path.Combine(System.Environment.CurrentDirectory, "Data")
    If (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 "Overriden methods"

  ''' <summary>
  ''' Draws the layer to the specified display for the given draw phase. 
  ''' </summary>
  ''' <param name="drawPhase"></param>
  ''' <param name="Display"></param>
  ''' <param name="trackCancel"></param>
  ''' <remarks>the draw method is set as an abstruct method and therefor must be overridden</remarks>
    Public 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

    If m_bOnce Then
    m_display = Display
    m_bOnce = False
    End If

      Dim point As IPoint = New PointClass()
      point.SpatialReference = m_spatialRef
      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

          point.X = lon
          point.Y = lat

          'reproject the point to the DataFrame's spatial reference
          If Not Nothing Is m_layerSpatialRef Then
            point.Project(m_layerSpatialRef)
          End If

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

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

  ''' <summary>
  ''' The spatial reference of the underlying data.
  ''' </summary>
  Public Overrides ReadOnly Property SpatialReference1() 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 = "RSSWeatherLayer.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_layerSpatialRef Then
          env.Project(m_layerSpatialRef)
        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 ReadOnly Property TipText(ByVal X As Double, ByVal Y As Double, ByVal Tolerance As Double) As String
    Get
      Dim envelope As IEnvelope = New EnvelopeClass()
      envelope.PutCoords(X - Tolerance, Y - Tolerance, X + Tolerance, Y + Tolerance)

      'reproject the envelope to the datasource doordinate system
      If Not Nothing Is m_layerSpatialRef Then
        envelope.SpatialReference = m_layerSpatialRef
        envelope.Project(m_spatialRef)
      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 Get
  End Property
#End Region

#Region "public methods"

  ''' <summary>
  ''' connects to RSS weather service
  ''' </summary>
  Public Sub Connect()
    'enable the update timer
    m_timer.Enabled = 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 SelectItem(ByVal zipCode As Long, ByVal newSelection As Boolean) '[Select]
    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
  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
  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 AddNewItem(ByVal zipCode As Long) As Boolean
    Return AddNewItem(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 AddNewItem(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
        r(3) = lat
        r(4) = lon
        SyncLock m_table
          r.AcceptChanges()
        End SyncLock
      End If
    Else
      'add new zip code to the locations list
      Dim rec As DataRow = m_locations.NewRow()
      rec(1) = zipCode
      SyncLock m_locations
        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
        Return True
      End If
      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_layerSpatialRef Then
      point.Project(m_layerSpatialRef)
    End If

    Dim iconCode As Integer = Convert.ToInt32(r(8))
    'find the appropreate 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 = windowX - Convert.ToInt32(iconWidth / 2.0)
    uly = windowY - Convert.ToInt32(iconHeight / 2.0)
    Dim ulPnt As IPoint = displayTransformation.ToMapPoint(ulx, uly)

    'get the lower right coord
    Dim lrx, lry As Integer
    lrx = windowX + Convert.ToInt32(iconWidth / 2.0)
    lry = windowY + Convert.ToInt32(iconHeight / 2.0)
    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
    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

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

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

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

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

  Private Function GetSymbolSize() As Integer
    If Nothing Is m_symbolTable OrElse 0 = m_symbolTable.Rows.Count Then
      Return -1
    End If

    Dim r As DataRow = m_symbolTable.Rows(0)

    Dim symbol As ISymbol = CType(r(2), ISymbol)
    If Nothing Is symbol Then
      Return -1
    End If

    Dim pictureMarkerSymbol As IPictureMarkerSymbol = CType(symbol, IPictureMarkerSymbol)
    Return Convert.ToInt32(pictureMarkerSymbol.Size)
  End Function

  Public Property SymbolSize() As Integer
    Set(ByVal value As Integer)
      SetSymbolSize(value)
    End Set
    Get
      Return GetSymbolSize()
    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
    'in case that it does not exists set the layre's spatial reference
    If Nothing Is MyBase.m_spatialRef Then
      MyBase.m_spatialRef = CreateGeographicSpatialReference()
    End If

    'iterate through all the items in the layers DB and get the bounding envelope
    Dim env As IEnvelope = New EnvelopeClass()
    env.SpatialReference = MyBase.m_spatialRef
    Dim point As IPoint = New PointClass()
    point.SpatialReference = m_spatialRef
    For Each r As DataRow In m_table.Rows
      point.Y = Convert.ToDouble(r(3))
      point.X = Convert.ToDouble(r(4))

      env.Union(point.Envelope)
    Next r

    '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 calles 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

      'add columns to the table  in addition to the default 'ID' and 'Geometry'  
      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(2).Unique = True

      ' set the ZIPCODE primary key for the table
      m_table.PrimaryKey = New DataColumn() {m_table.Columns("ZIPCODE")}

    Else 'in case that the local cache exists, simply load the tables from the cache.
      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

      'displose 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

    'set the ID column to be auto increment
    m_symbolTable.Columns(0).AutoIncrement = True
    m_symbolTable.Columns(0).ReadOnly = True

    m_symbolTable.Columns(1).AllowDBNull = False

    'set ICONID as the primary key for the table
    m_symbolTable.PrimaryKey = New DataColumn() {m_symbolTable.Columns("ICONID")}
  End Sub

  ''' <summary>
  ''' Initialize the location table. Gets the location from a featureclass
  ''' </summary>
  Private Sub InitializeLocations()
    'ceate 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\ArcGIS")
    Dim path As String = Convert.ToString(key.GetValue("InstallDir"))

    If (Not System.IO.File.Exists(System.IO.Path.Combine(path, "DeveloperKit\SamplesNET\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 ShapefileWorkspaceFactory()
    Dim ws As IWorkspace = wf.OpenFromFile(System.IO.Path.Combine(path, "DeveloperKit\SamplesNET\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 = 31
    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 = 31

    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 whie waiting
  ''' for response. Therefor, running the request on a different thread frees the application to 
  ''' continue working while waiting for a response. 
  ''' Please note that in this case, synchronization of shared resources must be addressed,
  ''' otherwise you might end up getting unexpected results.</remarks>
  Private 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()

      '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 returnes its size
  ''' </summary>
  ''' <param name="iconPath"></param>
  ''' <param name="width"></param>
  ''' <param name="height"></param>
  Private Sub DownloadIcon(ByVal iconPath As String, <System.Runtime.InteropServices.Out()> ByRef width As Integer, <System.Runtime.InteropServices.Out()> ByRef height As Integer)
    '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 b As Bitmap
    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)
          b = New Bitmap(stream, True)

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

          'get the bitmap's dimensions
          width = b.Width
          height = b.Height
        End Using
      End Using
    Else
      'get the bitmap's dimensions
      b = New Bitmap(iconFileName)
      width = b.Width
      height = b.Height
    End If
  End Sub

  ''' <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

    '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)
      If Nothing Is symbol Then
        Return Nothing
      End If

      'update the symbol table
      r(2) = symbol
      r(3) = iconWidth
      r(4) = iconHeight
      SyncLock m_symbolTable
        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)
        If Nothing Is symbol Then
          Return Nothing
        End If

        'update the symbol table
        r(2) = symbol
        SyncLock m_symbolTable
          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

  ''' <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) As ISymbol
    iconWidth = 0
    iconHeight = 0
    Try
      'make sure that the icon exit on dist or else download it
      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 = 28
      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 Latitude As Double, ByVal Longitude As Double)
    Try
      Dim cityName As String
      Dim lat, lon As Double
      Dim temp As Integer
      Dim condition As String
      Dim desc As String
      Dim iconPath As String
      Dim day As String
      Dim currentDate 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

      '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 Latitude = 0.0 AndAlso Longitude = 0.0 Then
        'in cae that the caller did not specify a coordinate, get the default coordinate from the service
        lat = Convert.ToDouble(doc.DocumentElement.SelectSingleNode("/rss/channel/item/geo:lat", xmlnsManager).InnerXml)
        lon = Convert.ToDouble(doc.DocumentElement.SelectSingleNode("/rss/channel/item/geo:long", xmlnsManager).InnerXml)
      Else
        lat = Latitude
        lon = Longitude
      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
      currentDate = 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
          DownloadIcon(iconPath, iconWidth, iconHeight)

          'ceate a new record
          tr = m_symbolTable.NewRow()
          tr(1) = iconCode
          tr(3) = iconWidth
          tr(4) = iconHeight

          'update the symbol table. The initialization of the symbol cannot take place in here, since
          'this code gets executed on a backround thread.
          SyncLock m_symbolTable
            m_symbolTable.Rows.Add(tr)
          End SyncLock
        Else 'get the icon's dimensions from the table
          'get the icon's dimentions from the table
          iconWidth = Convert.ToInt32(tr(3))
          iconHeight = Convert.ToInt32(tr(4))
        End If
      Else
        iconPath = ""
      End If

      'test whether the record already exists in the layer's table.
      Dim dbr As DataRow = m_table.Rows.Find(zipCode)
      If Nothing Is dbr Then 'in case that the recored 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

        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) = currentDate
        dbr(11) = low
        dbr(12) = high
        dbr(13) = False
        dbr(14) = DateTime.Now

        'add the item to the table
        SyncLock m_table
          m_table.Rows.Add(dbr)
        End SyncLock
      Else 'in case that the record exists, just update it
        dbr(5) = temp
        dbr(6) = condition
        dbr(7) = iconPath
        dbr(8) = iconCode
        dbr(9) = day
        dbr(10) = currentDate
        dbr(11) = low
        dbr(12) = high
        dbr(14) = DateTime.Now

        'update the record
        SyncLock m_table
          dbr.AcceptChanges()
        End SyncLock
      End If

      '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, lat, lon, 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 ggeometry 
    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_layerSpatialRef AndAlso Not Nothing Is inEnv.SpatialReference Then
      inEnv.Project(m_spatialRef)
    End If

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

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

    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

#End Region
End Class