It’s a Small World, After All, part 2 – VB, C#, and GPS (Matt Gertz)

It’s a Small World, After All, part 2 – VB, C#, and GPS (Matt Gertz)

  • Comments 13

In my previous post, we fixed up a C# GPS library to provide more support, and wrote all of the UI for a VB GPS application, all based on (but modified from) a Mobile GPS sample in the Windows Mobile 6.0 SDK.  In this post, we’ll finish up the app by enabling a bunch of cool functionality not exposed in the original sample.

The C# sample uses a helper function called UpdateData – I will too (as noted above), except that the code is going to deviate significantly.  We’ll start out by checking is the GPS is actually running, just in case we called outside of an event:

    Protected Sub UpdateData(ByVal sender As Object, ByVal args As System.EventArgs)

        If vbgps.Opened Then

 

First, we’ll update the screen with whatever cached device data we have:

            If device IsNot Nothing Then

                Me.Text = "VBGPS: " & device.FriendlyName.ToString

                Me.StatusLabel.Text = device.DeviceState.ToString

            End If

 

This will change the title bar to include the name of the GPS device in use (in case you have multiple devices) and will set the StatusLabel to whatever the state of the GPS is (e.g., “On”).

All other interesting information comes from the cached position object:

            If position IsNot Nothing Then

 

We’ll start with the altitude.  There are two ways of calculating altitude – sea level (self-explanatory), and ellipsoid (which calculates altitude against a “perfect” ellipsoid representation of the otherwise pear-shaped earth).  We’re going to go with ellipsoid.  We’ll need to check if the information for it is reported as being valid, and if so, assigned the value to the appropriate label.  The value is returned to us in meters, and we’ll also show it in feet by multiplying the result by 3.281 feet/meters.  Again, we’ll format the results so that we only have two decimal places:

                If position.EllipsoidAltitudeValid Then

                    Me.AltLabel.Text = CDbl(position.EllipsoidAltitude).ToString("####0.00") _

                     & " m (" & CDbl(position.EllipsoidAltitude * 3.281).ToString("####0.00") _

 & "ft)"

                End If

 

