2020-07-31 04:57:44 +02:00
|
|
|
|
using Esri.ArcGISRuntime.Location;
|
|
|
|
|
|
using System;
|
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using System.Linq;
|
|
|
|
|
|
using System.Text;
|
|
|
|
|
|
using System.Transactions;
|
|
|
|
|
|
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 PointPlotView.xaml
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public partial class PointPlotView : UserControl
|
|
|
|
|
|
{
|
2020-08-03 06:55:47 +02:00
|
|
|
|
private List<Point> locations = new List<Point>();
|
2020-08-01 00:23:12 +02:00
|
|
|
|
|
2020-07-31 04:57:44 +02:00
|
|
|
|
public PointPlotView()
|
|
|
|
|
|
{
|
|
|
|
|
|
InitializeComponent();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void Grid_SizeChanged(object sender, SizeChangedEventArgs e)
|
|
|
|
|
|
{
|
|
|
|
|
|
PlotMap.Width = PlotMap.Height = Math.Min(e.NewSize.Width, e.NewSize.Height);
|
|
|
|
|
|
}
|
2020-08-01 00:23:12 +02:00
|
|
|
|
Size size = new Size();
|
2020-07-31 04:57:44 +02:00
|
|
|
|
private void PlotMap_SizeChanged(object sender, SizeChangedEventArgs e)
|
|
|
|
|
|
{
|
2020-08-01 00:23:12 +02:00
|
|
|
|
size = e.NewSize;
|
2020-07-31 04:57:44 +02:00
|
|
|
|
UpdatePlot();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-08-01 00:23:12 +02:00
|
|
|
|
private void ClearButton_Click(object sender, RoutedEventArgs e)
|
|
|
|
|
|
{
|
|
|
|
|
|
Clear();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-07-31 04:57:44 +02:00
|
|
|
|
public void Clear()
|
|
|
|
|
|
{
|
|
|
|
|
|
locations.Clear();
|
2020-08-01 00:23:12 +02:00
|
|
|
|
if(!autoFit)
|
|
|
|
|
|
autoFitToggle.IsChecked = true;
|
|
|
|
|
|
else
|
|
|
|
|
|
UpdatePlot();
|
2020-07-31 04:57:44 +02:00
|
|
|
|
}
|
2020-08-01 00:23:12 +02:00
|
|
|
|
|
2020-08-03 06:55:47 +02:00
|
|
|
|
private struct Point
|
2020-07-31 04:57:44 +02:00
|
|
|
|
{
|
2020-08-03 06:55:47 +02:00
|
|
|
|
public double Latitude { get ;set; }
|
|
|
|
|
|
public double Longitude { get ;set; }
|
|
|
|
|
|
public double Z { get ;set; }
|
|
|
|
|
|
public NmeaParser.Messages.Gga.FixQuality Quality { get; set; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void AddLocation(double latitude, double longitude, double altitude, NmeaParser.Messages.Gga.FixQuality quality)
|
|
|
|
|
|
{
|
|
|
|
|
|
if(quality == NmeaParser.Messages.Gga.FixQuality.Invalid)
|
|
|
|
|
|
return;
|
|
|
|
|
|
locations.Add(new Point {
|
|
|
|
|
|
Latitude = latitude,
|
|
|
|
|
|
Longitude = longitude,
|
|
|
|
|
|
Z = altitude,
|
|
|
|
|
|
Quality = quality
|
|
|
|
|
|
});
|
2020-08-01 00:23:12 +02:00
|
|
|
|
UpdatePlot();
|
2020-07-31 04:57:44 +02:00
|
|
|
|
}
|
2020-08-01 00:23:12 +02:00
|
|
|
|
|
|
|
|
|
|
|
2020-07-31 04:57:44 +02:00
|
|
|
|
private void UpdatePlot()
|
|
|
|
|
|
{
|
2020-08-01 00:23:12 +02:00
|
|
|
|
if (size.Width == 0 || size.Height == 0 || !IsVisible)
|
2020-07-31 04:57:44 +02:00
|
|
|
|
return;
|
|
|
|
|
|
if (locations.Count == 0)
|
2020-08-01 00:23:12 +02:00
|
|
|
|
{
|
|
|
|
|
|
Dispatcher.Invoke(() =>
|
|
|
|
|
|
{
|
|
|
|
|
|
Status.Text = "";
|
|
|
|
|
|
plot.Source = null;
|
|
|
|
|
|
});
|
2020-07-31 04:57:44 +02:00
|
|
|
|
return;
|
2020-08-01 00:23:12 +02:00
|
|
|
|
}
|
2020-08-03 06:55:47 +02:00
|
|
|
|
var latAvr = locations.Select(l => l.Latitude).Average();
|
|
|
|
|
|
var lonAvr = locations.Select(l => l.Longitude).Average();
|
2020-07-31 04:57:44 +02:00
|
|
|
|
|
|
|
|
|
|
//List<double> lonDistances = new List<double>(locations.Count);
|
|
|
|
|
|
//List<double> latDistances = new List<double>(locations.Count);
|
|
|
|
|
|
List<double> distances = new List<double>(locations.Count);
|
2020-08-03 06:55:47 +02:00
|
|
|
|
var locations2 = new List<Point>();
|
2020-07-31 04:57:44 +02:00
|
|
|
|
foreach (var l in locations)
|
|
|
|
|
|
{
|
2020-08-03 06:55:47 +02:00
|
|
|
|
var d = Vincenty.GetDistanceVincenty(latAvr, lonAvr, l.Latitude, l.Longitude);
|
|
|
|
|
|
var dLat = Vincenty.GetDistanceVincenty(latAvr, lonAvr, l.Latitude, lonAvr);
|
|
|
|
|
|
var dLon = Vincenty.GetDistanceVincenty(latAvr, lonAvr, latAvr, l.Longitude);
|
2020-07-31 04:57:44 +02:00
|
|
|
|
distances.Add(d);
|
|
|
|
|
|
//latDistances.Add(dLat);
|
|
|
|
|
|
//lonDistances.Add(dLon);
|
2020-08-03 06:55:47 +02:00
|
|
|
|
if (latAvr > l.Latitude) dLat = -dLat;
|
|
|
|
|
|
if (lonAvr > l.Longitude) dLon = -dLon;
|
|
|
|
|
|
locations2.Add(new Point() { Latitude = dLat, Longitude= dLon, Quality = l.Quality });
|
2020-07-31 04:57:44 +02:00
|
|
|
|
}
|
2020-08-03 06:55:47 +02:00
|
|
|
|
var latMin = locations2.Select(l => l.Latitude).Min();
|
|
|
|
|
|
var lonMin = locations2.Select(l => l.Longitude).Min();
|
|
|
|
|
|
var latMax = locations2.Select(l => l.Latitude).Max();
|
|
|
|
|
|
var lonMax = locations2.Select(l => l.Longitude).Max();
|
|
|
|
|
|
var latAvr2 = locations2.Select(l => l.Latitude).Average();
|
|
|
|
|
|
var lonAvr2 = locations2.Select(l => l.Longitude).Average();
|
2020-07-31 04:57:44 +02:00
|
|
|
|
var maxDifLat = Math.Max(latAvr2 - latMin, latMax - latAvr2);
|
|
|
|
|
|
var maxDifLon = Math.Max(lonAvr2 - lonMin, lonMax - lonAvr2);
|
2020-08-01 00:23:12 +02:00
|
|
|
|
//var maxDif = Math.Max(maxDifLat, maxDifLon);
|
|
|
|
|
|
double maxDif = 1;
|
|
|
|
|
|
if (autoFit)
|
|
|
|
|
|
{
|
|
|
|
|
|
maxDif = distances.Max();
|
2020-08-03 05:37:15 +02:00
|
|
|
|
if (maxDif < 0.05)
|
|
|
|
|
|
maxDif = 0.05;
|
|
|
|
|
|
else if (maxDif < 1)
|
|
|
|
|
|
maxDif = Math.Ceiling(maxDif * 10) / 10d;
|
2020-08-01 00:23:12 +02:00
|
|
|
|
else
|
|
|
|
|
|
maxDif = Math.Ceiling(maxDif);
|
|
|
|
|
|
currentScale = maxDif / (Math.Min(size.Width, size.Height) * .5);
|
|
|
|
|
|
}
|
2020-07-31 04:57:44 +02:00
|
|
|
|
else
|
2020-08-01 00:23:12 +02:00
|
|
|
|
{
|
|
|
|
|
|
maxDif = currentScale * (Math.Min(size.Width, size.Height) * .5);
|
|
|
|
|
|
}
|
|
|
|
|
|
double scale = currentScale;
|
2020-07-31 04:57:44 +02:00
|
|
|
|
if (scale == 0)
|
|
|
|
|
|
scale = 1;
|
2020-08-01 00:23:12 +02:00
|
|
|
|
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 } };
|
2020-08-03 06:55:47 +02:00
|
|
|
|
Color col = Colors.Red;
|
2020-08-01 00:23:12 +02:00
|
|
|
|
for (int i = 0; i < locations2.Count; i++)
|
2020-07-31 04:57:44 +02:00
|
|
|
|
{
|
|
|
|
|
|
var l = locations2[i];
|
2020-08-03 06:55:47 +02:00
|
|
|
|
var x = (int)(width * .5 + (l.Longitude - lonAvr2) / scale);
|
|
|
|
|
|
var y = (int)(height * .5 - (l.Latitude - latAvr2) / scale);
|
2020-08-01 00:23:12 +02:00
|
|
|
|
var index = ((int)y) * stride + ((int)x) * 4;
|
|
|
|
|
|
for (int r = -1; r < stamp.Length-1; r++)
|
2020-07-31 04:57:44 +02:00
|
|
|
|
{
|
2020-08-01 00:23:12 +02:00
|
|
|
|
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];
|
2020-08-03 06:55:47 +02:00
|
|
|
|
switch(l.Quality)
|
|
|
|
|
|
{
|
|
|
|
|
|
case NmeaParser.Messages.Gga.FixQuality.Estimated:
|
|
|
|
|
|
col = Colors.Red; break;
|
|
|
|
|
|
case NmeaParser.Messages.Gga.FixQuality.GpsFix:
|
|
|
|
|
|
col = Colors.Orange; break;
|
|
|
|
|
|
case NmeaParser.Messages.Gga.FixQuality.DgpsFix:
|
|
|
|
|
|
col = Colors.Yellow; break;
|
|
|
|
|
|
case NmeaParser.Messages.Gga.FixQuality.FloatRtk:
|
|
|
|
|
|
col = Color.FromRgb(0,255,0); break;
|
|
|
|
|
|
case NmeaParser.Messages.Gga.FixQuality.Rtk:
|
|
|
|
|
|
col = Colors.LightBlue; break;
|
|
|
|
|
|
default:
|
|
|
|
|
|
col = Colors.Gray; break;
|
|
|
|
|
|
}
|
|
|
|
|
|
pixels[p + 1] = col.B;
|
|
|
|
|
|
pixels[p + 1] = col.G;
|
|
|
|
|
|
pixels[p + 2] = col.R;
|
2020-08-01 00:23:12 +02:00
|
|
|
|
pixels[p + 3] = (byte)Math.Min(255, pixels[p + 3] + val * 255); //Multiply alpha
|
|
|
|
|
|
}
|
2020-07-31 04:57:44 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2020-08-03 06:55:47 +02:00
|
|
|
|
var stdDevLat = Math.Sqrt(locations2.Sum(d => (d.Latitude - latAvr2) * (d.Latitude - latAvr2)) / locations2.Count);
|
|
|
|
|
|
var stdDevLon = Math.Sqrt(locations2.Sum(d => (d.Longitude - lonAvr2) * (d.Longitude - lonAvr2)) / locations2.Count);
|
|
|
|
|
|
var zAvr = locations.Select(l => l.Z).Where(l => !double.IsNaN(l)).Average();
|
|
|
|
|
|
var stdDevZ = Math.Sqrt(locations.Select(l => l.Z).Where(l => !double.IsNaN(l)).Sum(d => (d - zAvr) * (d - zAvr)) / locations.Select(l => l.Z).Where(l => !double.IsNaN(l)).Count());
|
2020-08-01 00:23:12 +02:00
|
|
|
|
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";
|
|
|
|
|
|
});
|
2020-07-31 04:57:44 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
internal static class Vincenty
|
|
|
|
|
|
{
|
|
|
|
|
|
private const double D2R = 0.01745329251994329576923690768489; //Degrees to radians
|
|
|
|
|
|
|
|
|
|
|
|
public static double GetDistanceVincenty(double lat1, double lon1, double lat2, double lon2)
|
|
|
|
|
|
{
|
|
|
|
|
|
return GetDistanceVincenty(lat1, lon1, lat2, lon2, 6378137, 6356752.31424518); //Uses WGS84 values
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Vincenty's formulae is used in geodesy to calculate the distance
|
|
|
|
|
|
/// between two points on the surface of a spheroid.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static double GetDistanceVincenty(double lat1, double lon1, double lat2, double lon2, double semiMajor, double semiMinor)
|
|
|
|
|
|
{
|
|
|
|
|
|
var a = semiMajor;
|
|
|
|
|
|
var b = semiMinor;
|
|
|
|
|
|
var f = (a - b) / a; //flattening
|
|
|
|
|
|
var L = (lon2 - lon1) * D2R;
|
|
|
|
|
|
var U1 = Math.Atan((1 - f) * Math.Tan(lat1 * D2R));
|
|
|
|
|
|
var U2 = Math.Atan((1 - f) * Math.Tan(lat2 * D2R));
|
|
|
|
|
|
var sinU1 = Math.Sin(U1);
|
|
|
|
|
|
var cosU1 = Math.Cos(U1);
|
|
|
|
|
|
var sinU2 = Math.Sin(U2);
|
|
|
|
|
|
var cosU2 = Math.Cos(U2);
|
|
|
|
|
|
|
|
|
|
|
|
double lambda = L;
|
|
|
|
|
|
double lambdaP;
|
|
|
|
|
|
double cosSigma, cosSqAlpha, sinSigma, cos2SigmaM, sigma, sinLambda, cosLambda;
|
|
|
|
|
|
|
|
|
|
|
|
int iterLimit = 100;
|
|
|
|
|
|
do
|
|
|
|
|
|
{
|
|
|
|
|
|
sinLambda = Math.Sin(lambda);
|
|
|
|
|
|
cosLambda = Math.Cos(lambda);
|
|
|
|
|
|
sinSigma = Math.Sqrt((cosU2 * sinLambda) * (cosU2 * sinLambda) +
|
|
|
|
|
|
(cosU1 * sinU2 - sinU1 * cosU2 * cosLambda) * (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda));
|
|
|
|
|
|
if (sinSigma == 0)
|
|
|
|
|
|
return 0; // co-incident points
|
|
|
|
|
|
|
|
|
|
|
|
cosSigma = sinU1 * sinU2 + cosU1 * cosU2 * cosLambda;
|
|
|
|
|
|
sigma = Math.Atan2(sinSigma, cosSigma);
|
|
|
|
|
|
double sinAlpha = cosU1 * cosU2 * sinLambda / sinSigma;
|
|
|
|
|
|
cosSqAlpha = 1 - sinAlpha * sinAlpha;
|
|
|
|
|
|
cos2SigmaM = cosSigma - 2 * sinU1 * sinU2 / cosSqAlpha;
|
|
|
|
|
|
if (double.IsNaN(cos2SigmaM))
|
|
|
|
|
|
cos2SigmaM = 0; // equatorial line: cosSqAlpha=0 (§6)
|
|
|
|
|
|
double C = f / 16 * cosSqAlpha * (4 + f * (4 - 3 * cosSqAlpha));
|
|
|
|
|
|
lambdaP = lambda;
|
|
|
|
|
|
lambda = L + (1 - C) * f * sinAlpha *
|
|
|
|
|
|
(sigma + C * sinSigma * (cos2SigmaM + C * cosSigma * (-1 + 2 * cos2SigmaM * cos2SigmaM)));
|
|
|
|
|
|
} while (Math.Abs(lambda - lambdaP) > 1e-12 && --iterLimit > 0);
|
|
|
|
|
|
|
|
|
|
|
|
if (iterLimit == 0) return double.NaN; // formula failed to converge
|
|
|
|
|
|
|
|
|
|
|
|
var uSq = cosSqAlpha * (a * a - b * b) / (b * b);
|
|
|
|
|
|
var A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq)));
|
|
|
|
|
|
var B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq)));
|
|
|
|
|
|
var deltaSigma = B * sinSigma * (cos2SigmaM + B / 4 * (cosSigma * (-1 + 2 * cos2SigmaM * cos2SigmaM) -
|
|
|
|
|
|
B / 6 * cos2SigmaM * (-3 + 4 * sinSigma * sinSigma) * (-3 + 4 * cos2SigmaM * cos2SigmaM)));
|
|
|
|
|
|
var s = b * A * (sigma - deltaSigma);
|
|
|
|
|
|
|
|
|
|
|
|
s = Math.Round(s, 3); // round to 1mm precision
|
|
|
|
|
|
return s;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2020-08-01 00:23:12 +02:00
|
|
|
|
bool autoFit = true;
|
|
|
|
|
|
double currentScale = 1;
|
|
|
|
|
|
private void ToggleButton_Unchecked(object sender, RoutedEventArgs e)
|
|
|
|
|
|
{
|
|
|
|
|
|
autoFit = false;
|
|
|
|
|
|
}
|
2020-07-31 04:57:44 +02:00
|
|
|
|
|
2020-08-01 00:23:12 +02:00
|
|
|
|
private void ToggleButton_Checked(object sender, RoutedEventArgs e)
|
2020-07-31 04:57:44 +02:00
|
|
|
|
{
|
2020-08-01 00:23:12 +02:00
|
|
|
|
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;
|
2020-08-03 05:37:15 +02:00
|
|
|
|
if (maxDif < 0.05)
|
|
|
|
|
|
maxDif = 0.05;
|
|
|
|
|
|
else if (maxDif < 1)
|
|
|
|
|
|
maxDif = Math.Ceiling(maxDif * 10) / 10d;
|
2020-08-01 00:23:12 +02:00
|
|
|
|
else
|
|
|
|
|
|
maxDif = Math.Ceiling(maxDif);
|
|
|
|
|
|
currentScale = maxDif / (Math.Min(size.Width, size.Height) * .5);
|
|
|
|
|
|
|
|
|
|
|
|
if (autoFitToggle.IsChecked == true)
|
|
|
|
|
|
{
|
|
|
|
|
|
autoFitToggle.IsChecked = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
UpdatePlot();
|
|
|
|
|
|
}
|
2020-07-31 04:57:44 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|