Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

updates to geolocation bits #63

Merged
merged 1 commit into from
Jun 12, 2024
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
36 changes: 36 additions & 0 deletions Source/Meadow.Units.Tests/AzimuthTests.cs
Original file line number Diff line number Diff line change
@@ -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)

Check warning on line 34 in Source/Meadow.Units.Tests/AzimuthTests.cs

View workflow job for this annotation

GitHub Actions / build

The variable 'ex' is declared but never used

Check warning on line 34 in Source/Meadow.Units.Tests/AzimuthTests.cs

View workflow job for this annotation

GitHub Actions / build

The variable 'ex' is declared but never used
{
failCount++;
}
}

Assert.True(failCount == 0, $"{failCount} out of {failCount + successCount} failed CompareTo(object)");
}
}

public class AzimuthTests
{
Expand Down
97 changes: 13 additions & 84 deletions Source/Meadow.Units/GeoLocation.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
using System;
using System.Diagnostics.Contracts;

namespace Meadow.Units;
namespace Meadow.Units;

/// <summary>
/// Represents a location on the surface of an ideal Earth (latitude and longitude)
/// </summary>
public record GeoLocation
{
private static Length? _earthRadius;

/// <summary>
/// Idealized earth radius used for internal calculations
/// </summary>
public Length EarthRadius => new Length(6371.01, Length.UnitType.Kilometers);
public static Length EarthRadius => _earthRadius ??= new Length(6371.01, Length.UnitType.Kilometers);

/// <summary>
/// The latitude portion of the GeoLocation
Expand All @@ -25,90 +24,20 @@ public record GeoLocation
/// <summary>
/// Creates a GeoLocation instance
/// </summary>
/// <param name="latitude"></param>
/// <param name="longitude"></param>
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;
}

/// <summary>
/// Calculates the distance to another GeoLocation
/// </summary>
/// <param name="other"></param>
/// <returns></returns>
[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;
}

/// <summary>
/// Calculates the bearing to another GeoLocation
/// </summary>
/// <param name="other"></param>
/// <returns></returns>
[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));
}

/// <summary>
/// Creates a new GeoLocation a given bearing and distance from the current GeoLocation
/// Creates a GeoLocation instance
/// </summary>
/// <param name="bearing">Bearing angle to the new location</param>
/// <param name="distance">Distance to the new location</param>
/// <returns></returns>
[Pure]
public GeoLocation Move(Azimuth bearing, Length distance) // double initialBearingRadians, double distanceKilometres)
/// <param name="latitude">The latitude portion of the GeoLocation</param>
/// <param name="longitude">The longitude portion of the GeoLocation</param>
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;
}
}
89 changes: 89 additions & 0 deletions Source/Meadow.Units/GeoLocationExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using System;
using System.Diagnostics.Contracts;

namespace Meadow.Units;

/// <summary>
/// Extension methods for the GeoLocation struct
/// </summary>
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;
}

/// <summary>
/// Calculates the distance to another GeoLocation
/// </summary>
/// <param name="self">A Geolocation</param>
/// <param name="other">A second GeoLocation</param>
[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;
}

/// <summary>
/// Calculates the bearing to another GeoLocation
/// </summary>
/// <param name="self">A Geolocation</param>
/// <param name="other">A second GeoLocation</param>
[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));
}

/// <summary>
/// Creates a new GeoLocation a given bearing and distance from the current GeoLocation
/// </summary>
/// <param name="self">A Geolocation</param>
/// <param name="bearing">Bearing angle to the new location</param>
/// <param name="distance">Distance to the new location</param>
[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));
}
}
12 changes: 12 additions & 0 deletions Source/Meadow.Units/GeographicCoordinate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Meadow.Units;

/// <summary>
/// A geographic
/// </summary>
public record GeographicCoordinate : GeoLocation
{
/// <summary>
/// The altitude portion of the GeographicCoordinate
/// </summary>
public Length Altitude { get; set; }
}
Loading