Next is velocity.  The speed value will be in meters per second; We’ll show it in kph and mph.  I’ll also show the heading (if valid), and separate the two by an “@” sign (e.g., “88.00 kph (54.69 mph) @ 45.23°”:

                If position.SpeedValid Then

                    Me.VelLabel.Text = CDbl(position.Speed * 1.852).ToString("###0.00") _

& " kph (" & CDbl(position.Speed * 1.151).ToString("###0.00") & " mph)"

                    If position.HeadingValid Then

                        Me.VelLabel.Text = Me.VelLabel.Text & " @ " _

& position.Heading.ToString("##0.00") & "°"

                    End If

                End If

 

Latitude and longitude are next.  We’ll be showing them in both DMS and DM format, using the methods we wrote in C#.  The only interesting thing here is converting negative values to W or S indicators:

                If position.LatitudeValid Then

                    Me.LatLabel.Text = position.LatitudeInDegreesMinutesSeconds.DMSString() _

& " (" & position.LatitudeInDegreesMinutesSeconds.DMString & ")"

                    If position.Latitude < 0 Then

                        Me.LatLabel.Text = Me.LatLabel.Text & " S"

                    Else

                        Me.LatLabel.Text = Me.LatLabel.Text & " N"

                    End If

                End If

 

                If position.LongitudeValid Then

                    Me.LongLabel.Text = position.LongitudeInDegreesMinutesSeconds.DMSString() _

& " (" & position.LongitudeInDegreesMinutesSeconds.DMString & ")"

                    If position.Longitude < 0 Then

                        Me.LongLabel.Text = Me.LongLabel.Text & " W"

                    Else

                        Me.LongLabel.Text = Me.LongLabel.Text & " E"

                    End If

                End If

 

Satellites are a bit more complicated, and if you do a straight port of the C# sample, you’ll get runtime errors.  This is because the C# code references values which may be NULL even if the values are valid – my VB code checks to see if the values are Nothing before referencing them.  (A value of Nothing is equivalent to zero satellites in this case, since the C# library uses the existence and size of the satellite array to generate a value.) 

There are three satellite components that we need to use – the number of satellites used to create a positioning solution, the number of them in view, and the total count of satellites – the resulting string should look something like (for example) “4/7 (8)”, depending on the actual number of satellites involved.

                If position.SatellitesInSolutionValid AndAlso _

                    position.SatellitesInViewValid AndAlso _

                    position.SatelliteCountValid Then

                    Dim SatSol As Satellite() = position.GetSatellitesInSolution()

                    Dim SatView As Satellite() = position.GetSatellitesInView()

                    Dim SatStr As String

                    If SatSol IsNot Nothing Then

                        SatStr = SatSol.Length & "/"

                    Else

                        SatStr = "0/"

                    End If

                    If SatView IsNot Nothing Then

                        SatStr += SatView.Length & " ("

                    Else

                        SatStr += "0 ("

                    End If

                    Me.SatLabel.Text = SatStr & position.SatelliteCount & ")"

                End If

 

You can actually get more information on each satellite (such as its location in orbit, etc.) simply by exploring the methods and fields on the satellite object, but for now this will do fine for our project.

 Now that we’ve shown the information as to where we are, we need to tell the user how to get to where they are going.  Creating maps and routes is just a bit beyond the scope of this blog, but we can certainly indicate direction and distance.  Let’s assume the existence of a helper function called UpdateTargetInfo at this point – we’ll define it later:

                UpdateTargetInfo()

            End If

        End If

    End Sub

 

Now, you’ll recall that we added fields to allow the user to specify the target point.  We’ll want to translate the users entries into actual lat/long coordinates, and it would also be really cool if we could update the distance and direction in real-time.  There are six fields and two combo boxes that are involved with the target, so let’s create a handler to deal with all of their change events (TextChanged for the text boxes, SelectedIndexChanged for the combo boxes):

    Private Sub Target_TextChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles _

        LatDegTB.TextChanged, LatMinTB.TextChanged, LatSecTB.TextChanged, _

        LongDegTB.TextChanged, LongMinTB.TextChanged, LongSecTB.TextChanged, _

        LatPosCB.SelectedIndexChanged, LongPosCB.SelectedIndexChanged

 

I’m going to use a helper function to collect the data, because collecting latitude data is identical to collecting longitude data, so we might as well treat them generically. The helper method will be called “CacheCoordinate” and we’ll define it later.  As arguments, we’ll pass through the relevant controls and caches, and then update the distance and direction afterwards:

            CacheCoordinate(LatDegTB, LatMinTB, LatSecTB, LatPosCB, TargetLatitude)

            CacheCoordinate(LongDegTB, LongMinTB, LongSecTB, LongPosCB, TargetLongitude)

            UpdateTargetInfo()

 

 Now, the user could enter all sorts of errors accidentally into the text fields, any of which could cause an exception to be thrown.  I could check and recheck for text and oddly-formed numbers being inserted into those fields, but for this exercise, I’m going to go with the moral equivalent of duct tape and simply use a Try block around the code we just wrote.  If there’s an error, I’ll simply tell the user that their entered target information is invalid, and so the distance and direction cannot be determined:

        Try

            CacheCoordinate(LatDegTB, LatMinTB, LatSecTB, LatPosCB, TargetLatitude)

            CacheCoordinate(LongDegTB, LongMinTB, LongSecTB, LongPosCB, TargetLongitude)

            UpdateTargetInfo()

        Catch ex As Exception

            Me.DistLabel.Text = "Invalid target."

            Me.DirLabel.Text = "Invalid target."

            TargetLatitude = Nothing

            TargetLongitude = Nothing

        End Try

    End Sub

 

Now, we’ll start to parse together the data, starting with latitude.  The combo box for it has two values – “N” or “E” (at the 0 position) and “S” or “W”(at the 1 position) – so a simple check on the index will tell us if we’ll be using positive or negative coordinates:

    Private Sub CacheCoordinate(ByVal DegField As TextBox, ByVal MinField As TextBox, _

                                ByVal SecField As TextBox, ByVal PosField As ComboBox, _

  ByRef cache As DegreesMinutesSeconds)

        Dim isPositive As Boolean ' South and west are negative values

        isPositive = (PosField.SelectedIndex = 0)

 

Note that the cache needs to be passed in as ByRef, since we’ll be allocating new memory around it!

So, a user might want to enter data in three ways:  degrees with a decimal portion; degrees and minutes, with minutes having a decimal portion; degrees, minutes, and seconds, with seconds having a decimal portion.  To help the user, we’ll hide all subsequent fields if a decimal point is used in one of the earlier fields.  That is, if the user enters “47.” in the degrees field, then we know that he/she won’t need the minutes or seconds fields, so we’ll just hide them:

        If DegField.Text.Contains(".") Then

            MinField.Hide()

            SecField.Hide()

 

Next, we’ll get the degrees value and makes sure that it is between 0 and 180 degrees:

            Dim ddeg As Double = CDbl(DegField.Text)

            If ddeg > 180 OrElse ddeg < 0 Then

                Throw New Exception()

            End If

 

Now, if the coordinate is south or west, we’ll have to multiply by -1 to indicate that:

            If Not isPositive Then

                ddeg *= -1.0

            End If

 

And then finally cache the coordinate:

            cache = New DegreesMinutesSeconds(ddeg)

 

Now, if the degrees field didn’t have a decimal, we’ll need to remember to show the minutes field in case it had been hidden earlier:

        Else

            MinField.Show()

 

We also know that degrees will be a whole number, so let’s cache that away:

            Dim deg As UInteger = CUInt(DegField.Text)

 

The next case is the degree-minutes case – if the minutes field has a decimal, we’ll know that seconds won’t be needed, so we’ll hide it:

            If MinField.Text.Contains(".") Then

                SecField.Hide()

 

 

We then validate both the degrees (0…180) and the minutes (0…60), and then cache the target using the appropriate DegreesMinutesSeconds constructor that we created earlier:

                Dim dmin As Double = CDbl(MinField.Text)

                If deg > 180 OrElse deg < 0 _

                  OrElse dmin > 60 OrElse dmin < 0 Then

                    Throw New Exception()

                End If

                cache = New DegreesMinutesSeconds(isPositive, deg, dmin)

 

If we’ve gotten this far without finding a decimal place, then we know that we’re using all of the fields (degrees, minutes, and seconds), so we should verify that seconds is showing, do the validation, and cache the target:

                SecField.Show()

                Dim min As UInteger = CUInt(MinField.Text)

                Dim dsec As Double = CDbl(SecField.Text)

                If deg > 180 OrElse deg < 0 _

                  OrElse min > 180 OrElse min < 0 _

                  OrElse dsec > 60 OrElse dsec < 0 Then

                    Throw New Exception()

                End If

 

                ' Cache this data for later use

                cache = New DegreesMinutesSeconds(isPositive, deg, min, dsec)

            End If

        End If

    End Sub

 

It’s time for us to write this UpdateTargetInfo() code.  That code will be responsible for calculating both the distance and direction from our current position to our desired position.  We’ll be using the Math library to do these calculations.  The functions in the math library use radians, not degrees, but we can whip up a quick translation between the two:

    Private Function DegToRad(ByVal deg As Double) As Double

        Return (deg * Math.PI) / 180

    End Function

 

    Private Function RadToDeg(ByVal rad As Double) As Double

        Return (rad * 180) / Math.PI

    End Function

 

To calculate distance, we’ll be using the Haversine formula that we all learned in high-school trigonometry.  I refreshed my memory on this formula (and the direction formula) at http://www.movable-type.co.uk/scripts/latlong.html.   These formulae do calculations based on a great-circle, which (if you’ll recall) is actually the path to follow for the shortest distance between two points on a sphere.  (Thus, if your destination is on the same line of latitude as you, the direction you want to go will not actually be due west or east unless you are at the equator.) 

Assuming our current position is valid, we’ll go ahead & get out starting point, as well as define the radius of the earth:

    Private Sub UpdateTargetInfo()

        If position IsNot Nothing AndAlso position.LatitudeValid AndAlso _

          position.LongitudeValid AndAlso _

          TargetLatitude IsNot Nothing AndAlso TargetLongitude IsNot Nothing Then

            Dim radius As Double = 6371 ' kilometers

            Dim rlat1 As Double = DegToRad(position.Latitude)

            Dim rlat2 As Double = DegToRad(TargetLatitude.ToDecimalDegrees)

            Dim rlong1 As Double = DegToRad(position.Longitude)

            Dim rlong2 As Double = DegToRad(TargetLongitude.ToDecimalDegrees)

 

We’ll also get the difference in latitude and longitude handy:

            Dim rdeltaLat As Double = rlat2 - rlat1

            Dim rdeltaLong As Double = rlong2 - rlong1

 

Now, we can calculate distance based on Haversine:

            Dim a As Double = Math.Pow(Math.Sin(rdeltaLat / 2), 2) + _

               Math.Cos(rlat1) * Math.Cos(rlat2) * Math.Pow(Math.Sin(rdeltaLong / 2), 2)

            Dim c As Double = 2.0 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a))

            Dim distance As Double = radius * c

 

