Skip to content

Commit

Permalink
Merge pull request #63 from WildernessLabs/feature/geolocation
Browse files Browse the repository at this point in the history
updates to geolocation bits
  • Loading branch information
adrianstevens authored Jun 12, 2024
2 parents a745eed + 3ea90db commit b42147e
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 84 deletions.
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; }
}

0 comments on commit b42147e

Please sign in to comment.