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

Imports Microsoft.VisualBasic
Imports System
Imports System.IO
Imports System.Drawing
Imports System.Xml
Imports System.Xml.XPath
Imports System.Threading
Imports System.Windows.Forms
Imports System.Collections.Generic
Imports System.Runtime.InteropServices
Imports ESRI.ArcGIS.ADF.BaseClasses
Imports ESRI.ArcGIS.ADF.CATIDs
Imports ESRI.ArcGIS.Controls
Imports ESRI.ArcGIS.Geometry
Imports ESRI.ArcGIS.Carto
Imports ESRI.ArcGIS.Display
Imports ESRI.ArcGIS.esriSystem

  ''' <summary>
  ''' Summary description for DynamicBikingCmd.
  ''' </summary>
  <Guid("f01054d2-0130-4124-8436-1bf2942bf2b6"), ClassInterface(ClassInterfaceType.None), ProgId("DynamicBikingCmd")> _
  Public NotInheritable Class DynamicBikingCmd : Inherits BaseCommand
  #Region "COM Registration Function(s)"
  <ComRegisterFunction(), ComVisible(False)> _
  Private Shared Sub RegisterFunction(ByVal registerType As Type)
    ' Required for ArcGIS Component Category Registrar support
    ArcGISCategoryRegistration(registerType)

    '
    ' TODO: Add any COM registration code here
    '
  End Sub

  <ComUnregisterFunction(), ComVisible(False)> _
  Private Shared Sub UnregisterFunction(ByVal registerType As Type)
    ' Required for ArcGIS Component Category Registrar support
    ArcGISCategoryUnregistration(registerType)

    '
    ' TODO: Add any COM unregistration code here
    '
  End Sub

  #Region "ArcGIS Component Category Registrar generated code"
  ''' <summary>
  ''' Required method for ArcGIS Component Category registration -
  ''' Do not modify the contents of this method with the code editor.
  ''' </summary>
  Private Shared Sub ArcGISCategoryRegistration(ByVal registerType As Type)
    Dim regKey As String = String.Format("HKEY_CLASSES_ROOT\CLSID\{{{0}}}", registerType.GUID)
    ControlsCommands.Register(regKey)

  End Sub
  ''' <summary>
  ''' Required method for ArcGIS Component Category unregistration -
  ''' Do not modify the contents of this method with the code editor.
  ''' </summary>
  Private Shared Sub ArcGISCategoryUnregistration(ByVal registerType As Type)
    Dim regKey As String = String.Format("HKEY_CLASSES_ROOT\CLSID\{{{0}}}", registerType.GUID)
    ControlsCommands.Unregister(regKey)

  End Sub

  #End Region
  #End Region

  #Region "class members"
  Private Enum GPSPlaybackFormat
    HST = 0
    GPX = 1
    XML = 2
  End Enum
  Private m_playbackFormat As GPSPlaybackFormat = GPSPlaybackFormat.GPX
  Private m_hookHelper As IHookHelper
  Private m_dynamicMap As IDynamicMap = Nothing
  Private m_activeView As IActiveView = Nothing
  Private m_bConnected As Boolean = False

  Private m_gpsPosition As IPoint = Nothing
  Private m_additionalInfoPoint As IPoint = Nothing
  Private m_bikeRouteGeometry As IPointCollection4 = Nothing
  Private m_geometryBridge As IGeometryBridge2 = Nothing
  Private m_wksPoints As WKSPoint() = New WKSPoint(0){}
  Private m_wksPrevPosition As WKSPoint

  Private m_dynamicSymbolProperties As IDynamicSymbolProperties2 = Nothing
  Private m_dynamicCompoundMarker As IDynamicCompoundMarker2 = Nothing
  Private m_dynamicScreenDisplay As IDynamicScreenDisplay = Nothing
  Private m_bikeGlyph As IDynamicGlyph = Nothing
  Private m_bikeRouteGlyph As IDynamicGlyph = Nothing
  Private m_textGlyph As IDynamicGlyph = Nothing
  Private m_catGlyph As IDynamicGlyph = Nothing
  Private m_gpsGlyph As IDynamicGlyph = Nothing
  Private m_heartRateGlyph As IDynamicGlyph()
  Private m_gpsSymbolScale As Single = 1.0f
  Private m_heading As Double = 0
  Private m_heartRateString As String = String.Empty
  Private m_altitudeString As String = String.Empty
  Private m_speed As String = String.Empty
  Private m_bOnce As Boolean = True
  Private m_heartRateCounter As Integer = 0
  Private m_drawCycles As Integer = 0
  Public m_playbackSpeed As Integer = 10
  Private m_bTrackMode As Boolean = False
  Private nullString As String() = Nothing

  Private m_xmlPath As String = String.Empty
  ' xml loader thread stuff
  Private m_dataLoaderThread As Thread = Nothing
  Private Shared m_autoEvent As AutoResetEvent = New AutoResetEvent(False)
  Private m_bikePositionCount As Integer = 0

  Private NotInheritable Class BikePositionInfo
    Public Sub New()

    End Sub

    Public position As WKSPoint
    Public time As DateTime
    Public altitudeMeters As Double
    Public heartRate As Integer
    Public lapCount As Integer
    Public lapAverageHeartRate As Integer
    Public lapMaximumHeartRate As Integer
    Public lapCalories As Integer
    Public lapMaximumSpeed As Double
    Public lapDistanceMeters As Double
    Public course As Double
    Public speed As Double
    Public positionCount As Integer
  End Class

  Private m_bikePositionInfo As BikePositionInfo = Nothing

  Private Structure XmlDocTaksData
    Public xmlDocPath As String
  End Structure

  #End Region

  #Region "class constructor"
  Public Sub New()
    MyBase.m_category = ".NET Samples"
    MyBase.m_caption = "Dynamic Biking"
    MyBase.m_message = "Dynamic Biking"
    MyBase.m_toolTip = "Dynamic Biking"
    MyBase.m_name = "DynamicBikingCmd"

    Try
      Dim bitmapResourceName As String = Me.GetType().Name & ".bmp"
      MyBase.m_bitmap = New Bitmap(Me.GetType(), bitmapResourceName)
    Catch ex As Exception
      System.Diagnostics.Trace.WriteLine(ex.Message, "Invalid Bitmap.")
    End Try
  End Sub

  Protected Overrides Sub Finalize()
    If Not m_dataLoaderThread Is Nothing AndAlso m_dataLoaderThread.ThreadState = ThreadState.Running Then
      m_autoEvent.Set()
      m_dataLoaderThread.Join()
    End If
  End Sub
  #End Region

#Region "Overridden Class Methods"

    ''' <summary>
    ''' Occurs when this command is created
    ''' </summary>
    ''' <param name="hook">Instance of the application</param>
    Public Overrides Sub OnCreate(ByVal hook As Object)
        If hook Is Nothing Then
            Return
        End If

        If m_hookHelper Is Nothing Then
            m_hookHelper = New HookHelperClass()
        End If

        m_hookHelper.Hook = hook

        m_activeView = m_hookHelper.ActiveView

        m_geometryBridge = New GeometryEnvironmentClass()

        m_wksPrevPosition.X = 0
        m_wksPrevPosition.Y = 0
    End Sub

    ''' <summary>
    ''' Occurs when this command is clicked
    ''' </summary>
    Public Overrides Sub OnClick()
        m_dynamicMap = TryCast(m_hookHelper.FocusMap, IDynamicMap)
        If m_dynamicMap Is Nothing Then
            Return
        End If

        If (Not m_dynamicMap.DynamicMapEnabled) Then
            MessageBox.Show("Please enable dynamic mode and try again.")
            Return
        End If

        If (Not m_bConnected) Then
            m_xmlPath = GetPlaybackXmlPath()
            If m_xmlPath = String.Empty Then
                Return
            End If

            m_bikePositionInfo = New BikePositionInfo()
            m_bikePositionInfo.positionCount = m_bikePositionCount
            m_bikePositionInfo.altitudeMeters = 0
            m_bikePositionInfo.time = DateTime.Now
            m_bikePositionInfo.position.X = 0
            m_bikePositionInfo.position.Y = 0
            m_bikePositionInfo.heartRate = 0
            m_bikePositionInfo.lapCount = 0
            m_bikePositionInfo.lapAverageHeartRate = 0
            m_bikePositionInfo.lapMaximumHeartRate = 0
            m_bikePositionInfo.lapDistanceMeters = 0
            m_bikePositionInfo.lapMaximumSpeed = 0
            m_bikePositionInfo.lapCalories = 0

            m_gpsPosition = New PointClass()
            m_additionalInfoPoint = New PointClass()
            m_additionalInfoPoint.PutCoords(70, 90)
            m_bikeRouteGeometry = New PolylineClass()

            ' wire dynamic map events
            AddHandler (CType(m_dynamicMap, IDynamicMapEvents_Event)).AfterDynamicDraw, AddressOf OnAfterDynamicDraw
            AddHandler (CType(m_dynamicMap, IDynamicMapEvents_Event)).DynamicMapStarted, AddressOf OnDynamicMapStarted

            ' spin the thread that plays the data from the xml file
            m_dataLoaderThread = New Thread(New ParameterizedThreadStart(AddressOf XmlReaderTask))

            Dim taskData As XmlDocTaksData
            taskData.xmlDocPath = m_xmlPath
            m_dataLoaderThread.Start(taskData)
        Else
            ' unwire wire dynamic map events
            RemoveHandler (CType(m_dynamicMap, IDynamicMapEvents_Event)).AfterDynamicDraw, AddressOf OnAfterDynamicDraw
            RemoveHandler (CType(m_dynamicMap, IDynamicMapEvents_Event)).DynamicMapStarted, AddressOf OnDynamicMapStarted

            ' force the bike xml playback thread to quite
            m_autoEvent.Set()
            m_dataLoaderThread.Join()

            System.Diagnostics.Trace.WriteLine("Done!!!")
        End If

        m_bConnected = Not m_bConnected
    End Sub

    Public Overrides ReadOnly Property Checked() As Boolean
        Get
            Return m_bConnected
        End Get
    End Property

