File: Compass\Compass.shared.cs
Web Access
Project: src\src\Essentials\src\Essentials.csproj (Microsoft.Maui.Essentials)
#nullable enable
using System;
using Microsoft.Maui.ApplicationModel;
 
namespace Microsoft.Maui.Devices.Sensors
{
	/// <summary>
	/// Monitor changes to the orientation of the user's device.
	/// </summary>
	public interface ICompass
	{
		/// <summary>
		/// Gets a value indicating whether reading the compass is supported on this device.
		/// </summary>
		bool IsSupported { get; }
 
		/// <summary>
		/// Gets if compass is actively being monitored.
		/// </summary>
		bool IsMonitoring { get; }
 
		/// <summary>
		/// Start monitoring for changes to the compass.
		/// </summary>
		/// <param name="sensorSpeed">The speed to monitor for changes.</param>
		void Start(SensorSpeed sensorSpeed);
 
		/// <summary>
		/// Start monitoring for changes to the compass.
		/// </summary>
		/// <param name="sensorSpeed">The speed to monitor for changes.</param>
		/// <param name="applyLowPassFilter">Whether or not to apply a moving average filter (only used on Android).</param>
		void Start(SensorSpeed sensorSpeed, bool applyLowPassFilter);
 
		/// <summary>
		/// Stop monitoring for changes to the compass.
		/// </summary>
		void Stop();
 
		/// <summary>
		/// Occurs when compass reading changes.
		/// </summary>
		event EventHandler<CompassChangedEventArgs> ReadingChanged;
	}
 
	/// <summary>
	/// Platform-specific APIs for use with <see cref="ICompass"/>.
	/// </summary>
	public interface IPlatformCompass
	{
#if IOS || MACCATALYST
		/// <summary>
		/// Gets or sets if heading calibration should be shown.
		/// </summary>
		bool ShouldDisplayHeadingCalibration { get; set; }
#endif
	}
 
	/// <summary>
	/// Monitor changes to the orientation of the user's device.
	/// </summary>
	public static class Compass
	{
		/// <summary>
		/// Occurs when compass reading changes.
		/// </summary>
		public static event EventHandler<CompassChangedEventArgs> ReadingChanged
		{
			add => Current.ReadingChanged += value;
			remove => Current.ReadingChanged -= value;
		}
 
		/// <summary>
		/// Gets a value indicating whether reading the compass is supported on this device.
		/// </summary>
		public static bool IsSupported
			=> Current.IsSupported;
 
		/// <summary>
		/// Gets a value indicating whether the compass is actively being monitored.
		/// </summary>
		public static bool IsMonitoring
			=> Current.IsMonitoring;
 
		/// <summary>
		/// Start monitoring for changes to the compass.
		/// </summary>
		/// <remarks>
		/// Will throw <see cref="FeatureNotSupportedException"/> if not supported on device.
		/// Will throw <see cref="InvalidOperationException"/> if <see cref="IsMonitoring"/> is <see langword="true"/>.
		/// </remarks>
		/// <param name="sensorSpeed">The speed to monitor for changes.</param>
		public static void Start(SensorSpeed sensorSpeed)
			=> Start(sensorSpeed, true);
 
		/// <summary>
		/// Start monitoring for changes to the compass.
		/// </summary>
		/// <remarks>
		/// Will throw <see cref="FeatureNotSupportedException"/> if not supported on device.
		/// Will throw <see cref="InvalidOperationException"/> if <see cref="IsMonitoring"/> is <see langword="true"/>.
		/// </remarks>
		/// <param name="sensorSpeed">The speed to monitor for changes.</param>
		/// <param name="applyLowPassFilter">Whether or not to apply a moving average filter (only used on Android).</param>
		public static void Start(SensorSpeed sensorSpeed, bool applyLowPassFilter)
			=> Current.Start(sensorSpeed, applyLowPassFilter);
 
		/// <summary>
		/// Stop monitoring for changes to the compass.
		/// </summary>
		public static void Stop()
			=> Current.Stop();
 
#if IOS || MACCATALYST
		/// <summary>
		/// Gets or sets a value specifying whether the calibration screen should be displayed.
		/// </summary>
		/// <remarks>Only available on iOS.</remarks>
		public static bool ShouldDisplayHeadingCalibration
		{
			get
			{
				if (Current is IPlatformCompass c)
					return c.ShouldDisplayHeadingCalibration;
				return false;
			}
			set
			{
				if (Current is IPlatformCompass c)
					c.ShouldDisplayHeadingCalibration = value;
			}
		}
#endif
 
		static ICompass Current => Devices.Sensors.Compass.Default;
 
		static ICompass? defaultImplementation;
 
		/// <summary>
		/// Provides the default implementation for static usage of this API.
		/// </summary>
		public static ICompass Default =>
			defaultImplementation ??= new CompassImplementation();
 
		internal static void SetDefault(ICompass? implementation) =>
			defaultImplementation = implementation;
	}
 