[Edit 8/7/09: Fixed typo in calculation of a -- used rlong2 instead of rlat2 accidentally.  Will repost on Temple of VB site.] 

The distance is described in kilometers at this point.  If we’re under one kilometer, it’d be easier for the user if we switch to meters instead – and of course, we’d like to show miles and feet as well, all formatted nicely using ToString formatting:

            If distance > 1.0 Then

                Me.DistLabel.Text = distance.ToString("####0.00") & " km (" & CDbl(distance * 0.621).ToString("####0.00") & "mi)"

            Else

                Me.DistLabel.Text = (distance * 1000.0).ToString("####0.00") & " m (" & CDbl(distance * 3280.84).ToString("####0.00") & "ft)"

            End If

 

Next is direction:

            Dim theta As Double

            theta = Math.Atan2(Math.Sin(rdeltaLong) * Math.Cos(rlat2), _

                               Math.Cos(rlat1) * Math.Sin(rlat2) - _

 Math.Sin(rlat1) * Math.Cos(rlat2) * Math.Cos(rdeltaLong))

            Dim degTheta As Double = (RadToDeg(theta) + 360.0) Mod 360

            Me.DirLabel.Text = degTheta.ToString("##0.00") & "°"

 

And of course if the position information isn’t valid, then we need to tell the user:

        Else

            Me.DistLabel.Text = "No GPS data available."

            Me.DirLabel.Text = "No GPS data available."

        End If

    End Sub

 

