Add custom ArcGIS location provider to desktop sample (#69)

* Location provider sample for ArcGIS Runtime
This commit is contained in:
Morten Nielsen 2020-01-28 20:44:16 -08:00 committed by GitHub
parent 9ab1c0cef6
commit c9c631530e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 766 additions and 527 deletions

View file

@ -0,0 +1,27 @@
<UserControl x:Class="SampleApp.WinDesktop.AltitudeGraph"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid Margin="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock x:Name="maxtb" VerticalAlignment="Top" FontSize="8" Margin="0,-4,0,0" />
<TextBlock x:Name="mintb" VerticalAlignment="Bottom" FontSize="8" Margin="0,0,0,-4" />
<Border BorderBrush="CornflowerBlue" BorderThickness="0,1"
Grid.Column="1" Opacity=".5" />
<Grid Grid.Column="1">
<Path Stretch="Fill"
StrokeLineJoin="Round"
Stroke="CornflowerBlue"
StrokeThickness="2"
x:Name="path"
/>
</Grid>
</Grid>
</UserControl>

View file

@ -0,0 +1,91 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace SampleApp.WinDesktop
{
/// <summary>
/// Interaction logic for AltitudeGraph.xaml
/// </summary>
public partial class AltitudeGraph : UserControl
{
Queue<double> datapoints = new Queue<double>();
double min = double.MaxValue;
double max = double.MinValue;
public AltitudeGraph()
{
InitializeComponent();
MaxDatapoints = 150;
}
public void AddDataPoint(double value)
{
if (double.IsNaN(value))
return;
datapoints.Enqueue(value);
min = Math.Min(value, double.IsNaN(min) ? value : min);
max = Math.Max(value, double.IsNaN(max) ? value : max);
if (datapoints.Count > MaxDatapoints)
{
double val = datapoints.Dequeue();
//If this is the limiting value, recalculate min/max
if (val == min)
min = datapoints.Min();
if (val == max)
max = datapoints.Max();
}
UpdatePath();
mintb.Text = min.ToString("0");
maxtb.Text = max.ToString("0");
}
private void UpdatePath()
{
if(!datapoints.Any())
{
path.Data = null;
return;
}
var data = datapoints.ToArray();
List<PathSegment> segments = new List<PathSegment>();
for(int i=1;i<data.Length;i++)
{
segments.Add(new LineSegment(new Point(i, max - data[i]), true));
}
PathFigure pf = new PathFigure(new Point(0, max - data[0]), segments, false);
path.Data = new PathGeometry(new PathFigure[] { pf });
}
public int MaxDatapoints { get; set; }
public double Value
{
get { return (double)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
// Using a DependencyProperty as the backing store for Value. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof(double), typeof(AltitudeGraph), new PropertyMetadata(0d, OnValuePropertyChanged));
private static void OnValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((AltitudeGraph)d).AddDataPoint((double)e.NewValue);
}
}
}

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.1"/>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1"/>
</startup>
</configuration>

View file

@ -1,7 +1,8 @@
<Window x:Class="SampleApp.WinDesktop.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:SampleApp.WinDesktop"
xmlns:local="clr-namespace:SampleApp.WinDesktop"
xmlns:esri="http://schemas.esri.com/arcgis/runtime/2013"
Title="Sample App" Height="500" Width="625">
<Window.Resources>
<local:NullToCollapsedConverter x:Key="nullConv" />
@ -12,89 +13,153 @@
<Setter Property="Effect">
<Setter.Value>
<DropShadowEffect Direction="0" ShadowDepth="0"
BlurRadius="20" Opacity=".5" />
BlurRadius="20" Opacity=".5" />
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Grid>
<TabControl>
<TabItem Header="GPS Info">
<ScrollViewer HorizontalScrollBarVisibility="Disabled" Background="#FFEEEEEE">
<WrapPanel x:Name="MessagePanel">
<local:GprmcControl x:Name="gprmcView" Style="{StaticResource card}" Visibility="{Binding Message, ElementName=gprmcView, Converter={StaticResource nullConv}}" />
<local:GpggaControl x:Name="gpggaView" Style="{StaticResource card}" Visibility="{Binding Message, ElementName=gpggaView, Converter={StaticResource nullConv}}" />
<local:GpgsaControl x:Name="gpgsaView" Style="{StaticResource card}" Visibility="{Binding Message, ElementName=gpgsaView, Converter={StaticResource nullConv}}" />
<local:GpgllControl x:Name="gpgllView" Style="{StaticResource card}" Visibility="{Binding Message, ElementName=gpgllView, Converter={StaticResource nullConv}}" />
<local:PgrmeControl x:Name="pgrmeView" Style="{StaticResource card}" Visibility="{Binding Message, ElementName=pgrmeView, Converter={StaticResource nullConv}}" />
</WrapPanel>
</ScrollViewer>
</TabItem>
<TabItem Header="GPS Satellite view">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="100" />
</Grid.RowDefinitions>
<local:SatelliteView MaxWidth="{Binding ActualHeight, ElementName=satView}"
Grid.Column="1" x:Name="satView" />
<local:SatelliteSnr Grid.Row="1"
GsvMessage="{Binding GsvMessage, ElementName=satView}" />
</Grid>
</TabItem>
<TabItem Header="Messages">
<TextBox x:Name="output"
AcceptsReturn="True"
IsReadOnly="True"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Visible"
/>
</TabItem>
<TabItem Header="Device">
<Grid>
<StackPanel Margin="10">
<StackPanel Orientation="Horizontal">
<TextBlock Text="Current device: " />
<TextBlock Text="None" x:Name="currentDeviceInfo" FontWeight="Bold" />
</StackPanel>
<Button Width="200" Content="Open NMEA Log..." Click="OpenNmeaLogButton_Click" HorizontalAlignment="Left" Padding="20,5" Margin="0,5" />
<StackPanel Orientation="Horizontal">
<Button Width="200" Content="Auto-discover serial port"
HorizontalAlignment="Left" Padding="20,5" Margin="0,5"
Click="AutoDiscoverButton_Click" />
<TextBlock x:Name="autoDiscoverStatus" VerticalAlignment="Center" />
</StackPanel>
<GroupBox Header="Open Serial device" Width="200" HorizontalAlignment="Left">
<StackPanel>
<TextBlock Text="Serial port:" />
<ComboBox x:Name="serialPorts" />
<TextBlock Text="Baud rate:" />
<ComboBox x:Name="baudRates" SelectedIndex="3">
<ComboBoxItem>1200</ComboBoxItem>
<ComboBoxItem>2400</ComboBoxItem>
<ComboBoxItem>4800</ComboBoxItem>
<ComboBoxItem>9600</ComboBoxItem>
<ComboBoxItem>19200</ComboBoxItem>
<ComboBoxItem>38400</ComboBoxItem>
<ComboBoxItem>57600</ComboBoxItem>
<ComboBoxItem>115200</ComboBoxItem>
</ComboBox>
<Button Content="Connect" HorizontalAlignment="Left" Padding="20,5" Margin="0,5" Click="ConnectToSerialButton_Click" />
</StackPanel>
</GroupBox>
</StackPanel>
</Grid>
</TabItem>
</TabControl>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
</Grid>
<TabControl>
<TabItem Header="GPS Info">
<ScrollViewer HorizontalScrollBarVisibility="Disabled" Background="#FFEEEEEE">
<WrapPanel x:Name="MessagePanel">
<local:GprmcControl x:Name="gprmcView" Style="{StaticResource card}" Visibility="{Binding Message, ElementName=gprmcView, Converter={StaticResource nullConv}}" />
<local:GpggaControl x:Name="gpggaView" Style="{StaticResource card}" Visibility="{Binding Message, ElementName=gpggaView, Converter={StaticResource nullConv}}" />
<local:GpgsaControl x:Name="gpgsaView" Style="{StaticResource card}" Visibility="{Binding Message, ElementName=gpgsaView, Converter={StaticResource nullConv}}" />
<local:GpgllControl x:Name="gpgllView" Style="{StaticResource card}" Visibility="{Binding Message, ElementName=gpgllView, Converter={StaticResource nullConv}}" />
<local:PgrmeControl x:Name="pgrmeView" Style="{StaticResource card}" Visibility="{Binding Message, ElementName=pgrmeView, Converter={StaticResource nullConv}}" />
</WrapPanel>
</ScrollViewer>
</TabItem>
<TabItem Header="GPS Satellite view">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="100" />
</Grid.RowDefinitions>
<local:SatelliteView MaxWidth="{Binding ActualHeight, ElementName=satView}"
Grid.Column="1" x:Name="satView" />
<local:SatelliteSnr Grid.Row="1"
GsvMessage="{Binding GsvMessage, ElementName=satView}" />
</Grid>
</TabItem>
<TabItem Header="Map">
<Grid>
<!--Map-->
<esri:MapView x:Name="mapView" Grid.Row="1">
<local:RestoreAutoPanMode.RestoreAutoPanSettings>
<local:RestoreAutoPanMode DelayInSeconds="2.5" PanMode="Navigation" IsEnabled="True" RestoreScale="5000" />
</local:RestoreAutoPanMode.RestoreAutoPanSettings>
</esri:MapView>
<!--North arrow-->
<Grid Width="50" Height="50" HorizontalAlignment="Right" VerticalAlignment="Bottom"
Margin="20" RenderTransformOrigin=".5,.5">
<Grid.Resources>
<local:ReverseConverter x:Key="conv" />
</Grid.Resources>
<Grid.Effect>
<DropShadowEffect Direction="0" ShadowDepth="0"
BlurRadius="10" Opacity=".75" />
</Grid.Effect>
<Grid.RenderTransform>
<RotateTransform Angle="{Binding ElementName=mapView, Path=MapRotation, Converter={StaticResource conv}}" />
</Grid.RenderTransform>
<TextBlock Text="Ù" FontFamily="Wingdings" HorizontalAlignment="Center" VerticalAlignment="Center"
FontSize="40" Foreground="White" />
<TextBlock Text="N" HorizontalAlignment="Center" VerticalAlignment="Top"
FontSize="12" Margin="0,-5,0,0" RenderTransformOrigin=".5,.5"
Foreground="White" >
<TextBlock.RenderTransform>
<RotateTransform Angle="{Binding ElementName=mapView, Path=MapRotation}" />
</TextBlock.RenderTransform>
</TextBlock>
</Grid>
<Grid HorizontalAlignment="Right" VerticalAlignment="Top"
Margin="10" Background="White" Width="150"
DataContext="{Binding ElementName=mapView, Path=LocationDisplay.Location}">
<Grid.Effect>
<DropShadowEffect Direction="0" ShadowDepth="0"
BlurRadius="10" Opacity=".75" />
</Grid.Effect>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Border Background="CornflowerBlue" Grid.ColumnSpan="2" Padding="10">
<TextBlock Text="Details" FontSize="20" FontWeight="Bold" Foreground="White" />
</Border>
<StackPanel Margin="10" Grid.Row="1">
<TextBlock Text="Speed:" FontWeight="Bold" />
<TextBlock Text="Course:" FontWeight="Bold" />
<TextBlock Text="Altitude:" FontWeight="Bold" />
<TextBlock Text="Accuracy:" FontWeight="Bold" />
</StackPanel>
<StackPanel Margin="0,10,10,10" Grid.Column="1" Grid.Row="1">
<TextBlock Text="{Binding Velocity, StringFormat='{}{0} km/h'}" />
<TextBlock Text="{Binding Course, StringFormat='{}{0}°'}" />
<TextBlock Text="{Binding Position.Z, StringFormat='{}{0} m'}" />
<TextBlock Text="{Binding HorizontalAccuracy, StringFormat='{}{0} m'}" />
</StackPanel>
<local:AltitudeGraph Grid.Row="2" Grid.ColumnSpan="2" Height="50" x:Name="altitude" Value="{Binding Position.Z}" />
</Grid>
</Grid>
</TabItem>
<TabItem Header="Messages">
<TextBox x:Name="output"
AcceptsReturn="True"
IsReadOnly="True"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Visible"
/>
</TabItem>
<TabItem Header="Device">
<Grid>
<StackPanel Margin="10">
<StackPanel Orientation="Horizontal">
<TextBlock Text="Current device: " />
<TextBlock Text="None" x:Name="currentDeviceInfo" FontWeight="Bold" />
</StackPanel>
<Button Width="200" Content="Open NMEA Log..." Click="OpenNmeaLogButton_Click" HorizontalAlignment="Left" Padding="20,5" Margin="0,5" />
<StackPanel Orientation="Horizontal">
<Button Width="200" Content="Auto-discover serial port"
HorizontalAlignment="Left" Padding="20,5" Margin="0,5"
Click="AutoDiscoverButton_Click" />
<TextBlock x:Name="autoDiscoverStatus" VerticalAlignment="Center" />
</StackPanel>
<GroupBox Header="Open Serial device" Width="200" HorizontalAlignment="Left">
<StackPanel>
<TextBlock Text="Serial port:" />
<ComboBox x:Name="serialPorts" />
<TextBlock Text="Baud rate:" />
<ComboBox x:Name="baudRates" SelectedIndex="3">
<ComboBoxItem>1200</ComboBoxItem>
<ComboBoxItem>2400</ComboBoxItem>
<ComboBoxItem>4800</ComboBoxItem>
<ComboBoxItem>9600</ComboBoxItem>
<ComboBoxItem>19200</ComboBoxItem>
<ComboBoxItem>38400</ComboBoxItem>
<ComboBoxItem>57600</ComboBoxItem>
<ComboBoxItem>115200</ComboBoxItem>
</ComboBox>
<Button Content="Connect" HorizontalAlignment="Left" Padding="20,5" Margin="0,5" Click="ConnectToSerialButton_Click" />
</StackPanel>
</GroupBox>
</StackPanel>
</Grid>
</TabItem>
</TabControl>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
</Grid>
</Window>

View file

@ -1,230 +1,248 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using NmeaParser;
namespace SampleApp.WinDesktop
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private Queue<string> messages = new Queue<string>(101);
private NmeaParser.NmeaDevice currentDevice;
//Dialog for browsing to nmea log files
private Microsoft.Win32.OpenFileDialog nmeaOpenFileDialog = new Microsoft.Win32.OpenFileDialog()
{
Filter = "Text files|*.txt|NMEA Log|*.nmea|All files|*.*",
InitialDirectory = new System.IO.FileInfo(typeof(MainWindow).Assembly.Location).DirectoryName
};
public MainWindow()
{
InitializeComponent();
//Get list of serial ports for device tab
var availableSerialPorts = System.IO.Ports.SerialPort.GetPortNames().OrderBy(s=>s);
serialPorts.ItemsSource = availableSerialPorts;
serialPorts.SelectedIndex = 0;
// Use serial portName:
//var comPort = availableSerialPorts.First();
//var portName = new System.IO.Ports.SerialPort(comPort, 4800);
//var device = new NmeaParser.SerialPortDevice(portName);
//Use a log file for playing back logged data
var device = new NmeaParser.NmeaFileDevice("NmeaSampleData.txt");
StartDevice(device);
}
/// <summary>
/// Unloads the current device, and opens the next device
/// </summary>
/// <param name="device"></param>
private async void StartDevice(NmeaParser.NmeaDevice device)
{
//Clean up old device
if (currentDevice != null)
{
currentDevice.MessageReceived -= device_MessageReceived;
if (currentDevice.IsOpen)
await currentDevice.CloseAsync();
currentDevice.Dispose();
}
output.Text = "";
messages.Clear();
gprmcView.Message = null;
gpggaView.Message = null;
gpgsaView.Message = null;
gpgllView.Message = null;
pgrmeView.Message = null;
satView.GsvMessage = null;
//Start new device
currentDevice = device;
currentDevice.MessageReceived += device_MessageReceived;
var _ = currentDevice.OpenAsync();
if (device is NmeaParser.NmeaFileDevice)
currentDeviceInfo.Text = string.Format("NmeaFileDevice( file={0} )", ((NmeaParser.NmeaFileDevice)device).FileName);
else if (device is NmeaParser.SerialPortDevice)
{
currentDeviceInfo.Text = string.Format("SerialPortDevice( port={0}, baud={1} )",
((NmeaParser.SerialPortDevice)device).Port.PortName,
((NmeaParser.SerialPortDevice)device).Port.BaudRate);
}
}
private void device_MessageReceived(object sender, NmeaParser.NmeaMessageReceivedEventArgs args)
{
Dispatcher.BeginInvoke((Action) delegate()
{
messages.Enqueue(args.Message.ToString());
if (messages.Count > 100) messages.Dequeue(); //Keep message queue at 100
output.Text = string.Join("\n", messages.ToArray());
output.Select(output.Text.Length - 1, 0); //scroll to bottom
if (args.Message is NmeaParser.Messages.Gsv gpgsv)
{
satView.GsvMessage = gpgsv;
}
else if (args.Message is NmeaParser.Messages.Rmc)
gprmcView.Message = args.Message as NmeaParser.Messages.Rmc;
else if (args.Message is NmeaParser.Messages.Gga)
gpggaView.Message = args.Message as NmeaParser.Messages.Gga;
else if (args.Message is NmeaParser.Messages.Gsa)
gpgsaView.Message = args.Message as NmeaParser.Messages.Gsa;
else if (args.Message is NmeaParser.Messages.Gll)
gpgllView.Message = args.Message as NmeaParser.Messages.Gll;
else if (args.Message is NmeaParser.Messages.Garmin.Pgrme)
pgrmeView.Message = args.Message as NmeaParser.Messages.Garmin.Pgrme;
else
{
var ctrl = MessagePanel.Children.OfType<UnknownMessageControl>().Where(c => c.Message.MessageType == args.Message.MessageType).FirstOrDefault();
if (ctrl == null)
{
ctrl = new UnknownMessageControl()
{
Style = this.Resources["card"] as Style
};
MessagePanel.Children.Add(ctrl);
}
ctrl.Message = args.Message;
}
});
}
//Browse to nmea file and create device from selected file
private void OpenNmeaLogButton_Click(object sender, RoutedEventArgs e)
{
var result = nmeaOpenFileDialog.ShowDialog();
if (result.HasValue && result.Value)
{
var file = nmeaOpenFileDialog.FileName;
var device = new NmeaParser.NmeaFileDevice(file);
StartDevice(device);
}
}
//Creates a serial port device from the selected settings
private void ConnectToSerialButton_Click(object sender, RoutedEventArgs e)
{
try
{
var portName = serialPorts.Text as string;
var baudRate = int.Parse(baudRates.Text);
var device = new NmeaParser.SerialPortDevice(new System.IO.Ports.SerialPort(portName, baudRate));
StartDevice(device);
}
catch(System.Exception ex)
{
MessageBox.Show("Error connecting: " + ex.Message);
}
}
//Attempts to perform an auto discovery of serial ports
private async void AutoDiscoverButton_Click(object sender, RoutedEventArgs e)
{
var button = sender as Button;
button.IsEnabled = false;
System.IO.Ports.SerialPort port = await Task.Run<System.IO.Ports.SerialPort>(() => {
return FindPort(
new System.Progress<string>((s) => { Dispatcher.BeginInvoke((Action)delegate() { autoDiscoverStatus.Text = s; }); }));
});
if (port != null) //we found a port
{
autoDiscoverStatus.Text = "";
serialPorts.Text = port.PortName;
baudRates.Text = port.BaudRate.ToString();
ConnectToSerialButton_Click(sender, e);
}
else
autoDiscoverStatus.Text = "No GPS port found";
button.IsEnabled = false;
}
//Iterates all serial ports and attempts to open them at different baud rates
//and looks for a GPS message.
private static System.IO.Ports.SerialPort FindPort(IProgress<string> progress = null)
{
var ports = System.IO.Ports.SerialPort.GetPortNames().OrderBy(s => s);
foreach (var portName in ports)
{
using (var port = new System.IO.Ports.SerialPort(portName))
{
var defaultRate = port.BaudRate;
List<int> baudRatesToTest = new List<int>(new[] { 9600, 4800, 115200, 19200, 57600, 38400, 2400 }); //Ordered by likelihood
//Move default rate to first spot
if (baudRatesToTest.Contains(defaultRate)) baudRatesToTest.Remove(defaultRate);
baudRatesToTest.Insert(0, defaultRate);
foreach (var baud in baudRatesToTest)
{
if (progress != null)
progress.Report(string.Format("Trying {0} @ {1}baud", portName, port.BaudRate));
port.BaudRate = baud;
port.ReadTimeout = 2000; //this might not be long enough
bool success = false;
try
{
port.Open();
if (!port.IsOpen)
continue; //couldn't open port
try
{
port.ReadTo("$GP");
}
catch (TimeoutException)
{
continue;
}
success = true;
}
catch
{
//Error reading
}
finally
{
port.Close();
}
if (success)
{
return new System.IO.Ports.SerialPort(portName, baud);
}
}
}
}
return null;
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using Esri.ArcGISRuntime.Mapping;
using NmeaParser;
namespace SampleApp.WinDesktop
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private Queue<string> messages = new Queue<string>(101);
private NmeaParser.NmeaDevice currentDevice;
//Dialog for browsing to nmea log files
private Microsoft.Win32.OpenFileDialog nmeaOpenFileDialog = new Microsoft.Win32.OpenFileDialog()
{
Filter = "Text files|*.txt|NMEA Log|*.nmea|All files|*.*",
InitialDirectory = new System.IO.FileInfo(typeof(MainWindow).Assembly.Location).DirectoryName
};
public MainWindow()
{
InitializeComponent();
mapView.Map = new Map(Basemap.CreateNavigationVector());
//Get list of serial ports for device tab
var availableSerialPorts = System.IO.Ports.SerialPort.GetPortNames().OrderBy(s=>s);
serialPorts.ItemsSource = availableSerialPorts;
serialPorts.SelectedIndex = 0;
// Use serial portName:
//var comPort = availableSerialPorts.First();
//var portName = new System.IO.Ports.SerialPort(comPort, 4800);
//var device = new NmeaParser.SerialPortDevice(portName);
//Use a log file for playing back logged data
var device = new NmeaParser.NmeaFileDevice("NmeaSampleData.txt");
StartDevice(device);
}
/// <summary>
/// Unloads the current device, and opens the next device
/// </summary>
/// <param name="device"></param>
private async void StartDevice(NmeaParser.NmeaDevice device)
{
//Clean up old device
if (currentDevice != null)
{
currentDevice.MessageReceived -= device_MessageReceived;
if (currentDevice.IsOpen)
await currentDevice.CloseAsync();
currentDevice.Dispose();
}
output.Text = "";
messages.Clear();
gprmcView.Message = null;
gpggaView.Message = null;
gpgsaView.Message = null;
gpgllView.Message = null;
pgrmeView.Message = null;
satView.GsvMessage = null;
//Start new device
currentDevice = device;
currentDevice.MessageReceived += device_MessageReceived;
mapView.LocationDisplay.DataSource = new NmeaLocationProvider(device);
mapView.LocationDisplay.IsEnabled = true;
mapView.LocationDisplay.InitialZoomScale = 5000;
mapView.LocationDisplay.AutoPanMode = Esri.ArcGISRuntime.UI.LocationDisplayAutoPanMode.Navigation;
if (device is NmeaParser.NmeaFileDevice)
currentDeviceInfo.Text = string.Format("NmeaFileDevice( file={0} )", ((NmeaParser.NmeaFileDevice)device).FileName);
else if (device is NmeaParser.SerialPortDevice)
{
currentDeviceInfo.Text = string.Format("SerialPortDevice( port={0}, baud={1} )",
((NmeaParser.SerialPortDevice)device).Port.PortName,
((NmeaParser.SerialPortDevice)device).Port.BaudRate);
}
}
private void device_MessageReceived(object sender, NmeaParser.NmeaMessageReceivedEventArgs args)
{
Dispatcher.BeginInvoke((Action) delegate()
{
messages.Enqueue(args.Message.ToString());
if (messages.Count > 100) messages.Dequeue(); //Keep message queue at 100
output.Text = string.Join("\n", messages.ToArray());
output.Select(output.Text.Length - 1, 0); //scroll to bottom
if (args.Message is NmeaParser.Messages.Gsv gpgsv)
{
satView.GsvMessage = gpgsv;
}
else if (args.Message is NmeaParser.Messages.Rmc)
gprmcView.Message = args.Message as NmeaParser.Messages.Rmc;
else if (args.Message is NmeaParser.Messages.Gga)
gpggaView.Message = args.Message as NmeaParser.Messages.Gga;
else if (args.Message is NmeaParser.Messages.Gsa)
gpgsaView.Message = args.Message as NmeaParser.Messages.Gsa;
else if (args.Message is NmeaParser.Messages.Gll)
gpgllView.Message = args.Message as NmeaParser.Messages.Gll;
else if (args.Message is NmeaParser.Messages.Garmin.Pgrme)
pgrmeView.Message = args.Message as NmeaParser.Messages.Garmin.Pgrme;
else
{
var ctrl = MessagePanel.Children.OfType<UnknownMessageControl>().Where(c => c.Message.MessageType == args.Message.MessageType).FirstOrDefault();
if (ctrl == null)
{
ctrl = new UnknownMessageControl()
{
Style = this.Resources["card"] as Style
};
MessagePanel.Children.Add(ctrl);
}
ctrl.Message = args.Message;
}
});
}
//Browse to nmea file and create device from selected file
private void OpenNmeaLogButton_Click(object sender, RoutedEventArgs e)
{
var result = nmeaOpenFileDialog.ShowDialog();
if (result.HasValue && result.Value)
{
var file = nmeaOpenFileDialog.FileName;
var device = new NmeaParser.NmeaFileDevice(file);
StartDevice(device);
}
}
//Creates a serial port device from the selected settings
private void ConnectToSerialButton_Click(object sender, RoutedEventArgs e)
{
try
{
var portName = serialPorts.Text as string;
var baudRate = int.Parse(baudRates.Text);
var device = new NmeaParser.SerialPortDevice(new System.IO.Ports.SerialPort(portName, baudRate));
StartDevice(device);
}
catch(System.Exception ex)
{
MessageBox.Show("Error connecting: " + ex.Message);
}
}
//Attempts to perform an auto discovery of serial ports
private async void AutoDiscoverButton_Click(object sender, RoutedEventArgs e)
{
var button = sender as Button;
button.IsEnabled = false;
System.IO.Ports.SerialPort port = await Task.Run<System.IO.Ports.SerialPort>(() => {
return FindPort(
new System.Progress<string>((s) => { Dispatcher.BeginInvoke((Action)delegate() { autoDiscoverStatus.Text = s; }); }));
});
if (port != null) //we found a port
{
autoDiscoverStatus.Text = "";
serialPorts.Text = port.PortName;
baudRates.Text = port.BaudRate.ToString();
ConnectToSerialButton_Click(sender, e);
}
else
autoDiscoverStatus.Text = "No GPS port found";
button.IsEnabled = false;
}
//Iterates all serial ports and attempts to open them at different baud rates
//and looks for a GPS message.
private static System.IO.Ports.SerialPort FindPort(IProgress<string> progress = null)
{
var ports = System.IO.Ports.SerialPort.GetPortNames().OrderBy(s => s);
foreach (var portName in ports)
{
using (var port = new System.IO.Ports.SerialPort(portName))
{
var defaultRate = port.BaudRate;
List<int> baudRatesToTest = new List<int>(new[] { 9600, 4800, 115200, 19200, 57600, 38400, 2400 }); //Ordered by likelihood
//Move default rate to first spot
if (baudRatesToTest.Contains(defaultRate)) baudRatesToTest.Remove(defaultRate);
baudRatesToTest.Insert(0, defaultRate);
foreach (var baud in baudRatesToTest)
{
if (progress != null)
progress.Report(string.Format("Trying {0} @ {1}baud", portName, port.BaudRate));
port.BaudRate = baud;
port.ReadTimeout = 2000; //this might not be long enough
bool success = false;
try
{
port.Open();
if (!port.IsOpen)
continue; //couldn't open port
try
{
port.ReadTo("$GP");
}
catch (TimeoutException)
{
continue;
}
success = true;
}
catch
{
//Error reading
}
finally
{
port.Close();
}
if (success)
{
return new System.IO.Ports.SerialPort(portName, baud);
}
}
}
}
return null;
}
}
public class ReverseConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return -(double)value;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return -(double)value;
}
}
}

