Click here to Skip to main content
Click here to Skip to main content
Add your own
alternative version
Go to top

Parsing Latitude and Longitude Information

, 21 Feb 2012
Parses user input and extracts latitude and longitude information, taking into account the user's language and regional settings
using System;
using System.Globalization;
using Geospatial;
using NUnit.Framework;

namespace UnitTests
{
    [TestFixture]
    public sealed class LocationTest // Needs to be public for the XmlSerializer test
    {
        private const double Delta = 0.000001;

        [Test]
        public void TestConstructors()
        {
            Assert.Throws<ArgumentNullException>(() => new Location(null, null));
            Assert.Throws<ArgumentNullException>(() => new Location(null, null, 0));
            Assert.Throws<ArgumentNullException>(() => new Location(Latitude.FromRadians(0), null));

            Assert.IsNull(new Location(Latitude.FromRadians(0), Longitude.FromRadians(0)).Altitude);
        }

        [Test]
        public void TestEquals()
        {
            Angle zero = Angle.FromRadians(0);
            Location location = new Location(new Latitude(zero), new Longitude(zero), 0);

            Assert.IsTrue(location == new Location(new Latitude(zero), new Longitude(zero), 0));
            Assert.IsTrue(location != new Location(new Latitude(zero), new Longitude(zero)));

            object box = new Location(new Latitude(zero), new Longitude(zero), 0);
            Assert.IsTrue(location.Equals(box));
            Assert.IsTrue((Location)null == null);
            Assert.IsFalse(location.Equals(null));
        }

        [Test]
        public void TestParseWithInvalidInput()
        {
            Location location;
            Assert.IsFalse(Location.TryParse(null, out location));
            Assert.IsFalse(Location.TryParse(string.Empty, out location));
            Assert.IsFalse(Location.TryParse("aaaa", out location));
            Assert.IsFalse(Location.TryParse("-48° 36° 12.20'", out location));
            Assert.IsFalse(Location.TryParse("-48°36°12.20'", out location));
            Assert.IsFalse(Location.TryParse("-48°N 36°12.20'W", out location));
            Assert.IsFalse(Location.TryParse("48°W 36°N", out location));
            Assert.IsFalse(Location.TryParse("NW", out location));
            Assert.IsFalse(Location.TryParse("12", out location));
            Assert.IsFalse(Location.TryParse("1,2", out location));
            Assert.IsFalse(Location.TryParse("XX° XX' XX\" N XX° XX' XX\" E", out location));
        }

