namespace Mapbox.ProbeExtractorCs { using Mapbox.CheapRulerCs; using System; using System.Collections.Generic; public class ProbeExtractorOptions { /// Seconds public double MinTimeBetweenProbes = 0; /// Do not include probes when the distance is X times bigger than the previous one public double MaxDistanceRatioJump = double.MaxValue; /// Do not include probes when the duration is X times bigger than the previous one public double MaxDurationRatioJump = double.MaxValue; /// Meters per second per second public double MaxAcceleration = double.MaxValue; /// Meters per second per second public double MaxDeceleration = double.MaxValue; /// Minimum probes extracted data should contain public int MinProbes = 2; /// Also return probes deemed not good public bool OutputBadProbes = false; } /// /// This module allows to pass a list of trace points and extract its probes and their properties. /// It can also act as a filter for those probes. /// public class ProbeExtractor { private CheapRuler _ruler; private ProbeExtractorOptions _options; /// /// /// /// A CheapRuler instance, expected in kilometers. /// public ProbeExtractor(CheapRuler ruler, ProbeExtractorOptions options) { _ruler = ruler; _options = options; } /// /// Extract probes according to ProbeExtractorOptions. /// /// List of trace points /// List of probes. Empty list if no trace point matched the options. public List ExtractProbes(List trace) { int tracePntCnt = trace.Count; long[] durations = new long[tracePntCnt - 1]; double[] distances = new double[tracePntCnt - 1]; double[] speeds = new double[tracePntCnt - 1]; double[] bearings = new double[tracePntCnt - 1]; for (int i = 1; i < tracePntCnt; i++) { TracePoint current = trace[i]; TracePoint previous = trace[i - 1]; int insertIdx = i - 1; durations[insertIdx] = (current.Timestamp - previous.Timestamp) / 1000; //seconds double[] currLocation = new double[] { current.Longitude, current.Latitude }; double[] prevLocation = new double[] { previous.Longitude, previous.Latitude }; distances[insertIdx] = _ruler.Distance(currLocation, prevLocation); speeds[insertIdx] = distances[insertIdx] / durations[insertIdx] * 3600; //kph double bearing = _ruler.Bearing(prevLocation, currLocation); bearings[insertIdx] = bearing < 0 ? 360 + bearing : bearing; } List probes = new List(); // 1st pass: iterate trace points and determine if they are good // bail early if !_options.OutputBadProbes bool negativeDuration = false; for (int i = 1; i < speeds.Length; i++) { //assume tracpoint is good bool isGood = true; if (negativeDuration) { // if trace already has a negative duration, then all probes are bad isGood = false; } else if (durations[i] < 0) { // if a trace has negative duration, the trace is likely noisy // bail, if we don't want bad probes if (!_options.OutputBadProbes) { return new List(); } negativeDuration = true; isGood = false; } else if (durations[i] < _options.MinTimeBetweenProbes) { // if shorter than the minTimeBetweenProbes, filter. isGood = false; } else if (durations[i] > _options.MaxDurationRatioJump * durations[i - 1]) { // if not a gradual decrease in sampling frequency, it's most likely a signal jump isGood = false; } else if (speeds[i] - speeds[i - 1] > _options.MaxAcceleration * durations[i]) { // if accelerating faster than maxAcceleration, it's most likely a glitch isGood = false; } else if (speeds[i - 1] - speeds[i] > _options.MaxDeceleration * durations[i]) { // if decelerating faster than maxDeceleration, it's most likely a glitch isGood = false; } else { bool isForwardDirection = compareBearing(bearings[i - 1], bearings[i], 89, false); if (!isForwardDirection) { isGood = false; } } if (isGood || _options.OutputBadProbes) { double[] coords = pointAtDistanceAndBearing( trace[i - 1] , distances[i] / 2 , bearings[i] ); probes.Add(new Probe() { Latitude = coords[1], Longitude = coords[0], StartTime = trace[i].Timestamp, Duration = durations[i], Distance = distances[i], Speed = speeds[i], Bearing = bearings[i], IsGood = isGood }); } } // if too few good probes, drop entire trace if (!_options.OutputBadProbes && probes.Count < _options.MinProbes) { return new List(); } // MinProbes can be 0, return if (probes.Count == 0 && _options.MinProbes == 0) { return new List(); } // 2nd pass // require at least two probes if (probes.Count > 1) { // check first probe in a trace against the average of first two good probes var avgSpeed = (probes[0].Speed + probes[1].Speed) / 2; var avgDistance = (probes[0].Distance + probes[1].Distance) / 2; var avgDuration = (probes[0].Duration + probes[1].Duration) / 2; var avgBearing = averageAngle(probes[0].Bearing, probes[1].Bearing); bool good = true; if (negativeDuration) { if (!_options.OutputBadProbes) { return new List(); } negativeDuration = true; good = false; // if a trace has negative duration, the trace is likely noisy } else if (durations[0] < 0) { good = false; } else if (durations[0] < _options.MinTimeBetweenProbes) { // if shorter than the minTimeBetweenProbes, filter. good = false; } else if (distances[0] > _options.MaxDistanceRatioJump * avgDistance) { // if not a gradual increase in distance, it's most likely a signal jump good = false; } else if (durations[0] > _options.MaxDurationRatioJump * avgDuration) { // if not a gradual decrease in sampling frequency, it's most likely a signal jump good = false; } else if (avgSpeed - speeds[0] > _options.MaxAcceleration * durations[0]) { // if accelerating faster than maxAcceleration, it's most likely a glitch good = false; } else if (speeds[0] - avgSpeed > _options.MaxDeceleration * durations[0]) { // if decelerating faster than maxDeceleration, it's most likely a glitch good = false; } else { // if in reverse direction, it's most likely signal jump bool isForwardDirection = compareBearing(bearings[0], avgBearing, 89, false); if (!isForwardDirection) { good = false; } } if (good || _options.OutputBadProbes) { double[] coords = pointAtDistanceAndBearing( trace[0] , distances[0] , bearings[0] ); probes.Insert( 0, new Probe() { Latitude = coords[1], Longitude = coords[0], StartTime = trace[0].Timestamp, Duration = durations[0], Distance = distances[0], Speed = speeds[0], Bearing = bearings[0], IsGood = good } ); } } return probes; } /// /// Computes the average of two angles. /// /// First angle. /// Second angle /// Angle midway between a and b. private double averageAngle(double a, double b) { var anorm = normalizeAngle(a); var bnorm = normalizeAngle(b); var minAngle = Math.Min(anorm, bnorm); var maxAngle = Math.Max(anorm, bnorm); var dist1 = Math.Abs(a - b); var dist2 = (minAngle + (360 - maxAngle)); if (dist1 <= dist2) { return normalizeAngle(minAngle + dist1 / 2); } else { return normalizeAngle(maxAngle + dist2 / 2); } } /// /// Map angle to positive modulo 360 space. /// /// An angle in degrees /// Equivalent angle in [0-360] space. private double normalizeAngle(double angle) { return (angle < 0) ? (angle % 360) + 360 : (angle % 360); } /// /// Compare bearing `baseBearing` to `bearing`, to determine if they are close enough to each other to be considered matching. /// /// Base bearing /// Number of degrees difference that is allowed between the bearings. /// /// allows bearings that are 180 degrees +/- `range` to be considered matching /// private bool compareBearing(double baseBearing, double bearing, double range, bool allowReverse) { // map base and bearing into positive modulo 360 space var normalizedBase = normalizeAngle(baseBearing); var normalizedBearing = normalizeAngle(bearing); var min = normalizeAngle(normalizedBase - range); var max = normalizeAngle(normalizedBase + range); if (min < max) { if (min <= normalizedBearing && normalizedBearing <= max) { return true; } } else if (min <= normalizedBearing || normalizedBearing <= max) { return true; } if (allowReverse) { return compareBearing(normalizedBase + 180, bearing, range, false); } return false; } /// /// Creates coordinate in between two trace points to smooth line /// /// double array containing lng/lat private double[] pointAtDistanceAndBearing(TracePoint tracePoint, double distance, double bearing) { return _ruler.Destination( new double[] { tracePoint.Longitude, tracePoint.Latitude } , distance , bearing ); } } }