View file

@ -0,0 +1,100 @@
using Esri.ArcGISRuntime.Geometry;
using System;
using System.Threading.Tasks;
namespace SampleApp.WinDesktop
{
public class NmeaLocationProvider : Esri.ArcGISRuntime.Location.LocationDataSource
{
private NmeaParser.NmeaDevice device;
double m_Accuracy = 0;
double m_altitude = double.NaN;
double m_speed = 0;
double m_course = 0;
public NmeaLocationProvider(NmeaParser.NmeaDevice device)
{
this.device = device;
if(device != null)
device.MessageReceived += device_MessageReceived;
}
void device_MessageReceived(object sender, NmeaParser.NmeaMessageReceivedEventArgs e)
{
var message = e.Message;
ParseMessage(message);
}
public void ParseMessage(NmeaParser.Messages.NmeaMessage message)
{
bool isNewFix = false;
bool lostFix = false;
double lat = 0;
double lon = 0;
if (message is NmeaParser.Messages.Garmin.Pgrme)
{
m_Accuracy = ((NmeaParser.Messages.Garmin.Pgrme)message).HorizontalError;
}
else if(message is NmeaParser.Messages.Gst)
{
Gst = ((NmeaParser.Messages.Gst)message);
m_Accuracy = Math.Sqrt(Gst.SigmaLatitudeError * Gst.SigmaLatitudeError + Gst.SigmaLongitudeError * Gst.SigmaLongitudeError);
}
else if(message is NmeaParser.Messages.Gga)
{
Gga = ((NmeaParser.Messages.Gga)message);
isNewFix = Gga.Quality != NmeaParser.Messages.Gga.FixQuality.Invalid;
lostFix = !isNewFix;
m_altitude = Gga.Altitude;
lat = Gga.Latitude;
lon = Gga.Longitude;
}
else if (message is NmeaParser.Messages.Rmc)
{
Rmc = (NmeaParser.Messages.Rmc)message;
if (Rmc.Active)
{
isNewFix = true;
m_speed = Rmc.Speed;
m_course = Rmc.Course;
lat = Rmc.Latitude;
lon = Rmc.Longitude;
}
else lostFix = true;
}
else if (message is NmeaParser.Messages.Gsa)
{
Gsa = (NmeaParser.Messages.Gsa)message;
}
if (isNewFix)
{
base.UpdateLocation(new Esri.ArcGISRuntime.Location.Location(new MapPoint(lon, lat, m_altitude, SpatialReferences.Wgs84), m_Accuracy, m_speed, m_course, false));
}
else if (lostFix)
{
}
}
protected override Task OnStartAsync()
{
if (device != null)
return this.device.OpenAsync();
else
return System.Threading.Tasks.Task<bool>.FromResult(true);
}
protected override Task OnStopAsync()
{
m_Accuracy = double.NaN;
if(this.device != null)
return this.device.CloseAsync();
else
return System.Threading.Tasks.Task<bool>.FromResult(true);
}
public NmeaParser.Messages.Gsa Gsa { get; private set; }
public NmeaParser.Messages.Gga Gga { get; private set; }
public NmeaParser.Messages.Rmc Rmc { get; private set; }
public NmeaParser.Messages.Gst Gst { get; private set; }
}
}