        [Test]
        public void TestParse()
        {
            var english = CultureInfo.GetCultureInfo("en-GB");
            var spanish = CultureInfo.GetCultureInfo("es-ES"); // Use Spanish as it uses a comma for decimal numbers

            ParseTestCase[] cases =
            {
                // DMS format
                new ParseTestCase("6°12′18″N 6°12′18″E", 6, 12, 18, 6, 12, 18, null),
                new ParseTestCase("6°12'18\" 6°12'18\"", 6, 12, 18, 6, 12, 18, null),
                new ParseTestCase("6 12 18 6 12 18", 6, 12, 18, 6, 12, 18, null),
                new ParseTestCase("6 12' 18\", 6 12' 18\"", 6, 12, 18, 6, 12, 18, null),
                new ParseTestCase("+6° 12′ 18,1″, +6° 12′ 18,1″", 6, 12, 18.1, 6, 12, 18.1, null, spanish),
                new ParseTestCase("N6D12M18 E 6d 12m 18", 6, 12, 18, 6, 12, 18, null),
                new ParseTestCase("s 6 12' 18\" W 6 12' 18\"", -6, -12, -18, -6, -12, -18, null),
                new ParseTestCase("6 12 18S 6 12 18W", -6, -12, -18, -6, -12, -18, null),

                // DM format
                new ParseTestCase("6° 12.3', 6° 12.3'", 6, 12, 18, 6, 12, 18, null, english),
                new ParseTestCase("6°12,3', 6°12,3'", 6, 12, 18, 6, 12, 18, null, spanish),
                new ParseTestCase("-6° 12.3' -6° 12.3'", -6, -12, -18, -6, -12, -18, null, english),
                new ParseTestCase("6°12,3' S 6°12,3' W", -6, -12, -18, -6, -12, -18, null, spanish),

                // Decimal degrees
                new ParseTestCase("6.205°, 6.205°", 6, 12, 18, 6, 12, 18, null, english),
                new ParseTestCase("6,205, 6,205", 6, 12, 18, 6, 12, 18, null, spanish),
                new ParseTestCase("-6.205 -6.205", -6, -12, -18, -6, -12, -18, null, english),
                new ParseTestCase("6,205 S 6,205 W", -6, -12, -18, -6, -12, -18, null, spanish),
                new ParseTestCase("N6.205d E6.205D", 6, 12, 18, 6, 12, 18, null, english),
                new ParseTestCase("N 6.205* w 6.205°", 6, 12, 18, -6, -12, -18, null, english),

                // Check with some invalid signs to make sure that only the suffix
                // is used or only the sign of the degrees if no suffix is found
                // (i.e. ignore minutes/seconds)
                new ParseTestCase("-6.205° N, +6.205° W", 6, 12, 18, -6, -12, -18, null, english),
                new ParseTestCase("-6° +12.3', +6° -12.3'", -6, -12, -18, 6, 12, 18, null, english),
                new ParseTestCase("+6° -12' -18\" -6° +12' +18\"", 6, 12, 18, -6, -12, -18, null),
                new ParseTestCase("+6° -12' -18\" S -6° +12' +18\" E", -6, -12, -18, 6, 12, 18, null),

                // Check near the zero (previous bug S0* 1' W0* 1' would be parsed
                // as N0° 1' W0° 1'
                new ParseTestCase("0.205° S, 0.205° W", 0, -12, -18, 0, -12, -18, null, english),
                new ParseTestCase("-0° 12.3' -0° 12.3'", 0, -12, -18, 0, -12, -18, null, english),
            };

            foreach (var test in cases)
            {
                Location location;
                Assert.IsTrue(Location.TryParse(test.Input, test.Provider, out location));

                // Make sure it can parse it's own output
                Location parsed;
                Assert.IsTrue(Location.TryParse(location.ToString("DMS", spanish), spanish, out parsed));

                TestHelpers.AssertLocationsAreEqual(location, parsed);
                TestHelpers.AssertLocationsAreEqual(test.Location, location);
            }
        }

        [Test]
        public void TestParseIsoWithInvalidInput()
        {
            Location location;
            Assert.IsFalse(Location.TryParse(null, LocationStyles.Iso, null, out location));
            Assert.IsFalse(Location.TryParse(string.Empty, LocationStyles.Iso, null, out location));
            Assert.IsFalse(Location.TryParse("aaaa", LocationStyles.Iso, null, out location));
            Assert.IsFalse(Location.TryParse("+40/", LocationStyles.Iso, null, out location));
            Assert.IsFalse(Location.TryParse("4000/", LocationStyles.Iso, null, out location));
            Assert.IsFalse(Location.TryParse("+40121300-075001500/", LocationStyles.Iso, null, out location));
            Assert.IsFalse(Location.TryParse("+40121-075001/", LocationStyles.Iso, null, out location));
            Assert.IsFalse(Location.TryParse("/+4012-07500+123/", LocationStyles.Iso, null, out location));
            Assert.IsFalse(Location.TryParse("+4012-07500+123", LocationStyles.Iso, null, out location)); // No trailing '/'
            Assert.IsFalse(Location.TryParse("+900001-1800001", LocationStyles.Iso, null, out location)); // Valid format, but out of range
        }

