Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / VB

Converting Geographic Coordinates

5.00/5 (5 votes)
16 Jan 2016CPOL6 min read 33.6K   646  
Geographic coordinates from decimal degree to degrees, minutes & seconds. And vice versa.

Introduction

Geodetic conventions and conversions are a prime example of: "why do it simple, if you can do it difficult?" Nonetheless, we have to live with conventions and therefore this tip & trick on the representation of geographic coordinates (a.k.a. geographicals or latitude/longitude or latlons). In short, latlons are the spherical coordinates of a location on earth. If you zoom in to the location, a latitude is a sort of Y coordinate and longitude a sort of X coordinate. Latlons are commonly given in degrees of an arc with a circle divided in 360 degrees. But because we do not like it simple, the earth's coordinate system ranges from -180o to 180o west to east and 90o to -90o north to south. Representations commonly are decimal degrees (D) e.g. 56.36782361 or degrees minutes and (decimal) seconds (DMS) e.g. 56o 56' 34.2561". The minus sign can be replaced by hemisphere indications N(orth, S(outh), W(est) or E(ast). So: -127.5 = -127<sup>o</sup> 30' 00" = 127<sup>o</sup> 30' 00" W. Converting between the representations is not difficult but has some quirks.

Background

In this tip/trick, I'll discuss a VB.NET class I wrote to easily convert between the different notations. The full class is attached and can be downloaded. The core of this class is not from me but written by Mario Vernari (2011) in C# (I think) and can be found here. I ported this class to VB.NET, added more 'quirk' handling and extended the functionality. I ported this class to VB.NET, added more 'quirk' handling and extended the functionality. In pseudo code, the conversion is simple:

