Converting GIS spatial coordinates
Different formats and standards exist for describing geographical coordinates in GIS systems and applications. This article explains how to convert between the most used formats, presenting a working library written in C#.
I have been playing with GIS systems recently, and a thing that fascinated me, in both good and bad way, is the existence of several types of spatial coordinate systems to express a position of a place on the globe. For those familiar with OGC-compliant GIS systems, you may know that the spatial_ref_sys metadata table holds conversion data to allow conversions from one coordinate system to another.
Each entry in this table contains specific information such as units of measurement, where the origin is located, and even the starting offset of a measurement. Most of us are familiar with seeing a coordinate pair such as this:
54.852726, -1.832299
If you have a GPS built into your mobile phone, fire it up and watch the display. You’ll see something similar to this coordinate pair. Note that on some devices and apps, the coordinates may be swapped.
This coordinate pair is known as latitude and longitude. The first number, latitude, is the degrees north or south from the equator with north being positive and south being negative.
The second number, longitude, is the degrees east or west of the Prime Meridian with west being negative and east being positive. The correct geospatial name for this coordinate system is WGS84, generally known as World Geodetic System.
Most commercial GPS applications and devices, however, use a different system for expressing geo coordinates, called National Marine Electronics Association, more specifically NMEA 0183. This system expresses latitude and longitude as a combination of degrees, minutes and seconds:
5321.5802 N, 00630.3372 W
The format of the string is DDMM.mmmm for the latitude (vertical) direction and DDDMM.mmmm for the longitude (horizontal) direction.
Starting with the latitude measurement in the string, the first two digits are the number of degrees, and the remaining numbers are the minutes. The numbers after the decimal point are fractions of a minute. This gives us:
Latitude: 53 degrees, 21.5802 minutes north
For the longitude measurement, the first three digits are the number of degrees, and the remaining digits are the minutes. All the numbers after the decimal are fractions of a minute.
This gives us:
Longitude: 6 Degrees, 30.3372 minutes west
Because this data is string data, it’s essentially an exercise in cutting the string at specific points to derive the values you want. Once you have them, the math to convert them to the more familiar latitude and longitude format (if you remember that was WGS84) is very simple.
GPS applications use the NMEA 0183 coordinate system
I have implemented latitude and longitude as a class, GeoCoordinates, and have exposed methods for system conversion, which include parsing and formatting strings in the different formats.
First, we need to separate the first two digits from the latitude string and the first three from the longitude. This gives us the following:
53 and 21.5802 for the north direction
006 and 30.3372 for west
Because there are 60 minutes in a degree, we must divide the minutes digits by sixty to find what fraction of a degree they are, and then combine them with our whole degrees. So, for our latitude:
53 + (21.5812 / 60) will give us 53.359686 degrees.
And for our longitude:
6 + (30.3372 / 60) will give us 6.505620 degrees.
To finish the conversion, we need to apply the north and west directions as positive or negative numbers. The easiest way to manage which directions are positive or negative is to change any west or south measurements to negative. So with our numbers, the final coordinates in WGS84 latitude and longitude are:
53.359686, -6.505620
Let’s see how this translates into C# code.
Latitude and Longitude are decimal properties of the GeoCoordinates class. I decided to use decimal as data type as it offers higher accuracy and is less prone to loss of precision in mathematical calculations, as compared to the floating-point data types float and double.
Latitude extends from the equator (0 degrees) to the North Pole (90 degrees) or the South Pole (-90 degrees). Longitude extends from the Greenwich meridian to the right-hand side (East) for 180 degrees, and to the left-hand side (West) for 180 degrees. Therefore, I have enforced range validation on setting the properties.
private decimal _latitude;
public decimal Latitude
{
get
{
return _latitude;
}
set
{
if (value < -90.0M || value > 90.0M)
{
throw new ArgumentOutOfRangeException("Latitude must be between -90.0 and 90.0.");
}
_latitude = value;
}
}
private decimal _longitude;
public decimal Longitude
{
get
{
return _longitude;
}
set
{
if (value < -180.0M || value > 180.0M)
{
throw new ArgumentOutOfRangeException("Longitude must be between -180.0 and 180.0.");
}
_longitude = value;
}
}
Latitude and Longitude as C# properties
The GeoCoordinates class expresses coordinates in WGS84 format (that is, decimal values). We want to convert coordinates from the format in use in GPS applications (NMEA 0183), which is expressed in degrees and minutes. For example, our hypothetical application would have a simple instruction like the following one:
string degrees = "5321.5802 N, 00630.3372 W";
GeoCoordinates coords = GeoCoordinates.FromNMEA0183(degrees);
The static function FromNMEA0183 converts the string representation of coordinates into its decimal elements.
public static GeoCoordinates FromNMEA0183(string degrees)
{
if (string.IsNullOrEmpty(degrees))
{
throw new ArgumentNullException(nameof(degrees));
}
string[] coords = degrees.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
if (coords.Length < 2)
{
throw new ArgumentException("Invalid degree coordinates.", nameof(degrees));
}
return new GeoCoordinates
{
Latitude = ParseLatitude(coords[0].Trim()),
Longitude = ParseLongitude(coords[1].Trim())
};
}
The method to convert from textual to decimal representation of coordinates
After some obvious validation, the coordinates’ string is split into its two elements of latitude and longitude, separated by comma, and each element is then parsed separately. ParseLatitude and ParseLongitude are very similar methods, but I decided to implement them individually to avoid confusing “if” conditions to handle either element of the geo coordinates. I’ll describe the code for ParseLatitude in this article, the full source code is available on CodePlex.
private static decimal ParseLatitude(string coords)
{
if (!coords.EndsWith("N", StringComparison.OrdinalIgnoreCase) && !coords.EndsWith("S", StringComparison.OrdinalIgnoreCase))
{
throw new ArgumentException("Latitude coordinate not found.");
}
if (coords.Length < 4)
{
throw new ArgumentException("Invalid latitude format.");
}
int dd = 0;
try
{
dd = int.Parse(coords.Substring(0, 2));
if (dd > 90)
{
throw new ArgumentOutOfRangeException();
}
}
catch when (dd > 90)
{
throw new ArgumentOutOfRangeException("Degrees in latitude cannot exceed 90.");
}
catch
{
throw new ArgumentException("Invalid degrees format in latitude.");
}
double mm = 0.0D;
try
{
string minutes = Regex.Match(coords.Substring(2), @"(\d+).(\d+)").Value;
mm = double.Parse(minutes);
if ((dd == 90 && mm > 0.0D) || mm >= 60.0D)
{
throw new ArgumentOutOfRangeException();
}
}
catch when (dd == 90 && mm > 0.0D)
{
throw new ArgumentOutOfRangeException("Degrees in latitude cannot exceed 90.");
}
catch when (mm >= 60.0D)
{
throw new ArgumentOutOfRangeException("Minutes in latitude cannot exceed 60.");
}
catch
{
throw new ArgumentException("Invalid minutes format in latitude.");
}
decimal latitude = Convert.ToDecimal(dd + mm / 60);
if (coords.EndsWith("S", StringComparison.OrdinalIgnoreCase))
{
latitude = decimal.Negate(latitude);
}
return latitude;
}
The method to parse the latitude coordinate
There is a lot of validation in place, as you would expect, to make sure the input string is in the expected format. The process of conversion of coordinates, basically, takes places into three steps:
1. Read the degrees portion and convert into an integer number; if the number is higher than 90, throw an out-of-range exception.
dd = int.Parse(coords.Substring(0, 2));
if (dd > 90)
{
throw new ArgumentOutOfRangeException();
}
2. Read the minutes portion, including decimals, and convert into a double number; minutes are read after extracting them from the string representation, by taking out the initial two digits for degrees, and any trailing non-numeric signs (for N or S). Range validation is also applied.
string minutes = Regex.Match(coords.Substring(2), @"(\d+).(\d+)").Value;
mm = double.Parse(minutes);
if ((dd == 90 && mm > 0.0D) || mm >= 60.0D)
{
throw new ArgumentOutOfRangeException();
}
3. Put all together by adding the minutes to the degrees and converting into decimal; sign is also applied depending on the direction of the latitude (North is positive, South is negative).
decimal latitude = Convert.ToDecimal(dd + mm / 60);
if (coords.EndsWith("S", StringComparison.OrdinalIgnoreCase))
{
latitude = decimal.Negate(latitude);
}
Exactly the same logic applies to parsing longitude, with the obvious difference on the input format.
After parsing input values, we now want to produce something in output, specifically represent coordinates in either format NMEA0183 or WGS84. Code-wise, we need something to print out coordinates as a string, for example:
Console.WriteLine("NMEA0183: {0}", coords.ToString(GeoCoordinates.NMEA0183));
Console.WriteLine("WGS84: {0}", coords.ToString(GeoCoordinates.WGS84));
Let’s start from defining two simple constant values that will help us identifying which format to apply. I defined these two constants as string, but an enum would suffice as well.
public const string NMEA0183 = "NMEA0183";
public const string WGS84 = "WGS84";
Then we need to implement a ToString method with an input parameter to specify which format to apply:
public string ToString(string format)
{
switch (format)
{
case NMEA0183:
return FormatNMEA0183();
case WGS84:
return FormatWGS84();
default:
throw new ArgumentOutOfRangeException("Invalid format specified.", nameof(format));
}
}
The FormatNMEA0183 is pretty straightforward, with the latitude and longitude components formatted individually. As an output, we want to obtain a string with the latitude and longitude coordinates separated by comma, as in 5321.5802 N, 00630.3372 W
private string FormatNMEA0183()
{
string latitude = FormatLatitudeDegrees(this.Latitude);
string longitude = FormatLongitudeDegrees(this.Longitude);
return $"{latitude}, {longitude}";
}
To format latitude, we need to convert from a decimal number into DDMM.mmmm, e.g. 53.359686 à 5321.5802 N
private string FormatLatitudeDegrees(decimal latitude)
{
string sign = latitude > 0 ? "N" : "S";
latitude = Math.Abs(latitude);
string dd = decimal.Truncate(latitude).ToString("0#");
string mm = (decimal.Subtract(latitude, decimal.Truncate(latitude)) * 60).ToString("0#.0000");
return $"{dd}{mm} {sign}";
}
To format longitude, we need to convert from a decimal number into DDDMM.mmmm, e.g. -6.505620 à 00630.3372 W
private string FormatLongitudeDegrees(decimal longitude)
{
string sign = longitude > 0 ? "E" : "W";
longitude = Math.Abs(longitude);
string dd = decimal.Truncate(longitude).ToString("00#");
string mm = (decimal.Subtract(longitude, decimal.Truncate(longitude)) * 60).ToString("0#.0000");
return $"{dd}{mm} {sign}";
}
Formatting coordinates into WGS84 is even easier, as implemented in the FormatWGS84 method; all we need to do is to combine latitude and longitude in a comma-separated string, e.g. 53.359686, 6.505620.
private string FormatWGS84()
{
return $"{this.Latitude}, {this.Longitude}";
}
Project Name: GisSpatial