Article

# Coordinate and CoordinateList classes - ISO 6709 compliant

, 7 Jun 2009
 Rate this:
Classes to store, handle, and retrieve geodesic coordinates, in memory, database, and XML, according to the ISO 6709 standard.

## Introduction

I have seen many articles here at CodeProject about coordinates and GPS signal handling, like "Writing your own GPS Applications" and "Distance between locations using latitude and longitude" among others. Most of them do some tasks around a latitude/longitude pair, but they don't handle coordinate storage with care. This article provides a solution to manage coordinates at both memory and persistent storage (XML or database), complying with the ISO 6709 standard (Annex H - Text string representation), in concordance with the World Wide Web Consortium's LatitudeLongitudeAltitude workgroup.

## Background

• ISO 6709 - "Standard representation of geographic point location by coordinates" - at Wikipedia.org
• LatitudeLongitudeAltitude workgroup - at World Wide Web Consortium (W3C)
• ISO 31-1 - "Quantities and Units - Space and time" - at Wikipedia.org

## In-memory storage

A geodesic coordinate has two main components: latitude (a.k.a. "y") and longitude (a.k.a. "x"). Both latitude and longitude are expressed in sexagesimal degrees and decimals, according to ISO standards, with the following constraints:

• Positive values for North and East hemispheres
• Negative values for South and West hemispheres
• Latitude values from -90.0 to +90.0
• Longitude values from -180 to +180.0

With these constraints, we can conclude that coordinates could be stored in a pair of 32-bit single precision data type, known as `float` in C# and C++ [see the addendum "Why to use float?" at the end of this article]. But, there are more decisions pending... latitude and longitude are usually expressed in degrees, minutes, and seconds. A minute is 1/60 of a degree, and a second is 1/3600. So, if we store a latitude or longitude in degrees unit, we will have to convert from 1/60 and 1/3600 fractions, to entire values, and back again to degrees and decimal; this will cause a cumulative loss of precision.

After many years working with coordinates, I concluded the best unit to store coordinates is not degrees but "seconds of degree". This way, all coordinate elements (degrees, minutes, and seconds) will remain on the integral portion of values, with the exception of decimal of seconds, avoiding the annoying loss of precision. The first approach to implement a coordinate class would be then:

```class Coordinate
{
float latitude;
float longitude;
}```

Here is an example on how to store a coordinate as explained above:

• Latitude: 05º 13' 01.3" S
• Longitude: 080º 37' 49.5" W
• In degrees and decimals: { -(5 + 13/60 + 1/3600), -(80 + 37/60 + 49/3600) } equals { -5.2169..., -80.6303... }
• Optimized storage: { -(5*3600 + 13*60 + 1.3), -(80*3600 + 37*60 + 49.5) } equals { -18781.3, -290269.5 }

## XML serialization

The .NET Framework provides facilities to easily store and retrieve data in XML format; if you associate an XML serializer object with the above class, you will have a result like:

```<Coordinate>
<Latitude>-18781.3</Latitude>
<Longitude>-290269.5</Longitude>
</Coordinate>```

This is barely 90 - 100 bytes per coordinate, including spacing characters. Just figure out a 100,000 node geographic file. This will require nearly 10 MB of file storage! Here is where ISO comes to the rescue. If you have read the ISO 6709 explanation, you will know a proper representation for our sample data would be, among others:

• -05.2169-080.6303/ expressed in pure degrees, or
• -051301.3-0803749.5/ expressed in degrees, minutes, and seconds

So, if we override the default serializer for our `Coordinate` class to use the second version, we can expect a result like:

`<Coordinate>-051301.3-0803749.5/</Coordinate>`

At first glance, you will notice a storage saving of more than 50%. Also notice that, besides the fact that our class stores data in-memory conveniently in "seconds of degree", XML storage is expressed in degrees, minutes, and seconds, so you can interpret the XML file by yourself, without conversion formulas. Now, you have the best of both worlds.

## Coordinate class implementation

The first part of the `Coordinate` class implementation has the following fields and properties declarations:

```[Serializable()]
public class Coordinate : ICloneable, IXmlSerializable, IFormattable
{
#region Private fields
// Expressed in seconds of degree, positive values for north
private float latitude;
// Expressed in seconds of degree, positive values for east
private float longitude;

#endregion

#region Constructors
public Coordinate()
{
Latitude = Longitude = 0.0f;
}
public Coordinate(float lat, float lon)
{
Latitude = lat;
Longitude = lon;
}
#endregion

#region Properties
public float Latitude
{
set
{
latitude = value * 3600.0f;
}
get
{
return latitude / 3600.0f;  // return degrees
}
}
public float Longitude
{
set
{
longitude = value * 3600.0f;
}
get
{
return longitude / 3600.0f;  // return degrees
}
}
#endregion```