DMS(127o 30' 00") = 127 + (30/60) + (0/3600) = DEC(127.5)
and
DEC(127.5) = D(floor(127.5))o M(floor(fraction of deg*60))' S(fraction of min*60)"

The quirks are in two things:

  1. handling of negative coordinates. This is done pretty good by Vernari's code.
  2. handling of precision. As a rule of thumb, I use 8 decimals for a degree, 6 decimals for a minute and 4 decimals for a second. This is precise enough for my work but may be not for yours; you can adjust this in the class. If I use a decimal degree of 127.9999999999 (ten decimals) and a precision of 8 decimals I convert it with the pseudo code to 127<sup>o</sup> 59' 60.0000" in DMS notation. But this is wrong because it should be: 128<sup>o</sup> 00' 00.0000". This, precision dependent quirk is handled in my class.

Discussion of the Code

The VB.NET class LatLong consists of a proper class and added Shared functions (as if part of a Module). This is quite a nifty construction I took over from Vernari as you will see later in the use of the class. The class starts off with:

VB.NET
Imports System.ComponentModel

Public Class LatLong

Private _round4 As String = "0.0000"
Private _round6 As String = "0.000000"
Private _round8 As String = "0.00000000"

Public IsNegative As Boolean = False
Public DecimalDegrees As Double = 0
Public DecimalMinutes As Double = 0
Public DecimalSeconds As Double = 0
Public Degrees As Integer = 0
Public Minutes As Integer = 0
Public IsOk As Boolean = True
Public EpsilonDeg As Double = 0.0000000001

Public Enum CoordinateType
    Longitude
    Latitude
    Undefined
End Enum

Public Enum CoordinateFormat
    <Description("Deg.dec")> D
    <Description("Deg Min.dec")> DM
    <Description("Deg Min Sec.dec")> DMS
End Enum

Public Sub New()
End Sub

'rest of the code

End Class

The private globals at the top define the three precision (discussed above) strings used to format the numbers. Then followed by the public properties of the class. Note that an instance of the class has all the number parts of a latlong that it can possibly consist of. So for degrees and minutes, there is an Integer and Double type. Seconds are always Double. The sign of the latlong is handled by the boolean IsNegative. And the threshold number to define if a double is zero or not is defined by EpsilonDeg, all coming back to the precision quirk. The Enum's can control the type of latlong (e.g. Longitude ranges from -90 to 90 and Latitude from -180 to 180) and the format they are represented in D for decimal degree, DM for decimal minute and DMS for decimal seconds (believe me, all kinds of variations occur in especially legal documents). The class is constructed without parameters.

The next section is the Shared part (i.e., functions which can be called without an instance of the class). I'll discuss here the functions FromDegreeMinutesDecimalSeconds and Empty. There are more functions in this section (i.e., FromDegreeDecimalMinutes, but they are analogous). In short, the function takes in the parameters degree, minute and second and does a minimal check if it fails in the earth's range (I do not make a distinction here between Latitude and Longitude; you could by passing the CoordinateType enum).

VB.NET
#Region " Shared "
 Public Shared Function FromDegreeMinutesDecimalSeconds(degree As Integer, _
    minute As Integer, second As Double) As LatLong

    Try
        'checks, make sure they are in the range -180 to 180
        If degree < -180 OrElse degree > 180 Then _
                Throw New Exception("Latlong out of range -180 to 180")
        
        Dim ll As New LatLong
        
        If degree < 0 Then ll.IsNegative = True
        degree = Math.Abs(degree)
        minute = Math.Min(Math.Max(Math.Abs(minute),0),59)
        second = Math.Min(Math.Max(Math.Abs(second),0),60-ll.EpsilonDeg)
        
        ll.Degrees = CInt(Math.Floor(degree))
        ll.Minutes = CInt(Math.Floor(minute))
        ll.DecimalDegrees = degree + (minute/60) + (second/3600)
        ll.DecimalMinutes = minute + (second/60)
        ll.DecimalSeconds = second
        
        Return ll
    Catch
        Return LatLong.Empty
    End Try
End Function

Public Shared Function Empty() As LatLong
    Dim em As New LatLong
    em.IsOk = False
    Return em
End Function

#End Region

As can be seen, instance ll of the LatLong class is created and the parameters are first trimmed to their proper form: if the degree is negative, the class IsNegative boolean is set. Then, the absolute value of all parameters is taken and trimmed between the ranges they should have (e.g., minute between integers 0 and 59 and second between doubles 0 and 60-EpsilonDeg = 0 and 60-0.0000000001). Next, the decimal degree and minute parts are calculated as described above. The instantiated class ll is fully defined and returned. If something fails, an empty LatLong class is return by function Empty. The reverse shared function FromDecimalDegrees looks like this:

VB.NET
Public Shared Function FromDecimalDegree(decdeg As Double) As LatLong
    Try
        'checks, make sure they are in the range -180 to 180
        If decdeg < -180 OrElse decdeg > 180 Then Throw _
              New Exception("Latlong out of range -180 to 180")
        
        Dim ll As New LatLong
        Dim delta As Double = 0
        
        If decdeg < 0 Then ll.IsNegative = True
        decdeg = Math.Abs(decdeg)
        
        'get the degree
        ll.DecimalDegrees = decdeg
        ll.Degrees = CInt(Math.Floor(ll.DecimalDegrees))
        delta = ll.DecimalDegrees - ll.Degrees
        
        'get the minutes
        ll.DecimalMinutes = delta * 60
        ll.Minutes = CInt(Math.Floor(ll.DecimalMinutes))
        delta = ll.DecimalMinutes - ll.Minutes
        
        'get the seconds
        ll.DecimalSeconds = delta * 60
        
        Return ll
    Catch
        Return LatLong.Empty
    End Try
End Function

Here again, a fully defined class is returned, only the calculation is different in the sense that it restores the DMS part of the decimal degree. Note that for finding the fraction, I use Double delta.

The following part of the class is the code belonging to a class instance. It mainly involves outputting the LatLong in the desired notation. As an example, I will discuss here the overridden function ToString and the normal function ToStringDMS. As can be seen, by simple string formatting, the desired notation is output. I have chosen the output of the ToString override to be the decimal degree because it is the least complex notation which can be easily converted to a Double.

VB.NET
Public Overrides Function ToString() As String
    If IsNegative Then
        Return (-1 * DecimalDegrees).ToString
    Else
        Return DecimalDegrees.ToString
    End If
End Function

Public Function ToStringDMS(decorate As Boolean) As String
    'decimal seconds will be rounded to 4 decimals, more is only apparent accuracy
    Dim s As String = ""
    Dim sign As String = ""
    If IsNegative Then sign = "-"
    
    CorrectMinuteOrSecondIs60(CoordinateFormat.DMS)
    
    If decorate Then
        s = String.Format("{0}{1}° {2}' {3}""",sign, _
            Degrees.ToString.PadLeft(3), _
            Minutes.ToString.PadLeft(2), _
            DecimalSeconds.ToString(_round4).PadLeft(7))
    Else
        s = String.Format("{0}{1} {2} {3}",sign, _
            Degrees.ToString.PadLeft(3), _
            Minutes.ToString.PadLeft(2), _
            DecimalSeconds.ToString(_round4).PadLeft(7))
    End If
    Return s
End Function

Public Function ToStringDMS() As String
    Return ToStringDMS(False)
End Function

Two points worth noting: (1) the sign of the LatLong is only returned in the string representation; it doesn't play a role in any of the calculations of the class. This is the quirk 1 handling described above. (2) The quirk 2 handling is done by the CorrectMinuteOrSecondIs60 method. This method changes the values in the class as follows:

VB.NET
Public Function CorrectMinuteOrSecondIs60(latlongFormat As CoordinateFormat) As Boolean
    If latlongFormat = CoordinateFormat.D Then
        'nothing to do
        Return True
    ElseIf latlongFormat = CoordinateFormat.DM
        'check if min is 60
        If minuteOrSecondIs60(DecimalMinutes,_round6) = 1 Then
            Degrees += 1
            Minutes = 0
        End If
        Return True
    ElseIf latlongFormat = CoordinateFormat.DMS
        'check if sec is 60
        If minuteOrSecondIs60(DecimalSeconds,_round4) = 1 Then
            Minutes += 1
            DecimalSeconds = 0
            'cascades?
            If Minutes = 60 Then
                Degrees += 1
                Minutes = 0
            End If
        End If
        Return True
    End If
    Return False
End Function

Private Function minuteOrSecondIs60_
        (minuteOrSec As Double, roundStr As String) As Integer
    Try
        Dim minorsecstr As String = minuteOrSec.ToString(roundStr)
        If CDbl(minorsecstr) >= 60 - EpsilonDeg _
           AndAlso CDbl(minorsecstr) <= 60 + EpsilonDeg Then
            Return 1
        Else
            Return 0
        End If
    Catch ex As Exception
        Debug.Print("MinuteOrSecondIs60 " & ex.ToString)
        Return -1
    End Try
End Function

The CorrectMinuteOrSecondIs60 is a Public wrapper for the function minuteOrSecondIs60 which does the real work. This latter method takes value together with a rounding string. It simply converts the number to a rounded string and checks if this string yields a minute or second as 60. If so, it simply adds 1 to the Minutes and/or Degrees. Remember that we do not want a notation like: 52<sup>o</sup> 59' 60.0000" but 53<sup>o</sup> 00' 00.0000". If the Minute is changed in the DMS format, this could cascade upward to the Degree (e.g., if Minutes would become 60 by adding 1, it should increment Degrees by 1 and become 0 itself). In this way, a wrong notation can be prevented but it is a real quirk; something you only come across with geographicals. Please also refer to comments below this Tip from Philippe Mori and myself on another way of dealing with quirk 2. I may implement the VB.NET version of the C# method from Philippe into my class.

Using the Code

You can simply add the class to your project and use it like this:

VB.NET
'Church tower Amersfoort, The Netherlands in decimal degrees
Dim lat As Double = 52.15800833
Dim lon As Double = 5.38995833
        
Debug.Print("input: {0}, {1}",lat,lon)
Debug.Print("toDMS: {0}, {1}",    LatLong.FromDecimalDegree(lat).ToStringDMS, _
                LatLong.FromDecimalDegree(lon).ToStringDMS)

'should be:
'input:  52.15800833, 5.38995833
'toDMS:  52  9 28.8300,   5 23 23.8500

'Church tower Amersfoort, The Netherlands in DMS
Dim latdeg As Integer = 52
Dim latmin As Integer = 9
Dim latsec As Double = 28.83
Dim londeg As Integer = 5
Dim lonmin As Integer = 23
Dim lonsec As Double = 23.85
        
Debug.Print("input: {0} {1} {2}, {3} {4} {5}",_
             latdeg,latmin,latsec,londeg,lonmin,lonsec)
Debug.Print("to D : {0}, {1}",LatLong.FromDegreeMinutesDecimalSeconds_
                              (latdeg,latmin,latsec).ToStringD, _
         LatLong.FromDegreeMinutesDecimalSeconds(londeg,lonmin,lonsec).ToStringD)

'should be:
'input:  52 9 28.83, 5 23 23.85
'to D :  52.15800833,   5.38995833

You can check on quirk 2 by coding the following:

VB.NET
Dim lat = 59.9999999999
Dim lon = 5.999999999999
        
Dim latff As LatLong = LatLong.FromDecimalDegree(lat)
Dim lonff As LatLong = LatLong.FromDecimalDegree(lon)
        
Debug.Print("input D      : {0} and {1}",lat,lon)
Debug.Print("corrected DMS: {0} and {1}", latff.ToStringDMS(False), _
                                          lonff.ToStringDMS(False))

'should be
'input D      :  59.9999999999 and 5.999999999999
'corrected DMS:  60  0  0.0000 and   6  0  0.0000

I hope the class will be helpful to someone...

History

  • 15th January, 2016: First draft

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)