Skip to content

Commit

Permalink
Merge pull request #12 from WildernessLabs/develop
Browse files Browse the repository at this point in the history
RC2
  • Loading branch information
jorgedevs authored Dec 31, 2022
2 parents 9d11ed5 + aa23ce8 commit 685da81
Show file tree
Hide file tree
Showing 7 changed files with 230 additions and 13 deletions.
Binary file added Design/banner.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
81 changes: 81 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,82 @@
<img src="Design/banner.jpg" style="margin-bottom:10px" />

# Meadow.Units

This repo contains a strong unitization into the entire stack of Meadow. No more ambiguous `float` returns for temperature, pressure, distance, voltage, or other units. Instead sensors return strongly-typed units such as `Temperature` and `Voltage`, with properties that do automatic conversions to the various representations, such as `Celsius` or `Fahrenheit`.

## Contents
* [Using unitization](#using-unitization)
* [Nullable Everywhere](#nullable-everywhere)
* [Unit types](#unit-types)
* [Unit Conversions](#unit-conversions)

## Using unitization

Consider the following `AnalogTemperature` sensor sample:

```csharp
analogTemperature.TemperatureUpdated += (object sender, IChangeResult<Temperature> result) =>
{
Console.WriteLine($"Temp Changed, temp: {result.New.Celsius:N2}C, old: {result.Old?.Celsius:N2}C");
};
```

When the `TemperatureUpdated` event is raised, it passes an `IChangeResult<Temperature>` parameter. That result has a `New` and `Old` property of type `Temperature`, whose value can be accessed via a direct conversion to `Celsius`, `Fahrenheit`, or `Kelvin` on properties of the same name.

## Nullable Everywhere

Not also that Meadow fully embraces C# 8’s nullable features, so the result’s `Old` property is nullable, and in fact, on the first reading, it will be `null` because there isn’t a previous reading to populate it with.

## Unit types

We've added a number of unit types that describe a measure of something, including:

* `AbsoluteHumidity`
* `Acceleration`
* `Acceleration3D`
* `Angle`
* `AngularAcceleration`
* `AngularAcceleration3D`
* `AngularVelocity`
* `Azimuth`
* `Concentration`
* `Current`
* `Density`
* `Energy`
* `Frequency`
* `Illuminance`
* `Length`
* `MagneticField`
* `MagneticField3D`
* `Mass`
* `Power`
* `Pressure`
* `RelativeHumidity`
* `Speed`
* `Temperature`
* `Torque`
* `Voltage`
* `Volume`

## Unit Conversions

Each of these units have an enum of `UnitTypes` that they can be described as, as well as accessed as, via properties.

For instance the `Temperature` type has properties such as `Fahrenheit`, `Celsius`, `Kelvin`, etc. that allow you to access the unit value, converted to that particular `UnitType`. Consider the following code:

```csharp
var temp = new Temperature(32, UnitType.Fahrenheit);
Console.WriteLine($"{temp.Celsius:N2}C"); // outputs `0C`
Console.WriteLine($"{temp.Fahrenheit:N2}F"); // outputs `32F`
```

The units are all lightweight `struct` types which help to reduce heap allocations (when not boxed by Nullable), and have built in math operator and comparison support so you can perform math operations and comparison such as:

```csharp
Temperature t1 = new Temperature(1);
Temperature t2 = new Temperature(10);
Assert.That(t1 != t2);
Assert.That((t1 + t2) == new Temperature(11));
Assert.That((t2 - t1) == new Temperature(9));
Assert.That(t1 < t2);
```
15 changes: 13 additions & 2 deletions Source/Meadow.Units/Azimuth.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ public enum UnitType
/// Gets the cardinal direction value expressed as a unit _Decimal Degrees_ (`°`)
/// </summary>
public double DecimalDegrees => Value;
/// <summary>
/// Gets the cardinal direction value expressed as Radians
/// </summary>
public double Radians => Value * Math.PI / 180.0;

/// <summary>
/// Gets the cardinal direction value expressed as a unit a 16 division cardinal point
Expand All @@ -95,9 +99,16 @@ public Azimuth16PointCardinalNames Compass16PointCardinalName
/// <summary>
/// Creates a new `Azimuth` object from a unit value in _Decimal Degrees_ (`°`).
/// </summary>
/// <param name="value">The cardinal direction value.</param>
/// <param name="degrees">The cardinal direction value.</param>
/// <returns>A new cardinal direction object.</returns>
[Pure] public static Azimuth FromDecimalDegrees(double degrees) => new(ConvertTo360(degrees));

/// <summary>
/// Creates a new `Azimuth` object from a unit value in Radians.
/// </summary>
/// <param name="radians">The cardinal direction value.</param>
/// <returns>A new cardinal direction object.</returns>
[Pure] public static Azimuth FromDecimalDegrees(double value) => new(ConvertTo360(value));
[Pure] public static Azimuth FromRadians(double radians) => new(ConvertTo360(radians * 180 / Math.PI));

/// <summary>
/// Creates a new `Azimuth` object
Expand Down
115 changes: 115 additions & 0 deletions Source/Meadow.Units/GeoLocation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
using System;
using System.Diagnostics.Contracts;

namespace Meadow.Units
{
/// <summary>
/// Represents a location on the surface of an ideal Earth (latitude and longitude)
/// </summary>
public record GeoLocation
{
/// <summary>
/// Idealized earth radius used for internal calculations
/// </summary>
public Length EarthRadius => new Length(6371.01, Length.UnitType.Kilometers);

/// <summary>
/// The latitude portion of the GeoLocation
/// </summary>
public double Latitude { get; set; }
/// <summary>
/// The Longitude portion of the GeoLocation
/// </summary>
public double Longitude { get; set; }

/// <summary>
/// Creates a GeoLocation instance
/// </summary>
/// <param name="latitude"></param>
/// <param name="longitude"></param>
public GeoLocation(double latitude, double longitude)
{
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;
}

/// <summary>
/// Calculates the distance to anotehr GeoLocation
/// </summary>
/// <param name="other"></param>
/// <returns></returns>
[Pure]
public Length DistanceTo(GeoLocation other)
{
var diffLat = DegreesToRadians(other.Latitude - this.Latitude);
var diffLong = DegreesToRadians(other.Longitude - this.Longitude);

var a = Math.Sin(diffLat / 2) * Math.Sin(diffLat / 2) +
Math.Cos(DegreesToRadians(this.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 - this.Longitude);
var dPhi = Math.Log(Math.Tan(DegreesToRadians(other.Latitude) / 2 + Math.PI / 4) / Math.Tan(DegreesToRadians(this.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="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)
{
var distRatio = distance.Meters / EarthRadius.Meters;
var distRatioSine = Math.Sin(distRatio);
var distRatioCosine = Math.Cos(distRatio);

var startLatRad = DegreesToRadians(this.Latitude);
var startLonRad = DegreesToRadians(this.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));
}
}
}
5 changes: 4 additions & 1 deletion Source/Meadow.Units/Meadow.Units.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<PackageIcon>icon.png</PackageIcon>
<RepositoryUrl>https://github.com/WildernessLabs/Meadow.Units</RepositoryUrl>
<PackageTags>Meadow</PackageTags>
<Version>0.33.0</Version>
<Version>0.33.1</Version>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<RootNamespace>Meadow</RootNamespace>
Expand All @@ -21,6 +21,9 @@
<LangVersion>9.0</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Content Include="..\icon.png" Link="icon.png" />
</ItemGroup>
<ItemGroup>
<None Include="images\icon.png" Pack="true" PackagePath="\" />
</ItemGroup>
Expand Down
27 changes: 17 additions & 10 deletions Source/Meadow.Units/Temperature.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
using System;
using Meadow.Units.Conversions;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.Contracts;
using System.Runtime.InteropServices;
using Meadow.Units.Conversions;

namespace Meadow.Units
{
Expand All @@ -20,6 +20,8 @@ public struct Temperature :
IComparable, IFormattable, IConvertible,
IEquatable<double>, IComparable<double>
{
public static Temperature AbsoluteZero = new Temperature(0, UnitType.Kelvin);

/// <summary>
/// Creates a new `Temperature` object.
/// </summary>
Expand All @@ -28,7 +30,7 @@ public struct Temperature :
public Temperature(double value, UnitType type = UnitType.Celsius)
{
Value = 0;
switch (type)
switch (type)
{
case UnitType.Celsius:
Value = value;
Expand All @@ -40,6 +42,8 @@ public Temperature(double value, UnitType type = UnitType.Celsius)
Value = TempConversions.KToC(value);
break;
}

if (this < AbsoluteZero) throw new ArgumentOutOfRangeException();
}

/// <summary>
Expand Down Expand Up @@ -98,7 +102,8 @@ public enum UnitType
/// </summary>
/// <param name="convertTo">unit to convert to</param>
/// <returns></returns>
[Pure] public double From(UnitType convertTo)
[Pure]
public double From(UnitType convertTo)
{
return TempConversions.Convert(Value, UnitType.Celsius, convertTo);
}
Expand All @@ -108,7 +113,8 @@ [Pure] public double From(UnitType convertTo)
/// </summary>
/// <param name="obj">The object to compare</param>
/// <returns>true if equal</returns>
[Pure] public override bool Equals(object obj)
[Pure]
public override bool Equals(object obj)
{
if (obj is null) { return false; }
if (Equals(this, obj)) { return true; }
Expand Down Expand Up @@ -201,31 +207,31 @@ [Pure] public override bool Equals(object obj)
/// <param name="left">left value</param>
/// <param name="right">right value</param>
/// <returns>A new Temperature object with a value of left + right</returns>
[Pure] public static Temperature operator +(Temperature left, Temperature right) => new (left.Value + right.Value);
[Pure] public static Temperature operator +(Temperature left, Temperature right) => new(left.Value + right.Value);

/// <summary>
/// Subtraction operator to subtract two Temperature objects
/// </summary>
/// <param name="left">left value</param>
/// <param name="right">right value</param>
/// <returns>A new Temperature object with a value of left - right</returns>
[Pure] public static Temperature operator -(Temperature left, Temperature right) => new (left.Value - right.Value);
[Pure] public static Temperature operator -(Temperature left, Temperature right) => new(left.Value - right.Value);

/// <summary>
/// Multipication operator to multiply by a double
/// </summary>
/// <param name="value">object to multiply</param>
/// <param name="operand">operand to multiply object</param>
/// <returns>A new Temperature object with a value of value multiplied by the operand</returns>
[Pure] public static Temperature operator *(Temperature value, double operand) => new (value.Value * operand);
[Pure] public static Temperature operator *(Temperature value, double operand) => new(value.Value * operand);

/// <summary>
/// Division operator to divide by a double
/// </summary>
/// <param name="value">object to be divided</param>
/// <param name="operand">operand to divide object</param>
/// <returns>A new Temperature object with a value of value divided by the operand</returns>
[Pure] public static Temperature operator /(Temperature value, double operand) => new (value.Value / operand);
[Pure] public static Temperature operator /(Temperature value, double operand) => new(value.Value / operand);

/// <summary>
/// Returns the absolute length, that is, the length without regards to
Expand Down Expand Up @@ -380,7 +386,8 @@ [Pure] public override bool Equals(object obj)
/// </summary>
/// <param name="other">value to compare</param>
/// <returns>0 if equal</returns>
[Pure] public int CompareTo(double? other)
[Pure]
public int CompareTo(double? other)
{
return (other is null) ? -1 : (Value).CompareTo(other.Value);
}
Expand Down
Binary file added Source/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 685da81

Please sign in to comment.