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