        [Test]
        public void TestParseIso()
        {
            ParseTestCase[] cases =
            {
                new ParseTestCase("+40-075/", 40, 0, 0, -75, 0, 0, null),
                new ParseTestCase("+40-075+350/", 40, 0, 0, -75, 0, 0, 350),
                new ParseTestCase("+40.2041666667-075.0041666667/", 40, 12, 15, -75, 0, -15, null),
                new ParseTestCase("+4012-07500/", 40, 12, 0, -75, 0, 0, null),
                new ParseTestCase("+4012-07500-169.2/", 40, 12, 0, -75, 0, 0, -169.2),
                new ParseTestCase("+4012.25-07500.25/", 40, 12, 15, -75, 0, -15, null),
                new ParseTestCase("+4012.25-07500.25-169.2/", 40, 12, 15, -75, 0, -15, -169.2),
                new ParseTestCase("+4012.25-07500.25+350.517/", 40, 12, 15, -75, 0, -15, 350.517),
                new ParseTestCase("+401213-0750015/", 40, 12, 13, -75, 0, -15, null),
                new ParseTestCase("+401213-0750015+2.79/", 40, 12, 13, -75, 0, -15, 2.79),
                new ParseTestCase("+401213.1-0750015.1/", 40, 12, 13.1, -75, 0, -15.1, null),
                new ParseTestCase("+401213.1-0750015.1+2.79/", 40, 12, 13.1, -75, 0, -15.1, 2.79)
            };

            Location location;
            var spanish = CultureInfo.GetCultureInfo("es-ES"); // Use Spanish as it uses a comma for decimal numbers
            foreach (var test in cases)
            {
                // Make sure it ignores the CultureInfo for ISO
                Assert.IsTrue(Location.TryParse(test.Input, LocationStyles.Iso, spanish, out location));

                // Make sure it can parse it's own output
                Location parsed;
                Assert.IsTrue(Location.TryParse(location.ToString("ISO", spanish), LocationStyles.Iso, null, out parsed));

                TestHelpers.AssertLocationsAreEqual(location, parsed);
                TestHelpers.AssertLocationsAreEqual(location, test.Location);
            }
        }

        [Test]
        public void TestXmlSerialization()
        {
            var data = new XmlSerializerTestStruct
            {
                Location = TestHelpers.CreateLocation(12, 34, 56),
                Name = "Test Data"
            };
            var deserialized = TestHelpers.Serialize(data);

            Assert.AreEqual(data.Name, deserialized.Name);
            TestHelpers.AssertLocationsAreEqual(data.Location, deserialized.Location);

            // Don't know why, but XmlSerializer won't return a null object
            Assert.DoesNotThrow(() => TestHelpers.Serialize<Location>(null));
        }

        [Test]
        public void TestXmlSerializationArrays()
        {
            var data = new Location[]
            {
                TestHelpers.CreateLocation(0, 0),
                TestHelpers.CreateLocation(12, 34, 56),
                TestHelpers.CreateLocation(-65, -43, 21),
            };
            var deserialized = TestHelpers.Serialize(data);

            Assert.AreEqual(data.Length, deserialized.Length);
            for (int i = 0; i < data.Length; i++)
            {
                TestHelpers.AssertLocationsAreEqual(data[i], deserialized[i]);
            }
        }

        [Test]
        public void TestCourse()
        {
            var point = TestHelpers.CreateLocation(0, 0);
            Assert.AreEqual(0.0, point.Course(TestHelpers.CreateLocation(1, 0)).TotalDegrees);
            Assert.AreEqual(90.0, point.Course(TestHelpers.CreateLocation(0, 1)).TotalDegrees);
            Assert.AreEqual(180.0, point.Course(TestHelpers.CreateLocation(-1, 0)).TotalDegrees);
            Assert.AreEqual(-90.0, point.Course(TestHelpers.CreateLocation(0, -1)).TotalDegrees);
            Assert.AreEqual(-180.0, point.Course(TestHelpers.CreateLocation(-1, -0.0000001)).TotalDegrees, 0.0001);

            // Examples from the Aviation Formulary.
            var jfk = TestHelpers.CreateLocation(40.642480, -73.788071);
            var lax = TestHelpers.CreateLocation(33.944066, -118.408294);

            Assert.AreEqual(65.8687, lax.Course(jfk).TotalDegrees, 0.0001);
            Assert.AreEqual(-86.1617, jfk.Course(lax).TotalDegrees, 0.0001);
        }

