Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- Added GeocentriConverter.
- Added GeographicConverter.
- Added GeohashConverter.

### Changed
- Changed license from GNU General Public License Version 3 to Apache License Version 2.0.
- Changed folder structure to match the TensionDev.CoordinateSystems namespace.
Expand Down
57 changes: 57 additions & 0 deletions TensionDev.CoordinateSystems.Tests/GeocentricConverterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System;
using Xunit;

namespace TensionDev.CoordinateSystems.Tests
{
public class GeocentricConverterTests
{
private const Double PositionToleranceMetres = 10.0;

[Fact]
public void FromThrowsForNullGeographicCoordinateSystem()
{
GeographicCoordinateSystem source = null;

Assert.Throws<ArgumentNullException>(() => GeocentricConverter.From(source));
}

[Fact]
public void FromConvertsEquatorPrimeMeridian()
{
GeographicCoordinateSystem source = new GeographicCoordinateSystem
{
LatitudeDecimalDegrees = 0.0,
LongitudeDecimalDegrees = 0.0,
AltitudeMetres = 0.0,
};

GeocentricCoordinateSystem actual = GeocentricConverter.From(source);

AssertWithin(6378137.0, actual.X, PositionToleranceMetres);
AssertWithin(0.0, actual.Y, PositionToleranceMetres);
AssertWithin(0.0, actual.Z, PositionToleranceMetres);
}

[Fact]
public void FromConvertsNorthPole()
{
GeographicCoordinateSystem source = new GeographicCoordinateSystem
{
LatitudeDecimalDegrees = 90.0,
LongitudeDecimalDegrees = 0.0,
AltitudeMetres = 0.0,
};

GeocentricCoordinateSystem actual = GeocentricConverter.From(source);

AssertWithin(0.0, actual.X, PositionToleranceMetres);
AssertWithin(0.0, actual.Y, PositionToleranceMetres);
AssertWithin(6356752.31425, actual.Z, PositionToleranceMetres);
}

private static void AssertWithin(Double expected, Double actual, Double tolerance)
{
Assert.InRange(actual, expected - tolerance, expected + tolerance);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using TensionDev.CoordinateSystems;
using Xunit;

namespace TensionDev.CoordinateSystems.Tests
Expand Down
77 changes: 77 additions & 0 deletions TensionDev.CoordinateSystems.Tests/GeographicConverterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using System;
using Xunit;

namespace TensionDev.CoordinateSystems.Tests
{
public class GeographicConverterTests
{
private const Double PositionToleranceMetres = 10.0;
private const Double Wgs84EquatorialRadiusMetres = 6378137.0;
private const Double PositionToleranceRadians = PositionToleranceMetres / Wgs84EquatorialRadiusMetres;

[Fact]
public void FromThrowsForNullGeocentricCoordinateSystem()
{
GeocentricCoordinateSystem source = null;

Assert.Throws<ArgumentNullException>(() => GeographicConverter.From(source));
}

[Fact]
public void FromConvertsEquatorPrimeMeridian()
{
GeocentricCoordinateSystem source = new GeocentricCoordinateSystem
{
X = 6378137.0,
Y = 0.0,
Z = 0.0,
};

GeographicCoordinateSystem actual = GeographicConverter.From(source);

AssertWithin(0.0, actual.LatitudeDecimalRadians, PositionToleranceRadians);
AssertWithin(0.0, actual.LongitudeDecimalRadians, PositionToleranceRadians);
AssertWithin(0.0, actual.AltitudeMetres, PositionToleranceMetres);
}

[Fact]
public void FromConvertsNorthPole()
{
GeocentricCoordinateSystem source = new GeocentricCoordinateSystem
{
X = 0.0,
Y = 0.0,
Z = 6356752.31425,
};

GeographicCoordinateSystem actual = GeographicConverter.From(source);

AssertWithin(Math.PI / 2.0, actual.LatitudeDecimalRadians, PositionToleranceRadians);
AssertWithin(0.0, actual.LongitudeDecimalRadians, PositionToleranceRadians);
AssertWithin(0.0, actual.AltitudeMetres, PositionToleranceMetres);
}

[Fact]
public void FromRoundTripsRepresentativeCoordinate()
{
GeographicCoordinateSystem source = new GeographicCoordinateSystem
{
LatitudeDecimalDegrees = 1.309432,
LongitudeDecimalDegrees = 103.780349,
AltitudeMetres = 25.0,
};

GeocentricCoordinateSystem geocentric = GeocentricConverter.From(source);
GeographicCoordinateSystem actual = GeographicConverter.From(geocentric);

AssertWithin(source.LatitudeDecimalRadians, actual.LatitudeDecimalRadians, PositionToleranceRadians);
AssertWithin(source.LongitudeDecimalRadians, actual.LongitudeDecimalRadians, PositionToleranceRadians);
AssertWithin(source.AltitudeMetres, actual.AltitudeMetres, PositionToleranceMetres);
}

private static void AssertWithin(Double expected, Double actual, Double tolerance)
{
Assert.InRange(actual, expected - tolerance, expected + tolerance);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using TensionDev.CoordinateSystems;
using Xunit;

namespace TensionDev.CoordinateSystems.Tests
Expand Down Expand Up @@ -131,4 +130,4 @@ public void Dispose()
GC.SuppressFinalize(this);
}
}
}
}
33 changes: 33 additions & 0 deletions TensionDev.CoordinateSystems.Tests/GeohashBase32Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System;
using Xunit;

namespace TensionDev.CoordinateSystems.Tests
{
public class GeohashBase32Tests
{
[Theory]
[InlineData("0123456789bc")]
[InlineData("ezs42")]
public void IsValidAcceptsGeohashBase32(String value)
{
Assert.True(GeohashBase32.IsValid(value));
}

[Theory]
[InlineData("")]
[InlineData("ezs42a")]
[InlineData("iezs42o")]
public void IsValidRejectsNonGeohashBase32(String value)
{
Assert.False(GeohashBase32.IsValid(value));
}

[Fact]
public void EncodeReturnsGeohashBase32String()
{
String actual = GeohashBase32.Encode(0b11010, 1);

Assert.Equal("u", actual);
}
}
}
46 changes: 32 additions & 14 deletions TensionDev.CoordinateSystems.Tests/GeohashTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using TensionDev.CoordinateSystems;
using Xunit;

namespace TensionDev.CoordinateSystems.Tests
Expand Down Expand Up @@ -67,15 +66,15 @@ public void TestConstructorLimits()
String geohashString = "w21z3w6ecruf";
Geohash geohash = new Geohash(geohashString);

GeographicCoordinateSystem actual = geohash.ToGeographicCoordinateSystem();
GeographicCoordinateSystem actual = GeographicConverter.From(geohash);

Assert.Equal(expected.LatitudeDecimalDegrees, actual.LatitudeDecimalDegrees, LatitudeError8);
Assert.Equal(expected.LongitudeDecimalDegrees, actual.LongitudeDecimalDegrees, LongitudeError8);
Assert.Equal(expected.AltitudeMetres, actual.AltitudeMetres);
}

[Fact]
public void TestToGeographicCoordinateSystem()
public void TestGeographicConverterFromGeohash()
{
GeographicCoordinateSystem expected = new GeographicCoordinateSystem()
{
Expand All @@ -87,15 +86,15 @@ public void TestToGeographicCoordinateSystem()
String geohashString = "ezs42";
Geohash geohash = new Geohash(geohashString);

GeographicCoordinateSystem actual = geohash.ToGeographicCoordinateSystem();
GeographicCoordinateSystem actual = GeographicConverter.From(geohash);

Assert.Equal(expected.LatitudeDecimalDegrees, actual.LatitudeDecimalDegrees, LatitudeError5);
Assert.Equal(expected.LongitudeDecimalDegrees, actual.LongitudeDecimalDegrees, LongitudeError5);
Assert.Equal(expected.AltitudeMetres, actual.AltitudeMetres);
}

[Fact]
public void TestFromGeographicCoordinateSystemUnderLimit()
public void TestGeohashConverterFromGeographicCoordinateSystemUnderLimit()
{
GeographicCoordinateSystem value = new GeographicCoordinateSystem()
{
Expand All @@ -104,11 +103,11 @@ public void TestFromGeographicCoordinateSystemUnderLimit()
AltitudeMetres = 0,
};

Assert.Throws<ArgumentOutOfRangeException>(() => Geohash.FromGeographicCoordinateSystem(value, 0));
Assert.Throws<ArgumentOutOfRangeException>(() => GeohashConverter.From(value, 0));
}

[Fact]
public void TestFromGeographicCoordinateSystemLengthOne()
public void TestGeohashConverterFromGeographicCoordinateSystemLengthOne()
{
String geohashString = "m";
Geohash expected = new Geohash(geohashString);
Expand All @@ -120,13 +119,13 @@ public void TestFromGeographicCoordinateSystemLengthOne()
AltitudeMetres = 0,
};

Geohash actual = Geohash.FromGeographicCoordinateSystem(value, 1);
Geohash actual = GeohashConverter.From(value, 1);

Assert.Equal(expected.Hash, actual.Hash);
}

[Fact]
public void TestFromGeographicCoordinateSystem()
public void TestGeohashConverterFromGeographicCoordinateSystem()
{
String geohashString = "u4pruydqqvj";
Geohash expected = new Geohash(geohashString);
Expand All @@ -138,13 +137,13 @@ public void TestFromGeographicCoordinateSystem()
AltitudeMetres = 0,
};

Geohash actual = Geohash.FromGeographicCoordinateSystem(value, 11);
Geohash actual = GeohashConverter.From(value, 11);

Assert.Equal(expected.Hash, actual.Hash);
}

