Use bitmap for rendering plot view + allow zoom + improve ui

This commit is contained in:
Morten Nielsen 2020-07-31 15:23:12 -07:00
parent f83600de07
commit b699eab194
2 changed files with 139 additions and 57 deletions

View file

@ -4,11 +4,18 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SampleApp.WinDesktop"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<SolidColorBrush x:Key="GridLinesBrush" Color="#AAFFFFFF" />
<DoubleCollection x:Key="GridLinesDash">4 4</DoubleCollection>
<sys:Double x:Key="GridLinesWidth">.5</sys:Double>
</UserControl.Resources>
<Grid SizeChanged="Grid_SizeChanged" Background="Black" >
<Grid Margin="10" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" x:Name="PlotMap" SizeChanged="PlotMap_SizeChanged">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="1*" />
@ -17,10 +24,10 @@
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="50" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="50" />
<RowDefinition Height="1*" />
<RowDefinition Height="1*" />
<RowDefinition Height="1*" />
@ -29,27 +36,28 @@
<RowDefinition Height="1*" />
<RowDefinition Height="1*" />
<RowDefinition Height="1*" />
<RowDefinition Height="1*" />
<RowDefinition Height="1*" />
<RowDefinition Height="50" />
</Grid.RowDefinitions>
<Ellipse HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Stroke="White" StrokeThickness="1" Grid.ColumnSpan="8" Grid.RowSpan="8" Grid.Row="1" Grid.Column="1" StrokeDashArray="2 2" x:Name="OuterRing" />
<Ellipse HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Stroke="White" StrokeThickness="1" Grid.ColumnSpan="6" Grid.RowSpan="6" Grid.Row="2" Grid.Column="2" StrokeDashArray="2 2" />
<Ellipse HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Stroke="White" StrokeThickness="1" Grid.ColumnSpan="4" Grid.RowSpan="4" Grid.Row="3" Grid.Column="3" StrokeDashArray="2 2" />
<Ellipse HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Stroke="White" StrokeThickness="1" Grid.ColumnSpan="2" Grid.RowSpan="2" Grid.Row="4" Grid.Column="4" StrokeDashArray="2 2" />
<Path Data="M0,0 L1,0" HorizontalAlignment="Stretch" VerticalAlignment="Center" Stroke="White" StrokeThickness="1" Grid.RowSpan="8" Grid.ColumnSpan="8" Grid.Row="1" Grid.Column="1" StrokeDashArray="2 2" Stretch="Fill" />
<Path Data="M0,0 L0,1" HorizontalAlignment="Center" VerticalAlignment="Stretch" Stroke="White" StrokeThickness="1" Grid.RowSpan="8" Grid.ColumnSpan="8" Grid.Row="1" Grid.Column="1" StrokeDashArray="2 2" Stretch="Fill" />
<Ellipse HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Stroke="{StaticResource GridLinesBrush}" StrokeThickness="{StaticResource GridLinesWidth}" Grid.ColumnSpan="8" Grid.RowSpan="8" Grid.Row="1" Grid.Column="1" StrokeDashArray="{StaticResource GridLinesDash}" x:Name="OuterRing" />
<Ellipse HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Stroke="{StaticResource GridLinesBrush}" StrokeThickness="{StaticResource GridLinesWidth}" Grid.ColumnSpan="6" Grid.RowSpan="6" Grid.Row="2" Grid.Column="2" StrokeDashArray="{StaticResource GridLinesDash}" />
<Ellipse HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Stroke="{StaticResource GridLinesBrush}" StrokeThickness="{StaticResource GridLinesWidth}" Grid.ColumnSpan="4" Grid.RowSpan="4" Grid.Row="3" Grid.Column="3" StrokeDashArray="{StaticResource GridLinesDash}" />
<Ellipse HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Stroke="{StaticResource GridLinesBrush}" StrokeThickness="{StaticResource GridLinesWidth}" Grid.ColumnSpan="2" Grid.RowSpan="2" Grid.Row="4" Grid.Column="4" StrokeDashArray="{StaticResource GridLinesDash}" />
<Path Data="M0,0 L1,0" HorizontalAlignment="Stretch" VerticalAlignment="Center" Stroke="{StaticResource GridLinesBrush}" StrokeThickness="{StaticResource GridLinesWidth}" Grid.RowSpan="8" Grid.ColumnSpan="8" Grid.Row="1" Grid.Column="1" StrokeDashArray="{StaticResource GridLinesDash}" Stretch="Fill" />
<Path Data="M0,0 L0,1" HorizontalAlignment="Center" VerticalAlignment="Stretch" Stroke="{StaticResource GridLinesBrush}" StrokeThickness="{StaticResource GridLinesWidth}" Grid.RowSpan="8" Grid.ColumnSpan="8" Grid.Row="1" Grid.Column="1" StrokeDashArray="{StaticResource GridLinesDash}" Stretch="Fill" />
<TextBlock x:Name="FirstMeterLabel" Text="5m" Margin="0,15,0,0" FontSize="12" HorizontalAlignment="Center" VerticalAlignment="Center" Foreground="White" Grid.RowSpan="10" Grid.Column="6" Grid.ColumnSpan="2" Background="#55000000" />
<TextBlock x:Name="SecondMeterLabel" Text="10m" Margin="0,15,0,0" FontSize="12" HorizontalAlignment="Center" VerticalAlignment="Center" Foreground="White" Grid.RowSpan="10" Grid.Column="8" Grid.ColumnSpan="2" Background="#55000000" />
<TextBlock x:Name="SecondMeterLabel" Text="10m" Margin="0,15,-10,0" FontSize="12" HorizontalAlignment="Right" VerticalAlignment="Center" Foreground="White" Grid.RowSpan="10" Grid.Column="8" Grid.ColumnSpan="1" Background="#55000000" />
<TextBlock Text="N" Margin="10" FontSize="9" HorizontalAlignment="Center" VerticalAlignment="Bottom" Foreground="White" Grid.ColumnSpan="10" />
<TextBlock Text="S" Margin="10" FontSize="9" HorizontalAlignment="Center" VerticalAlignment="Top" Foreground="White" Grid.ColumnSpan="10" Grid.Row="9" />
<TextBlock Text="W" Margin="10" FontSize="9" HorizontalAlignment="Right" VerticalAlignment="Center" Foreground="White" Grid.RowSpan="10" Grid.Column="0" />
<TextBlock Text="E" Margin="10" FontSize="9" HorizontalAlignment="Left" VerticalAlignment="Center" Foreground="White" Grid.RowSpan="10" Grid.Column="9" />
<Canvas Grid.RowSpan="10" Grid.ColumnSpan="10" x:Name="canvas">
<Ellipse Fill="LightGreen" Canvas.Left="10" Width="3" Height="3" Canvas.Top="20" />
<Canvas Grid.RowSpan="10" Grid.ColumnSpan="10" x:Name="canvas" Opacity="0">
</Canvas>
<Image x:Name="plot" Grid.RowSpan="8" Grid.ColumnSpan="8" Grid.Row="1" Grid.Column="1" MouseWheel="plot_MouseWheel" />
</Grid>
<StackPanel HorizontalAlignment="Left" VerticalAlignment="Bottom" Margin="20">
<Button Content="Clear" FontSize="10" Padding="2" HorizontalAlignment="Stretch" Click="ClearButton_Click"/>
<StackPanel HorizontalAlignment="Left" VerticalAlignment="Bottom" Margin="20" Orientation="Horizontal">
<Button Content="Clear" Padding="2" Margin="5" Click="ClearButton_Click" />
<ToggleButton IsChecked="True" x:Name="autoFitToggle" Content="Autofit" Padding="2" Margin="5" Unchecked="ToggleButton_Unchecked" Checked="ToggleButton_Checked" />
</StackPanel>
<StackPanel HorizontalAlignment="Right" VerticalAlignment="Top" Margin="20">
<TextBlock x:Name="Status" Foreground="White" FontSize="12" />