        [Test]
        public void TestDistance()
        {
            // Check handling over the Meridian
            var west = TestHelpers.CreateLocation(0, -120);
            var east = TestHelpers.CreateLocation(0, 120);
            var center = TestHelpers.CreateLocation(0, 0);
            Assert.Throws<ArgumentNullException>(() => center.Distance(null));
            Assert.AreEqual(center.Distance(west), center.Distance(east), Delta);
            Assert.AreEqual(east.Distance(center), east.Distance(west), Delta);
            Assert.AreEqual(west.Distance(center), west.Distance(east), Delta);

            // Examples from the Aviation Formulary.
            var jfk = TestHelpers.CreateLocation(40.642480, -73.788071);
            var lax = TestHelpers.CreateLocation(33.944066, -118.408294);

            Assert.AreEqual(3970683.0, jfk.Distance(lax), 0.1);
            Assert.AreEqual(3970683.0, lax.Distance(jfk), 0.1); // Make sure it's the same distance either way around!

            var ground = TestHelpers.CreateLocation(0, 0, 0);
            Assert.AreEqual(0.0, ground.Distance(TestHelpers.CreateLocation(0, 0, 0)));
            Assert.AreEqual(1.0, ground.Distance(TestHelpers.CreateLocation(0, 0, 1)));
            Assert.AreEqual(1000.0, ground.Distance(TestHelpers.CreateLocation(0, 0, 1000)));

            // Put JFK 10,000 km in the sky...
            jfk = new Location(jfk.Latitude, jfk.Longitude, 1000000);

            // Because lax still doesn't have an altitude, the altitude of
            // jfk should be ignored
            Assert.AreEqual(3970683.0, jfk.Distance(lax), 0.1);

            lax = new Location(lax.Latitude, lax.Longitude, 0);
            Assert.AreEqual(4094670.171, jfk.Distance(lax), 0.1);
            Assert.AreEqual(4094670.171, lax.Distance(jfk), 0.1);
        }

        [Test]
        public void TestGetPoint()
        {
            var lax = TestHelpers.CreateLocation(33.944066, -118.408294);
            Assert.Throws<ArgumentNullException>(() => lax.GetPoint(0, null));

            var result = lax.GetPoint(185200.0, Angle.FromDegrees(66.0));
            Assert.AreEqual(34.608154, result.Latitude.TotalDegrees, 0.000001);
            Assert.AreEqual(-116.558327, result.Longitude.TotalDegrees, 0.000001);
        }

        [Test]
        public void TestToString()
        {
            Location location1 = new Location(Latitude.FromDegrees(1.1), Longitude.FromDegrees(-2.2));
            Location location2 = new Location(Latitude.FromDegrees(1.1), Longitude.FromDegrees(-2.2), 3.3);

            Assert.AreEqual("1,1\u00B0 N 2,2\u00B0 W", location1.ToString("D", CultureInfo.GetCultureInfo("es-ES")));
            Assert.AreEqual("1,1\u00B0 N 2,2\u00B0 W 3,3m", location2.ToString("D", CultureInfo.GetCultureInfo("es-ES")));

            Assert.AreEqual("1.1\u00B0 N 2.2\u00B0 W", location1.ToString("D", CultureInfo.InvariantCulture));
            Assert.AreEqual("1.1\u00B0 N 2.2\u00B0 W 3.3m", location2.ToString("D", CultureInfo.InvariantCulture));

            Assert.AreEqual("+010600-0021200/", location1.ToString("ISO", CultureInfo.GetCultureInfo("es-ES")));
            Assert.AreEqual("+010600-0021200+3.3/", location2.ToString("ISO", CultureInfo.GetCultureInfo("es-ES")));

            Assert.Throws<ArgumentException>(() => location1.ToString("invalid", null));
        }

        // Can't use anonymous classes as they don't have a parameterless constructor
        public struct XmlSerializerTestStruct
        {
            public Location Location;
            public string Name;
        }

        private struct ParseTestCase
        {
            public string Input;
            public Location Location;
            public IFormatProvider Provider;

            public ParseTestCase(string input, Angle lat, Angle lon, double? alt, IFormatProvider provider)
            {
                this.Input = input;
                this.Provider = provider;
                if (alt.HasValue)
                {
                    this.Location = new Location(new Latitude(lat), new Longitude(lon), alt.Value);
                }
                else
                {
                    this.Location = new Location(new Latitude(lat), new Longitude(lon));
                }
            }

            public ParseTestCase(string input, int latD, int latM, double latS, int lonD, int lonM, double lonS, double? alt, IFormatProvider provider = null)
                : this(input, Angle.FromDegrees(latD, latM, latS), Angle.FromDegrees(lonD, lonM, lonS), alt, provider)
            {
            }
        }
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

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

Share

About the Author

Samuel Cragg

United Kingdom United Kingdom
No Biography provided

| Advertise | Privacy | Mobile
Web04 | 2.8.140921.1 | Last Updated 21 Feb 2012
Article Copyright 2011 by Samuel Cragg
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid