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)
{
}
}
}
}