You can see how latitude and longitude fields have been hidden from external usage and serialization. There are a couple of properties instead, `Latitude` and `Longitude` (with lead uppercase character); they can be accessed by the user, and return values as degrees and decimals, hiding our underlying storage format (seconds of degrees). Also, two constructors have been implemented: a default constructor, which sets lat/lon as zeroes, and a custom constructor which receive lat/lon as degrees and decimal with proper sign.

Following with code, you will find the public methods, to either set and get values in a friendly way:

```#region Public methods
// Multi-argument setters
public void SetD(float latDeg, float lonDeg) {...}
public void SetDM(float latDeg, float latMin, bool north, float lonDeg, float lonMin,
bool east) {...}
public void SetDMS(float latDeg, float latMin, float latSec, bool north, float lonDeg,
float lonMin, float lonSec, bool east) {...}

// Multi-argument getters
public void GetD(out float latDeg, out float lonDeg) {...}
public void GetDM(out float latDeg, out float latMin, out bool north, out float lonDeg,
out float lonMin, out bool east) {...}
public void GetDMS(out float latDeg, out float latMin, out float latSec, out bool north,
out float lonDeg, out float lonMin, out float lonSec, out bool east) {...}

// Distance in meters
public float Distance(Coordinate other) {...}

// Parsing method
public void ParseIsoString(string isoStr) {...}
#endregion```

The first version of the setter, `SetD`, just needs two arguments expressed in degrees and decimal with proper sign for the hemisphere. It is equivalent to setting the `Latitude` and `Longitude` properties independently. The following two overrides will need minutes and hemisphere explicitly, and seconds optionally. In this case, the degrees arguments should not contain the hemisphere sign. The getter has a one-by one correspondence with the setters.

Finally, you will find a method to calculate the distance against other coordinate. It is implemented using the classical Haversine formula (you will find web references inside the code). The result is expressed in meters, the ISO unit for distances. You can add your own code for more calculations.

The following methods in the code implement some fundamental overrides:

```#region Overrides
public override string ToString() {...}
public override bool Equals(object obj) {...}
public override int GetHashCode() {...}
#endregion```

The default implementation of `Coordinate.ToString()` will return a string with the coordinate expressed in degrees, minutes, and seconds. There will be more implementations of this method. `Equals()` compares both the latitude and longitude values for equality, and `GetHashCode()` returns a hash value based on the lat/lon values. The following code implements the `IFormattable` interface, allowing to display coordinates in different fashions:

```#region IFormattable Members
// Not really IFormattable member
public string ToString(string format) {...}
// ToString version with formatting
public string ToString(string format, IFormatProvider formatProvider) {...}
#endregion```

As the `IFormattable.ToString()` method accepts a second argument that is unused in this implementation (`formatProvider`), an abbreviated override has been added with just an argument: the format string. Valid format strings are the following, with a little example:

• "D": 05.2169ºS 080.6303ºW
• "DM": 05º13.02'S 080º37.82'W
• "DMS": 05º13'01.3"S 080º37'49.5"W (default)
• "ISO": -051301.3-0803749.5/

Any other formatting string will produce an exception. Calling any version with an empty or null formatting string will output the default version. But, we still have not seen the best feature of the `IFormattable` interface; we can embed the formatting string inside a more complex formatting case; for example:

`string s = string.Format("Our sample coordinate is: {0:DM}\r\n.", someCoord);`

Finally, there is the `IXmlSerializable` implementation. It overrides the default XML formatting, as explained earlier. Here is the abbreviated source code:

```#region IXmlSerializable Members
System.Xml.Schema.XmlSchema IXmlSerializable.GetSchema() {...}
void IXmlSerializable.WriteXml(XmlWriter writer) {...}
#endregion```

You don't have to invoke these functions directly, they will be invoked by any serialization function, as you can find in the test source code below. `WriteXml()` will produce XML code for storing the latitude and longitude in ISO format with the pattern: "±DDMMSS.S±DDDMMSS.S/". `ReadXml()`, countersense, will parse a coordinate stored with any valid ISO format, even those including the height or depth, not considered for this implementation. An incorrect format will produce an exception.

