Adds property change notification to properties

Also fixes a threading issue with AllMessages - Issue #76
This commit is contained in:
Morten Nielsen 2020-08-25 15:24:48 -07:00
parent da3b3f8788
commit 02579ab3e8
3 changed files with 214 additions and 71 deletions

View file

@ -14,7 +14,9 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading;
using NmeaParser.Messages;
namespace NmeaParser.Gnss
@ -22,10 +24,12 @@ namespace NmeaParser.Gnss
/// <summary>
/// Helper class for monitoring GNSS messages and combine them into a single useful location info
/// </summary>
public class GnssMonitor
public class GnssMonitor : INotifyPropertyChanged
{
private bool m_supportGNMessages; // If device detect GN* messages, ignore all other Talker ID
private bool m_supportGGaMessages; //If device support GGA, ignore RMC for location
private Dictionary<string, NmeaMessage> m_allMessages { get; } = new Dictionary<string, NmeaMessage>();
private object m_lock = new object();
/// <summary>
/// Initializes a new instance of the <see cref="GnssMonitor"/> class.
@ -37,8 +41,18 @@ namespace NmeaParser.Gnss
throw new ArgumentNullException(nameof(device));
Device = device;
Device.MessageReceived += NmeaMessageReceived;
SynchronizationContext = SynchronizationContext.Current;
}
/// <summary>
/// Gets or sets the syncronization context that <see cref="PropertyChanged"/> should be fired on
/// </summary>
/// <remarks>
/// The default is the context this thread was created monitor was created on, but for use in UI applications,
/// it can be beneficial to ensure this is the UI Thread. You can also set this to <c>null</c> for best performance
/// </remarks>
public SynchronizationContext? SynchronizationContext { get; set; }
/// <summary>
/// Gets the NMEA device that is being monitored
/// </summary>
@ -53,14 +67,16 @@ namespace NmeaParser.Gnss
/// Called when a message is received.
/// </summary>
/// <param name="message">The NMEA message that was received</param>
protected virtual void OnMessageReceived(NmeaMessage message)
{
protected virtual void OnMessageReceived(NmeaMessage message)
{
bool isNewFix = false;
bool lostFix = false;
double lat = 0;
double lon = 0;
AllMessages[message.MessageType] = message;
List<string> properties = new List<string>();
lock (m_lock)
m_allMessages[message.MessageType] = message;
properties.Add(nameof(AllMessages));
if (message.TalkerId == NmeaParser.Talker.GlobalNavigationSatelliteSystem)
m_supportGNMessages = true; // Support for GN* messages detected
else if (m_supportGNMessages && message.TalkerId != NmeaParser.Talker.GlobalNavigationSatelliteSystem)
@ -68,25 +84,52 @@ namespace NmeaParser.Gnss
if (message is NmeaParser.Messages.Garmin.Pgrme rme)
{
HorizontalError = rme.HorizontalError;
VerticalError = rme.VerticalError;
if (rme.HorizontalError != HorizontalError)
{
properties.Add(nameof(HorizontalError));
HorizontalError = rme.HorizontalError;
}
if (rme.VerticalError != VerticalError)
{
VerticalError = rme.VerticalError;
properties.Add(nameof(VerticalError));
}
}
else if (message is Gst gst)
{
Gst = gst;
VerticalError = gst.SigmaHeightError;
HorizontalError = Math.Round(Math.Sqrt(Gst.SigmaLatitudeError * Gst.SigmaLatitudeError + Gst.SigmaLongitudeError * Gst.SigmaLongitudeError), 3);
properties.Add(nameof(Gst));
var error = Math.Round(Math.Sqrt(Gst.SigmaLatitudeError * Gst.SigmaLatitudeError + Gst.SigmaLongitudeError * Gst.SigmaLongitudeError), 3);
if (error != HorizontalError)
{
HorizontalError = error;
properties.Add(nameof(HorizontalError));
}
if (VerticalError != gst.SigmaHeightError)
{
VerticalError = gst.SigmaHeightError;
properties.Add(nameof(VerticalError));
}
}
else if (message is Rmc rmc)
{
if (Speed != rmc.Speed)
properties.Add(nameof(Speed));
if (Course != rmc.Course)
properties.Add(nameof(Course));
Rmc = rmc;
properties.Add(nameof(Rmc));
if (!m_supportGGaMessages)
{
if (Rmc.Active)
{
lat = Rmc.Latitude;
lon = Rmc.Longitude;
FixTime = Rmc.FixTime.TimeOfDay;
if (FixTime != Rmc.FixTime.TimeOfDay)
{
FixTime = Rmc.FixTime.TimeOfDay;
properties.Add(nameof(FixTime));
}
isNewFix = true;
}
else
@ -101,52 +144,102 @@ namespace NmeaParser.Gnss
{
// Datum change
Dtm = dtm;
properties.Add(nameof(Dtm));
Latitude = double.NaN;
Longitude = double.NaN;
IsFixValid = false;
properties.Add(nameof(Dtm));
properties.Add(nameof(Datum));
properties.Add(nameof(Latitude));
properties.Add(nameof(Longitude));
properties.Add(nameof(IsFixValid));
}
}
else if (message is Gga gga)
{
if (gga.Hdop != Hdop)
properties.Add(nameof(Hdop));
if (gga.Quality != FixQuality)
properties.Add(nameof(FixQuality));
Gga = gga;
properties.Add(nameof(Gga));
m_supportGGaMessages = true;
if (gga.Quality != Gga.FixQuality.Invalid)
{
lat = gga.Latitude;
lon = gga.Longitude;
GeoidHeight = gga.GeoidalSeparation;
properties.Add(nameof(GeoidHeight));
Altitude = gga.Altitude + gga.GeoidalSeparation; //Convert to ellipsoidal height
properties.Add(nameof(Altitude));
}
if (gga.Quality == Gga.FixQuality.Invalid || gga.Quality == Gga.FixQuality.Estimated)
{
lostFix = true;
}
FixTime = Gga.FixTime;
if (FixTime != Gga.FixTime)
{
FixTime = Gga.FixTime;
properties.Add(nameof(FixTime));
}
isNewFix = true;
}
else if (message is Gsa gsa)
{
if (gsa.Hdop != Hdop)
properties.Add(nameof(Hdop));
if (gsa.Pdop != Pdop)
properties.Add(nameof(Pdop));
if (gsa.Vdop != Vdop)
properties.Add(nameof(Vdop));
Gsa = gsa;
properties.Add(nameof(Gsa));
}
else if (message is Vtg vtg)
{
if (Speed != vtg.SpeedKnots)
properties.Add(nameof(Speed));
Vtg = vtg;
properties.Add(nameof(Vtg));
}
else if (message is Gsv)
{
properties.Add(nameof(Satellites));
properties.Add(nameof(SatellitesInView));
}
if (lostFix)
{
if (!IsFixValid)
{
IsFixValid = false;
properties.Add(nameof(IsFixValid));
properties.Add(nameof(FixQuality));
LocationLost?.Invoke(this, EventArgs.Empty);
}
}
if (isNewFix)
{
Latitude = lat;
Longitude = lon;
IsFixValid = true;
if (Latitude != lat)
{
properties.Add(nameof(Latitude));
Latitude = lat;
}
if (Longitude != lon)
{
properties.Add(nameof(Longitude));
Longitude = lon;
}
if (!IsFixValid)
{
properties.Add(nameof(IsFixValid));
if (Gga == null)
properties.Add(nameof(FixQuality));
IsFixValid = true;
}
LocationChanged?.Invoke(this, EventArgs.Empty);
}
if (properties.Count > 0)
OnPropertyChanged(properties);
}
/// <summary>
@ -157,7 +250,7 @@ namespace NmeaParser.Gnss
/// </remarks>
/// <seealso cref="LocationLost"/>
public bool IsFixValid { get; private set; }
/// <summary>
/// Gets the latitude for the current or last known location.
/// </summary>
@ -171,7 +264,7 @@ namespace NmeaParser.Gnss
/// <seealso cref="IsFixValid"/>
/// <seealso cref="Latitude"/>
public double Longitude { get; private set; } = double.NaN;
/// <summary>
/// Gets the geight above the ellipsoid
/// </summary>
@ -254,22 +347,51 @@ namespace NmeaParser.Gnss
/// <summary>
/// Gets a list of satellite vehicles in the sky
/// </summary>
public IEnumerable<SatelliteVehicle> Satellites => AllMessages.Values.OfType<Gsv>().SelectMany(s => s.SVs);
public IEnumerable<SatelliteVehicle> Satellites
{
get
{
lock (m_lock)
{
return m_allMessages.Values.OfType<Gsv>().SelectMany(s => s.SVs).ToArray();
}
}
}
/// <summary>
/// Gets the number of satellites in the sky
/// </summary>
public int SatellitesInView => AllMessages.Values.OfType<Gsv>().Sum(s => s.SatellitesInView);
public int SatellitesInView
{
get
{
lock (m_lock)
{
return m_allMessages.Values.OfType<Gsv>().Sum(s => s.SatellitesInView);
}
}
}
/// <summary>
/// Gets the quality of the current fix
/// </summary>
public Gga.FixQuality FixQuality => !IsFixValid ? Gga.FixQuality.Invalid : (Gga?.Quality ?? Gga.FixQuality.GpsFix);
/// <summary>
/// Gets a list of all NMEA messages currently part of this location
/// </summary>
public Dictionary<string, NmeaMessage> AllMessages { get; } = new Dictionary<string, NmeaMessage>();
public IEnumerable<KeyValuePair<string, NmeaMessage>> AllMessages
{
get
{
lock (m_lock)
{
return m_allMessages.ToArray();
}
}
}
/// <summary>
/// Gets a value indicating the current Datum being used.
@ -301,5 +423,27 @@ namespace NmeaParser.Gnss
/// </summary>
/// <seealso cref="IsFixValid"/>
public event EventHandler? LocationLost;
/// <inheritdoc />
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged(IEnumerable<string> properties)
{
if (PropertyChanged == null)
return;
if (SynchronizationContext != null)
{
SynchronizationContext.Post((d) =>
{
foreach (string propertyName in (IEnumerable<string>)d)
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}, properties);
}
else
{
foreach (string propertyName in properties)
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}

View file

@ -10,19 +10,11 @@
<Border Background="CornflowerBlue" Padding="20,10" Margin="-10,-10,-10,5" x:Name="HeaderPanel">
<TextBlock Text="GNSS Monitor" FontSize="20" FontWeight="Bold" Foreground="White" />
</Border>
<ItemsControl x:Name="Values" Margin="20,10">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid >
<Grid.ColumnDefinitions>
<ColumnDefinition MinWidth="100" Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Key}" Margin="0,0,5,0" VerticalAlignment="Top" FontWeight="Bold" />
<TextBlock Text="{Binding Value}" TextWrapping="Wrap" Grid.Column="1" VerticalAlignment="Top" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Grid x:Name="data" Margin="20,10">
<Grid.ColumnDefinitions>
<ColumnDefinition MinWidth="100" Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
</Grid>
</StackPanel>
</UserControl>

View file

@ -1,7 +1,9 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
using NmeaParser.Gnss;
using NmeaParser.Messages;
@ -16,9 +18,33 @@ namespace SampleApp.WinDesktop
public GnssMonitorView()
{
InitializeComponent();
}
SetupBindings();
}
public GnssMonitor Monitor
private void SetupBindings()
{
var props = typeof(GnssMonitor).GetProperties(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public);
List<KeyValuePair<string, object>> values = new List<KeyValuePair<string, object>>();
int count = 0;
ArrayConverter conv = new ArrayConverter();
foreach (var prop in props.OrderBy(t => t.Name))
{
if (prop.Name == nameof(GnssMonitor.AllMessages) || prop.Name == nameof(GnssMonitor.SynchronizationContext)) continue;
data.RowDefinitions.Add(new RowDefinition());
var title = new TextBlock() { Text = prop.Name, FontWeight = FontWeights.Bold, Margin = new Thickness(0, 0, 5, 0), VerticalAlignment = VerticalAlignment.Top };
Grid.SetRow(title, count);
data.Children.Add(title);
var valuebox = new TextBlock() { VerticalAlignment = VerticalAlignment.Top, TextWrapping = TextWrapping.Wrap };
Grid.SetRow(valuebox, count);
Grid.SetColumn(valuebox, 1);
var binding = new Binding(prop.Name) { Converter = conv };
valuebox.SetBinding(TextBlock.TextProperty, binding);
data.Children.Add(valuebox);
count++;
}
}
public GnssMonitor Monitor
{
get { return (GnssMonitor)GetValue(MonitorProperty); }
set { SetValue(MonitorProperty, value); }
@ -29,48 +55,29 @@ namespace SampleApp.WinDesktop
private void OnMonitorPropertyChanged(DependencyPropertyChangedEventArgs e)
{
if (e.OldValue is GnssMonitor oldMonitor)
{
oldMonitor.LocationChanged -= LocationChanged;
oldMonitor.LocationLost -= LocationChanged;
}
if (e.NewValue is GnssMonitor newMonitor)
{
newMonitor.LocationChanged += LocationChanged;
newMonitor.LocationLost += LocationChanged;
}
UpdateValues();
data.DataContext = Monitor;
}
private void LocationChanged(object sender, System.EventArgs e)
private class ArrayConverter : IValueConverter
{
Dispatcher.Invoke(UpdateValues);
}
private void UpdateValues()
{
if (Monitor == null)
Values.ItemsSource = null;
else
public object Convert(object value, System.Type targetType, object parameter, CultureInfo culture)
{
var props = Monitor.GetType().GetProperties(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public);
List<KeyValuePair<string, object>> values = new List<KeyValuePair<string, object>>();
foreach (var prop in props.OrderBy(t => t.Name))
if (!(value is string) && value is System.Collections.IEnumerable arr)
{
if (prop.Name == nameof(GnssMonitor.AllMessages)) continue;
if (prop.PropertyType.IsSubclassOf(typeof(NmeaMessage)))
continue;
var value = prop.GetValue(Monitor);
if (!(value is string) && value is System.Collections.IEnumerable arr)
{
var str = "[" + string.Join(",", arr.OfType<object>().ToArray()) + "]";
if (str.Length == 2)
str = "[ ]";
value = str;
}
values.Add(new KeyValuePair<string, object>(prop.Name, value));
var str = "[" + string.Join(",", arr.OfType<object>().ToArray()) + "]";
if (str.Length == 2)
str = "[ ]";
value = str;
}
Values.ItemsSource = values;
if (value is null)
return "<null>";
return value;
}
}
public object ConvertBack(object value, System.Type targetType, object parameter, CultureInfo culture)
{
throw new System.NotImplementedException();
}
}
}
}