#End Region

  #Region "public properties"
  Public ReadOnly Property IsPlaying() As Boolean
    Get
      Return m_bConnected
    End Get
  End Property

  Public Property TrackMode() As Boolean
    Get
      Return m_bTrackMode
    End Get
    Set
      m_bTrackMode = Value
    End Set
  End Property

  Public Property PlaybackSpeed() As Integer
    Get
      Return m_playbackSpeed
    End Get
    Set
      m_playbackSpeed = Value
    End Set
  End Property
  #End Region

  #Region "Private methods"
  Private Sub OnAfterDynamicDraw(ByVal DynamicMapDrawPhase As esriDynamicMapDrawPhase, ByVal Display As IDisplay, ByVal dynamicDisplay As IDynamicDisplay)
    
    if (DynamicMapDrawPhase <> esriDynamicMapDrawPhase.esriDMDPDynamicLayers) then
      return
    End if

    ' initialize symbology for dynamic drawing
    If m_bOnce Then
      ' create the glyphs for the bike position as well as for the route
      Dim dynamicGlyphFactory As IDynamicGlyphFactory2 = TryCast(dynamicDisplay.DynamicGlyphFactory, IDynamicGlyphFactory2)
            Dim whiteTransparentColor As IColor = CType(ESRI.ArcGIS.ADF.Connection.Local.Converter.ToRGBColor(Color.FromArgb(255, 255, 255)), IColor)

      Dim bitmap As Bitmap = New Bitmap(Me.GetType(), "bicycle-icon.bmp")
      m_bikeGlyph = dynamicGlyphFactory.CreateDynamicGlyphFromBitmap(esriDynamicGlyphType.esriDGlyphMarker, bitmap.GetHbitmap().ToInt32(), False, whiteTransparentColor)

      bitmap = New Bitmap(Me.GetType(), "cat.bmp")
      m_catGlyph = dynamicGlyphFactory.CreateDynamicGlyphFromBitmap(esriDynamicGlyphType.esriDGlyphMarker, bitmap.GetHbitmap().ToInt32(), False, whiteTransparentColor)

      bitmap = New Bitmap(Me.GetType(), "gps.png")
      m_gpsGlyph = dynamicGlyphFactory.CreateDynamicGlyphFromBitmap(esriDynamicGlyphType.esriDGlyphMarker, bitmap.GetHbitmap().ToInt32(), False, whiteTransparentColor)

      Dim routeSymbol As ISymbol = CreateBikeRouteSymbol()
      m_bikeRouteGlyph = dynamicGlyphFactory.CreateDynamicGlyph(routeSymbol)

      ' create the heart rate glyphs series
      CreateHeartRateAnimationGlyphs(dynamicGlyphFactory)

      ' get the default internal text glyph
      m_textGlyph = dynamicGlyphFactory.DynamicGlyph(1, esriDynamicGlyphType.esriDGlyphText, 1)

      ' do one time casting
      m_dynamicSymbolProperties = TryCast(dynamicDisplay, IDynamicSymbolProperties2)
      m_dynamicCompoundMarker = TryCast(dynamicDisplay, IDynamicCompoundMarker2)
      m_dynamicScreenDisplay = TryCast(dynamicDisplay, IDynamicScreenDisplay)

      m_bOnce = False
    End If

    ' draw the trail
    m_dynamicSymbolProperties.DynamicGlyph(esriDynamicSymbolType.esriDSymbolLine)= m_bikeRouteGlyph
    m_dynamicSymbolProperties.SetColor(esriDynamicSymbolType.esriDSymbolLine, 1.0f, 1.0f, 1.0f, 1.0f)
    m_dynamicSymbolProperties.SetScale(esriDynamicSymbolType.esriDSymbolLine, 1.0f, 1.0f)
    m_dynamicSymbolProperties.LineContinuePattern = True
    dynamicDisplay.DrawPolyline(m_bikeRouteGeometry)

    If m_playbackFormat = GPSPlaybackFormat.HST Then
      ' adjust the bike lap additional info point to draw at the top left corner of the window
      m_additionalInfoPoint.Y = Display.DisplayTransformation.DeviceFrame().bottom - 70

            ' draw additional lap information
      DrawLapInfo(dynamicDisplay)

      ' draw the heart-rate and altitude
      DrawHeartRateAnimation(dynamicDisplay, m_gpsPosition)

      ' draw the current position as a marker glyph
      m_dynamicSymbolProperties.DynamicGlyph(esriDynamicSymbolType.esriDSymbolMarker) = m_bikeGlyph
      m_dynamicSymbolProperties.SetColor(esriDynamicSymbolType.esriDSymbolMarker, 1.0F, 1.0F, 1.0F, 1.0F)
      m_dynamicSymbolProperties.SetScale(esriDynamicSymbolType.esriDSymbolMarker, 1.2F, 1.2F)
      m_dynamicSymbolProperties.RotationAlignment(esriDynamicSymbolType.esriDSymbolMarker) = esriDynamicSymbolRotationAlignment.esriDSRANorth
      m_dynamicSymbolProperties.Heading(esriDynamicSymbolType.esriDSymbolMarker) = CSng(m_heading - 90)
      dynamicDisplay.DrawMarker(m_gpsPosition)
    Else
        DrawGPSInfo(dynamicDisplay, m_gpsPosition)
    End If

  End Sub

  Private Sub OnDynamicMapStarted(ByVal Display As IDisplay, ByVal dynamicDisplay As IDynamicDisplay)
    SyncLock m_bikePositionInfo
    ' update the bike position
    If m_bikePositionInfo.positionCount <> m_bikePositionCount Then
      ' update the geometry
      m_gpsPosition.PutCoords(m_bikePositionInfo.position.X, m_bikePositionInfo.position.Y)

      ' check if needed to update the map extent
      If m_bTrackMode Then
        Dim mapExtent As IEnvelope = m_hookHelper.ActiveView.ScreenDisplay.DisplayTransformation.FittedBounds
        mapExtent.CenterAt(m_gpsPosition)
        m_hookHelper.ActiveView.ScreenDisplay.DisplayTransformation.VisibleBounds = mapExtent
      End If

      ' update the bike trail
      m_wksPoints(0) = m_bikePositionInfo.position
      m_geometryBridge.AddWKSPoints(m_bikeRouteGeometry, m_wksPoints)

      ' get the GPS altitude reading
      m_altitudeString = String.Format("Altitude: {0} m", m_bikePositionInfo.altitudeMeters.ToString("####.#"))

      If m_playbackFormat = GPSPlaybackFormat.HST Then
        ' calculate the heading
        m_heading = Math.Atan2(m_bikePositionInfo.position.X - m_wksPrevPosition.X, m_bikePositionInfo.position.Y - m_wksPrevPosition.Y)
        m_heading *= (180 / Math.PI)
        If m_heading < 0 Then
          m_heading += 360
        End If

        m_heartRateString = String.Format("{0} BPM", m_bikePositionInfo.heartRate)
      Else
        m_heading = m_bikePositionInfo.course
        m_speed = String.Format("Speed: {0} MPH", m_bikePositionInfo.speed.ToString("###.#"))
      End If

      m_wksPrevPosition.X = m_bikePositionInfo.position.X
      m_wksPrevPosition.Y = m_bikePositionInfo.position.Y
      m_bikePositionCount = m_bikePositionInfo.positionCount
    End If
    End SyncLock

    ' explicitly call refresh in order to make the dynamic display fire AfterDynamicDraw event 
    m_hookHelper.ActiveView.Refresh()
  End Sub

  Private Sub XmlReaderTask(ByVal data As Object)
    Dim bFirst As Boolean = True
    Dim nextTime As DateTime = DateTime.Now
    Dim currentTime As DateTime = DateTime.Now
    Dim lat As Double = 0
    Dim lon As Double = 0
    Dim altitude As Double = 0
    Dim course As Double = 0
    Dim speed As Double = 0
    Dim heartRate As Integer = 0
    Dim trackPoint As XmlNode = Nothing
    Dim nextTrackPointTimeNode As XmlNode = Nothing

    Dim taskData As XmlDocTaksData = CType(data, XmlDocTaksData)

    Dim bikeDataDoc As XmlDocument = New XmlDocument()
    Dim xmlTextReader As XmlTextReader = New XmlTextReader(m_xmlPath)
    bikeDataDoc.Load(xmlTextReader)

    Dim rootElement As XmlElement = bikeDataDoc.DocumentElement

    If m_playbackFormat = GPSPlaybackFormat.HST Then
      Dim laps As XmlNodeList = rootElement.GetElementsByTagName("Lap")
      For Each lap As XmlNode In laps
        ' get lap average and maximum heart rate
        Dim averageHeartRate As XmlNode = (CType(lap, XmlElement)).GetElementsByTagName("AverageHeartRateBpm")(0)
        Dim maximumHeartRate As XmlNode = (CType(lap, XmlElement)).GetElementsByTagName("MaximumHeartRateBpm")(0)
        Dim calories As XmlNode = (CType(lap, XmlElement)).GetElementsByTagName("Calories")(0)
        Dim maxSpeed As XmlNode = (CType(lap, XmlElement)).GetElementsByTagName("MaximumSpeed")(0)
        Dim distance As XmlNode = (CType(lap, XmlElement)).GetElementsByTagName("DistanceMeters")(0)

        ' update the position info
        SyncLock m_bikePositionInfo
          m_bikePositionInfo.lapCount += 1
          m_bikePositionInfo.lapAverageHeartRate = Convert.ToInt32(averageHeartRate.InnerText)
          m_bikePositionInfo.lapMaximumHeartRate = Convert.ToInt32(maximumHeartRate.InnerText)
          m_bikePositionInfo.lapCalories = Convert.ToInt32(calories.InnerText)
          m_bikePositionInfo.lapMaximumSpeed = Convert.ToDouble(maxSpeed.InnerText)
          m_bikePositionInfo.lapDistanceMeters = Convert.ToDouble(distance.InnerText)
        End SyncLock

        Dim tracks As XmlNodeList = (CType(lap, XmlElement)).GetElementsByTagName("Track")
        For Each track As XmlNode In tracks
          Dim trackpoints As XmlNodeList = (CType(track, XmlElement)).GetElementsByTagName("Trackpoint")
                    ' read each time one track point ahead in order to calculate the lag time between 
                    ' the current track point and the next one. This time will be used as the wait time
          ' for the AutoResetEvent.
          For Each nextTrackPoint As XmlNode In trackpoints
            Dim bNextTime As Boolean = False
            Dim bTime As Boolean = False
            Dim bPosition As Boolean = False
            Dim bAltitude As Boolean = False
            Dim bHeartRate As Boolean = False

            ' if this is the first node in the first track make it current
            If bFirst Then
              trackPoint = nextTrackPoint
              bFirst = False
              Continue For
            End If

            ' get the time from the next point in order to calculate the lag time
            nextTrackPointTimeNode = (CType(nextTrackPoint, XmlElement)).GetElementsByTagName("Time")(0)
            If nextTrackPointTimeNode Is Nothing Then
              Continue For
            Else
              nextTime = Convert.ToDateTime(nextTrackPointTimeNode.InnerText)
              bNextTime = True
            End If

            If trackPoint.ChildNodes.Count = 4 Then
              For Each trackPointNode As XmlNode In trackPoint.ChildNodes

                If trackPointNode.Name = "Time" Then
                  currentTime = Convert.ToDateTime(trackPointNode.InnerText)
                  bTime = True
                ElseIf trackPointNode.Name = "Position" Then
                  Dim latNode As XmlNode = trackPointNode("LatitudeDegrees")
                  lat = Convert.ToDouble(latNode.InnerText)

                  Dim lonNode As XmlNode = trackPointNode("LongitudeDegrees")
                  lon = Convert.ToDouble(lonNode.InnerText)

                  bPosition = True
                ElseIf trackPointNode.Name = "AltitudeMeters" Then
                  altitude = Convert.ToDouble(trackPointNode.InnerText)
                  bAltitude = True
                ElseIf trackPointNode.Name = "HeartRateBpm" Then
                  heartRate = Convert.ToInt32(trackPointNode.InnerText)
                  bHeartRate = True
                End If
              Next trackPointNode

              If bNextTime AndAlso bTime AndAlso bPosition AndAlso bAltitude AndAlso bHeartRate Then

                Dim ts As TimeSpan = nextTime.Subtract(currentTime)

                SyncLock m_bikePositionInfo
                  m_bikePositionInfo.position.X = lon
                  m_bikePositionInfo.position.Y = lat
                  m_bikePositionInfo.altitudeMeters = altitude
                  m_bikePositionInfo.heartRate = heartRate
                  m_bikePositionInfo.time = currentTime
                  m_bikePositionInfo.positionCount += 1
                End SyncLock

                ' wait for the duration of the time span or bail out if the thread was signaled
                If m_autoEvent.WaitOne(CInt(ts.TotalMilliseconds / m_playbackSpeed), True) Then
                  Return
                End If
              End If
            End If

            ' make the next track point current
            trackPoint = nextTrackPoint
          Next nextTrackPoint
        Next track
      Next lap
    Else ' GPX
      Dim trackpoints As XmlNodeList = bikeDataDoc.DocumentElement.GetElementsByTagName("trkpt")

            ' read each time one track point ahead in order to calculate the lag time between 
            ' the current track point and the next one. This time will be used as the wait time
      ' for the AutoResetEvent.
      For Each nextTrackPoint As XmlNode In trackpoints
        ' if this is the first node in the first track make it current
        If bFirst Then
          trackPoint = nextTrackPoint
          bFirst = False
          Continue For
        End If

        ' get the time from the next point in order to calculate the lag time
        nextTrackPointTimeNode = (CType(nextTrackPoint, XmlElement)).GetElementsByTagName("time")(0)
        If nextTrackPointTimeNode Is Nothing Then
          Continue For
        Else
          nextTime = Convert.ToDateTime(nextTrackPointTimeNode.InnerText)
        End If

        lat = Convert.ToDouble(trackPoint.Attributes("lat").InnerText)
        lon = Convert.ToDouble(trackPoint.Attributes("lon").InnerText)

        For Each trackPointNode As XmlNode In trackPoint.ChildNodes
          If trackPointNode.Name = "time" Then
            currentTime = Convert.ToDateTime(trackPointNode.InnerText)
          ElseIf trackPointNode.Name = "ele" Then
            altitude = Convert.ToDouble(trackPointNode.InnerText)
          ElseIf trackPointNode.Name = "course" Then
            course = Convert.ToDouble(trackPointNode.InnerText)
          ElseIf trackPointNode.Name = "speed" Then
            speed = Convert.ToDouble(trackPointNode.InnerText)
          End If
        Next trackPointNode

        Dim ts As TimeSpan = nextTime.Subtract(currentTime)

        SyncLock m_bikePositionInfo
          m_bikePositionInfo.position.X = lon
          m_bikePositionInfo.position.Y = lat
          m_bikePositionInfo.altitudeMeters = altitude
          m_bikePositionInfo.time = currentTime
          m_bikePositionInfo.course = course
          m_bikePositionInfo.speed = speed
          m_bikePositionInfo.positionCount += 1
        End SyncLock

        ' wait for the duration of the time span or bail out if the thread was signaled
        If m_autoEvent.WaitOne(CInt(ts.TotalMilliseconds / m_playbackSpeed), True) Then
          Return
        End If

        ' make the next track point current
        trackPoint = nextTrackPoint
      Next nextTrackPoint
    End If

    ' close the reader when done
    xmlTextReader.Close()
  End Sub

  Private Function CreateBikeRouteSymbol() As ISymbol
        Dim newColor As IColor = CType(ESRI.ArcGIS.ADF.Connection.Local.Converter.ToRGBColor(Color.FromArgb(0, 90, 250)), IColor)
    Dim charMarkerSymbol As ICharacterMarkerSymbol = New CharacterMarkerSymbolClass()
    charMarkerSymbol.Color = newColor
        charMarkerSymbol.Font = ESRI.ArcGIS.ADF.Connection.Local.Converter.ToStdFont(New Font("ESRI Default Marker", 17.0F, FontStyle.Bold))
    charMarkerSymbol.CharacterIndex = 189
    charMarkerSymbol.Size = 17

    Dim markerLineSymbol As IMarkerLineSymbol = New MarkerLineSymbolClass()
    markerLineSymbol.Color = newColor
    markerLineSymbol.Width = 17.0
    markerLineSymbol.MarkerSymbol = CType(charMarkerSymbol, IMarkerSymbol)

    ' Makes a new Cartographic Line symbol and sets its properties
    Dim cartographicLineSymbol As ICartographicLineSymbol = TryCast(markerLineSymbol, ICartographicLineSymbol)

    ' In order to set additional properties like offsets and dash patterns we must create an ILineProperties object
    Dim lineProperties As ILineProperties = TryCast(cartographicLineSymbol, ILineProperties)
    lineProperties.Offset = 0

    ' Here's how to do a template for the pattern of marks and gaps
    Dim hpe As Double() = New Double(3){}
    hpe(0) = 0
    hpe(1) = 39
    hpe(2) = 1
    hpe(3) = 0

    Dim template As ITemplate = New TemplateClass()
    template.Interval = 1
    Dim i As Integer = 0
    Do While i < hpe.Length
      template.AddPatternElement(hpe(i), hpe(i + 1))
      i = i + 2
    Loop
    lineProperties.Template = template

    ' Set the basic and cartographic line properties
    cartographicLineSymbol.Color = newColor

        newColor = CType(ESRI.ArcGIS.ADF.Connection.Local.Converter.ToRGBColor(Color.FromArgb(0, 220, 100)), IColor)

    ' create a simple line
    Dim simpleLineSymbol As ISimpleLineSymbol = New SimpleLineSymbolClass()
    simpleLineSymbol.Color = newColor
    simpleLineSymbol.Style = esriSimpleLineStyle.esriSLSSolid
    simpleLineSymbol.Width = 1.2

    Dim multiLayerLineSymbol As IMultiLayerLineSymbol = New MultiLayerLineSymbolClass()
    multiLayerLineSymbol.AddLayer(CType(cartographicLineSymbol, ILineSymbol))
    multiLayerLineSymbol.AddLayer(CType(simpleLineSymbol, ILineSymbol))

    Return TryCast(multiLayerLineSymbol, ISymbol)
  End Function

  Private Sub CreateHeartRateAnimationGlyphs(ByVal dynamicGlyphFactory As IDynamicGlyphFactory2)
        Dim whiteTransparentColor As IColor = CType(ESRI.ArcGIS.ADF.Connection.Local.Converter.ToRGBColor(Color.FromArgb(255, 255, 255)), IColor)

    m_heartRateGlyph = New IDynamicGlyph(4){}
    Dim heartRateIcon As String
    Dim heartIconBaseName As String = "valentine-heart"
    Dim bitmap As Bitmap = Nothing
    Dim imagesize As Integer = 16
    For i As Integer = 0 To 4
      heartRateIcon = heartIconBaseName & imagesize & ".bmp"
      bitmap = New Bitmap(Me.GetType(), heartRateIcon)
      m_heartRateGlyph(i) = dynamicGlyphFactory.CreateDynamicGlyphFromBitmap(esriDynamicGlyphType.esriDGlyphMarker, bitmap.GetHbitmap().ToInt32(), False, whiteTransparentColor)
      m_heartRateGlyph(i).SetAnchor(20.0F, -40.0F)
      imagesize += 2
    Next i
  End Sub

  Private Sub DrawHeartRateAnimation(ByVal dynamicDisplay As IDynamicDisplay, ByVal bikePoint As IPoint)
    m_dynamicSymbolProperties.DynamicGlyph(esriDynamicSymbolType.esriDSymbolMarker)= m_heartRateGlyph(m_heartRateCounter)
    m_dynamicSymbolProperties.SetColor(esriDynamicSymbolType.esriDSymbolMarker, 1.0f, 1.0f, 1.0f, 1.0f)
    m_dynamicSymbolProperties.SetScale(esriDynamicSymbolType.esriDSymbolMarker, 1.0f, 1.0f)
    m_dynamicSymbolProperties.RotationAlignment(esriDynamicSymbolType.esriDSymbolMarker)= esriDynamicSymbolRotationAlignment.esriDSRAScreen
    m_dynamicSymbolProperties.Heading(esriDynamicSymbolType.esriDSymbolMarker)= 0.0f
    dynamicDisplay.DrawMarker(bikePoint)

    m_textGlyph.SetAnchor(-35.0f, -50.0f)
    m_dynamicSymbolProperties.DynamicGlyph(esriDynamicSymbolType.esriDSymbolText)= m_textGlyph
    m_dynamicSymbolProperties.TextBoxUseDynamicFillSymbol = True
    m_dynamicSymbolProperties.SetColor(esriDynamicSymbolType.esriDSymbolText, 0.0f, 0.8f, 0.0f, 1.0f)
    m_dynamicSymbolProperties.SetColor(esriDynamicSymbolType.esriDSymbolFill, 0.0f, 0.0f, 0.0f, 1.0f)
    m_dynamicSymbolProperties.TextBoxHorizontalAlignment = esriTextHorizontalAlignment.esriTHALeft
    dynamicDisplay.DrawText(bikePoint, m_heartRateString)

    m_textGlyph.SetAnchor(-20.0f, -30.0f)
    m_dynamicSymbolProperties.DynamicGlyph(esriDynamicSymbolType.esriDSymbolText)= m_textGlyph
    dynamicDisplay.DrawText(bikePoint, m_altitudeString)

    If m_drawCycles Mod 5 = 0 Then
      m_heartRateCounter += 1

      If m_heartRateCounter > 4 Then
        m_heartRateCounter = 0
      End If
    End If

    m_drawCycles += 1
    If m_drawCycles = 5 Then
      m_drawCycles = 0
    End If
  End Sub

  Private Sub DrawGPSInfo(ByVal dynamicDisplay As IDynamicDisplay, ByVal gpsPosition As IPoint)

    ' altitude is already available
    Dim course As String
    Dim speed As String

    SyncLock m_bikePositionInfo
      course = String.Format("Course {0} DEG", m_bikePositionInfo.course.ToString("###.##"))
      speed = String.Format("Speed {0} MPH", m_bikePositionInfo.speed.ToString("###.##"))
    End SyncLock

    Dim gpsInfo As String = String.Format("{0}" & Constants.vbLf & "{1}" & Constants.vbLf & "{2}", course, speed, m_altitudeString)

    m_textGlyph.SetAnchor(-35.0F, -47.0F)
    m_dynamicSymbolProperties.DynamicGlyph(esriDynamicSymbolType.esriDSymbolText) = m_textGlyph
    m_dynamicSymbolProperties.TextBoxUseDynamicFillSymbol = True
    m_dynamicSymbolProperties.Heading(esriDynamicSymbolType.esriDSymbolText) = 0.0F
    m_dynamicSymbolProperties.SetColor(esriDynamicSymbolType.esriDSymbolText, 0.0F, 0.8F, 0.0F, 1.0F)
    m_dynamicSymbolProperties.SetScale(esriDynamicSymbolType.esriDSymbolText, 1.0F, 1.0F)
    m_dynamicSymbolProperties.SetColor(esriDynamicSymbolType.esriDSymbolFill, 0.0F, 0.0F, 0.0F, 0.6F)
    m_dynamicSymbolProperties.TextBoxHorizontalAlignment = esriTextHorizontalAlignment.esriTHALeft
    dynamicDisplay.DrawText(m_gpsPosition, gpsInfo)

    m_dynamicSymbolProperties.DynamicGlyph(esriDynamicSymbolType.esriDSymbolMarker) = m_gpsGlyph
    m_dynamicSymbolProperties.SetColor(esriDynamicSymbolType.esriDSymbolMarker, 1.0F, 1.0F, 1.0F, 1.0F)
    m_dynamicSymbolProperties.SetScale(esriDynamicSymbolType.esriDSymbolMarker, m_gpsSymbolScale, m_gpsSymbolScale)
    m_dynamicSymbolProperties.RotationAlignment(esriDynamicSymbolType.esriDSymbolMarker) = esriDynamicSymbolRotationAlignment.esriDSRANorth
    m_dynamicSymbolProperties.Heading(esriDynamicSymbolType.esriDSymbolMarker) = CSng(m_heading - 90)
    dynamicDisplay.DrawMarker(m_gpsPosition)

    If m_drawCycles Mod 5 = 0 Then
      ' increment the symbol size
      m_gpsSymbolScale += 0.05F

      If m_gpsSymbolScale > 1.2F Then
        m_gpsSymbolScale = 0.8F
      End If
    End If

    m_drawCycles += 1
    If m_drawCycles = 5 Then
      m_drawCycles = 0
    End If
  End Sub

  Private Sub DrawLapInfo(ByVal dynamicDisplay As IDynamicDisplay)
    Dim lapCount As String
    Dim lapInfo As String
    Dim lapHeartRateInfo As String

    SyncLock m_bikePositionInfo
    lapCount = String.Format("Lap #{0}", m_bikePositionInfo.lapCount)
    lapInfo = String.Format("Lap information:" & Constants.vbLf & "Distance: {0}m" & Constants.vbLf & "Maximum speed - {1}" & Constants.vbLf & "Calories - {2}", m_bikePositionInfo.lapDistanceMeters.ToString("#####.#"), m_bikePositionInfo.lapMaximumSpeed.ToString("###.#"), m_bikePositionInfo.lapCalories)
    lapHeartRateInfo = String.Format("Lap heart rate info:" & Constants.vbLf & "Average - {0}" & Constants.vbLf & "Maximum - {1}", m_bikePositionInfo.lapAverageHeartRate, m_bikePositionInfo.lapMaximumHeartRate)
    End SyncLock

    m_dynamicSymbolProperties.DynamicGlyph(esriDynamicSymbolType.esriDSymbolMarker)= m_catGlyph
    m_dynamicSymbolProperties.SetColor(esriDynamicSymbolType.esriDSymbolMarker, 1.0f, 1.0f, 1.0f, 1.0f)
    m_dynamicSymbolProperties.SetScale(esriDynamicSymbolType.esriDSymbolMarker, 1.0f, 1.0f)
    m_dynamicSymbolProperties.RotationAlignment(esriDynamicSymbolType.esriDSymbolMarker)= esriDynamicSymbolRotationAlignment.esriDSRAScreen
    m_dynamicSymbolProperties.Heading(esriDynamicSymbolType.esriDSymbolMarker)= 0.0f

    m_dynamicSymbolProperties.TextBoxUseDynamicFillSymbol = True
    m_dynamicSymbolProperties.SetColor(esriDynamicSymbolType.esriDSymbolText, 0.0f, 0.8f, 0.0f, 1.0f)
    m_dynamicSymbolProperties.SetColor(esriDynamicSymbolType.esriDSymbolFill, 0.0f, 0.0f, 0.0f, 1.0f)
    m_dynamicSymbolProperties.TextBoxHorizontalAlignment = esriTextHorizontalAlignment.esriTHALeft
    m_textGlyph.SetAnchor(0.0f, 0.0f)
    m_dynamicSymbolProperties.DynamicGlyph(esriDynamicSymbolType.esriDSymbolText)= m_textGlyph
    
    Dim strLapInfo As String() = New String() { lapCount, lapInfo, lapHeartRateInfo }
    m_dynamicCompoundMarker.DrawScreenArrayMarker(m_additionalInfoPoint, nullString, nullString, strLapInfo, nullString, nullString)

  End Sub

  Private Function GetPlaybackXmlPath() As String
    Dim ofd As OpenFileDialog = New OpenFileDialog()
    ofd.CheckFileExists = True
    ofd.Multiselect = False
      ofd.Filter = "HST files (*.hst)|*.hst|GPX files (*.gpx)|*.gpx|XML files (*.xml)|*.xml"
    ofd.RestoreDirectory = True
    ofd.Title = "Input biking log file"

    Dim result As DialogResult = ofd.ShowDialog()
    If result = System.Windows.Forms.DialogResult.OK Then
      Dim fileExtension As String = System.IO.Path.GetExtension(ofd.FileName).ToUpper()
      If fileExtension = ".GPX" Then
        m_playbackFormat = GPSPlaybackFormat.GPX
      ElseIf fileExtension = ".HST" Then
        m_playbackFormat = GPSPlaybackFormat.HST
      ElseIf fileExtension = ".XML" Then
        m_playbackFormat = GPSPlaybackFormat.XML
      End If

      Return ofd.FileName
    End If
    Return String.Empty

  End Function
  #End Region
End Class