View file

@ -1,63 +0,0 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace SampleApp.WinDesktop.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("SampleApp.WinDesktop.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
}
}

View file

@ -1,117 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View file

@ -1,26 +0,0 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace SampleApp.WinDesktop.Properties {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.5.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
public static Settings Default {
get {
return defaultInstance;
}
}
}
}

View file

@ -1,7 +0,0 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="uri:settings" CurrentProfile="(Default)">
<Profiles>
<Profile Name="(Default)" />
</Profiles>
<Settings />
</SettingsFile>

View file

@ -0,0 +1,148 @@
using Esri.ArcGISRuntime.UI.Controls;
using Esri.ArcGISRuntime.Location;
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.ComponentModel;
#if NETFX_CORE
using Windows.UI.Xaml;
#else
using System.Windows.Threading;
#endif
namespace SampleApp.WinDesktop
{
public class RestoreAutoPanMode
{
private class DelayTimer
{
private Action m_action;
DispatcherTimer m_timer;
public DelayTimer(Action action)
{
m_timer = new DispatcherTimer();
m_timer.Tick += m_timer_Tick;
m_action = action;
}
#if NETFX_CORE
void m_timer_Tick(object sender, object e)
#else
void m_timer_Tick(object sender, EventArgs e)
#endif
{
m_timer.Stop();
if (m_action != null)
m_action();
}
public void Invoke(TimeSpan delay)
{
m_timer.Stop();
m_timer.Interval = delay;
m_timer.Start();
}
public void Cancel()
{
m_timer.Stop();
}
}
private MapView m_mapView;
private DelayTimer m_timer;
public RestoreAutoPanMode()
{
m_timer = new DelayTimer(ResetPanMode);
RestoreScale = double.NaN;
}
private void ResetPanMode()
{
if (m_mapView != null && m_mapView.LocationDisplay != null)
{
if (!double.IsNaN(RestoreScale))
m_mapView.SetViewpointScaleAsync(RestoreScale);
m_mapView.LocationDisplay.AutoPanMode = this.PanMode;
}
}
internal void AttachToMapView(MapView mv)
{
if (m_mapView != null && m_mapView != mv)
throw new InvalidOperationException("RestoreAutoPanMode can only be assigned to one mapview");
m_mapView = mv;
(m_mapView as INotifyPropertyChanged).PropertyChanged += m_mapView_PropertyChanged;
}
internal void DetachFromMapView(MapView mv)
{
(m_mapView as INotifyPropertyChanged).PropertyChanged -= m_mapView_PropertyChanged;
m_mapView = null;
}
private void m_mapView_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
//If user stopped navigating and we're not in the correct autopan mode,
//restore autopan after the set delay.
if (IsEnabled && e.PropertyName == "IsNavigating")
{
if (m_mapView.LocationDisplay != null &&
m_mapView.LocationDisplay.AutoPanMode != PanMode)
{
if (!m_mapView.IsNavigating)
m_timer.Invoke(TimeSpan.FromSeconds(DelayInSeconds));
else
m_timer.Cancel();
}
}
}
public bool IsEnabled { get; set; }
public double DelayInSeconds { get; set; }
public Esri.ArcGISRuntime.UI.LocationDisplayAutoPanMode PanMode { get; set; }
public double RestoreScale { get; set; }
public static RestoreAutoPanMode GetRestoreAutoPanSettings(DependencyObject obj)
{
return (RestoreAutoPanMode)obj.GetValue(RestoreAutoPanSettingsProperty);
}
public static void SetRestoreAutoPanSettings(DependencyObject obj, RestoreAutoPanMode value)
{
obj.SetValue(RestoreAutoPanSettingsProperty, value);
}
public static readonly DependencyProperty RestoreAutoPanSettingsProperty =
DependencyProperty.RegisterAttached("RestoreAutoPanSettings", typeof(RestoreAutoPanMode), typeof(RestoreAutoPanMode),
new PropertyMetadata(null, OnRestoreAutoPanSettingsChanged));
private static void OnRestoreAutoPanSettingsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(d is MapView))
throw new InvalidOperationException("This property must be attached to a mapview");
MapView mv = (MapView)d;
var oldValue = e.OldValue as RestoreAutoPanMode;
if (oldValue != null)
oldValue.DetachFromMapView(mv);
var newValue = e.NewValue as RestoreAutoPanMode;
if (newValue != null)
newValue.AttachToMapView(mv);
}
}
}

View file

@ -15,4 +15,7 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Esri.ArcGISRuntime.WPF" Version="100.7.0" />
</ItemGroup>
</Project>

View file

@ -41,7 +41,7 @@
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel VerticalAlignment="Bottom" HorizontalAlignment="Left">
<TextBlock Text="{Binding PrnNumber}"
<TextBlock Text="{Binding Id}"
FontWeight="Bold"
HorizontalAlignment="Center" >
<TextBlock.Foreground>

View file

@ -68,7 +68,7 @@
</StackPanel>
</Ellipse.ToolTip>
</Ellipse>
<TextBlock Text="{Binding PrnNumber}" Margin="8,-8" />
<TextBlock Text="{Binding Id}" Margin="8,-8" />
</Canvas>
</local:PolarPlacementItem>
</DataTemplate>