diff --git a/OpenLocationCode.Test/DecodingTest.cs b/OpenLocationCode.Test/DecodingTest.cs new file mode 100644 index 0000000..dbc01ae --- /dev/null +++ b/OpenLocationCode.Test/DecodingTest.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using Google.OpenLocationCode; +using NUnit.Framework; + +public static class DecodingTest { + + private const double Precision = 1e-10; + + // Test cases for decoding valid codes into code areas + // https://github.com/google/open-location-code/blob/master/test_data/decoding.csv + private static readonly IEnumerable DecodingTestCases = TestDataUtils.ReadTestData("decoding.csv"); + + + public class TheDecodeMethod { + [Test] + public void ShouldDecodeFullCodesToExpectedCodeArea() { + foreach (var testData in DecodingTestCases) { + AssertExpectedDecodedArea(testData, OpenLocationCode.Decode(testData.Code)); + } + } + + [Test] + public void ShouldDecodeFullCodesWithLowercaseCharactersToExpectedCodeArea() { + foreach (var testData in DecodingTestCases) { + AssertExpectedDecodedArea(testData, OpenLocationCode.Decode(testData.Code.ToLower())); + } + } + + [Test] + public void ShouldDecodeToCodeAreaWithValidContainmentRelation() { + foreach (var testData in DecodingTestCases) { + var olc = new OpenLocationCode(testData.Code); + var decoded = olc.Decode(); + Assert.True(decoded.Contains(decoded.CenterLatitude, decoded.CenterLongitude), + $"Containment relation is broken for the decoded middle point of code {testData.Code}"); + Assert.True(decoded.Contains(decoded.SouthLatitude, decoded.WestLongitude), + $"Containment relation is broken for the decoded bottom left corner of code {testData.Code}"); + Assert.False(decoded.Contains(decoded.NorthLatitude, decoded.EastLongitude), + $"Containment relation is broken for the decoded top right corner of code {testData.Code}"); + Assert.False(decoded.Contains(decoded.SouthLatitude, decoded.EastLongitude), + $"Containment relation is broken for the decoded bottom right corner of code {testData.Code}"); + Assert.False(decoded.Contains(decoded.NorthLatitude, decoded.WestLongitude), + $"Containment relation is broken for the decoded top left corner of code {testData.Code}"); + } + } + + [Test] + public void ShouldDecodeToCodeAreaWithExpectedDimensions() { + Assert.AreEqual(20.0, OpenLocationCode.Decode("67000000+").LongitudeWidth, 0); + Assert.AreEqual(20.0, OpenLocationCode.Decode("67000000+").LatitudeHeight, 0); + Assert.AreEqual(1.0, OpenLocationCode.Decode("67890000+").LongitudeWidth, 0); + Assert.AreEqual(1.0, OpenLocationCode.Decode("67890000+").LatitudeHeight, 0); + Assert.AreEqual(0.05, OpenLocationCode.Decode("6789CF00+").LongitudeWidth, 0); + Assert.AreEqual(0.05, OpenLocationCode.Decode("6789CF00+").LatitudeHeight, 0); + Assert.AreEqual(0.0025, OpenLocationCode.Decode("6789CFGH+").LongitudeWidth, 0); + Assert.AreEqual(0.0025, OpenLocationCode.Decode("6789CFGH+").LatitudeHeight, 0); + Assert.AreEqual(0.000125, OpenLocationCode.Decode("6789CFGH+JM").LongitudeWidth, 0); + Assert.AreEqual(0.000125, OpenLocationCode.Decode("6789CFGH+JM").LatitudeHeight, 0); + Assert.AreEqual(0.00003125, OpenLocationCode.Decode("6789CFGH+JMP").LongitudeWidth, 0); + Assert.AreEqual(0.000025, OpenLocationCode.Decode("6789CFGH+JMP").LatitudeHeight, 0); + } + + [Test] + public void ShouldThrowArgumentExceptionForInvalidOrShortCodes() { + foreach (string code in new[] { null, "INVALID", "9QCJ+2VX" }) { + Assert.Throws(() => OpenLocationCode.Decode(code), + $"Expected exception was not thrown for code {code}"); + } + } + + } + + public class TheDecodeConstructor { + [Test] + public void ShouldAcceptFullCodesAndDecodeToExpectedCodeArea() { + foreach (var testData in DecodingTestCases) { + AssertExpectedDecodedArea(testData, new OpenLocationCode(testData.Code).Decode()); + } + } + + [Test] + public void ShouldNormalizeFullCodeDigitsToExpectedCode() { + foreach (var testData in DecodingTestCases) { + string codeDigits = OpenLocationCode.TrimCode(testData.Code); + Assert.AreEqual(testData.Code, new OpenLocationCode(codeDigits).Code, + $"Wrong code normalized from code digits {codeDigits}"); + } + } + + [Test] + public void ShouldTrimCodesIntoToExpectedCodeDigits() { + foreach (var testData in DecodingTestCases) { + Assert.AreEqual(OpenLocationCode.TrimCode(testData.Code), new OpenLocationCode(testData.Code).CodeDigits, + $"Wrong digits trimmed from code {testData.Code}."); + } + } + + [Test] + public void ShouldThrowArgumentExceptionForInvalidOrShortCodes() { + foreach (string code in new[] { null, "INVALID", "9QCJ+2VX" }) { + Assert.Throws(() => new OpenLocationCode(code), + $"Expected exception was not thrown for code {code}"); + } + } + } + + private static void AssertExpectedDecodedArea(TestData testData, CodeArea decoded) { + Assert.AreEqual(testData.Length, decoded.CodeLength, $"Wrong length for code {testData.Code}"); + Assert.AreEqual(testData.LatLo, decoded.SouthLatitude, Precision, $"Wrong low latitude for code {testData.Code}"); + Assert.AreEqual(testData.LatHi, decoded.NorthLatitude, Precision, $"Wrong high latitude for code {testData.Code}"); + Assert.AreEqual(testData.LngLo, decoded.WestLongitude, Precision, $"Wrong low longitude for code {testData.Code}"); + Assert.AreEqual(testData.LngHi, decoded.EastLongitude, Precision, $"Wrong high longitude for code {testData.Code}"); + } + + + public class TestData { + + public string Code { get; set; } + public int Length { get; set; } + public double LatLo { get; set; } + public double LngLo { get; set; } + public double LatHi { get; set; } + public double LngHi { get; set; } + + } +} diff --git a/OpenLocationCode.Test/EncodingTest.cs b/OpenLocationCode.Test/EncodingTest.cs index 5ab8be1..d7ca819 100644 --- a/OpenLocationCode.Test/EncodingTest.cs +++ b/OpenLocationCode.Test/EncodingTest.cs @@ -5,37 +5,17 @@ public static class EncodingTest { - // Test cases for encoding latitude and longitude to codes and expected - // https://github.com/google/open-location-code/blob/master/test_data/encodingTests.csv - private static readonly List TestDataList = new List { - new TestData("7FG49Q00+", "7FG49Q", 20.375, 2.775, 20.35, 2.75, 20.4, 2.8), - new TestData("7FG49QCJ+2V", "7FG49QCJ2V", 20.3700625, 2.7821875, 20.37, 2.782125, 20.370125, 2.78225), - new TestData("7FG49QCJ+2VX", "7FG49QCJ2VX", 20.3701125, 2.782234375, 20.3701, 2.78221875, 20.370125, 2.78225), - new TestData("7FG49QCJ+2VXGJ", "7FG49QCJ2VXGJ", 20.3701135, 2.78223535156, 20.370113, 2.782234375, 20.370114, 2.78223632813), - new TestData("8FVC2222+22", "8FVC222222", 47.0000625, 8.0000625, 47.0, 8.0, 47.000125, 8.000125), - new TestData("4VCPPQGP+Q9", "4VCPPQGPQ9", -41.2730625, 174.7859375, -41.273125, 174.785875, -41.273, 174.786), - new TestData("62G20000+", "62G2", 0.5, -179.5, 0.0, -180.0, 1, -179), - new TestData("22220000+", "2222", -89.5, -179.5, -90, -180, -89, -179), - new TestData("7FG40000+", "7FG4", 20.5, 2.5, 20.0, 2.0, 21.0, 3.0), - new TestData("22222222+22", "2222222222", -89.9999375, -179.9999375, -90.0, -180.0, -89.999875, -179.999875), - new TestData("6VGX0000+", "6VGX", 0.5, 179.5, 0, 179, 1, 180), - new TestData("6FH32222+222", "6FH32222222", 1, 1, 1, 1, 1.000025, 1.00003125), - // Special cases over 90 latitude and 180 longitude - new TestData("CFX30000+", "CFX3", 90, 1, 89, 1, 90, 2), - new TestData("CFX30000+", "CFX3", 92, 1, 89, 1, 90, 2), - new TestData("62H20000+", "62H2", 1, 180, 1, -180, 2, -179), - new TestData("62H30000+", "62H3", 1, 181, 1, -179, 2, -178), - new TestData("CFX3X2X2+X2", "CFX3X2X2X2", 90, 1, 89.9998750, 1, 90, 1.0001250), - // Test non-precise latitude/longitude value - new TestData("6FH56C22+22", "6FH56C2222", 1.2, 3.4, 1.2000000000000028, 3.4000000000000057, 1.2001249999999999, 3.4001250000000027) - }; + // Test cases for encoding latitude and longitude to codes + // https://github.com/google/open-location-code/blob/master/test_data/encoding.csv + private static readonly IEnumerable EncodingTestCases = TestDataUtils.ReadTestData("encoding.csv"); + public class TheEncodeMethod { [Test] public void ShouldEncodePointToExpectedLocationCode() { - foreach (var testData in TestDataList) { - Assert.AreEqual(testData.Code, OpenLocationCode.Encode(testData.EncodedLatitude, testData.EncodedLongitude, testData.CodeDigits.Length), - $"Latitude {testData.EncodedLatitude} and longitude {testData.EncodedLongitude} were wrongly encoded."); + foreach (var testData in EncodingTestCases) { + Assert.AreEqual(testData.Code, OpenLocationCode.Encode(testData.Latitude, testData.Longitude, testData.Length), + $"Latitude {testData.Latitude} and longitude {testData.Longitude} were wrongly encoded."); } } @@ -57,154 +37,39 @@ public void ShouldClipCoordinatesWhenExceedingMaximum() { public void ShouldLimitCodeLengthWhenExceedingMaximum() { string code = OpenLocationCode.Encode(51.3701125, -10.202665625, 1000000); - Assert.AreEqual(code.Length, OpenLocationCode.MaxCodeLength + 1, - "Encoded code should have a length of MaxCodeLength + 1 for the plus symbol"); + Assert.AreEqual(code.Length, 16, + "Encoded code should have a length of 16 (15 + 1 for the plus symbol)"); } } public class TheEncodeConstructor { [Test] public void ShouldEncodePointToExpectedLocationCode() { - foreach (var testData in TestDataList) { - OpenLocationCode olc = new OpenLocationCode(testData.EncodedLatitude, testData.EncodedLongitude, testData.CodeDigits.Length); + foreach (var testData in EncodingTestCases) { + OpenLocationCode olc = new OpenLocationCode(testData.Latitude, testData.Longitude, testData.Length); Assert.AreEqual(testData.Code, olc.Code, - $"Wrong code enocded for latitude {testData.EncodedLatitude} and longitude {testData.EncodedLongitude}."); - } - } - [Test] - public void ShouldTrimCodesIntoToExpectedCodeDigits() { - foreach (var testData in TestDataList) { - OpenLocationCode olc = new OpenLocationCode(testData.EncodedLatitude, testData.EncodedLongitude, testData.CodeDigits.Length); - Assert.AreEqual(testData.CodeDigits, olc.CodeDigits, - $"Wrong digits trimmed for encoded latitude {testData.EncodedLatitude} and longitude {testData.EncodedLongitude}."); - } - } - } - - - public class TheDecodeMethod { - [Test] - public void ShouldDecodeFullCodesToExpectedCodeArea() { - foreach (var testData in TestDataList) { - AssertExpectedDecodedArea(testData, OpenLocationCode.Decode(testData.Code)); - } - } - - [Test] - public void ShouldDecodeFullCodesWithLowercaseCharactersToExpectedCodeArea() { - foreach (var testData in TestDataList) { - AssertExpectedDecodedArea(testData, OpenLocationCode.Decode(testData.Code.ToLower())); - } - } - - [Test] - public void ShouldDecodeToCodeAreaWithValidContainmentRelation() { - foreach (var testData in TestDataList) { - var olc = new OpenLocationCode(testData.Code); - var decoded = olc.Decode(); - Assert.True(decoded.Contains(decoded.CenterLatitude, decoded.CenterLongitude), - $"Containment relation is broken for the decoded middle point of code {testData.Code}"); - Assert.True(decoded.Contains(decoded.SouthLatitude, decoded.WestLongitude), - $"Containment relation is broken for the decoded bottom left corner of code {testData.Code}"); - Assert.False(decoded.Contains(decoded.NorthLatitude, decoded.EastLongitude), - $"Containment relation is broken for the decoded top right corner of code {testData.Code}"); - Assert.False(decoded.Contains(decoded.SouthLatitude, decoded.EastLongitude), - $"Containment relation is broken for the decoded bottom right corner of code {testData.Code}"); - Assert.False(decoded.Contains(decoded.NorthLatitude, decoded.WestLongitude), - $"Containment relation is broken for the decoded top left corner of code {testData.Code}"); - } - } - - [Test] - public void ShouldDecodeToCodeAreaWithExpectedDimensions() { - Assert.AreEqual(20.0, OpenLocationCode.Decode("67000000+").LongitudeWidth, 0); - Assert.AreEqual(20.0, OpenLocationCode.Decode("67000000+").LatitudeHeight, 0); - Assert.AreEqual(1.0, OpenLocationCode.Decode("67890000+").LongitudeWidth, 0); - Assert.AreEqual(1.0, OpenLocationCode.Decode("67890000+").LatitudeHeight, 0); - Assert.AreEqual(0.05, OpenLocationCode.Decode("6789CF00+").LongitudeWidth, 0); - Assert.AreEqual(0.05, OpenLocationCode.Decode("6789CF00+").LatitudeHeight, 0); - Assert.AreEqual(0.0025, OpenLocationCode.Decode("6789CFGH+").LongitudeWidth, 0); - Assert.AreEqual(0.0025, OpenLocationCode.Decode("6789CFGH+").LatitudeHeight, 0); - Assert.AreEqual(0.000125, OpenLocationCode.Decode("6789CFGH+JM").LongitudeWidth, 0); - Assert.AreEqual(0.000125, OpenLocationCode.Decode("6789CFGH+JM").LatitudeHeight, 0); - Assert.AreEqual(0.00003125, OpenLocationCode.Decode("6789CFGH+JMP").LongitudeWidth, 0); - Assert.AreEqual(0.000025, OpenLocationCode.Decode("6789CFGH+JMP").LatitudeHeight, 0); - } - - [Test] - public void ShouldThrowArgumentExceptionForInvalidOrShortCodes() { - foreach (string code in new[] { null, "INVALID", "9QCJ+2VX" }) { - Assert.Throws(() => OpenLocationCode.Decode(code), - $"Expected exception was not thrown for code {code}"); - } - } - - } - - public class TheDecodeConstructor { - [Test] - public void ShouldAcceptFullCodesAndDecodeToExpectedCodeArea() { - foreach (var testData in TestDataList) { - AssertExpectedDecodedArea(testData, new OpenLocationCode(testData.Code).Decode()); - } - } - - [Test] - public void ShouldNormalizeFullCodeDigitsToExpectedCode() { - foreach (var testData in TestDataList) { - Assert.AreEqual(testData.Code, new OpenLocationCode(testData.CodeDigits).Code, - $"Wrong code normalized from code digits {testData.CodeDigits}"); + $"Wrong code encoded for latitude {testData.Latitude} and longitude {testData.Longitude}."); } } - [Test] public void ShouldTrimCodesIntoToExpectedCodeDigits() { - foreach (var testData in TestDataList) { - Assert.AreEqual(testData.CodeDigits, new OpenLocationCode(testData.Code).CodeDigits, - $"Wrong digits trimmed from code {testData.Code}."); - } - } - - [Test] - public void ShouldThrowArgumentExceptionForInvalidOrShortCodes() { - foreach (string code in new[] { null, "INVALID", "9QCJ+2VX" }) { - Assert.Throws(() => new OpenLocationCode(code), - $"Expected exception was not thrown for code {code}"); + foreach (var testData in EncodingTestCases) { + OpenLocationCode olc = new OpenLocationCode(testData.Latitude, testData.Longitude, testData.Length); + int expectedLength = Math.Min(testData.Length, 15); + Assert.AreEqual(expectedLength, olc.CodeDigits.Length, + $"Wrong length of digits trimmed for encoded latitude {testData.Latitude} and longitude {testData.Longitude}."); + Assert.AreEqual(testData.Code.Replace("+", "").Substring(0, expectedLength), olc.CodeDigits, + $"Wrong digits trimmed for encoded latitude {testData.Latitude} and longitude {testData.Longitude}."); } } } - private static void AssertExpectedDecodedArea(TestData testData, CodeArea decodedArea) { - Assert.True(IsNear(testData.DecodedArea.SouthLatitude, decodedArea.SouthLatitude), - $"Wrong decoded low latitude for code {testData.Code}"); - Assert.True(IsNear(testData.DecodedArea.NorthLatitude, decodedArea.NorthLatitude), - $"Wrong decoded high latitude for code {testData.Code}"); - Assert.True(IsNear(testData.DecodedArea.WestLongitude, decodedArea.WestLongitude), - $"Wrong decoded low longitude for code {testData.Code}"); - Assert.True(IsNear(testData.DecodedArea.EastLongitude, decodedArea.EastLongitude), - $"Wrong decoded high longitude for code {testData.Code}"); - } - - private static bool IsNear(double a, double b) { - return Math.Abs(a - b) < 1e-10; - } - - - private struct TestData { - - internal TestData(string code, string codeDigits, double lat, double lon, double decodedMinLat, double decodedMinLon, double decodedMaxLat, double decodedMaxLon) { - Code = code; - CodeDigits = codeDigits; - EncodedLatitude = lat; - EncodedLongitude = lon; - DecodedArea = new CodeArea(decodedMinLat, decodedMinLon, decodedMaxLat, decodedMaxLon); - } + public class TestData { - internal string Code { get; } - internal string CodeDigits { get; } - internal double EncodedLatitude { get; } - internal double EncodedLongitude { get; } - internal CodeArea DecodedArea { get; } + public double Latitude { get; set; } + public double Longitude { get; set; } + public int Length { get; set; } + public string Code { get; set; } } } diff --git a/OpenLocationCode.Test/OpenLocationCode.Test.csproj b/OpenLocationCode.Test/OpenLocationCode.Test.csproj index 58cdf19..12a41c5 100644 --- a/OpenLocationCode.Test/OpenLocationCode.Test.csproj +++ b/OpenLocationCode.Test/OpenLocationCode.Test.csproj @@ -7,6 +7,7 @@ + diff --git a/OpenLocationCode.Test/ShorteningTest.cs b/OpenLocationCode.Test/ShorteningTest.cs index b09f73a..1364cfe 100644 --- a/OpenLocationCode.Test/ShorteningTest.cs +++ b/OpenLocationCode.Test/ShorteningTest.cs @@ -5,50 +5,23 @@ public static class ShorteningTest { - // Test cases for shortening full codes and recovering short codes. - // test_type is R for recovery only, S for shorten only, or B for both. - // See: https://github.com/google/open-location-code/blob/master/test_data/shortCodeTests.csv - private static readonly List TestDataList = new List { - new TestData("9C3W9QCJ+2VX", 51.3701125, -1.217765625, "+2VX", "B"), - // Adjust so we can't trim by 8 (+/- .000755) - new TestData("9C3W9QCJ+2VX", 51.3708675, -1.217765625, "CJ+2VX", "B"), - new TestData("9C3W9QCJ+2VX", 51.3693575, -1.217765625, "CJ+2VX", "B"), - new TestData("9C3W9QCJ+2VX", 51.3701125, -1.218520625, "CJ+2VX", "B"), - new TestData("9C3W9QCJ+2VX", 51.3701125, -1.217010625, "CJ+2VX", "B"), - // Adjust so we can't trim by 6 (+/- .0151) - new TestData("9C3W9QCJ+2VX", 51.3852125, -1.217765625, "9QCJ+2VX", "B"), - new TestData("9C3W9QCJ+2VX", 51.3550125, -1.217765625, "9QCJ+2VX", "B"), - new TestData("9C3W9QCJ+2VX", 51.3701125, -1.232865625, "9QCJ+2VX", "B"), - new TestData("9C3W9QCJ+2VX", 51.3701125, -1.202665625, "9QCJ+2VX", "B"), - // Added to detect error in recoverNearest functionality - new TestData("8FJFW222+", 42.899, 9.012, "22+", "B"), - new TestData("796RXG22+", 14.95125, -23.5001, "22+", "B"), - // Reference location is in the 4 digit cell to the south. - new TestData("8FVC2GGG+GG", 46.976, 8.526, "2GGG+GG", "B"), - // Reference location is in the 4 digit cell to the north. - new TestData("8FRCXGGG+GG", 47.026, 8.526, "XGGG+GG", "B"), - // Reference location is in the 4 digit cell to the east. - new TestData("8FR9GXGG+GG", 46.526, 8.026, "GXGG+GG", "B"), - // Reference location is in the 4 digit cell to the west. - new TestData("8FRCG2GG+GG", 46.526, 7.976, "G2GG+GG", "B"), - // Added to detect errors recovering codes near the poles. - // This tests recovery function, but these codes won't shorten. - new TestData("CFX22222+22", 89.6, 0.0, "2222+22", "R"), - new TestData("2CXXXXXX+XX", -81.0, 0.0, "XXXXXX+XX", "R") - }; + // Test cases for validating codes and determining code type + // https://github.com/google/open-location-code/blob/master/test_data/validityTests.csv + private static readonly IEnumerable ShorteningTestCases = TestDataUtils.ReadTestData("shortening.csv"); + public class TheShortenMethod { [Test] public void ShouldShortenFullCodeToShortCodeFromReferencePoint() { - foreach (var testData in TestDataList) { - if (testData.TestType != "B" && testData.TestType != "S") { + foreach (var testData in ShorteningTestCases) { + if (testData.TestType != 'B' && testData.TestType != 'S') { continue; } OpenLocationCode.ShortCode shortened = OpenLocationCode.Shorten(testData.Code, - testData.ReferenceLatitude, testData.ReferenceLongitude); + testData.Latitude, testData.Longitude); Assert.AreEqual(testData.ShortCode, shortened.Code, - $"Wrong shortening of code {testData.Code} from reference latitude {testData.ReferenceLatitude} and longitude {testData.ReferenceLongitude}."); + $"Wrong shortening of code {testData.Code} from reference latitude {testData.Latitude} and longitude {testData.Longitude}."); } } @@ -64,14 +37,14 @@ public void ShouldThrowArgumentExceptionForInvalidOrShortOrPaddedCodes() { public class TheRecoverNearestMethod { [Test] public void ShouldRecoverShortCodeToLongCodeFromReferencePoint() { - foreach (var testData in TestDataList) { - if (testData.TestType != "B" && testData.TestType != "R") { + foreach (var testData in ShorteningTestCases) { + if (testData.TestType != 'B' && testData.TestType != 'R') { continue; } OpenLocationCode recovered = OpenLocationCode.ShortCode.RecoverNearest(testData.ShortCode, - testData.ReferenceLatitude, testData.ReferenceLongitude); + testData.Latitude, testData.Longitude); Assert.AreEqual(testData.Code, recovered.Code, - $"Wrong recovery of short code {testData.ShortCode} from reference latitude {testData.ReferenceLatitude} and longitude {testData.ReferenceLongitude}."); + $"Wrong recovery of short code {testData.ShortCode} from reference latitude {testData.Latitude} and longitude {testData.Longitude}."); } } @@ -96,21 +69,14 @@ public void ShouldThrowArgumentExceptionForInvalidShortCodes() { } } - private struct TestData { - - internal TestData(string code, double referenceLatitude, double referenceLongitude, string shortCode, string testType) { - Code = code; - ReferenceLatitude = referenceLatitude; - ReferenceLongitude = referenceLongitude; - ShortCode = shortCode; - TestType = testType; - } + public class TestData { - internal string Code { get; } - internal double ReferenceLatitude { get; } - internal double ReferenceLongitude { get; } - internal string ShortCode { get; } - internal string TestType { get; } + public string Code { get; set; } + public double Latitude { get; set; } + public double Longitude { get; set; } + public string ShortCode { get; set; } + public char TestType { get; set; } } + } diff --git a/OpenLocationCode.Test/TestData/decoding.csv b/OpenLocationCode.Test/TestData/decoding.csv new file mode 100644 index 0000000..daf88c9 --- /dev/null +++ b/OpenLocationCode.Test/TestData/decoding.csv @@ -0,0 +1,29 @@ +# Test decoding Open Location Codes. +# +# Provides test cases for decoding valid codes. +# +# Format: +# code,length,latLo,lngLo,latHi,lngHi +7FG49Q00+,6,20.35,2.75,20.4,2.8 +7FG49QCJ+2V,10,20.37,2.782125,20.370125,2.78225 +7FG49QCJ+2VX,11,20.3701,2.78221875,20.370125,2.78225 +7FG49QCJ+2VXGJ,13,20.370113,2.782234375,20.370114,2.78223632813 +8FVC2222+22,10,47.0,8.0,47.000125,8.000125 +4VCPPQGP+Q9,10,-41.273125,174.785875,-41.273,174.786 +62G20000+,4,0.0,-180.0,1,-179 +22220000+,4,-90,-180,-89,-179 +7FG40000+,4,20.0,2.0,21.0,3.0 +22222222+22,10,-90.0,-180.0,-89.999875,-179.999875 +6VGX0000+,4,0,179,1,180 +6FH32222+222,11,1,1,1.000025,1.00003125 +# Special cases over 90 latitude and 180 longitude +CFX30000+,4,89,1,90,2 +CFX30000+,4,89,1,90,2 +62H20000+,4,1,-180,2,-179 +62H30000+,4,1,-179,2,-178 +CFX3X2X2+X2,10,89.9998750,1,90,1.0001250 +# Test non-precise latitude/longitude value +6FH56C22+22,10,1.2000000000000028,3.4000000000000057,1.2001249999999999,3.4001250000000027 +# Validate that digits after the first 15 are ignored when decoding +849VGJQF+VX7QR3J,15,37.5396691200,-122.3750698242,37.5396691600,-122.3750697021 +849VGJQF+VX7QR3J7QR3J,15,37.5396691200,-122.3750698242,37.5396691600,-122.3750697021 diff --git a/OpenLocationCode.Test/TestData/encoding.csv b/OpenLocationCode.Test/TestData/encoding.csv new file mode 100644 index 0000000..59d3ac9 --- /dev/null +++ b/OpenLocationCode.Test/TestData/encoding.csv @@ -0,0 +1,115 @@ +# Test encoding Open Location Codes. +# +# Provides test cases for encoding latitude and longitude to codes. +# +# Format: +# latitude,longitude,length,expected code (empty if the input should cause an error) +20.375,2.775,6,7FG49Q00+ +20.3700625,2.7821875,10,7FG49QCJ+2V +20.3701125,2.782234375,11,7FG49QCJ+2VX +20.3701135,2.78223535156,13,7FG49QCJ+2VXGJ +47.0000625,8.0000625,10,8FVC2222+22 +-41.2730625,174.7859375,10,4VCPPQGP+Q9 +0.5,-179.5,4,62G20000+ +-89.5,-179.5,4,22220000+ +20.5,2.5,4,7FG40000+ +-89.9999375,-179.9999375,10,22222222+22 +0.5,179.5,4,6VGX0000+ +1,1,11,6FH32222+222 +################################################################################ +# +# Special cases over 90 latitude and 180 longitude +# +################################################################################ +90,1,4,CFX30000+ +92,1,4,CFX30000+ +1,180,4,62H20000+ +1,181,4,62H30000+ +90,1,10,CFX3X2X2+X2 +################################################################################ +# +# Test non-precise latitude/longitude value +# +################################################################################ +1.2,3.4,10,6FH56C22+22 +################################################################################ +# +# Validate that codes generated with a length exceeding 15 significant digits +# return a 15-digit code +# +################################################################################ +37.539669125,-122.375069724,15,849VGJQF+VX7QR3J +37.539669125,-122.375069724,16,849VGJQF+VX7QR3J +37.539669125,-122.375069724,100,849VGJQF+VX7QR3J +################################################################################ +# +# Test floating point representation/rounding errors. +# +################################################################################ +35.6,3.033,10,8F75J22M+26 +-48.71,142.78,8,4R347QRJ+ +-70,163.7,8,3V252P22+ +-2.804,7.003,13,6F9952W3+C6222 +13.9,164.88,12,7V56WV2J+2222 +-13.23,172.77,8,5VRJQQCC+ +40.6,129.7,8,8QGFJP22+ +-52.166,13.694,14,3FVMRMMV+JJ2222 +-14,106.9,6,5PR82W00+ +70.3,-87.64,13,C62J8926+22222 +66.89,-106,10,95RPV2R2+22 +2.5,-64.23,11,67JQGQ2C+222 +-56.7,-47.2,14,38MJ8R22+222222 +-34.45,-93.719,6,46Q8H700+ +-35.849,-93.75,12,46P85722+C222 +65.748,24.316,12,9GQ6P8X8+6C22 +-57.32,130.43,12,3QJGMCJJ+2222 +17.6,-44.4,6,789QJJ00+ +-27.6,-104.8,6,554QC600+ +41.87,-145.59,13,83HPVCC6+22222 +-4.542,148.638,13,6R7CFJ5Q+66222 +-37.014,-159.936,10,43J2X3P7+CJ +-57.25,125.49,15,3QJ7QF2R+2222222 +48.89,-80.52,13,86WXVFRJ+22222 +53.66,170.97,14,9V5GMX6C+222222 +0.49,-76.97,15,67G5F2RJ+2222222 +40.44,-36.7,12,89G5C8R2+2222 +58.73,69.95,8,9JCFPXJ2+ +16.179,150.075,12,7R8G53HG+J222 +-55.574,-70.061,12,37PFCWGQ+CJ22 +76.1,-82.5,15,C68V4G22+2222222 +58.66,149.17,10,9RCFM56C+22 +-67.2,48.6,6,3H4CRJ00+ +-5.6,-54.5,14,6867CG22+222222 +-34,145.5,14,4RR72G22+222222 +-34.2,66.4,12,4JQ8RC22+2222 +17.8,-108.5,6,759HRG00+ +10.734,-168.294,10,722HPPM4+JC +-28.732,54.32,8,5H3P789C+ +64.1,107.9,12,9PP94W22+2222 +79.7525,6.9623,8,CFF8QX36+ +-63.6449,-25.1475,8,398P9V43+ +35.019,148.827,11,8R7C2R9G+JR2 +71.132,-98.584,15,C6334CJ8+RC22222 +53.38,-51.34,12,985C9MJ6+2222 +-1.2,170.2,12,6VCGR622+2222 +50.2,-162.8,11,922V6622+222 +-25.798,-59.812,10,5862652Q+R6 +81.654,-162.422,14,C2HVMH3H+J62222 +-75.7,-35.4,8,29P68J22+ +-80.01,58.57,15,2HFWXHRC+2222222 +67.2,115.1,11,9PVQ6422+222 +-78.137,-42.995,12,28HVV274+6222 +-56.3,114.5,11,3PMPPG22+222 +10.767,-62.787,13,772VQ687+R6222 +-19.212,107.423,10,5PG9QCQF+66 +21.192,-45.145,15,78HP5VR4+R222222 +16.701,148.648,14,7R8CPJ2X+C62222 +52.25,-77.45,15,97447H22+2222222 +-68.54504,-62.81725,11,373VF53M+X4J +76.7,-86.172,12,C68MPR2H+2622 +-6.2,96.6,13,6M5RRJ22+22222 +59.32,-157.21,12,93F48QCR+2222 +29.7,39.6,12,7GXXPJ22+2222 +-18.32,96.397,10,5MHRM9JW+2R +-30.3,76.5,11,4JXRPG22+222 +50.342,-112.534,15,95298FR8+RC22222 diff --git a/OpenLocationCode.Test/TestData/shortening.csv b/OpenLocationCode.Test/TestData/shortening.csv new file mode 100644 index 0000000..5c6cf69 --- /dev/null +++ b/OpenLocationCode.Test/TestData/shortening.csv @@ -0,0 +1,35 @@ +# Test shortening and extending codes. +# +# Format: +# full code,lat,lng,shortcode,test_type +# test_type is R for recovery only, S for shorten only, or B for both. +9C3W9QCJ+2VX,51.3701125,-1.217765625,+2VX,B +# Adjust so we can't trim by 8 (+/- .000755) +9C3W9QCJ+2VX,51.3708675,-1.217765625,CJ+2VX,B +9C3W9QCJ+2VX,51.3693575,-1.217765625,CJ+2VX,B +9C3W9QCJ+2VX,51.3701125,-1.218520625,CJ+2VX,B +9C3W9QCJ+2VX,51.3701125,-1.217010625,CJ+2VX,B +# Adjust so we can't trim by 6 (+/- .0151) +9C3W9QCJ+2VX,51.3852125,-1.217765625,9QCJ+2VX,B +9C3W9QCJ+2VX,51.3550125,-1.217765625,9QCJ+2VX,B +9C3W9QCJ+2VX,51.3701125,-1.232865625,9QCJ+2VX,B +9C3W9QCJ+2VX,51.3701125,-1.202665625,9QCJ+2VX,B +# Added to detect error in recoverNearest functionality +8FJFW222+,42.899,9.012,22+,B +796RXG22+,14.95125,-23.5001,22+,B +# Reference location is in the 4 digit cell to the south. +8FVC2GGG+GG,46.976,8.526,2GGG+GG,B +# Reference location is in the 4 digit cell to the north. +8FRCXGGG+GG,47.026,8.526,XGGG+GG,B +# Reference location is in the 4 digit cell to the east. +8FR9GXGG+GG,46.526,8.026,GXGG+GG,B +# Reference location is in the 4 digit cell to the west. +8FRCG2GG+GG,46.526,7.976,G2GG+GG,B +# Added to detect errors recovering codes near the poles. +# This tests recovery function, but these codes won't shorten. +CFX22222+22,89.6,0.0,2222+22,R +2CXXXXXX+XX,-81.0,0.0,XXXXXX+XX,R +# Recovered full codes should be the full code +8FRCG2GG+GG,46.526,7.976,8FRCG2GG+GG,R +# Recovered full codes should be the uppercased full code +8FRCG2GG+GG,46.526,7.976,8frCG2GG+gG,R diff --git a/OpenLocationCode.Test/TestData/validity.csv b/OpenLocationCode.Test/TestData/validity.csv new file mode 100644 index 0000000..86ad357 --- /dev/null +++ b/OpenLocationCode.Test/TestData/validity.csv @@ -0,0 +1,33 @@ +# Test data for validity tests. +# Format of each line is: +# code,isValid,isShort,isFull,isPadded +# Valid full codes: +8FWC2345+G6,true,false,true,false +8FWC2345+G6G,true,false,true,false +8fwc2345+,true,false,true,false +8FWCX400+,true,false,true,true +8FWC0000+,true,false,true,true +8F000000+,true,false,true,true +# Valid short codes: +WC2345+G6g,true,true,false,false +2345+G6,true,true,false,false +45+G6,true,true,false,false ++G6,true,true,false,false +# Invalid codes +G+,false,false,false,false ++,false,false,false,false +8FWC2345+G,false,false,false,false +8FWC2_45+G6,false,false,false,false +8FWC2η45+G6,false,false,false,false +8FWC2345+G6+,false,false,false,false +8FWC2345G6+,false,false,false,false +8FWC2300+G6,false,false,false,false +WC2300+G6g,false,false,false,false +WC2345+G,false,false,false,false +WC2300+,false,false,false,false +# Validate that codes at and exceeding 15 digits are still valid when all their +# digits are valid, and invalid when not. +849VGJQF+VX7QR3J,true,false,true,false +849VGJQF+VX7QR3U,false,false,false,false +849VGJQF+VX7QR3JW,true,false,true,false +849VGJQF+VX7QR3JU,false,false,false,false diff --git a/OpenLocationCode.Test/TestDataUtils.cs b/OpenLocationCode.Test/TestDataUtils.cs new file mode 100644 index 0000000..e66d559 --- /dev/null +++ b/OpenLocationCode.Test/TestDataUtils.cs @@ -0,0 +1,17 @@ +using System.Collections.Immutable; +using System.IO; +using CsvHelper; +using CsvHelper.Configuration; + + +public static class TestDataUtils { + + public static ImmutableList ReadTestData(string fileName) { + using (var reader = new StreamReader($"..\\..\\..\\TestData\\{fileName}")) { + using (var csv = new CsvReader(reader, new Configuration { HasHeaderRecord = false, AllowComments = true })) { + return csv.GetRecords().ToImmutableList(); + } + } + } + +} diff --git a/OpenLocationCode.Test/ValidityTest.cs b/OpenLocationCode.Test/ValidityTest.cs index 975cac5..5f074bc 100644 --- a/OpenLocationCode.Test/ValidityTest.cs +++ b/OpenLocationCode.Test/ValidityTest.cs @@ -4,41 +4,15 @@ public static class ValidityTest { - // Test cases for validating codes and determining code type. - // See: https://github.com/google/open-location-code/blob/master/test_data/validityTests.csv - private static readonly List TestDataList = new List { - // Valid full codes: - FullCode("8FWC2345+G6"), - FullCode("8FWC2345+G6G"), - FullCode("8fwc2345+"), - FullCode("8FWCX400+", padded: true), - FullCode("8FWC0000+", padded: true), - FullCode("8F000000+", padded: true), - // Valid short codes: - ShortCode("WC2345+G6g"), - ShortCode("2345+G6"), - ShortCode("45+G6"), - ShortCode("+G6"), - // Invalid codes: - InvalidCode("G+"), - InvalidCode("+"), - InvalidCode("8FWC2345+G"), - InvalidCode("8FWC2_45+G6"), - InvalidCode("8FWC2η45+G6"), - InvalidCode("8FWC2345+G6+"), - InvalidCode("8FWC2345G6+"), - InvalidCode("8FWC2300+G6"), - InvalidCode("8FWC2300+00"), - InvalidCode("WC2300+G6g"), - InvalidCode("WC2345+G"), - InvalidCode("WC2300+") - }; + // Test cases for validating codes and determining code type + // https://github.com/google/open-location-code/blob/master/test_data/validityTests.csv + private static readonly IEnumerable ValidityTestCases = TestDataUtils.ReadTestData("validity.csv"); - public class TheIsValidCodeMethod { + public class TheIsValidMethod { [Test] public void ShouldDetermineValidityOfACode() { - foreach (TestData testData in TestDataList) { + foreach (TestData testData in ValidityTestCases) { Assert.AreEqual(testData.IsValid, OpenLocationCode.IsValid(testData.Code), $"Validity of code {testData.Code} is wrong."); } @@ -62,7 +36,7 @@ public void ShouldValidateCodesExceedingMaximumLength() { public class TheIsShortMethod { [Test] public void ShouldDetermineShortnessOfACode() { - foreach (TestData testData in TestDataList) { + foreach (TestData testData in ValidityTestCases) { Assert.AreEqual(testData.IsShort, OpenLocationCode.IsShort(testData.Code), $"Shortness of code {testData.Code} is wrong."); } @@ -72,7 +46,7 @@ public void ShouldDetermineShortnessOfACode() { public class TheIsFullMethod { [Test] public void ShouldDetermineFullnessOfACode() { - foreach (TestData testData in TestDataList) { + foreach (TestData testData in ValidityTestCases) { Assert.AreEqual(testData.IsFull, OpenLocationCode.IsFull(testData.Code), $"Fullness of code {testData.Code} is wrong."); } @@ -83,35 +57,20 @@ public void ShouldDetermineFullnessOfACode() { public class TheIsPaddedMethod { [Test] public void ShouldDeterminePaddingOfACode() { - foreach (TestData testData in TestDataList) { + foreach (TestData testData in ValidityTestCases) { Assert.AreEqual(testData.IsPadded, OpenLocationCode.IsPadded(testData.Code), $"Padding for code {testData.Code} is wrong."); } } } + public class TestData { - private static TestData FullCode(string code, bool padded = false) => new TestData(code, true, false, true, padded); - - private static TestData ShortCode(string code) => new TestData(code, true, true, false, false); - - private static TestData InvalidCode(string code) => new TestData(code, false, false, false, false); - - private struct TestData { - - internal TestData(string code, bool isValid, bool isShort, bool isFull, bool isPadded) { - Code = code; - IsValid = isValid; - IsShort = isShort; - IsFull = isFull; - IsPadded = isPadded; // Nonstandard - } - - internal string Code { get; } - internal bool IsValid { get; } - internal bool IsShort { get; } - internal bool IsFull { get; } - internal bool IsPadded { get; } + public string Code { get; set; } + public bool IsValid { get; set; } + public bool IsShort { get; set; } + public bool IsFull { get; set; } + public bool IsPadded { get; set; } } diff --git a/OpenLocationCode/CodeArea.cs b/OpenLocationCode/CodeArea.cs index 9c6c5b7..a786d02 100644 --- a/OpenLocationCode/CodeArea.cs +++ b/OpenLocationCode/CodeArea.cs @@ -2,86 +2,24 @@ namespace Google.OpenLocationCode { /// - /// Coordinates of a decoded Open Location Code area. - /// The coordinates include the latitude and longitude of the lower left (south west) and upper right (north east) corners - /// and the center of the bounding box of the code area. + /// A square for the coordinates of a decoded Open Location Code area. + /// The of the decoded Open Location Code is also included. /// - public class CodeArea { + public class CodeArea : GeoArea { - internal CodeArea(double southLatitude, double westLongitude, double northLatitude, double eastLongitude) { + internal CodeArea(double southLatitude, double westLongitude, double northLatitude, double eastLongitude, int codeLength) : + base(southLatitude, westLongitude, northLatitude, eastLongitude) { if (southLatitude >= northLatitude || westLongitude >= eastLongitude) { throw new ArgumentException("min must be less than max"); } - Min = new GeoPoint(southLatitude, westLongitude); - Max = new GeoPoint(northLatitude, eastLongitude); + CodeLength = codeLength; } /// - /// The min (south west) point coordinates of the area bounds. + /// The length of the decoded Open Location Code. /// - public GeoPoint Min { get; } - - /// - /// The max (north east) point coordinates of the area bounds. - /// - public GeoPoint Max { get; } - - /// - /// The center point of the area which is equidistant between and . - /// - public GeoPoint Center => new GeoPoint(CenterLatitude, CenterLongitude); - - - /// - /// The width of the area in longitude degrees. - /// - public double LongitudeWidth => (double) ((decimal) Max.Longitude - (decimal) Min.Longitude); - - /// - /// The height of the area in latitude degrees. - /// - public double LatitudeHeight => (double) ((decimal) Max.Latitude - (decimal) Min.Latitude); - - - /// The south (min) latitude coordinate in decimal degrees. - /// Alias to .Latitude - public double SouthLatitude => Min.Latitude; - - /// The west (min) longitude coordinate in decimal degrees. - /// Alias to .Longitude - public double WestLongitude => Min.Longitude; - - /// The north (max) latitude coordinate in decimal degrees. - /// Alias to .Latitude - public double NorthLatitude => Max.Latitude; - - /// The east (max) longitude coordinate in decimal degrees. - /// Alias to .Longitude - public double EastLongitude => Max.Longitude; - - /// The center latitude coordinate in decimal degrees. - /// Alias to .Latitude - public double CenterLatitude => (Min.Latitude + Max.Latitude) / 2; - - /// The center longitude coordinate in decimal degrees. - /// Alias to .Longitude - public double CenterLongitude => (Min.Longitude + Max.Longitude) / 2; - - - /// true if this code area contains the provided point, false otherwise. - /// The point coordinates to check. - public bool Contains(GeoPoint point) { - return Contains(point.Latitude, point.Longitude); - } - - /// true if this code area contains the provided point, false otherwise. - /// The latitude coordinate of the point to check. - /// The longitude coordinate of the point to check. - public bool Contains(double latitude, double longitude) { - return Min.Latitude <= latitude && latitude < Max.Latitude - && Min.Longitude <= longitude && longitude < Max.Longitude; - } + public int CodeLength { get; } } } diff --git a/OpenLocationCode/GeoArea.cs b/OpenLocationCode/GeoArea.cs new file mode 100644 index 0000000..7c88a8d --- /dev/null +++ b/OpenLocationCode/GeoArea.cs @@ -0,0 +1,105 @@ +using System; + +namespace Google.OpenLocationCode { + /// + /// A rectangular area on the geographic coordinate system specified by the minimum and maximum coordinates. + /// The coordinates include the latitude and longitude of the lower left (south west) and upper right (north east) corners. + /// + /// Additional properties exist to calculate the of the bounding box, + /// and the or area dimensions in degrees. + /// + /// + public class GeoArea { + + /// + /// Create a new rectangular GeoArea of the provided min and max geo points. + /// + /// The minimum GeoPoint + /// The maximum GeoPoint + /// If min is greater than or equal to max. + public GeoArea(GeoPoint min, GeoPoint max) { + if (min.Latitude >= max.Latitude || min.Longitude >= max.Longitude) { + throw new ArgumentException("min must be less than max"); + } + Min = min; + Max = max; + } + + /// + /// Create a new rectangular GeoArea of the provided min and max geo coordinates. + /// + /// The minimum south latitude + /// The minimum west longitude + /// The maximum north latitude + /// The maximum east longitude + public GeoArea(double southLatitude, double westLongitude, double northLatitude, double eastLongitude) : + this(new GeoPoint(southLatitude, westLongitude), new GeoPoint(northLatitude, eastLongitude)) { } + + /// + /// The min (south west) point coordinates of the area bounds. + /// + public GeoPoint Min { get; } + + /// + /// The max (north east) point coordinates of the area bounds. + /// + public GeoPoint Max { get; } + + /// + /// The center point of the area which is equidistant between and . + /// + public GeoPoint Center => new GeoPoint(CenterLatitude, CenterLongitude); + + + /// + /// The width of the area in longitude degrees. + /// + public double LongitudeWidth => (double)((decimal)Max.Longitude - (decimal)Min.Longitude); + + /// + /// The height of the area in latitude degrees. + /// + public double LatitudeHeight => (double)((decimal)Max.Latitude - (decimal)Min.Latitude); + + + /// The south (min) latitude coordinate in decimal degrees. + /// Alias to .Latitude + public double SouthLatitude => Min.Latitude; + + /// The west (min) longitude coordinate in decimal degrees. + /// Alias to .Longitude + public double WestLongitude => Min.Longitude; + + /// The north (max) latitude coordinate in decimal degrees. + /// Alias to .Latitude + public double NorthLatitude => Max.Latitude; + + /// The east (max) longitude coordinate in decimal degrees. + /// Alias to .Longitude + public double EastLongitude => Max.Longitude; + + /// The center latitude coordinate in decimal degrees. + /// Alias to .Latitude + public double CenterLatitude => (Min.Latitude + Max.Latitude) / 2; + + /// The center longitude coordinate in decimal degrees. + /// Alias to .Longitude + public double CenterLongitude => (Min.Longitude + Max.Longitude) / 2; + + + /// true if this geo area contains the provided point, false otherwise. + /// The point coordinates to check. + public bool Contains(GeoPoint point) { + return Contains(point.Latitude, point.Longitude); + } + + /// true if this geo area contains the provided point, false otherwise. + /// The latitude coordinate of the point to check. + /// The longitude coordinate of the point to check. + public bool Contains(double latitude, double longitude) { + return Min.Latitude <= latitude && latitude < Max.Latitude + && Min.Longitude <= longitude && longitude < Max.Longitude; + } + + } +} diff --git a/OpenLocationCode/OpenLocationCode.cs b/OpenLocationCode/OpenLocationCode.cs index 19cc546..8ca492a 100644 --- a/OpenLocationCode/OpenLocationCode.cs +++ b/OpenLocationCode/OpenLocationCode.cs @@ -59,46 +59,70 @@ public sealed class OpenLocationCode { // A separator used to break the code into two parts to aid memorability. - internal const char SeparatorCharacter = '+'; + private const char SeparatorCharacter = '+'; // The number of characters to place before the separator. - internal const int SeparatorPosition = 8; + private const int SeparatorPosition = 8; // The character used to pad codes. - internal const char PaddingCharacter = '0'; + private const char PaddingCharacter = '0'; // The character set used to encode the digit values. internal const string CodeAlphabet = "23456789CFGHJMPQRVWX"; // The base to use to convert numbers to/from. - internal static readonly int EncodingBase = CodeAlphabet.Length; + private const int EncodingBase = 20; // CodeAlphabet.Length; // The encoding base squared also rep - internal static readonly int EncodingBaseSquared = EncodingBase * EncodingBase; + private const int EncodingBaseSquared = EncodingBase * EncodingBase; // The maximum value for latitude in degrees. - internal const int LatitudeMax = 90; + private const int LatitudeMax = 90; // The maximum value for longitude in degrees. - internal const int LongitudeMax = 180; + private const int LongitudeMax = 180; // Maximum code length using just lat/lng pair encoding. - internal const int PairCodeLength = 10; + private const int PairCodeLength = 10; + + // Number of digits in the grid coding section. + private const int GridCodeLength = MaxDigitCount - PairCodeLength; // Maximum code length for any plus code - internal const int MaxCodeLength = 15; + private const int MaxDigitCount = 15; // Number of columns in the grid refinement method. - internal const int RefinementGridColumns = 4; + private const int GridColumns = 4; // Number of rows in the grid refinement method. - internal const int RefinementGridRows = 5; + private const int GridRows = 5; // The maximum latitude digit value for the first grid layer - internal const int FirstLatitudeDigitValueMax = 8; // lat -> 90 + private const int FirstLatitudeDigitValueMax = 8; // lat -> 90 // The maximum longitude digit value for the first grid layer - internal const int FirstLongitudeDigitValueMax = 17; // lon -> 180 + private const int FirstLongitudeDigitValueMax = 17; // lon -> 180 + + + private const long GridRowsMultiplier = 3125; // Pow(GridRows, GridCodeLength) + + private const long GridColumnsMultiplier = 1024; // Pow(GridColumns, GridCodeLength) + + // Value to multiple latitude degrees to convert it to an integer with the maximum encoding + // precision. I.e. ENCODING_BASE**3 * GRID_ROWS**GRID_CODE_LENGTH + private const long LatIntegerMultiplier = 8000 * GridRowsMultiplier; + + // Value to multiple longitude degrees to convert it to an integer with the maximum encoding + // precision. I.e. ENCODING_BASE**3 * GRID_COLUMNS**GRID_CODE_LENGTH + private const long LngIntegerMultiplier = 8000 * GridColumnsMultiplier; + + // Value of the most significant latitude digit after it has been converted to an integer. + private const long LatMspValue = LatIntegerMultiplier * EncodingBaseSquared; + + // Value of the most significant longitude digit after it has been converted to an integer. + private const long LngMspValue = LngIntegerMultiplier * EncodingBaseSquared; + + // The ASCII integer of the minimum digit character used as the offset for indexed code digits private static readonly int IndexedDigitValueOffset = CodeAlphabet[0]; // 50 @@ -126,7 +150,7 @@ public OpenLocationCode(string code) { } Code = NormalizeCode(code.ToUpper()); if (!IsValidUpperCase(Code) || !IsCodeFull(Code)) { - throw new ArgumentException($"The provided code '{code}' is not a valid full Open Location Code (or code digits)."); + throw new ArgumentException($"code '{code}' is not a valid full Open Location Code (or code digits)."); } CodeDigits = TrimCode(Code); } @@ -391,7 +415,7 @@ private static bool IsCodePadded(string code) { /// If the code length is not valid. public static string Encode(double latitude, double longitude, int codeLength = CodePrecisionNormal) { // Limit the maximum number of digits in the code. - codeLength = Math.Min(codeLength, MaxCodeLength); + codeLength = Math.Min(codeLength, MaxDigitCount); // Check that the code length requested is valid. if (codeLength < 4 || (codeLength < PairCodeLength && codeLength % 2 == 1)) { throw new ArgumentException($"Illegal code length {codeLength}."); @@ -402,54 +426,59 @@ public static string Encode(double latitude, double longitude, int codeLength = // Latitude 90 needs to be adjusted to be just less, so the returned code can also be decoded. if ((int) latitude == LatitudeMax) { - latitude = latitude - 0.9 * ComputeLatitudePrecision(codeLength); - } - - // Adjust latitude and longitude to be in positive number ranges. - double remainingLatitude = latitude + LatitudeMax; - double remainingLongitude = longitude + LongitudeMax; - - // Count how many digits have been created. - int generatedDigits = 0; - // Store the code. - StringBuilder codeBuilder = new StringBuilder(); - // The precisions are initially set to ENCODING_BASE^2 because they will be immediately divided. - double latPrecision = EncodingBaseSquared; - double lngPrecision = EncodingBaseSquared; - while (generatedDigits < codeLength) { - if (generatedDigits < PairCodeLength) { - // Use the normal algorithm for the first set of digits. - latPrecision /= EncodingBase; - lngPrecision /= EncodingBase; - int latDigit = (int) Math.Floor(remainingLatitude / latPrecision); - int lngDigit = (int) Math.Floor(remainingLongitude / lngPrecision); - remainingLatitude -= latPrecision * latDigit; - remainingLongitude -= lngPrecision * lngDigit; - codeBuilder.Append(CodeAlphabet[latDigit]); - codeBuilder.Append(CodeAlphabet[lngDigit]); - generatedDigits += 2; - } else { - // Use the 4x5 grid for remaining digits. - latPrecision /= RefinementGridRows; - lngPrecision /= RefinementGridColumns; - int row = (int) Math.Floor(remainingLatitude / latPrecision); - int col = (int) Math.Floor(remainingLongitude / lngPrecision); - remainingLatitude -= latPrecision * row; - remainingLongitude -= lngPrecision * col; - codeBuilder.Append(CodeAlphabet[row * RefinementGridColumns + col]); - generatedDigits += 1; + latitude -= 0.9 * ComputeLatitudePrecision(codeLength); + } + + // Store the code - we build it in reverse and reorder it afterwards. + StringBuilder reverseCodeBuilder = new StringBuilder(); + + // Compute the code. + // This approach converts each value to an integer after multiplying it by + // the final precision. This allows us to use only integer operations, so + // avoiding any accumulation of floating point representation errors. + + // Multiply values by their precision and convert to positive. Rounding + // avoids/minimises errors due to floating point precision. + long latVal = (long) (Math.Round((latitude + LatitudeMax) * LatIntegerMultiplier * 1e6) / 1e6); + long lngVal = (long) (Math.Round((longitude + LongitudeMax) * LngIntegerMultiplier * 1e6) / 1e6); + + if (codeLength > PairCodeLength) { + for (int i = 0; i < GridCodeLength; i++) { + long latDigit = latVal % GridRows; + long lngDigit = lngVal % GridColumns; + int ndx = (int) (latDigit * GridColumns + lngDigit); + reverseCodeBuilder.Append(CodeAlphabet[ndx]); + latVal /= GridRows; + lngVal /= GridColumns; } + } else { + latVal /= GridRowsMultiplier; + lngVal /= GridColumnsMultiplier; + } + // Compute the pair section of the code. + for (int i = 0; i < PairCodeLength / 2; i++) { + reverseCodeBuilder.Append(CodeAlphabet[(int) (lngVal % EncodingBase)]); + reverseCodeBuilder.Append(CodeAlphabet[(int) (latVal % EncodingBase)]); + latVal /= EncodingBase; + lngVal /= EncodingBase; // If we are at the separator position, add the separator. - if (generatedDigits == SeparatorPosition) { - codeBuilder.Append(SeparatorCharacter); + if (i == 0) { + reverseCodeBuilder.Append(SeparatorCharacter); } } - // If the generated code is shorter than the separator position, pad the code and add the separator. - if (generatedDigits < SeparatorPosition) { - codeBuilder.Append(PaddingCharacter, SeparatorPosition - generatedDigits); - codeBuilder.Append(SeparatorCharacter); + // Reverse the code. + char[] reversedCode = reverseCodeBuilder.ToString().ToCharArray(); + Array.Reverse(reversedCode); + StringBuilder codeBuilder = new StringBuilder(new string(reversedCode)); + + // If we need to pad the code, replace some of the digits. + if (codeLength < SeparatorPosition) { + codeBuilder.Remove(codeLength, SeparatorPosition - codeLength); + for (int i = codeLength; i < SeparatorPosition; i++) { + codeBuilder.Insert(i, PaddingCharacter); + } } - return codeBuilder.ToString(); + return codeBuilder.ToString(0, Math.Max(SeparatorPosition + 1, codeLength + 1)); } /// @@ -480,44 +509,36 @@ public static CodeArea Decode(string code) { } private static CodeArea DecodeValid(string codeDigits) { - int codeLength = Math.Min(codeDigits.Length, MaxCodeLength); - - int digit = 0; - // The precisions are initially set to ENCODING_BASE^2 because they will be immediately divided. - double latPrecision = EncodingBaseSquared; - double lngPrecision = EncodingBaseSquared; - // Save the coordinates. - double southLatitude = 0; - double westLongitude = 0; - - // Decode the digits. - while (digit < codeLength) { - if (digit < PairCodeLength) { - // Decode a pair of digits, the first being latitude and the second being longitude. - latPrecision /= EncodingBase; - lngPrecision /= EncodingBase; - int digitVal = DigitValueOf(codeDigits[digit]); - southLatitude += latPrecision * digitVal; - digitVal = DigitValueOf(codeDigits[digit + 1]); - westLongitude += lngPrecision * digitVal; - digit += 2; - } else { - // Use the 4x5 grid for digits after 10. - int digitVal = DigitValueOf(codeDigits[digit]); - int row = digitVal / RefinementGridColumns; - int col = digitVal % RefinementGridColumns; - latPrecision /= RefinementGridRows; - lngPrecision /= RefinementGridColumns; - southLatitude += latPrecision * row; - westLongitude += lngPrecision * col; - digit += 1; - } + // Initialise the values. We work them out as integers and convert them to doubles at the end. + long latVal = -LatitudeMax * LatIntegerMultiplier; + long lngVal = -LongitudeMax * LngIntegerMultiplier; + // Define the place value for the digits. We'll divide this down as we work through the code. + long latPlaceVal = LatMspValue; + long lngPlaceVal = LngMspValue; + + int pairPartLength = Math.Min(codeDigits.Length, PairCodeLength); + int codeLength = Math.Min(codeDigits.Length, MaxDigitCount); + for (int i = 0; i < pairPartLength; i += 2) { + latPlaceVal /= EncodingBase; + lngPlaceVal /= EncodingBase; + latVal += DigitValueOf(codeDigits[i]) * latPlaceVal; + lngVal += DigitValueOf(codeDigits[i + 1]) * lngPlaceVal; + } + for (int i = PairCodeLength; i < codeLength; i++) { + latPlaceVal /= GridRows; + lngPlaceVal /= GridColumns; + int digit = DigitValueOf(codeDigits[i]); + int row = digit / GridColumns; + int col = digit % GridColumns; + latVal += row * latPlaceVal; + lngVal += col * lngPlaceVal; } return new CodeArea( - southLatitude - LatitudeMax, - westLongitude - LongitudeMax, - (southLatitude - LatitudeMax) + latPrecision, - (westLongitude - LongitudeMax) + lngPrecision + (double)latVal / LatIntegerMultiplier, + (double)lngVal / LngIntegerMultiplier, + (double)(latVal + latPlaceVal) / LatIntegerMultiplier, + (double)(lngVal + lngPlaceVal) / LngIntegerMultiplier, + codeLength ); } @@ -586,10 +607,10 @@ private static double ClipLatitude(double latitude) { private static double NormalizeLongitude(double longitude) { while (longitude < -LongitudeMax) { - longitude = longitude + LongitudeMax * 2; + longitude += LongitudeMax * 2; } while (longitude >= LongitudeMax) { - longitude = longitude - LongitudeMax * 2; + longitude -= LongitudeMax * 2; } return longitude; } @@ -633,7 +654,7 @@ private static double ComputeLatitudePrecision(int codeLength) { if (codeLength <= CodePrecisionNormal) { return Math.Pow(EncodingBase, codeLength / -2 + 2); } - return Math.Pow(EncodingBase, -3) / Math.Pow(RefinementGridRows, codeLength - PairCodeLength); + return Math.Pow(EncodingBase, -3) / Math.Pow(GridRows, codeLength - PairCodeLength); } @@ -658,7 +679,7 @@ public class ShortCode { /// A valid short Open Location Code. /// If the code is null, not valid, or not short. public ShortCode(string shortCode) { - Code = ValidateShortCode(shortCode); + Code = ValidateShortCode(ValidateCode(shortCode)); } // Used internally for short codes which are guaranteed to be valid @@ -705,6 +726,10 @@ public override string ToString() { } + /// + /// Note: if shortCode is already a valid full code, + /// this will immediately return a new OpenLocationCode instance with that code + /// /// /// A new OpenLocationCode instance representing a full Open Location Code /// recovered from the provided short Open Location Code, given the reference location. @@ -712,9 +737,12 @@ public override string ToString() { /// The valid short Open Location Code to recover /// The reference latitude in decimal degrees. /// The reference longitude in decimal degrees. - /// If the code is null, not valid, or not short. + /// If the code is null or not valid. public static OpenLocationCode RecoverNearest(string shortCode, double referenceLatitude, double referenceLongitude) { - return RecoverNearestValid(ValidateShortCode(shortCode), referenceLatitude, referenceLongitude); + string validCode = ValidateCode(shortCode); + if (IsCodeFull(validCode)) return new OpenLocationCode(validCode); + + return RecoverNearestValid(ValidateShortCode(validCode), referenceLatitude, referenceLongitude); } private static OpenLocationCode RecoverNearestValid(string shortCode, double referenceLatitude, double referenceLongitude) { @@ -757,12 +785,8 @@ private static OpenLocationCode RecoverNearestValid(string shortCode, double ref } private static string ValidateShortCode(string shortCode) { - if (shortCode == null) { - throw new ArgumentException("shortCode cannot be null"); - } - shortCode = shortCode.ToUpper(); - if (!IsValidUpperCase(shortCode) || !IsCodeShort(shortCode)) { - throw new ArgumentException($"The provided code '{shortCode}' is not a valid short Open Location Code."); + if (!IsCodeShort(shortCode)) { + throw new ArgumentException($"code '{shortCode}' is not a valid short Open Location Code."); } return shortCode; } diff --git a/OpenLocationCode/OpenLocationCode.csproj b/OpenLocationCode/OpenLocationCode.csproj index a4fc22c..5d24ece 100644 --- a/OpenLocationCode/OpenLocationCode.csproj +++ b/OpenLocationCode/OpenLocationCode.csproj @@ -2,10 +2,22 @@ netstandard2.0 + 2.1.0 + Jon McPherson + OpenLocationCode + JonMcPherson + https://plus.codes/ + The C# (.NET Standard) implementation of the Google Open Location Code API. + OLC google open location code plus codes latitude longitude encode decode + + See the pull request on Github: https://github.com/JonMcPherson/open-location-code/pull/9 bin\Debug\netstandard2.0\OpenLocationCode.xml + + bin\Release\netstandard2.0\OpenLocationCode.xml + diff --git a/OpenLocationCode/OpenLocationCode.nuspec b/OpenLocationCode/OpenLocationCode.nuspec index 1d4d066..a7dea1d 100644 --- a/OpenLocationCode/OpenLocationCode.nuspec +++ b/OpenLocationCode/OpenLocationCode.nuspec @@ -1,22 +1,20 @@ - - + + - OpenLocationCode - 2.0.0 - OpenLocationCode - Jon McPherson - JonMcPherson + $id$ + $version$ + $title$ + $author$ + $owner$ https://github.com/google/open-location-code/blob/master/LICENSE https://plus.codes/ false - The C# (.NET Standard) implementation of the Google Open Location Code API. - OLC google open location code plus codes latitude longitude encode decode + $description$ + $packageReleaseNotes$ + $packageTags$ - - - \ No newline at end of file