[Fact]
public void TestFromGeographicCoordinateSystemLengthMax()
public void TestGeohashConverterFromGeographicCoordinateSystemLengthMax()
{
String geohashString = "09bhjkmnpz2c";
Geohash expected = new Geohash(geohashString);
Expand All @@ -156,13 +155,13 @@ public void TestFromGeographicCoordinateSystemLengthMax()
AltitudeMetres = 0,
};

Geohash actual = Geohash.FromGeographicCoordinateSystem(value, 12);
Geohash actual = GeohashConverter.From(value, 12);

Assert.Equal(expected.Hash, actual.Hash);
}

[Fact]
public void TestFromGeographicCoordinateSystemOverLimit()
public void TestGeohashConverterFromGeographicCoordinateSystemOverLimit()
{
GeographicCoordinateSystem value = new GeographicCoordinateSystem()
{
Expand All @@ -171,7 +170,26 @@ public void TestFromGeographicCoordinateSystemOverLimit()
AltitudeMetres = 0,
};

Assert.Throws<ArgumentOutOfRangeException>(() => Geohash.FromGeographicCoordinateSystem(value, 13));
Assert.Throws<ArgumentOutOfRangeException>(() => GeohashConverter.From(value, 13));
}

[Fact]
public void TestGeographicConverterFromGeohashViaDestinationConverter()
{
GeographicCoordinateSystem expected = new GeographicCoordinateSystem()
{
LatitudeDecimalDegrees = 42.605,
LongitudeDecimalDegrees = -5.603,
AltitudeMetres = 0,
};

Geohash geohash = new Geohash("ezs42");

GeographicCoordinateSystem actual = GeographicConverter.From(geohash);

Assert.Equal(expected.LatitudeDecimalDegrees, actual.LatitudeDecimalDegrees, LatitudeError5);
Assert.Equal(expected.LongitudeDecimalDegrees, actual.LongitudeDecimalDegrees, LongitudeError5);
Assert.Equal(expected.AltitudeMetres, actual.AltitudeMetres);
}

