From 3ea90dba7ed98922ecbd7e1c18fc816663715d74 Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Sun, 9 Jun 2024 10:37:37 -0500 Subject: [PATCH] updates to geolocation bits --- Source/Meadow.Units.Tests/AzimuthTests.cs | 36 ++++++++ Source/Meadow.Units/GeoLocation.cs | 97 +++----------------- Source/Meadow.Units/GeoLocationExtensions.cs | 89 ++++++++++++++++++ Source/Meadow.Units/GeographicCoordinate.cs | 12 +++ 4 files changed, 150 insertions(+), 84 deletions(-) create mode 100644 Source/Meadow.Units/GeoLocationExtensions.cs create mode 100644 Source/Meadow.Units/GeographicCoordinate.cs diff --git a/Source/Meadow.Units.Tests/AzimuthTests.cs b/Source/Meadow.Units.Tests/AzimuthTests.cs index 1cf137b..98de336 100644 --- a/Source/Meadow.Units.Tests/AzimuthTests.cs +++ b/Source/Meadow.Units.Tests/AzimuthTests.cs @@ -1,9 +1,45 @@ using System; using System.Collections.Generic; +using System.Linq; using Xunit; namespace Meadow.Units.Tests; +public class CompareToTests +{ + [Fact] + public void CompareToObjectOfSameType() + { + // find all comparable structs in the assembly + var testTypes = typeof(Temperature) + .Assembly + .GetTypes() + .Where(t => t.IsValueType && typeof(IComparable).IsAssignableFrom(t)) + .ToArray(); + + var failCount = 0; + var successCount = 0; + + foreach (var t in testTypes) + { + var testItemA = Activator.CreateInstance(t); + var testItemB = Activator.CreateInstance(t); + + try + { + var shouldbeZero = (testItemA as IComparable).CompareTo(testItemB); + Assert.Equal(0, shouldbeZero); + successCount++; + } + catch (Exception ex) + { + failCount++; + } + } + + Assert.True(failCount == 0, $"{failCount} out of {failCount + successCount} failed CompareTo(object)"); + } +} public class AzimuthTests { diff --git a/Source/Meadow.Units/GeoLocation.cs b/Source/Meadow.Units/GeoLocation.cs index 60cfb5c..cedbda0 100644 --- a/Source/Meadow.Units/GeoLocation.cs +++ b/Source/Meadow.Units/GeoLocation.cs @@ -1,17 +1,16 @@ -using System; -using System.Diagnostics.Contracts; - -namespace Meadow.Units; +namespace Meadow.Units; /// /// Represents a location on the surface of an ideal Earth (latitude and longitude) /// public record GeoLocation { + private static Length? _earthRadius; + /// /// Idealized earth radius used for internal calculations /// - public Length EarthRadius => new Length(6371.01, Length.UnitType.Kilometers); + public static Length EarthRadius => _earthRadius ??= new Length(6371.01, Length.UnitType.Kilometers); /// /// The latitude portion of the GeoLocation @@ -25,90 +24,20 @@ public record GeoLocation /// /// Creates a GeoLocation instance /// - /// - /// - public GeoLocation(double latitude, double longitude) + public GeoLocation() { - Latitude = latitude; - Longitude = longitude; - } - - [Pure] - private static double DegreesToRadians(double degrees) - { - return degrees * Math.PI / 180.0; - } - - [Pure] - private static double RadiansToDegrees(double radians) - { - return radians * 180 / Math.PI; + Latitude = 0; + Longitude = 0; } /// - /// Calculates the distance to another GeoLocation - /// - /// - /// - [Pure] - public Length DistanceTo(GeoLocation other) - { - var diffLat = DegreesToRadians(other.Latitude - Latitude); - var diffLong = DegreesToRadians(other.Longitude - Longitude); - - var a = Math.Sin(diffLat / 2) * Math.Sin(diffLat / 2) + - Math.Cos(DegreesToRadians(Latitude)) * Math.Cos(DegreesToRadians(other.Latitude)) * - Math.Sin(diffLong / 2) * Math.Sin(diffLong / 2); - var c = 2 * Math.Asin(Math.Min(1, Math.Sqrt(a))); - var d = EarthRadius * c; - - return d; - } - - /// - /// Calculates the bearing to another GeoLocation - /// - /// - /// - [Pure] - public Azimuth BearingTo(GeoLocation other) - { - var dLon = DegreesToRadians(other.Longitude - Longitude); - var dPhi = Math.Log(Math.Tan(DegreesToRadians(other.Latitude) / 2 + Math.PI / 4) / Math.Tan(DegreesToRadians(Latitude) / 2 + Math.PI / 4)); - if (Math.Abs(dLon) > Math.PI) - { - dLon = dLon > 0 ? -(2 * Math.PI - dLon) : (2 * Math.PI + dLon); - } - - return Azimuth.FromRadians(Math.Atan2(dLon, dPhi)); - } - - /// - /// Creates a new GeoLocation a given bearing and distance from the current GeoLocation + /// Creates a GeoLocation instance /// - /// Bearing angle to the new location - /// Distance to the new location - /// - [Pure] - public GeoLocation Move(Azimuth bearing, Length distance) // double initialBearingRadians, double distanceKilometres) + /// The latitude portion of the GeoLocation + /// The longitude portion of the GeoLocation + public GeoLocation(double latitude, double longitude) { - var distRatio = distance.Meters / EarthRadius.Meters; - var distRatioSine = Math.Sin(distRatio); - var distRatioCosine = Math.Cos(distRatio); - - var startLatRad = DegreesToRadians(Latitude); - var startLonRad = DegreesToRadians(Longitude); - - var startLatCos = Math.Cos(startLatRad); - var startLatSin = Math.Sin(startLatRad); - - var endLatRads = Math.Asin((startLatSin * distRatioCosine) + (startLatCos * distRatioSine * Math.Cos(bearing.Radians))); - - var endLonRads = startLonRad - + Math.Atan2( - Math.Sin(bearing.Radians) * distRatioSine * startLatCos, - distRatioCosine - startLatSin * Math.Sin(endLatRads)); - - return new GeoLocation(RadiansToDegrees(endLatRads), RadiansToDegrees(endLonRads)); + Latitude = latitude; + Longitude = longitude; } } \ No newline at end of file diff --git a/Source/Meadow.Units/GeoLocationExtensions.cs b/Source/Meadow.Units/GeoLocationExtensions.cs new file mode 100644 index 0000000..0b85f6d --- /dev/null +++ b/Source/Meadow.Units/GeoLocationExtensions.cs @@ -0,0 +1,89 @@ +using System; +using System.Diagnostics.Contracts; + +namespace Meadow.Units; + +/// +/// Extension methods for the GeoLocation struct +/// +public static class GeoLocationExtensions +{ + [Pure] + private static double DegreesToRadians(double degrees) + { + return degrees * Math.PI / 180.0; + } + + [Pure] + private static double RadiansToDegrees(double radians) + { + return radians * 180 / Math.PI; + } + + /// + /// Calculates the distance to another GeoLocation + /// + /// A Geolocation + /// A second GeoLocation + [Pure] + public static Length DistanceTo(this GeoLocation self, GeoLocation other) + { + var diffLat = DegreesToRadians(other.Latitude - self.Latitude); + var diffLong = DegreesToRadians(other.Longitude - self.Longitude); + + var a = Math.Sin(diffLat / 2) * Math.Sin(diffLat / 2) + + Math.Cos(DegreesToRadians(self.Latitude)) * Math.Cos(DegreesToRadians(other.Latitude)) * + Math.Sin(diffLong / 2) * Math.Sin(diffLong / 2); + var c = 2 * Math.Asin(Math.Min(1, Math.Sqrt(a))); + var d = GeoLocation.EarthRadius * c; + + return d; + } + + /// + /// Calculates the bearing to another GeoLocation + /// + /// A Geolocation + /// A second GeoLocation + [Pure] + public static Azimuth BearingTo(this GeoLocation self, GeoLocation other) + { + var dLon = DegreesToRadians(other.Longitude - self.Longitude); + var dPhi = Math.Log(Math.Tan(DegreesToRadians(other.Latitude) / 2 + Math.PI / 4) / Math.Tan(DegreesToRadians(self.Latitude) / 2 + Math.PI / 4)); + if (Math.Abs(dLon) > Math.PI) + { + dLon = dLon > 0 ? -(2 * Math.PI - dLon) : (2 * Math.PI + dLon); + } + + return Azimuth.FromRadians(Math.Atan2(dLon, dPhi)); + } + + /// + /// Creates a new GeoLocation a given bearing and distance from the current GeoLocation + /// + /// A Geolocation + /// Bearing angle to the new location + /// Distance to the new location + [Pure] + public static GeoLocation Move(this GeoLocation self, Azimuth bearing, Length distance) // double initialBearingRadians, double distanceKilometres) + { + var distRatio = distance.Meters / GeoLocation.EarthRadius.Meters; + var distRatioSine = Math.Sin(distRatio); + var distRatioCosine = Math.Cos(distRatio); + + var startLatRad = DegreesToRadians(self.Latitude); + var startLonRad = DegreesToRadians(self.Longitude); + + var startLatCos = Math.Cos(startLatRad); + var startLatSin = Math.Sin(startLatRad); + + var endLatRads = Math.Asin((startLatSin * distRatioCosine) + (startLatCos * distRatioSine * Math.Cos(bearing.Radians))); + + var endLonRads = startLonRad + + Math.Atan2( + Math.Sin(bearing.Radians) * distRatioSine * startLatCos, + distRatioCosine - startLatSin * Math.Sin(endLatRads)); + + return new GeoLocation(RadiansToDegrees(endLatRads), RadiansToDegrees(endLonRads)); + } +} diff --git a/Source/Meadow.Units/GeographicCoordinate.cs b/Source/Meadow.Units/GeographicCoordinate.cs new file mode 100644 index 0000000..98b9be5 --- /dev/null +++ b/Source/Meadow.Units/GeographicCoordinate.cs @@ -0,0 +1,12 @@ +namespace Meadow.Units; + +/// +/// A geographic +/// +public record GeographicCoordinate : GeoLocation +{ + /// + /// The altitude portion of the GeographicCoordinate + /// + public Length Altitude { get; set; } +}