View file

@ -21,7 +21,8 @@ namespace SampleApp.WinDesktop
/// </summary>
public partial class PointPlotView : UserControl
{
List<double[]> locations = new List<double[]>();
private List<double[]> locations = new List<double[]>();
public PointPlotView()
{
InitializeComponent();
@ -31,34 +32,47 @@ namespace SampleApp.WinDesktop
{
PlotMap.Width = PlotMap.Height = Math.Min(e.NewSize.Width, e.NewSize.Height);
}
Size size = new Size();
private void PlotMap_SizeChanged(object sender, SizeChangedEventArgs e)
{
size = e.NewSize;
UpdatePlot();
}
private void ClearButton_Click(object sender, RoutedEventArgs e)
{
Clear();
}
public void Clear()
{
locations.Clear();
UpdatePlot();
if(!autoFit)
autoFitToggle.IsChecked = true;
else
UpdatePlot();
}
public void AddLocation(double latitude, double longitude, double altitude)
{
Dispatcher.Invoke(() =>
{
locations.Add(new double[] { latitude, longitude, altitude });
UpdatePlot();
});
locations.Add(new double[] { latitude, longitude, altitude });
UpdatePlot();
}
static SolidColorBrush dotFill = new SolidColorBrush(Color.FromRgb(0, 255, 0));
private void UpdatePlot()
{
if (canvas.ActualWidth == 0 || canvas.ActualHeight == 0 || !IsVisible)
if (size.Width == 0 || size.Height == 0 || !IsVisible)
return;
Status.Text = "";
canvas.Children.Clear();
if (locations.Count == 0)
{
Dispatcher.Invoke(() =>
{
Status.Text = "";
plot.Source = null;
});
return;
}
var latAvr = locations.Select(l => l[0]).Average();
var lonAvr = locations.Select(l => l[1]).Average();
@ -86,45 +100,70 @@ namespace SampleApp.WinDesktop
var lonAvr2 = locations2.Select(l => l[1]).Average();
var maxDifLat = Math.Max(latAvr2 - latMin, latMax - latAvr2);
var maxDifLon = Math.Max(lonAvr2 - lonMin, lonMax - lonAvr2);
var maxDif = Math.Max(maxDifLat, maxDifLon);
if (maxDif < .1)
maxDif = .1;
if (maxDif < .25)
maxDif = .25;
else if (maxDif < .5)
maxDif = .5;
//var maxDif = Math.Max(maxDifLat, maxDifLon);
double maxDif = 1;
if (autoFit)
{
maxDif = distances.Max();
if (maxDif < .1)
maxDif = .1;
if (maxDif < .25)
maxDif = .25;
else if (maxDif < .5)
maxDif = .5;
else
maxDif = Math.Ceiling(maxDif);
currentScale = maxDif / (Math.Min(size.Width, size.Height) * .5);
}
else
maxDif = Math.Ceiling(maxDif);
//if(maxDif < 1)
SecondMeterLabel.Text = $"{maxDif}m";
FirstMeterLabel.Text = $"{maxDif/2}m";
double scale = maxDif / Math.Min(OuterRing.ActualWidth, OuterRing.ActualHeight) / .5;
{
maxDif = currentScale * (Math.Min(size.Width, size.Height) * .5);
}
double scale = currentScale;
if (scale == 0)
scale = 1;
var MAXCOUNT = 1000;
for (int i = Math.Max(0, locations2.Count - MAXCOUNT); i < locations2.Count; i++) // Only draw the last 1000 points
int width = (int)size.Width;
int height = (int)size.Height;
int stride = width * 4;
byte[] pixels = new byte[width * height * 4];
double[][] stamp = new double[][] {new double[] { .3, .5, .3 }, new double[] { .5, 1, .5 }, new double[] { .3, .5, .3 } };
for (int i = 0; i < locations2.Count; i++)
{
var l = locations2[i];
var x = canvas.ActualWidth * .5 + (l[1] - lonAvr2) / scale;
var y = canvas.ActualHeight * .5 - (l[0] - latAvr2) / scale;
Ellipse e = new Ellipse() { Width = 3, Height = 3, Fill = dotFill };
if (canvas.Children.Count == locations2.Count - 1 || canvas.Children.Count == MAXCOUNT - 1)
var x = (int)(width * .5 + (l[1] - lonAvr2) / scale);
var y = (int)(height * .5 - (l[0] - latAvr2) / scale);
var index = ((int)y) * stride + ((int)x) * 4;
for (int r = -1; r < stamp.Length-1; r++)
{
e.Fill = new SolidColorBrush(Colors.Red);
e.Width = 5;
e.Height = 5;
for (int c = -1; c < stamp[r + 1].Length-1; c++)
{
if (x + c >= width || x + c < 0 ||
y + r >= width || y + r < 0)
continue;
var p = index + r * stride + c * 4;
var val = stamp[r + 1][c + 1];
pixels[p + 1] = 0;
pixels[p + 1] = 255;
pixels[p + 2] = 0;
pixels[p + 3] = (byte)Math.Min(255, pixels[p + 3] + val * 255); //Multiply alpha
}
}
Canvas.SetLeft(e, x - e.Width * .5);
Canvas.SetTop(e, y - e.Height * .5);
canvas.Children.Add(e);
}
var stdDevLat = Math.Sqrt(locations2.Sum(d => (d[0] - latAvr2) * (d[0] - latAvr2)) / locations2.Count);
var stdDevLon = Math.Sqrt(locations2.Sum(d => (d[1] - lonAvr2) * (d[1] - lonAvr2)) / locations2.Count);
var zAvr = locations.Select(l => l[2]).Where(l => !double.IsNaN(l)).Average();
var stdDevZ = Math.Sqrt(locations.Select(l => l[2]).Where(l => !double.IsNaN(l)).Sum(d => (d - zAvr) * (d - zAvr)) / locations.Select(l => l[2]).Where(l => !double.IsNaN(l)).Count());
Status.Text = $"Measurements: {locations.Count}\nAverage:\n - Latitude: {latAvr.ToString("0.0000000")}\n - Longitude: {lonAvr.ToString("0.0000000")}\n - Elevation: {zAvr.ToString("0.000")}m\nStandard Deviation:\n - Latitude: {stdDevLat.ToString("0.###")}m\n - Longitude: {stdDevLon.ToString("0.###")}m\n - Horizontal: {distances.Average().ToString("0.###")}m\n - Elevation: {stdDevZ.ToString("0.###")}m";
Dispatcher.Invoke(() =>
{
SecondMeterLabel.Text = $"{maxDif.ToString("0.###")}m";
FirstMeterLabel.Text = $"{(maxDif / 2).ToString("0.###")}m";
// Specify the area of the bitmap that changed.
var writeableBitmap = new WriteableBitmap((int)size.Width, (int)size.Height, 96, 96, PixelFormats.Bgra32, null);
writeableBitmap.WritePixels(new Int32Rect(0, 0, width, height), pixels, stride, 0);
plot.Source = writeableBitmap;
Status.Text = $"Measurements: {locations.Count}\nAverage:\n - Latitude: {latAvr.ToString("0.0000000")}\n - Longitude: {lonAvr.ToString("0.0000000")}\n - Elevation: {zAvr.ToString("0.000")}m\nStandard Deviation:\n - Latitude: {stdDevLat.ToString("0.###")}m\n - Longitude: {stdDevLon.ToString("0.###")}m\n - Horizontal: {distances.Average().ToString("0.###")}m\n - Elevation: {stdDevZ.ToString("0.###")}m";
});
}
internal static class Vincenty
@ -193,10 +232,45 @@ namespace SampleApp.WinDesktop
return s;
}
}
private void ClearButton_Click(object sender, RoutedEventArgs e)
bool autoFit = true;
double currentScale = 1;
private void ToggleButton_Unchecked(object sender, RoutedEventArgs e)
{
Clear();
autoFit = false;
}
private void ToggleButton_Checked(object sender, RoutedEventArgs e)
{
autoFit = true;
UpdatePlot();
}
private void plot_MouseWheel(object sender, MouseWheelEventArgs e)
{
var maxDif = Math.Round(currentScale * (Math.Min(size.Width, size.Height) * .5), 3);
var dif = 1;
if (maxDif < 1)
maxDif = e.Delta < 0 ? maxDif * 2 : maxDif / 2;
else
maxDif = e.Delta < 0 ? maxDif + 1 : maxDif - 1;
if (maxDif < .1)
maxDif = .1;
if (maxDif < .25)
maxDif = .25;
else if (maxDif < .5)
maxDif = .5;
else
maxDif = Math.Ceiling(maxDif);
currentScale = maxDif / (Math.Min(size.Width, size.Height) * .5);
if (autoFitToggle.IsChecked == true)
{
autoFitToggle.IsChecked = false;
}
else
{
UpdatePlot();
}
}
}
}