5,699,997 members and growing! (17,163 online)
Email Password   helpLost your password?
Languages » C# » General     Intermediate License: The Code Project Open License (CPOL)

Coordinate and CoordinateList classes - ISO 6709 compliant

By Jaime Olivares

Classes to store, handle and retrieve geodesic coordinates, in memory, database and XML, according to ISO 6709 standard
C# (C# 2.0, C# 3.0, C#), XML, .NET CF, .NET (.NET, .NET 3.5, .NET 3.0, .NET 2.0, Mono, DotGNU), SQL Server, VS2008, Visual Studio, Architect, DBA, Dev, Design

Posted: 2 May 2007
Updated: 25 Jan 2008
Views: 34,545
Bookmarked: 74 times
Announcements
Loading...



Search    
Advanced Search
Sitemap
21 votes for this Article.
Popularity: 5.81 Rating: 4.39 out of 5
2 votes, 9.5%
1
0 votes, 0.0%
2
1 vote, 4.8%
3
4 votes, 19.0%
4
14 votes, 66.7%
5
Note: This is an unedited contribution. If this article is inappropriate, needs attention or copies someone else's work without reference then please Report This Article

Download Coordinate_CS.zip - 3 KB - Coordinate.cs class source code
Download Coordinate_Test.zip - 75 KB - Test project and executable


Screenshot - MsgBox_1.jpgScreenshot - MsgBox_2.jpgScreenshot - MsgBox_3.jpg

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 them 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 ISO 6709 standard (Annex H - Text string representation), in concordance with World Wide Web Consortium's LatitudeLongitudeAltitude workgroup.

Background

Previous readings:

  • 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 2 main components: latitude (aka "y") and longitude (aka "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 contraints 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 cummulative loose 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 exception of decimal of seconds, avoiding the annoying loose 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

.Net Framework provides facilities to easily store and retrieve data in XML format, if you associate a 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 near 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 a first glance you will notice a storage saving of more than 50%. Also notice that, besides the fact 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 convertion formulas. Now you have the best of both worlds.

Coordinate class implementation

First part of the Coordinate class implementation has the following fields and properties declarations:

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

    #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); them can be accesed by 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

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

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

Next methods in 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 coordinate expressed in degrees, minutes and seconds. There will be more implementations of this method. Equals() compares both latitude and longitude values for equality, and GetHashCode() returns a hash value based on lat/lon values. Next in code implement 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

Due to IFormattable.ToString() method accepts a second argument that is unused in this implementation (formatProvider), an abreviated 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 empty or null formatting string will output the default version. But still we have not seen the best feature of IFormattable interface; we can embed the formatting string inside a more complex formatting case, by example:

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

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

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

You don't have to invoke these functions directly, them will be invoked by any serialization function, as you can find in the test source code bellow. WriteXml() will produce XML code 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 CoordinateList class is quiet easy. 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.ReadXml(XmlReader reader) {...}
   void IXmlSerializable.WriteXml(XmlWriter writer) {...}
   #endregion
}

Database storage/retrieving

A single coodinate 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 when inserting a single coordinate, the "ISO" formatting should be used, since 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
string iso = datareader["LOCATION"].ToString(); 
Coordinate coord = new Coordinate();
coord.ParseIsoString(iso);

// Coordinate list example
string iso = datareader["POLYGON"].ToString(); 
CoordinateList coord_list = new CoordinateList();
coord_list.ParseIsoString(iso);

The sample code

Supplied sample code at Program.Main() method, will do some little tasks to demonstrate 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();
   cl.Add(new Coordinate( 1.0f, 1.0f));
   cl.Add(new Coordinate(-2.4f, 1.5f));
   cl.Add(new Coordinate(-3.7f, 2.2f));
   cl.Add(new Coordinate(-2.0f, -.5f));

   // 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\nCoordinate list:\r\n{1}", part1, part2), 
       "Serialization example");

   // Deserialize single coordinate from a string object
   System.IO.StringReader sr = new System.IO.StringReader(part1);
   Coordinate c1 = (Coordinate)xs.Deserialize(sr);
   sr.Close();

   // Deserialize coordinate list from a string object
   sr = new System.IO.StringReader(part2);
   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");
}