	/// <summary>
	/// This class contains static extension methods for use with <see cref="ICompass"/>.
	/// </summary>
	public static class CompassExtensions
	{
#if IOS || MACCATALYST
		/// <summary>
		/// Gets or sets a value specifying whether the calibration screen should be displayed.
		/// </summary>
		/// <remarks>Only available on iOS.</remarks>
		public static void SetShouldDisplayHeadingCalibration(this ICompass compass, bool shouldDisplay)
		{
			if (compass is IPlatformCompass platform)
			{
				platform.ShouldDisplayHeadingCalibration = shouldDisplay;
			}
		}
#endif
	}
 
	/// <summary>
	/// Event arguments when compass reading changes.
	/// </summary>
	public class CompassChangedEventArgs : EventArgs
	{
		/// <summary>
		/// Initializes a new instance of the <see cref="CompassChangedEventArgs"/> class.
		/// </summary>
		/// <param name="reading">The compass data reading.</param>
		public CompassChangedEventArgs(CompassData reading) =>
			Reading = reading;
 
		/// <summary>
		/// The current values of the compass.
		/// </summary>
		public CompassData Reading { get; }
	}
 
	/// <summary>
	/// Contains the orientation of the user's device.
	/// </summary>
	public readonly struct CompassData : IEquatable<CompassData>
	{
		/// <summary>
		/// Initializes a new instance of the <see cref="CompassData"/> class.
		/// </summary>
		/// <param name="headingMagneticNorth">A reading of compass data for heading magnetic north.</param>
		public CompassData(double headingMagneticNorth) =>
			HeadingMagneticNorth = headingMagneticNorth;
 
		/// <summary>
		/// The heading (measured in degrees) relative to magnetic north.
		/// </summary>
		public double HeadingMagneticNorth { get; }
 
		/// <summary>
		/// Compares the underlying <see cref="CompassData"/> instances.
		/// </summary>
		/// <param name="obj">Object to compare with.</param>
		/// <returns><see langword="true"/> if they are equal, otherwise <see langword="false"/>.</returns>
		public override bool Equals(object? obj) =>
			(obj is CompassData data) && Equals(data);
 
		/// <summary>
		/// Compares the underlying <see cref="CompassData.HeadingMagneticNorth"/> instances.
		/// </summary>
		/// <param name="other">Object to compare with.</param>
		/// <returns><see langword="true"/> if they are equal, otherwise <see langword="false"/>.</returns>
		public bool Equals(CompassData other) =>
			HeadingMagneticNorth.Equals(other.HeadingMagneticNorth);
 
		/// <summary>
		///	Equality operator for equals.
		/// </summary>
		/// <param name="left">Left to compare.</param>
		/// <param name="right">Right to compare.</param>
		/// <returns><see langword="true"/> if objects are equal, otherwise <see langword="false"/>.</returns>
		public static bool operator ==(CompassData left, CompassData right) =>
			left.Equals(right);
 
		/// <summary>
		/// Inequality operator.
		/// </summary>
		/// <param name="left">Left to compare.</param>
		/// <param name="right">Right to compare.</param>
		/// <returns><see langword="true"/> if objects are not equal, otherwise <see langword="false"/>.</returns>
		public static bool operator !=(CompassData left, CompassData right) =>
		   !left.Equals(right);
 
		/// <inheritdoc cref="object.GetHashCode"/>
		public override int GetHashCode() =>
			HeadingMagneticNorth.GetHashCode();
 
		/// <summary>
		/// Returns a string representation of the current values of <see cref="CompassData.HeadingMagneticNorth"/>.
		/// </summary>
		/// <returns>A string representation of this instance in the format of <c>HeadingMagneticNorth: {value}</c>.</returns>
		public override string ToString() =>
			$"{nameof(HeadingMagneticNorth)}: {HeadingMagneticNorth}";
	}
 
	partial class CompassImplementation : ICompass
	{
		bool UseSyncContext => SensorSpeed == SensorSpeed.Default || SensorSpeed == SensorSpeed.UI;
 
		public event EventHandler<CompassChangedEventArgs>? ReadingChanged;
 
		public bool IsSupported
			=> PlatformIsSupported;
 
		public bool IsMonitoring { get; private set; }
 
		SensorSpeed SensorSpeed { get; set; }
 
		public void Start(SensorSpeed sensorSpeed) => Start(sensorSpeed, true);
 
		public void Start(SensorSpeed sensorSpeed, bool applyLowPassFilter)
		{
			if (!PlatformIsSupported)
				throw new FeatureNotSupportedException();
 
			if (IsMonitoring)
				throw new InvalidOperationException("Compass has already been started.");
 
			IsMonitoring = true;
 
 
			try
			{
				PlatformStart(sensorSpeed, applyLowPassFilter);
			}
			catch
			{
				IsMonitoring = false;
				throw;
			}
		}
 
		public void Stop()
		{
			if (!PlatformIsSupported)
				throw new FeatureNotSupportedException();
 
			if (!IsMonitoring)
				return;
 
			IsMonitoring = false;
 
			try
			{
				PlatformStop();
			}
			catch
			{
				IsMonitoring = true;
				throw;
			}
		}
 
		internal void RaiseReadingChanged(CompassData data)
		{
			var args = new CompassChangedEventArgs(data);
 
			if (UseSyncContext)
				MainThread.BeginInvokeOnMainThread(() => ReadingChanged?.Invoke(null, args));
			else
				ReadingChanged?.Invoke(null, args);
		}
	}
}