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:
- handling of negative coordinates. This is done pretty good by Vernari's code.
- 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:
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
End Class
The private
globals at the top define the three precision (discussed above) string
s 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
).
#Region " Shared "
Public Shared Function FromDegreeMinutesDecimalSeconds(degree As Integer, _
minute As Integer, second As Double) As LatLong
Try
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:
Public Shared Function FromDecimalDegree(decdeg As Double) As LatLong
Try
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)
ll.DecimalDegrees = decdeg
ll.Degrees = CInt(Math.Floor(ll.DecimalDegrees))
delta = ll.DecimalDegrees - ll.Degrees
ll.DecimalMinutes = delta * 60
ll.Minutes = CInt(Math.Floor(ll.DecimalMinutes))
delta = ll.DecimalMinutes - ll.Minutes
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
.
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
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:
Public Function CorrectMinuteOrSecondIs60(latlongFormat As CoordinateFormat) As Boolean
If latlongFormat = CoordinateFormat.D Then
Return True
ElseIf latlongFormat = CoordinateFormat.DM
If minuteOrSecondIs60(DecimalMinutes,_round6) = 1 Then
Degrees += 1
Minutes = 0
End If
Return True
ElseIf latlongFormat = CoordinateFormat.DMS
If minuteOrSecondIs60(DecimalSeconds,_round4) = 1 Then
Minutes += 1
DecimalSeconds = 0
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:
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)
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)
You can check on quirk 2 by coding the following:
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))
I hope the class will be helpful to someone...
History
- 15th January, 2016: First draft