## Coordinate collections

Once using the `Coordinate` class, you will notice another source of space wasting. Consider the following example:

```<CoordinateList>
<Coordinate>+010000.0+0010000.0/</Coordinate>
<Coordinate>-022400.0+0013000.0/</Coordinate>
<Coordinate>-034200.0+0021200.0/</Coordinate>
<Coordinate>-020000.0-0003000.0/</Coordinate>
</CoordinateList>```

A real-world GIS application will need hundreds of `<Coordinate>` entries for each polygon. So, the ISO standard comes again to the rescue by storing the data in a more compact fashion:

```<CoordinateList>+010000.0+0010000.0/-022400.0+0013000.0/
-034200.0+0021200.0/-020000.0-0003000.0/</CoordinateList>```

Again, we have a storage saving of more than 50%. Implementing the `CoordinateList` class is quiet easy. You just need to derive a generic `List<Coordinate>` collection and override the serialization methods from the `IXmlSerializable` interface. Here is the template:

```[Serializable()]
public class CoordinateList : List<Coordinate>, IXmlSerializable
{
#region Constructors
public CoordinateList() {...}
#endregion

#region Overrides
public override string ToString() {...}
#endregion

#region Public methods
public void ParseIsoString(string isoStr) {...}
#endregion

#region System.Xml.Schema.IXmlSerializable Members
XmlSchema IXmlSerializable.GetSchema() {...}
void IXmlSerializable.WriteXml(XmlWriter writer) {...}
#endregion
}```

## Database storage/retrieving

A single coordinate or a coordinate collection can be stored in any text field type into a database. For storage, you have to prepare the query string using the proper `ToString()` method, as in the following example:

```// Single point example
string query =
string.Format("INSERT INTO PointMarks SET ID={0}, LOCATION='{1:ISO}'",
someID, coord);

// Coordinate list example
string query =
string.Format("INSERT INTO Boundaries SET ID={0}, POLYGON='{1}'",
someID, coord_list);```

Notice that when inserting a single coordinate, the "ISO" formatting should be used, since the `Coordinate.ToString()` method has many formatting options, as explained earlier.

To retrieve a single coordinate or coordinate collection, just create a new object and invoke the `ParseIsoString()` method, passing the string retrieved from the database, like in the following example. A `try/catch` block also will be recommendable, to avoid an unexpected exception.

```// Single point example
Coordinate coord = new Coordinate();
coord.ParseIsoString(iso);

// Coordinate list example
CoordinateList coord_list = new CoordinateList();
coord_list.ParseIsoString(iso);```

## The sample code

The supplied sample code in the `Program.Main()` method will do some little tasks to demonstrate the `Coordinate` and `CoordinateList` classes functionality. The solution file included with this article has been produced with Visual Studio 2008, so you won't be able to load directly from Visual Studio 2005, but you can create a new solution and attach the project file manually.

```static void Main()
{
// Create a new coordinate with some values
Coordinate c = new Coordinate();
c.SetDMS(00, 10, 20.8f, true, 30, 40, 50.9f, false);

// Create a new coordinate list with
CoordinateList cl = new CoordinateList();

// Show formatting capabilities
MessageBox.Show(
string.Format("String Formatting:\r\nD: {0:D}\r\nDM: {0:DM}\r\nDMS:" +
" {0:DMS}\r\nISO: {0:ISO}", c),
"Formatting example");

// Serialize coordinates to a string object
System.Text.StringBuilder sb = new System.Text.StringBuilder();
System.IO.StringWriter sw = new System.IO.StringWriter(sb);
string part1, part2;

// Single coordinate
System.Xml.Serialization.XmlSerializer xs =
new System.Xml.Serialization.XmlSerializer(typeof(Coordinate));
xs.Serialize(sw, c);
sw.Flush();
part1 = sb.ToString();

// Coordinate list
sb.Length = 0;
System.Xml.Serialization.XmlSerializer xsl =
new System.Xml.Serialization.XmlSerializer(typeof(CoordinateList));
xsl.Serialize(sw, cl);
sw.Flush();
part2 = sb.ToString();
sw.Close();

// Show serialized data
MessageBox.Show(string.Format("Single coordinate:\r\n{0}\r\n\r\n" +
"Coordinate list:\r\n{1}", part1, part2),
"Serialization example");

// Deserialize single coordinate from a string object
Coordinate c1 = (Coordinate)xs.Deserialize(sr);
sr.Close();

// Deserialize coordinate list from a string object
CoordinateList cl1 = (CoordinateList)xsl.Deserialize(sr);
sr.Close();

// Show properties of deserialized data
string message = string.Format(
"Single coordinate:\r\n{0}\r\nLatitude: {1}°\r\nLongitude:" +
" {2}°\r\n\r\nCoordinate list:\r\n",
c1, c1.Latitude, c1.Longitude);
foreach (Coordinate coord in cl1)
message += coord.ToString() + "\r\n";

MessageBox.Show(message, "Deserialization example");
}```

