解析经纬度信息






4.73/5 (24投票s)
解析用户输入并提取纬度和经度信息,同时考虑用户的语言和区域设置。
引言
在我地图控件文章中,我尝试解析用户输入,以确定它是否为纬度和经度,并在地图上显示结果。当时,我不想编写完整的用户输入解析器,所以(非常懒惰地)只使用逗号分隔用户输入,并尝试解析两个十进制值。
这有很多问题。首先,坐标可能不是度分秒格式,这对于坐标来说很普遍。第二点(评论中也指出了这一点),一些国家使用逗号作为十进制分隔符(例如西班牙)。
背景
Jaime Olivares在此写了一篇优秀的文章,该文章根据ISO 6709标准解析和序列化纬度和经度坐标(该标准的良好指南可在此页面上找到)。
这篇文章很好地解释了标准是什么,并提供了简洁的代码来完成这项工作,但期望应用程序用户按照此格式输入坐标是有点不合理的!因此,我将使用一些简单的正则表达式来尝试尽可能灵活地进行解析,同时考虑到不同用户的语言设置。

ISO 6709 解析
ISO 6709 格式(从开发者的角度来看)的好处是,我们确切知道在string
中会期望什么。例如,要分隔多个坐标,使用'/'
字符。此外,数据不会因用户的文化设置而异;十进制分隔符将始终是'.'
。但是,仍然需要一些猜测,因为我们不知道它代表十进制度数(以下简称D)、度数和十进分数(DM)还是度数、分数和十进秒数(DMS)。此外,我们也不知道是否会包含海拔高度分量。不过,让我们列出我们所知道的:
- 唯一有效的数字是 0 - 9。
- 唯一有效的十进制分隔符是
'.'
。 - 数字的小数部分必须包含十进制分隔符,后跟至少一位数字。
- 纬度分量将是第一个,并以
'+'
或'-'
开头。 - 纬度至少有三个字符,加上一个可选的小数部分
[±DD(.D)]
。 - 纬度最多有七个字符,加上一个可选的小数部分
[±DDMMSS(.S)]
。 - 经度分量将是下一个,并以
'+'
或'-'
开头。 - 经度至少有四个字符,加上一个可选的小数部分
[±DDD(.D)]
。 - 经度最多有八个字符,加上一个可选的小数部分
[±DDDMMSS(.S)]
。 - 如果指定了海拔高度,则它将是下一个,并以
'+'
或'-'
开头。 - 海拔高度至少有两个字符,加上一个可选的小数部分
[±A(.A)]
。 - 字符串将以
'/'
字符结尾。
现在我们知道了有效格式,我们可以轻松地将其转换为正则表达式(并使用Regex
类)。这就是我们将使用的正则表达式(如果您想尝试,请记住使用RegexOptions.IgnorePatternWhitespace
标志)。
^\s* # Match the start of the string,
ignoring any whitespace
(?<latitude> [+-][0-9]{2,6}(?: \. [0-9]+)?) # The decimal part is optional.
(?<longitude>[+-][0-9]{3,7}(?: \. [0-9]+)?)
(?<altitude> [+-][0-9]+(?: \. [0-9]+)?)? # The altitude component is optional
/ # The string must be terminated by '/'
此正则表达式将告诉我们输入字符串*可能*是ISO 6709格式,如果全部匹配,则允许我们使用各种命名组从string
中获取各个分量。我说string
*可能*是正确的格式,因为所示的表达式也允许'+123+1234/'
作为有效值(即±DDM±DDDM/
),并且不对值进行任何范围检查(例如,分钟和秒不能大于或等于60)。因此,我们需要将成功匹配的输出传递给另一个函数,以将string
转换为可用于计算的数字。
对于海拔高度部分,这非常简单;检查海拔高度Group.Success
属性,如果找到海拔高度,请使用double.Parse
转换string
值(确保传入CultureInfo.InvariantCulture
以避免任何本地化问题)。请注意,无需使用double.TryParse
,因为我们已经通过正则表达式检查了输入是否有效。
对于经度和纬度,则有点棘手。基本思想是将string
分成两部分;整数部分和可选的小数部分。根据整数部分的长度,我们知道string
是D、DM还是DMS格式,然后可以单独分割和解析每个分量,确保将小数部分(如果有)添加到最后一个分量。
用户输入
如引言中所述,本文的目的是在友好对待不同文化设置的同时,从用户提供的string
中提取坐标。我所采取的方法是将string
分割成组,然后使用double.TryParse
方法(传入当前文化设置)来实际进行数字处理,因为我认为.NET Framework在本地化方面可以做得比我更好!
这不禁让人想知道如何将string
分割成组?我假设纬度和经度将由空格分隔。我还假设纬度和经度将采用相同的格式(即,如果纬度是DM,则经度也是DM)。让我们看一些关于如何编写纬度的示例:
12° 34' 56? S |
这使用了ISO 31-1符号。 |
-12° 34' 56? |
这使用负号而不是'S' 。 |
-12°34'56?S |
这使用负号和'S' ,并省略了空格。我们将假设坐标位于南半球。 |
-12 34" 56' |
这省略了度数符号并使用了引号。这可能是最容易键入的,因为它不使用普通键盘上找不到的任何特殊符号。 |
-12 34’ 56” |
与上面相同,但使用了智能引号(想象从Microsoft Word复制)。 |
+12 34 56 S |
我们可以假设这是DMS格式,因为有三组数字。 |
S 12d34m56s |
一些程序允许使用D表示度,M表示分,S表示秒,并将南北后缀放在前面。 |
S 12* 34' 56" |
这在法律描述中很常见。 |
当然,还有更多的组合(只指定一个符号,混合使用智能引号和普通引号等)。此外,这只是针对DMS格式,甚至没有考虑十进秒(例如,-12 34' 56.78" 是否有效?在某些国家可能有效,但在西班牙则无效)。关于'S'
应该是什么意思,也可能存在歧义?如果我们允许'D'
表示度,'M'
表示分,那么自然'S'
应该解释为秒。但在大多数示例中,'S'
表示纬度在南半球。因此,我们将排除'S'
作为秒的符号,因此12d 34m 56s
将被解释为12° 34' 56? S
。
由于我们不打算验证数字,我们只需要找到一种将string
分割成组的方法。与ISO格式一样,我们可以使用正则表达式,并将任何非符号或空格的字符组合在一起。这是度数最简单的形式:
^\s* # Ignore any whitespace at the start of the string
(?<latitudeSuffix>[NS])? # The suffix could be at the start
(?<latitude>.+?) # Match anything and we'll try to parse it later
[D\*\u00B0]?\s* # Degree symbols (optional) followed by optional whitespace
(?<latitudeSuffix>[NS])?\s+ # Optional suffix with at least some whitespace to separate
哇,真是一团糟!在跳过字符串开头的空格后,Regex
将查找南北指示符,如果找到,则将其存储在名为latitudeSuffix
的组中。然后,它将匹配任何字符('.'
)多次,但次数越少越好('+?'
)。这意味着,如果找到一个可选的度数符号(例如'*'
(这是一个保留字符,因此需要转义)、'D'
或'°'
(写成Unicode数字)),匹配就会停止。如果找不到,它将查找任何空格。如果仍然找不到任何匹配项,它将查找纬度后缀。最后,如果仍然找不到这些,则必须找到至少一个空格字符(记住我们说过纬度和经度必须用空格分隔)。假设正则表达式成功匹配整个string
,然后我们进入第二阶段,尝试使用当前文化设置解析提取的组。这包括将latitude
组传递给double.TryParse
,并根据latitudeSuffix
组调整符号(如果需要)。
Using the Code
Angle
类作为Latitude
和Longitude
的基类,并允许在弧度和度数之间进行转换。它实现了IComparable<T>
、IEquatable<T>
和IFormattable
接口,这意味着您可以比较Angle
s(或Latitude
或Longitude
),但不能比较Latitude
与Longitude
(这没有意义)。这也意味着您可以选择如何显示它们。
var latitude = Latitude.FromDegrees(-5, -10, -15.1234);
Console.WriteLine("{0:DMS1}", latitude); // 5° 10' 15.1? S
Console.WriteLine("{0:DM3}", latitude); // 5° 10.252' S
Console.WriteLine("{0:D}", latitude); // 5.17° S
Console.WriteLine("{0:ISO}", latitude); // -051015.1234
该类没有任何public
可见的构造函数,因此您需要使用static
初始化器。以下是该类的所有方法和属性的完整列表:
public class Angle : IComparable<Angle>, IEquatable<Angle>, IFormattable
{
// Gets the whole number of degrees from the angle.
public int Degrees { get; }
// Gets the whole number of minutes from the angle.
public int Minutes { get; }
// Gets the number of seconds from the angle.
public double Seconds { get; }
// Gets the value of the angle in radians.
public double Radians { get; }
// Gets the value of the angle in degrees.
public double TotalDegrees { get; }
// Gets the value of the angle in minutes.
public double TotalMinutes { get; }
// Gets the value of the angle in seconds.
public double TotalSeconds { get; }
// Creates a new angle from an amount in degrees.
public static Angle FromDegrees(double degrees);
public static Angle FromDegrees(double degrees, double minutes);
public static Angle FromDegrees(double degrees, double minutes, double seconds);
// Creates a new angle from an amount in radians.
public static Angle FromRadians(double radians);
// Returns the result of multiplying the specified value by negative one.
public static Angle Negate(Angle angle);
public static bool operator !=(Angle angleA, Angle angleB);
public static bool operator <(Angle angleA, Angle angleB);
public static bool operator <=(Angle angleA, Angle angleB);
public static bool operator ==(Angle angleA, Angle angleB);
public static bool operator >(Angle angleA, Angle angleB);
public static bool operator >=(Angle angleA, Angle angleB);
// Compares this instance with a specified Angle object and indicates
// whether the value of this instance is less than, equal to, or greater
// than the value of the specified Angle object.
public int CompareTo(Angle other);
// Determines whether this instance and a specified object have
// the same value.
public override bool Equals(object obj);
public bool Equals(Angle other);
// Returns the hash code for this instance.
public override int GetHashCode();
// Returns a string that represents the current Angle in degrees,
// minutes and seconds form.
public override string ToString();
// Formats the value of the current instance using the specified format.
public virtual string ToString(string format, IFormatProvider formatProvider);
}
Location
类包含Latitude
、Longitude
和可选的海拔高度。它实现了IEquatable<T>
、IFormattable
和IXmlSerializable
接口,使用ISO格式对其进行序列化/反序列化。它还接受与Latitude
/Longitude
相同的格式化string
。有一些static
解析方法接受各种选项以允许识别不同的格式,并且该类还具有一些助手函数,这些函数源自Ed Williams的Aviation Formulary V1.45。
public sealed class Location : IEquatable<Location>, IFormattable, IXmlSerializable
{
// Initializes a new instance of the Location class.
public Location(Latitude latitude, Longitude longitude);
public Location(Latitude latitude, Longitude longitude, double altitude);
// Gets the altitude of the coordinate, or null if the coordinate doesn't
// contain altitude information.
public double? Altitude { get; }
// Gets the latitude of the coordinate.
public Latitude Latitude { get; }
// Gets the longitude of the coordinate.
public Longitude Longitude { get; }
// Converts the string into a Location.
public static Location Parse(string value);
public static Location Parse(string value, IFormatProvider provider);
public static Location Parse(string value, LocationStyles style,
IFormatProvider provider);
// Converts the string into a Location (without throwing an exception).
public static bool TryParse(string value, out Location location);
public static bool TryParse(string value, IFormatProvider provider,
out Location location);
public static bool TryParse(string value, LocationStyles style,
IFormatProvider provider, out Location location);
public static bool operator !=(Location locationA, Location locationB);
public static bool operator ==(Location locationA, Location locationB);
// Determines whether this instance and a specified object have the
// same value.
public override bool Equals(object obj);
public bool Equals(Location other);
// Returns the hash code for this instance.
public override int GetHashCode();
// Returns a string that represents the current Location in degrees,
// minutes and seconds form.
public override string ToString();
// Formats the value of the current instance using the specified format.
public string ToString(string format, IFormatProvider formatProvider);
// Calculates the initial course (or azimuth; the angle measured clockwise
// from true north) from this instance to the specified value.
public Angle Course(Location point);
// Calculates the great circle distance, in meters, between this instance
// and the specified value.
public double Distance(Location point);
// Calculates a point at the specified distance along the specified
// radial from this instance.
public Location GetPoint(double distance, Angle radial);
}
为了完整起见,还有一个可序列化的LocationCollection
类,它像Location
一样,使用ISO格式对其进行序列化/反序列化。
关注点
Angle
、Latitude
或Longitude
类均不包含与内置类型(如double
)的转换(无论是implicit
还是explicit
)。这是故意的。在.NET Framework的Math
类中,处理角度的方法(如Math.Cos
)期望角度以弧度为单位。但是,在处理纬度/经度时,度数更为常见。因此,将double
转换为Angle
并假定数字以弧度为单位,而将double
转换为Latitude
并假定数字以度为单位,似乎不一致。因此,最好由开发人员明确数字代表的内容,使用FromDegrees
或FromRadians static
方法。
此外,出于效率原因,如果Latitude
和Longitude
是struct
s会很好。但是,您不能对struct
s使用继承,并且我认为Angle
与Latitude
/Longitude
之间的代码重用证明了使用类的合理性,但欢迎您提供意见。
历史
- 21/02/12 - 修复了小于一度的值的ISO格式错误,并改进了分数和秒的一般四舍五入。
- 12/11/11 - 修复了一个解析错误,在该错误中,当度数元素为零时,会丢失半球信息。
- 31/01/11 – 添加了Erik Anderson提出的格式。
- 30/01/11 – 第一个版本。