Addendum: Why to use float?

I have receive many comments about why to use float data type, instead of Int32 or double datatypes. Here are some reasons about the use of float against Int32 (expressed in miliseconds of degree or other scale):

  • There is not meaningful advantage in store size with an Int32, since float is 32-bits too.
  • Neither about performance, 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 too.
  • Also float conversion will be needed to store in ISO 6709 format.
  • If expressed in miliseconds, code will be less legible due to continuos multiplication/division by 1000.

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

  • The double datatype (32 bits) needs the twice of space than float (32 bits).
  • The half space, the half 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 datatype, as explained at 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. Then let's go to the most pesimistic calculation:

A longitude with a maximum value of 180 degrees, at the Equator 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 longitude magnitude will vary according to latitude, with the equation decreasing up to zero at the poles. Then precision will increase when latitude is greater, according to the following chart:

Screenshot - Longitude_precision.png

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 pesimistic calculation for 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, acording to latitude value:

Screenshot - Latitude_precision.png

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

Class enhacements

There are 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 23th, 2007 Fixed bug in string.Format() example
Nov 07th, 2007 Finally added CoordinateList class
Nov 28th, 2007 Added database examples
Fixed globalization bug (thanks to Antoine Polatouche)
Nov 30th, 2007 Added explanation about float values

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Jaime Olivares




Computer Electronics professional and senior Windows C++ and C# developer with experience in many other programming languages, platforms and application areas including communications, simulation systems, GIS, graphics and mobile issues.
Also have experience in electronic interfaces development, specially for military applications.
Currently intensively working with Visual C# 2008.
Top-100 contributor at Experts-Exchange forum.
If you have an interesting project, you can contact him at: jaimeolivares.com
Occupation: Software Developer (Senior)
Company: Freelance contractor
Location: Peru Peru

Other popular C# articles:

Article Top
Sign Up to vote for this article
You must Sign In to use this message board.
FAQ FAQ Noise ToleranceSearch Search Messages 
 Layout  Per page   
 Msgs 1 to 25 of 41 (Total in Forum: 41) (Refresh)FirstPrevNext
GeneralWhy class, not a struct?memberamx300020:41 27 Jan '08  
AnswerRe: Why class, not a struct?memberJaime Olivares19:03 28 Jan '08  
GeneralNicememberPaul Conrad7:55 27 Jan '08  
GeneralRe: NicememberDarrenJames14:46 21 Sep '08  
GeneralRe: NicememberJaime Olivares16:12 21 Sep '08  
Generalfloat ...membergmt5221:07 28 Nov '07  
GeneralRe: float ...memberJaime Olivares21:20 28 Nov '07  
GeneralRe: float ...memberJaime Olivares21:37 28 Nov '07  
GeneralRe: float ... Error in previous postmemberJaime Olivares21:42 28 Nov '07  
GeneralGlobalizationmemberpolatouche15:07 27 Nov '07  
AnswerRe: GlobalizationmemberJaime Olivares15:21 27 Nov '07  
GeneralRe: Globalizationmemberpolatouche16:00 27 Nov '07  
AnswerRe: GlobalizationmemberJaime Olivares12:38 28 Nov '07  
GeneralRe: Globalizationmemberpolatouche15:31 28 Nov '07  
GeneralFormulae for calculating distances and general observationsmemberMirko Pontrelli1:34 15 Nov '07  
AnswerRe: Formulae for calculating distances and general observationsmemberJaime Olivares2:38 15 Nov '07  
GeneralRe: Formulae for calculating distances and general observationsmemberMirko Pontrelli3:23 15 Nov '07  
GeneralRe: Formulae for calculating distances and general observationsmemberJaime Olivares3:33 15 Nov '07  
GeneralRe: Formulae for calculating distances and general observationsmemberMirko Pontrelli3:57 15 Nov '07  
NewsUPDATED ARTICLEmemberJaime Olivares3:48 1 Dec '07  
QuestionStore in milliseconds of degreesmemberPinx14:28 30 Oct '07