I have received many questions about why I use the `float` data type, instead of the `Int32`, `double`, or `decimal` data types. Here are some reasons about the use of `float` against `Int32` (expressed in milliseconds of degree or other scales):

• There is no meaningful advantage in storing size with an `Int32`, since `float` is 32-bits too.
• When you have to calculate coordinates components (D/M/S), you will need to convert to floating point anyway.
• When painting, it has to be converted to floating point.
• Also, float conversion will be needed to store in ISO 6709 format.
• If expressed in milliseconds, the code will be less legible due to continuous multiplication/division by 1000.

About `float` against `decimal`, which is four times larger than `float`, the main disadvantage, besides the obvious space consumption, is performance. Calculations with `decimal` are 20-30 times slower than with `float`. This is due to the fact that `decimal` is stored as base-10 by microprocessor, not as base-2 like all other data types, forcing it to do several converdions to perform any math calculation.

About `float` against `double`, there are concerns about precision that deserves a bigger explanation. First, I will mention the advantages:

• The `double` data type (64 bits) needs twice the space as `float` (32 bits).
• Half the space and half the time to load in memory.
• Precision is enough compared to GPS precision (5-10 meters).

About precision, you have to be aware of the internals of the `float` data type, as explained in Wikipedia (http://en.wikipedia.org/wiki/IEEE_754). It reserves 8 bits for the exponent and 23 bits for the mantissa. This means that a `float` value can contain 223 different values, besides the exponent. Now, let's go to the most pessimistic calculation:

A longitude with a maximum value of 180 degrees at the Equator that has 60 nautical miles per minute, when stored in a `float` variable, will have a precision of:

180 * 60 * 1,852 meters / 223 values = 20,001,600 / 8,388,608 = about 2.4 meters

But, the longitude magnitude will vary according to the latitude, with the equation decreasing up to zero at the poles. The precision will increase when the latitude is greater, according to the following chart:

The case of latitude is different. It doesn't vary in any zone of the earth, with a constant magnitude of 60 nautical miles per minute of degree, and a maximum value of 90 in the poles. The most pessimistic calculation for the latitude (near to the poles) will be:

90 * 60 * 1,852 meters / 223 values = 10,000,800 / 8,388,608 = about 1.2 meters

The following chart depicts the precision of latitude stored in a `float` variable, according to the latitude value:

So, we can conclude that in practical terms, we will have an average precision value fewer than a meter. This is far more precise than a GPS output, and suitable for most GIS applications up to street level.

## Class enhancements

There are a lot of things you can do to enhance this fundamental class, I will include some of the following in the next version:

• Altitude/depth element
• NMEA sentences parsing
• UTM system translation
• More geodesic calculations

## History

• Apr 29th, 2007
• First version.
• Jun 10th, 2007
• Fixed bug in `ToString()` method (thanks to Travis Butcher).
• Oct 23rd, 2007
• Fixed bug in the `string.Format()` example.
• Nov 07th, 2007
• Finally added the `CoordinateList` class.
• Nov 28th, 2007
• Fixed globalization bug (thanks to Antoine Polatouche).
• Nov 30th, 2007
• Added explanation about `float` values.
• June 08th, 2009

## You may also be interested in...

Architect Freelance (jaimeolivares.com)
Peru

Computer Electronics professional, Software Architect and senior Windows C++ and C# developer with experience in many other programming languages, platforms and application areas including communications, simulation systems, PACS/DICOM (radiology), GIS, 3D graphics and HTML5-based web applications.
Currently intensively working with Visual C# 2013 and TFS.
Can be reached at http://www.jaimeolivares.com

 First Prev Next
 GetHashCode Problem dmbrider 31-Jan-10 5:15
 Jamie,   Great work! This class has come to good use and the article is well written! I have re-written it a bit to make it a generic class, that is one that can either be a float or double, ala, Coordinate…   In doing this work, I came across an anomaly in your GetHashCode() implementation. It does not always return a unique hash code for an instance as one might expect, e.g.,   ISO_Classes.Coordinate coord1 = new ISO_Classes.Coordinate(50, -90); ISO_Classes.Coordinate coord2 = new ISO_Classes.Coordinate(-90, 50); Console.WriteLine(coord1.GetHashCode() == coord2.GetHashCode());   This will display true when in most cases one would expect false. Borrowing from Paul B.’s CodeProject article "Looking up items in HashTable/Dictionary objects that have multiple keys," I have rewritten the GetHashCode method as such (non-generic version):   ```public override int GetHashCode() { const int firstCoPrimeNumber = 17; //first co-prime number const int otherCoPrimeNumber = 37; //other co-prime number const int firstTimesOther = firstCoPrimeNumber * otherCoPrimeNumber;   unchecked { int finalHashCode = firstTimesOther + this.latitude.GetHashCode(); return finalHashCode * otherCoPrimeNumber + this.longitude.GetHashCode(); { }```   David
 Re: GetHashCode Problem Jaime Olivares 31-Jan-10 6:08
 Re: GetHashCode Problem dmbrider 31-Jan-10 6:35
 Why not use Decimal Brennan A. Fee 5-Jun-09 12:06
 Re: Why not use Decimal Jaime Olivares 7-Jun-09 19:30
 Nothing for Decimal Degree? Wes Jones 16-Apr-09 8:38
 Re: Nothing for Decimal Degree? Wes Jones 16-Apr-09 9:00
 Re: Nothing for Decimal Degree? Jaime Olivares 7-Jun-09 19:33
 Re: Nothing for Decimal Degree? Wes Jones 7-Jun-09 19:45
 Why class, not a struct? amx3000 27-Jan-08 19:41
 Re: Why class, not a struct? Jaime Olivares 28-Jan-08 18:03
 Re: Nice DarrenJames 21-Sep-08 13:46
 Re: Nice Jaime Olivares 21-Sep-08 15:12
 float ... gmt52 28-Nov-07 20:07
 Re: float ... Jaime Olivares 28-Nov-07 20:20
 Re: float ... Jaime Olivares 28-Nov-07 20:37
 Re: float ... Error in previous post Jaime Olivares 28-Nov-07 20:42
 Globalization polatouche 27-Nov-07 14:07
 Re: Globalization Jaime Olivares 27-Nov-07 14:21
 Re: Globalization polatouche 27-Nov-07 15:00
 Re: Globalization Jaime Olivares 28-Nov-07 11:38
 Re: Globalization polatouche 28-Nov-07 14:31
 UPDATED ARTICLE Jaime Olivares 1-Dec-07 2:48
 Store in milliseconds of degrees Pinx 30-Oct-07 13:28
 Re: Store in milliseconds of degrees Jaime Olivares 30-Oct-07 14:21
 UPDATED ARTICLE Jaime Olivares 1-Dec-07 2:47
 Good job! Just missing the Parse method. George Mamaladze 30-Jun-07 11:36
 Re: Good job! Just missing the Parse method. Jaime Olivares 1-Jul-07 16:11
 UPDATED ARTICLE Jaime Olivares 1-Dec-07 2:46
 .NET stores DateTime in Ticks, so... Pinx 14-Jun-07 23:37
 other projects gmt52 10-Jun-07 19:42
 Re: other projects Jaime Olivares 14-Jun-07 2:30
 seconds of degree Philip99 6-Jun-07 12:54
 Re: seconds of degree Jaime Olivares 10-Jun-07 16:29
 UPDATED ARTICLE Jaime Olivares 1-Dec-07 2:45
 Longitude Formatting error Travis Butcher 21-May-07 9:07
 Re: Longitude Formatting error Jaime Olivares 10-Jun-07 16:26
 UPDATED ARTICLE Jaime Olivares 1-Dec-07 2:44
 Thanks... Paul Selormey 2-May-07 11:25
 Re: Thanks... Jaime Olivares 6-May-07 11:44
 Re: Thanks... Jaime Olivares 10-Jun-07 16:43
 Re: Thanks... Paul Selormey 10-Jun-07 17:49
 UPDATED ARTICLE Jaime Olivares 1-Dec-07 2:43
 Re: UPDATED ARTICLE Paul Selormey 1-Dec-07 4:03
 Last Visit: 31-Dec-99 18:00     Last Update: 28-Aug-14 20:18 Refresh 1