protected virtual void Dispose(bool disposing)
Expand Down
55 changes: 55 additions & 0 deletions TensionDev.CoordinateSystems/GeocentricConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright 2021 - 2026 TensionDev <TensionDev@outlook.com>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System;

namespace TensionDev.CoordinateSystems
{
/// <summary>
/// Converts supported coordinate representations to geocentric coordinates.
/// </summary>
public static class GeocentricConverter
{
/// <summary>
/// Converts geographic coordinates to geocentric coordinates using WGS 84 ellipsoid assumptions.
/// </summary>
/// <param name="source">The geographic coordinates to convert.</param>
/// <returns>The converted geocentric coordinates.</returns>
public static GeocentricCoordinateSystem From(GeographicCoordinateSystem source)
{
if (source == null)
{
throw new ArgumentNullException(nameof(source));
}

double latitude = source.LatitudeDecimalRadians;
double longitude = source.LongitudeDecimalRadians;
double altitude = source.AltitudeMetres;

double sinLatitude = Math.Sin(latitude);
double cosLatitude = Math.Cos(latitude);
double primeVerticalRadius = Wgs84.SemiMajorAxisMetres
/ Math.Sqrt(1.0 - Wgs84.FirstEccentricitySquared * sinLatitude * sinLatitude);

return new GeocentricCoordinateSystem
{
X = (primeVerticalRadius + altitude) * cosLatitude * Math.Cos(longitude),
Y = (primeVerticalRadius + altitude) * cosLatitude * Math.Sin(longitude),
Z = (primeVerticalRadius * (1.0 - Wgs84.FirstEccentricitySquared) + altitude) * sinLatitude,
};
}
}
}
Loading
Loading