From 02579ab3e84a7aed3a53057fd62c33a50f7c07d4 Mon Sep 17 00:00:00 2001 From: Morten Nielsen Date: Tue, 25 Aug 2020 15:24:48 -0700 Subject: [PATCH] Adds property change notification to properties Also fixes a threading issue with AllMessages - Issue #76 --- src/NmeaParser/Gnss/GnssMonitor.cs | 182 ++++++++++++++++-- src/SampleApp.WinDesktop/GnssMonitorView.xaml | 20 +- .../GnssMonitorView.xaml.cs | 83 ++++---- 3 files changed, 214 insertions(+), 71 deletions(-) diff --git a/src/NmeaParser/Gnss/GnssMonitor.cs b/src/NmeaParser/Gnss/GnssMonitor.cs index c78db20..669f2a9 100644 --- a/src/NmeaParser/Gnss/GnssMonitor.cs +++ b/src/NmeaParser/Gnss/GnssMonitor.cs @@ -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 /// /// Helper class for monitoring GNSS messages and combine them into a single useful location info /// - 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 m_allMessages { get; } = new Dictionary(); + private object m_lock = new object(); /// /// Initializes a new instance of the class. @@ -37,8 +41,18 @@ namespace NmeaParser.Gnss throw new ArgumentNullException(nameof(device)); Device = device; Device.MessageReceived += NmeaMessageReceived; + SynchronizationContext = SynchronizationContext.Current; } + /// + /// Gets or sets the syncronization context that should be fired on + /// + /// + /// 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 null for best performance + /// + public SynchronizationContext? SynchronizationContext { get; set; } + /// /// Gets the NMEA device that is being monitored /// @@ -53,14 +67,16 @@ namespace NmeaParser.Gnss /// Called when a message is received. /// /// The NMEA message that was received - 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 properties = new List(); + 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); } /// @@ -157,7 +250,7 @@ namespace NmeaParser.Gnss /// /// public bool IsFixValid { get; private set; } - + /// /// Gets the latitude for the current or last known location. /// @@ -171,7 +264,7 @@ namespace NmeaParser.Gnss /// /// public double Longitude { get; private set; } = double.NaN; - + /// /// Gets the geight above the ellipsoid /// @@ -254,22 +347,51 @@ namespace NmeaParser.Gnss /// /// Gets a list of satellite vehicles in the sky /// - public IEnumerable Satellites => AllMessages.Values.OfType().SelectMany(s => s.SVs); + public IEnumerable Satellites + { + get + { + lock (m_lock) + { + return m_allMessages.Values.OfType().SelectMany(s => s.SVs).ToArray(); + } + } + } /// /// Gets the number of satellites in the sky /// - public int SatellitesInView => AllMessages.Values.OfType().Sum(s => s.SatellitesInView); + public int SatellitesInView + { + get + { + lock (m_lock) + { + return m_allMessages.Values.OfType().Sum(s => s.SatellitesInView); + } + } + } + /// /// Gets the quality of the current fix /// public Gga.FixQuality FixQuality => !IsFixValid ? Gga.FixQuality.Invalid : (Gga?.Quality ?? Gga.FixQuality.GpsFix); + /// /// Gets a list of all NMEA messages currently part of this location /// - public Dictionary AllMessages { get; } = new Dictionary(); + public IEnumerable> AllMessages + { + get + { + lock (m_lock) + { + return m_allMessages.ToArray(); + } + } + } /// /// Gets a value indicating the current Datum being used. @@ -301,5 +423,27 @@ namespace NmeaParser.Gnss /// /// public event EventHandler? LocationLost; + + /// + public event PropertyChangedEventHandler? PropertyChanged; + + private void OnPropertyChanged(IEnumerable properties) + { + if (PropertyChanged == null) + return; + if (SynchronizationContext != null) + { + SynchronizationContext.Post((d) => + { + foreach (string propertyName in (IEnumerable)d) + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + }, properties); + } + else + { + foreach (string propertyName in properties) + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } } } diff --git a/src/SampleApp.WinDesktop/GnssMonitorView.xaml b/src/SampleApp.WinDesktop/GnssMonitorView.xaml index 565f480..959b74c 100644 --- a/src/SampleApp.WinDesktop/GnssMonitorView.xaml +++ b/src/SampleApp.WinDesktop/GnssMonitorView.xaml @@ -10,19 +10,11 @@ - - - - - - - - - - - - - - + + + + + + diff --git a/src/SampleApp.WinDesktop/GnssMonitorView.xaml.cs b/src/SampleApp.WinDesktop/GnssMonitorView.xaml.cs index 6bf6837..216e0d3 100644 --- a/src/SampleApp.WinDesktop/GnssMonitorView.xaml.cs +++ b/src/SampleApp.WinDesktop/GnssMonitorView.xaml.cs @@ -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> values = new List>(); + 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> values = new List>(); - 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().ToArray()) + "]"; - if (str.Length == 2) - str = "[ ]"; - value = str; - } - values.Add(new KeyValuePair(prop.Name, value)); + var str = "[" + string.Join(",", arr.OfType().ToArray()) + "]"; + if (str.Length == 2) + str = "[ ]"; + value = str; } - Values.ItemsSource = values; + if (value is null) + return ""; + return value; } - } + + public object ConvertBack(object value, System.Type targetType, object parameter, CultureInfo culture) + { + throw new System.NotImplementedException(); + } + } } }