地理坐标转换
将十进度的地理坐标转换为度、分和秒。反之亦然。
引言
大地测量学的惯例和转换是“为什么不简单做,如果你能做复杂?”的一个典型例子。 尽管如此,我们必须遵守惯例,因此这个技巧是关于地理坐标(又称地理信息、经纬度或 latlons)的表示。简而言之,latlons 是地球上一个位置的球坐标。如果你放大到那个位置,纬度就像 Y 坐标,经度就像 X 坐标。Latlons 通常用圆周的度数表示,一个圆分为 360 度。但因为我们不喜欢简单,所以地球的坐标系统从西到东是 -180o 到 180o,从北到南是 90o 到 -90o。常见的表示方法是十进制角度(D),例如 56.36782361,或者度、分和(十进制)秒(DMS),例如 56o 56' 34.2561"。减号可以被半球指示 N(北)、S(南)、W(西)或 E(东)替换。所以:-127.5 = -127<sup>o</sup> 30' 00" = 127<sup>o</sup> 30' 00" W
。不同表示方法之间的转换并不难,但有一些细微差别。
背景
在本技巧中,我将介绍一个我编写的 VB.NET 类,用于轻松地在不同表示法之间进行转换。完整的类已附带,可以下载。这个类的核心不是我写的,而是 Mario Vernari(2011 年)用 C#(我想)写的,可以在 这里找到。我将这个类移植到了 VB.NET,增加了更多的“细微差别”处理,并扩展了功能。我将这个类移植到了 VB.NET,增加了更多的“细微差别”处理,并扩展了功能。用伪代码表示,转换很简单。
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)"
细微差别在于两点:
- 处理负坐标。Vernari 的代码在这方面做得很好。
- 处理精度。作为经验法则,我为度使用 8 位小数,为分使用 6 位小数,为秒使用 4 位小数。这对于我的工作来说足够精确,但可能对您来说不够;您可以在类中进行调整。如果我使用一个十进制角度
127.9999999999
(十位小数)和 8 位小数的精度,我用伪代码将其转换为 DMS 表示法的127<sup>o</sup> 59' 60.0000"
。但这错了,因为它应该是:128<sup>o</sup> 00' 00.0000"
。这个取决于精度的细微差别在我的类中得到了处理。
代码讨论
VB.NET 类 LatLong
由一个标准的类和添加的共享函数(就像模块的一部分)组成。正如您稍后在类用法中看到的,这是我从 Vernari 那里继承过来的一个非常巧妙的结构。类以以下内容开始:
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
顶部的 private
全局变量定义了三个精度(如上所述)用于格式化数字的 string
。然后是类的 public
属性。请注意,类的实例拥有 latlong 可能包含的所有数字部分。因此,对于度和分,有 Integer
和 Double
类型。秒总是 Double
。latlong
的符号由布尔值 IsNegative
处理。定义一个双精度数是否为零的阈值由 EpsilonDeg
定义,所有这些都回到了精度细微差别。Enum
s 可以控制 latlong
的类型(例如,Longitude
的范围是 -90
到 90
,Latitude
的范围是 -180
到 180
)以及它们表示的格式 D
表示十进制角度,DM
表示十进制分,DMS
表示十进制秒(相信我,各种变体尤其出现在法律文件中)。该类在没有参数的情况下构造。
下一节是 Shared
部分(即,可以在没有类实例的情况下调用的函数)。我将在这里讨论 FromDegreeMinutesDecimalSeconds
和 Empty
函数。此部分还有更多函数(例如 FromDegreeDecimalMinutes
),但它们是类似的。简而言之,该函数接收度、分和秒作为参数,并进行最少的检查,以确定其是否在地球的范围内(我在这里不区分 Latitude
和 Longitude
;您可以通过传递 CoordinateType
enum
来区分)。
#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
如您所见,创建了 LatLong
类的实例 ll
,并首先将参数修剪到其正确形式:如果度数是负数,则设置类 IsNegative
布尔值。然后,取所有参数的绝对值,并将其限制在应有的范围内(例如,minute
在整数 0
和 59
之间,second
在双精度数 0
和 60-EpsilonDeg = 0
和 60-0.0000000001
之间)。接下来,计算十进制角度和分部分,如上所述。实例化类 ll
后完全定义并返回。如果出现任何问题,将通过函数 Empty
返回一个空的 LatLong
类。反向共享函数 FromDecimalDegrees
如下所示:
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
此处再次返回一个完全定义的类,只是计算方式不同,因为它恢复了十进制角度的 DMS 部分。请注意,为了找到分数,我使用了 Double delta
。
类的以下部分是属于类实例的代码。它主要涉及以所需的表示法输出 LatLong
。作为示例,我将在此讨论重写的 ToString
函数和普通函数 ToStringDMS
。如您所见,通过简单的 string
格式化,即可输出所需的表示法。我选择 ToString
重写的输出为十进制角度,因为它是最不复杂的表示法,可以轻松转换为 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
'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
有两点值得注意:(1)LatLong
的符号仅在 string
表示法中返回;它在类的任何计算中都不起作用。这是上面描述的细微差别 1 的处理。(2)细微差别 2 的处理由 CorrectMinuteOrSecondIs60
方法完成。此方法按如下方式更改类中的值:
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
CorrectMinuteOrSecondIs60
是函数 minuteOrSecondIs60
的一个 Public
包装器,该函数执行实际工作。后者方法接受值以及一个舍入 string
。它只需将数字转换为舍入的 string
,然后检查该 string
是否产生 60
分钟或秒。如果是,它只需将 1
添加到 Minutes
和/或 Degrees
。请记住,我们不想要像 52<sup>o</sup> 59' 60.0000"
这样的表示法,而是 53<sup>o</sup> 00' 00.0000"
。如果 DMS 格式中的 Minute
被更改,这可能会向上级联到 Degree
(例如,如果 Minutes
通过加 1
变成 60
,它应该将 Degrees
增加 1
并自身变为 0
)。这样,就可以避免错误的表示法,但这是一个真正的细微差别;只有在处理地理信息时才会遇到。另请参阅 Philippe Mori 和我在此提示下方的评论,了解处理细微差别 2 的另一种方法。我可能会将 Philippe 的 C# 方法的 VB.NET 版本实现到我的类中。
Using the Code
您可以直接将该类添加到您的项目中,并按如下方式使用它:
'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
您可以通过编写以下代码来检查细微差别 2:
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
希望这个类对某人有所帮助……
历史
- 2016 年 1 月 15 日:初稿