And that’s it!  Debugging it is a bit tricky, since you have to take it outside to try it out (I was basically running in and out of my house to try out new ideas I’d just coded), but it’s fully functional and would be useful for (for instance) geocaching or surveying.  Going forward, I’ll probably add more functional to mark areas that I was interested in persisting – once I get some more free time. J  The current version’s code is available at my Temple of VB site – it also includes a setup project for CABbing up the file, as well as a console application to test out the direction/distance functionality.  Enjoy!

‘Til next time,

 --Matt--*

Leave a Comment
  • Please add 4 and 8 and type the answer here:
  • Post
  • I'm trying to migrate from VB6 to VB 2008 and I'm finding certain things very annoying

    Could we have the option of defaulting parameters to byref?

    I'm constantly having to declare variables like

    dim tempstr as string = ""

    otherwise it complains it's a null value. Can we have it default to "" like 6 did? And give us back the empty constant

    The immediate window, makes me cry.

    Can you change it from a listbox with only the last line editable, back to a textbox with every line editable? I kid you not, it got so annoying I opened VB6 to do some stuff in it's immediate window.

    When we mouseover an array, can you have it tell use the value of the selected cell after the length of the array? Hell, I've found mouseover doesn't even work most of the time.

    VB6 supported multiple monitors very well, in that if I undocked all the little windows from the main one, and moved them close together they'd dock together. Could you add this back in to 2008?

    Why does the freefile function work if "open filename for input" doesn't?

    Why doesn't 2008 support control arrays?

    Why don't listitems in the listview support a tag property anymore?

    Could these be put back? Controls array (especially due to "load/unload"ing of new items are VERY useful. Only line of code, new item/item gone.

    I know you have some tutorial somewhere to fake it somehow, but that's no where near as useful.

  • Hey Neo,

    There's a bunch of questions here so I won't be able to answer them all, but hopefully this info'll help a bit:

    re:freefile - we don't have language support for "Open" anymore, but we do have a compatibility function that allows you to do this:

    FileOpen(FreeFile(), "abc.txt", OpenMode.Input)

    http://msdn.microsoft.com/en-us/library/aa711463(VS.71).aspx

    re: byref params - we can't make this the default, but the reason the prettylister explicitly inserts "ByVal" is so that this is clear to customers coming from VB6 that the behavior has changed.  We actually get a lot more requests to have the prettylister *stop* doing this

    re:control arrays - VB.NET doesn't support control arrays, but it does support arrays of controls, which is actually more powerful: http://msdn.microsoft.com/en-us/library/kxt4418a(VS.80).aspx

    re:multi-monitor - VB2010 will have support for this

    re:empty constant - you can use "String.Empty" for working with Strings or "vbEmpty" if you're working with Variants and using the VarType function

    re:ListViews - the Tag property is still there: http://msdn.microsoft.com/en-us/library/system.windows.forms.listviewitem.tag.aspx

    re:immediate window - it takes some getting used to and yeah I hated it at first too, but if you press the up arrow you can scroll through previous commands and I find that makes it workable

    Hope that helps,

    Jonathan Aneja

    Program Manager, VB Team

  • Hi, very glad to see your VB sample. I am developping an application for GPS based on Windows CE 6.0 . I will use the serial port in the GPS. Someone told me I could not use VB or C# to program. I wonder if VB or VC# can do this work.

    Thanks a lot.

  • Currently, the lower-level support for GPS is all native -- that is why the MSDN sample wraps the native API calls to present a managed front, using C# in the file gps.cs.  Such an API wrapper could also be written in VB.  But if you want to connect directly to the serial port, it could be done using the System.IO.Ports namespace.   I'm not a port-level programmer typically -- I prefer APIs -- but AFAIK it would involve connecting to the COM port (usually COM8), and then reading and understanding the raw data.  (I'm not sure how one connects to the GPS Intermediate driver, though there's certainly info online somewhere.)  I glanced at the internet quickly just now and noticed a few tutorials on connecting to COM ports -- http://www.devasp.net/net/articles/display/727.html is one, http://www.dreamincode.net/forums/showtopic37361.htm is another.  The thread at http://www.tech-archive.net/Archive/DotNet/microsoft.public.dotnet.languages.vb/2006-08/msg00368.html seems to have some GPS-specific detail.

    Hope this helps,

     --Matt--*

  • IMPORTANT:  I had a typo in the code I originally posted to this blog and to Temple of VB -- I accidentally typed in "rlong2" instead of "rlat2" when calculating the value of "a".  Not only will this give the wrong answer for distance -- it will, in fact, cause the code to try to take the square root of a negative number for large distances.  If you've downloaded the code, you should make this fix.  (See my comments in the blog above for the location.)

    --Matt--*

  • Nice Tutorial indeed.

    I did a GPS tracking system for military vehicles. I intended to use VB.NET on I-Mate pocket PC 2003 as

    I did successfully on PC with some third party controls like Dundas map and Syncfusion Essential Tools.

    Unfortunately, the problem was in sending SMS as it was so complicated on pocket PC 2003 (Windows Mobile solved that). I had to resort to LABVIEW for PDA :(

    Thanks again Matt. You rule :-)

    Cheers

  • I'm glad you enjoyed it, Waleed!  I had an HP iPaq 4350 running PPC2003 (or was it PPC2002?) for which I bought a bluetooth GPS module -- this was a big upgrade from my hand-held (and mapless, and non-programmable) Magellan that I got in 1999, and I always wanted to dig into the GPS code on it to write my own  -- I agree that it's definitely much easier now with the recent releases of Windows Mobile.  (It's ironic, though, because the tutorial just duplicates the state of my old Magellan handheld -- at some point I'll have to add some new features. :-))

    --Matt--*

  • Nice tutorial.

    Helpful and easy to follow.

    Thanks.

  • Thanks, I'm glad you enjoyed it!  I'm starting to run out of ideas, BTW, so if anyone has any, I'd love to hear them.

    --Matt--*

  • C # library, I had absolutely need the added flexibility in terms of my new project

  • I use vb.net 2005 (on Part 1) . I have some problem

    1. where is Updatedata

    2. I have an error at INVOKE(updatedatahandler) ... it return NullReference ..... Could you help me please ,Why?

    Thanks.

  • Hi, Pattama,

    1. UpdateData (the first method I describe above) lives in the VBGPS project in the example (in the "Helper Functions" region).

    2. In the same file, make sure that this line exists in the Form1_Load method (it's supposed to be the first line):

    updateDataHandler = New EventHandler(AddressOf UpdateData)

    and make sure UpdateData() exists and is defined as given in the blog above.

    Hope this helps,

     --Matt--*

  • hmm nice information thanks to the visual basic language, so your best way I understand

Page 1 of 1 (13 items)