2a2342b8caab878e4ee7b953ebe54f17ab4804ab
java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/CourseChangeBasedTrackApproximation.java
| ... | ... | @@ -25,7 +25,11 @@ import com.sap.sailing.domain.tracking.GPSTrackListener; |
| 25 | 25 | import com.sap.sse.common.Duration; |
| 26 | 26 | import com.sap.sse.common.TimePoint; |
| 27 | 27 | import com.sap.sse.common.TimeRange; |
| 28 | +import com.sap.sse.common.Util.Pair; |
|
| 28 | 29 | import com.sap.sse.common.impl.TimeRangeImpl; |
| 30 | +import com.sap.sse.common.scalablevalue.KadaneExtremeSubsequenceFinder; |
|
| 31 | +import com.sap.sse.common.scalablevalue.KadaneExtremeSubsequenceFinderLinkedNodesImpl; |
|
| 32 | +import com.sap.sse.common.scalablevalue.ScalableDouble; |
|
| 29 | 33 | |
| 30 | 34 | /** |
| 31 | 35 | * Given a {@link GPSFixTrack} containing {@link GPSFixMoving}, an instance of this class finds areas on the track where |
| ... | ... | @@ -113,35 +117,18 @@ public class CourseChangeBasedTrackApproximation implements Serializable, GPSTra |
| 113 | 117 | * one shorter than "window"; {@link #totalCourseChangeFromBeginningOfWindow}{@code [i]} is from |
| 114 | 118 | * {@link #window}{@code [0]} to {@link #window}{@code [i+1]} |
| 115 | 119 | */ |
| 116 | - private final List<Double> totalCourseChangeFromBeginningOfWindow; |
|
| 120 | + private final KadaneExtremeSubsequenceFinder<Double, Double, ScalableDouble> courseChangeBetweenFixesInWindow; |
|
| 117 | 121 | |
| 118 | 122 | private final double maneuverAngleInDegreesThreshold; |
| 119 | 123 | private Duration windowDuration; |
| 120 | 124 | |
| 121 | - /** |
|
| 122 | - * The absolute of the value found at index {@link #indexOfMaximumTotalCourseChange} in |
|
| 123 | - * {@link #totalCourseChangeFromBeginningOfWindow}. |
|
| 124 | - */ |
|
| 125 | - private double absoluteMaximumTotalCourseChangeFromBeginningOfWindowInDegrees; |
|
| 126 | - |
|
| 127 | - /** |
|
| 128 | - * -1 means undefined because window is empty; otherwise an index into |
|
| 129 | - * {@link #totalCourseChangeFromBeginningOfWindow} such that the absolute |
|
| 130 | - * value at that index is maximal. |
|
| 131 | - */ |
|
| 132 | - private int indexOfMaximumTotalCourseChange; |
|
| 133 | - |
|
| 134 | 125 | FixWindow() { |
| 135 | 126 | this.window = new LinkedList<>(); |
| 136 | 127 | this.speedForFixesInWindow = new LinkedList<>(); |
| 137 | - this.absoluteMaximumTotalCourseChangeFromBeginningOfWindowInDegrees = 0; |
|
| 138 | - this.indexOfMaximumTotalCourseChange = -1; |
|
| 139 | 128 | this.windowDuration = Duration.NULL; |
| 140 | 129 | // use twice the maneuver duration to also catch slowly-executed gybes |
| 141 | 130 | this.maneuverAngleInDegreesThreshold = boatClass.getManeuverDegreeAngleThreshold(); |
| 142 | - final Duration averageIntervalBetweenRawFixes = track.getAverageIntervalBetweenRawFixes(); |
|
| 143 | - this.totalCourseChangeFromBeginningOfWindow = new ArrayList<>(((int) getMaximumWindowLength().divide( |
|
| 144 | - averageIntervalBetweenRawFixes==null?Duration.ONE_SECOND:averageIntervalBetweenRawFixes))+10); |
|
| 131 | + this.courseChangeBetweenFixesInWindow = new KadaneExtremeSubsequenceFinderLinkedNodesImpl<>(); |
|
| 145 | 132 | } |
| 146 | 133 | |
| 147 | 134 | /** |
| ... | ... | @@ -203,19 +190,10 @@ public class CourseChangeBasedTrackApproximation implements Serializable, GPSTra |
| 203 | 190 | if (insertPosition == window.size()-1) { // if not appended to the end, the window duration won't change |
| 204 | 191 | windowDuration = windowDuration.plus(previous.getTimePoint().until(next.getTimePoint())); |
| 205 | 192 | } |
| 206 | - if (totalCourseChangeFromBeginningOfWindow.isEmpty()) { |
|
| 207 | - totalCourseChangeFromBeginningOfWindow.add(courseChangeBetweenPreviousAndNextInDegrees); |
|
| 208 | - absoluteMaximumTotalCourseChangeFromBeginningOfWindowInDegrees = Math.abs(courseChangeBetweenPreviousAndNextInDegrees); |
|
| 209 | - indexOfMaximumTotalCourseChange = 0; |
|
| 193 | + if (courseChangeBetweenFixesInWindow.isEmpty()) { |
|
| 194 | + courseChangeBetweenFixesInWindow.add(new ScalableDouble(courseChangeBetweenPreviousAndNextInDegrees)); |
|
| 210 | 195 | } else { |
| 211 | - final double totalCourseChangeFromBeginningOfWindowForCurrentFix = totalCourseChangeFromBeginningOfWindow.get(insertPosition-2) |
|
| 212 | - + courseChangeBetweenPreviousAndNextInDegrees; |
|
| 213 | - totalCourseChangeFromBeginningOfWindow.add(insertPosition-1, totalCourseChangeFromBeginningOfWindowForCurrentFix); |
|
| 214 | - if (Math.abs(totalCourseChangeFromBeginningOfWindowForCurrentFix) > absoluteMaximumTotalCourseChangeFromBeginningOfWindowInDegrees) { |
|
| 215 | - absoluteMaximumTotalCourseChangeFromBeginningOfWindowInDegrees = Math.abs(totalCourseChangeFromBeginningOfWindowForCurrentFix); |
|
| 216 | - indexOfMaximumTotalCourseChange = insertPosition-1; |
|
| 217 | - } |
|
| 218 | - // TODO bug6209: now check if the course change direction has changed (from "to port" to "to starboard" or vice versa); if so, remove fixes from beginning of window up to the change point |
|
| 196 | + courseChangeBetweenFixesInWindow.add(insertPosition-1, new ScalableDouble(courseChangeBetweenPreviousAndNextInDegrees)); |
|
| 219 | 197 | } |
| 220 | 198 | if (windowDuration.compareTo(getMaximumWindowLength()) > 0) { |
| 221 | 199 | result = tryToExtractManeuverCandidate(); |
| ... | ... | @@ -224,13 +202,13 @@ public class CourseChangeBasedTrackApproximation implements Serializable, GPSTra |
| 224 | 202 | // otherwise, keep removing fixes from the beginning of the window until the window duration |
| 225 | 203 | // is again at or below the maximum allowed: |
| 226 | 204 | while (windowDuration.compareTo(getMaximumWindowLength()) > 0) { |
| 227 | - removeFirst(); |
|
| 205 | + removeFirst(1); |
|
| 228 | 206 | } |
| 229 | 207 | } |
| 230 | 208 | } else { |
| 231 | 209 | result = null; |
| 232 | 210 | } |
| 233 | - assert window.isEmpty() && totalCourseChangeFromBeginningOfWindow.isEmpty() || window.size() == totalCourseChangeFromBeginningOfWindow.size()+1; |
|
| 211 | + assert window.isEmpty() && courseChangeBetweenFixesInWindow.isEmpty() || window.size() == courseChangeBetweenFixesInWindow.size()+1; |
|
| 234 | 212 | } else { // the window was empty so far; we added the next fix, but no maneuver can yet be identified in lack of a course change |
| 235 | 213 | result = null; |
| 236 | 214 | } |
| ... | ... | @@ -245,7 +223,7 @@ public class CourseChangeBasedTrackApproximation implements Serializable, GPSTra |
| 245 | 223 | * Tries to extract a maneuver candidate from the current {@link #window}. See {@link #getManeuverCandidate()}. |
| 246 | 224 | * Basically, the maximum course change in the window has to be equal to or exceed the maneuver threshold. If |
| 247 | 225 | * such a candidate is found, all fixes before the candidate as well as the candidate itself are |
| 248 | - * {@link #removeFirst() removed} from the {@link #window}, and all invariants are re-established. |
|
| 226 | + * {@link #removeFirst(int) removed} from the {@link #window}, and all invariants are re-established. |
|
| 249 | 227 | * <p> |
| 250 | 228 | * |
| 251 | 229 | * Usually, this method will be called by {@link #add(GPSFixMoving)}, but especially after having added the last |
| ... | ... | @@ -258,13 +236,12 @@ public class CourseChangeBasedTrackApproximation implements Serializable, GPSTra |
| 258 | 236 | * was found. |
| 259 | 237 | */ |
| 260 | 238 | GPSFixMoving tryToExtractManeuverCandidate() { |
| 261 | - final GPSFixMoving result; |
|
| 262 | 239 | // analysis window has exceeded the typical maneuver duration for the boat class; |
| 263 | - result = getManeuverCandidate(); |
|
| 264 | - if (result != null) { |
|
| 265 | - while (!removeFirst().equals(result)); // remove all including the maneuver fix |
|
| 240 | + final Pair<GPSFixMoving, Integer> candidateAndItsIndex = getManeuverCandidate(); |
|
| 241 | + if (candidateAndItsIndex != null) { |
|
| 242 | + removeFirst(candidateAndItsIndex.getB()); // remove all including the maneuver fix |
|
| 266 | 243 | } |
| 267 | - return result; |
|
| 244 | + return candidateAndItsIndex == null ? null : candidateAndItsIndex.getA(); |
|
| 268 | 245 | } |
| 269 | 246 | |
| 270 | 247 | /** |
| ... | ... | @@ -274,34 +251,18 @@ public class CourseChangeBasedTrackApproximation implements Serializable, GPSTra |
| 274 | 251 | * |
| 275 | 252 | * @return the fix removed from the beginning of this window |
| 276 | 253 | */ |
| 277 | - private GPSFixMoving removeFirst() { |
|
| 254 | + private void removeFirst(int howManyElementsToRemove) { |
|
| 278 | 255 | assert !window.isEmpty(); |
| 279 | - final GPSFixMoving removed = window.removeFirst(); |
|
| 280 | - speedForFixesInWindow.removeFirst(); |
|
| 281 | - windowDuration = window.isEmpty() ? Duration.NULL : windowDuration.minus(removed.getTimePoint().until(window.getFirst().getTimePoint())); |
|
| 256 | + for (int i=0; i<howManyElementsToRemove; i++) { |
|
| 257 | + final GPSFixMoving removed = window.removeFirst(); |
|
| 258 | + speedForFixesInWindow.removeFirst(); |
|
| 259 | + windowDuration = window.isEmpty() ? Duration.NULL : windowDuration.minus(removed.getTimePoint().until(window.getFirst().getTimePoint())); |
|
| 260 | + } |
|
| 282 | 261 | // adjust totalCourseChangeFromBeginningOfWindow by subtracting the first course change from all others |
| 283 | 262 | // and shifting all by one position to the "left" |
| 284 | - absoluteMaximumTotalCourseChangeFromBeginningOfWindowInDegrees = 0; |
|
| 285 | - if (totalCourseChangeFromBeginningOfWindow.size() <= 1) { // no more than one element left; can't tell any course change |
|
| 286 | - indexOfMaximumTotalCourseChange = -1; |
|
| 287 | - } else { |
|
| 288 | - final double courseChangeOfFirstInDegrees = totalCourseChangeFromBeginningOfWindow.get(0); |
|
| 289 | - for (int i=0; i<totalCourseChangeFromBeginningOfWindow.size()-1; i++) { |
|
| 290 | - // adjust all total course changes by subtracting the |
|
| 291 | - final double totalCourseChangeFromBeginningOfWindowForFixAtIndex = totalCourseChangeFromBeginningOfWindow.get(i+1)-courseChangeOfFirstInDegrees; |
|
| 292 | - if (Math.abs(totalCourseChangeFromBeginningOfWindowForFixAtIndex) > absoluteMaximumTotalCourseChangeFromBeginningOfWindowInDegrees) { |
|
| 293 | - absoluteMaximumTotalCourseChangeFromBeginningOfWindowInDegrees = Math.abs(totalCourseChangeFromBeginningOfWindowForFixAtIndex); |
|
| 294 | - indexOfMaximumTotalCourseChange = i; |
|
| 295 | - } |
|
| 296 | - totalCourseChangeFromBeginningOfWindow.set(i, totalCourseChangeFromBeginningOfWindowForFixAtIndex); |
|
| 297 | - } |
|
| 298 | - } |
|
| 299 | - if (!totalCourseChangeFromBeginningOfWindow.isEmpty()) { // only try to remove if not removing last element of window |
|
| 300 | - totalCourseChangeFromBeginningOfWindow.remove(totalCourseChangeFromBeginningOfWindow.size()-1); |
|
| 301 | - } else { |
|
| 302 | - assert window.isEmpty(); |
|
| 263 | + if (!courseChangeBetweenFixesInWindow.isEmpty()) { |
|
| 264 | + courseChangeBetweenFixesInWindow.removeFirst(howManyElementsToRemove); |
|
| 303 | 265 | } |
| 304 | - return removed; |
|
| 305 | 266 | } |
| 306 | 267 | |
| 307 | 268 | /** |
| ... | ... | @@ -313,28 +274,38 @@ public class CourseChangeBasedTrackApproximation implements Serializable, GPSTra |
| 313 | 274 | * per time. |
| 314 | 275 | * <p> |
| 315 | 276 | * The {@link #window} is left unchanged. |
| 277 | + * |
|
| 278 | + * @return a pair holding the maneuver candidate fix if one was found, or {@code null} if no candidate was |
|
| 279 | + * found, as well as the index of that fix within the {@link #window} |
|
| 316 | 280 | */ |
| 317 | - private GPSFixMoving getManeuverCandidate() { |
|
| 281 | + private Pair<GPSFixMoving, Integer> getManeuverCandidate() { |
|
| 318 | 282 | final GPSFixMoving result; |
| 283 | + final Double maximumCourseChangeToStarboard = courseChangeBetweenFixesInWindow.getMaxSum().divide(1); |
|
| 284 | + final double maximumCourseChangeToPort = -courseChangeBetweenFixesInWindow.getMinSum().divide(1); |
|
| 285 | + final double absoluteMaximumTotalCourseChangeFromBeginningOfWindowInDegrees = Math.max(maximumCourseChangeToStarboard, maximumCourseChangeToPort); |
|
| 286 | + int indexOfMaximumAbsoluteCourseChangeInCorrectDirection = -1; |
|
| 319 | 287 | if (absoluteMaximumTotalCourseChangeFromBeginningOfWindowInDegrees >= maneuverAngleInDegreesThreshold) { |
| 320 | - final double signumOfMaximumAbsoluteCourseChange = Math.signum(this.totalCourseChangeFromBeginningOfWindow.get(indexOfMaximumTotalCourseChange)); |
|
| 321 | - double previousTotalCourseChange = 0; |
|
| 322 | - double maximumAbsoluteCourseChangeInCorrectDirection = -1; |
|
| 323 | - int indexOfMaximumAbsoluteCourseChangeInCorrectDirection = -1; |
|
| 324 | - for (int i=0; i<=indexOfMaximumTotalCourseChange; i++) { |
|
| 325 | - final double currentTotalCourseChange = totalCourseChangeFromBeginningOfWindow.get(i); |
|
| 326 | - final double courseChange = currentTotalCourseChange-previousTotalCourseChange; |
|
| 327 | - if (courseChange*signumOfMaximumAbsoluteCourseChange > maximumAbsoluteCourseChangeInCorrectDirection) { |
|
| 328 | - maximumAbsoluteCourseChangeInCorrectDirection = courseChange*signumOfMaximumAbsoluteCourseChange; |
|
| 288 | + final int indexOfMaximumTotalCourseChangeStart = maximumCourseChangeToStarboard >= maximumCourseChangeToPort ? |
|
| 289 | + courseChangeBetweenFixesInWindow.getStartIndexOfMaxSumSequence(): |
|
| 290 | + courseChangeBetweenFixesInWindow.getStartIndexOfMinSumSequence(); |
|
| 291 | + final Iterable<ScalableDouble> maximumCourseChangeSequence = ()->maximumCourseChangeToStarboard >= maximumCourseChangeToPort ? |
|
| 292 | + courseChangeBetweenFixesInWindow.getSubSequenceWithMaxSum() : |
|
| 293 | + courseChangeBetweenFixesInWindow.getSubSequenceWithMinSum(); |
|
| 294 | + int i=indexOfMaximumTotalCourseChangeStart; |
|
| 295 | + double maximumAbsoluteCourseChangeInCorrectDirection = Double.NEGATIVE_INFINITY; |
|
| 296 | + for (final ScalableDouble courseChange : maximumCourseChangeSequence) { |
|
| 297 | + final double absoluteCourseChangeInDegrees = courseChange.divide(maximumCourseChangeToStarboard >= maximumCourseChangeToPort ? 1 : -1); |
|
| 298 | + if (absoluteCourseChangeInDegrees > maximumAbsoluteCourseChangeInCorrectDirection) { |
|
| 299 | + maximumAbsoluteCourseChangeInCorrectDirection = absoluteCourseChangeInDegrees; |
|
| 329 | 300 | indexOfMaximumAbsoluteCourseChangeInCorrectDirection = i; |
| 330 | 301 | } |
| 331 | - previousTotalCourseChange = currentTotalCourseChange; |
|
| 302 | + i++; |
|
| 332 | 303 | } |
| 333 | 304 | result = window.get(indexOfMaximumAbsoluteCourseChangeInCorrectDirection); // pick the fix introducing, not finishing, the highest turn rate |
| 334 | 305 | } else { |
| 335 | 306 | result = null; |
| 336 | 307 | } |
| 337 | - return result; |
|
| 308 | + return result == null ? null : new Pair<>(result, indexOfMaximumAbsoluteCourseChangeInCorrectDirection); |
|
| 338 | 309 | } |
| 339 | 310 | |
| 340 | 311 | /** |
java/com.sap.sse.common.test/src/com/sap/sse/common/test/KadaneExtremeSubsequenceFinderTest.java
| ... | ... | @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; |
| 4 | 4 | import static org.junit.jupiter.api.Assertions.assertTrue; |
| 5 | 5 | |
| 6 | 6 | import java.io.IOException; |
| 7 | +import java.util.Arrays; |
|
| 7 | 8 | import java.util.Random; |
| 8 | 9 | import java.util.TreeSet; |
| 9 | 10 | import java.util.logging.Logger; |
| ... | ... | @@ -11,6 +12,7 @@ import java.util.logging.Logger; |
| 11 | 12 | import org.junit.jupiter.api.AfterAll; |
| 12 | 13 | import org.junit.jupiter.api.Test; |
| 13 | 14 | |
| 15 | +import com.sap.sse.common.Util; |
|
| 14 | 16 | import com.sap.sse.common.scalablevalue.KadaneExtremeSubsequenceFinder; |
| 15 | 17 | import com.sap.sse.common.scalablevalue.ScalableDouble; |
| 16 | 18 | import com.sap.sse.testutils.Measurement; |
| ... | ... | @@ -73,6 +75,7 @@ public abstract class KadaneExtremeSubsequenceFinderTest { |
| 73 | 75 | assertEquals(9.0, finder.getMaxSum().divide(1.0), EPSILON); |
| 74 | 76 | assertEquals(4, finder.getStartIndexOfMaxSumSequence()); |
| 75 | 77 | assertEquals(5, finder.getEndIndexOfMaxSumSequence()); |
| 78 | + assertTrue(Util.equals(Arrays.asList(new ScalableDouble(4), new ScalableDouble(5)), ()->finder.getSubSequenceWithMaxSum())); |
|
| 76 | 79 | } |
| 77 | 80 | |
| 78 | 81 | @Test |
| ... | ... | @@ -118,6 +121,7 @@ public abstract class KadaneExtremeSubsequenceFinderTest { |
| 118 | 121 | assertEquals(15.0, finder.getMaxSum().divide(1.0), EPSILON); |
| 119 | 122 | assertEquals(5, finder.getStartIndexOfMaxSumSequence()); |
| 120 | 123 | assertEquals(9, finder.getEndIndexOfMaxSumSequence()); |
| 124 | + assertTrue(Util.equals(Arrays.asList(new ScalableDouble(1), new ScalableDouble(2), new ScalableDouble(3), new ScalableDouble(4), new ScalableDouble(5)), ()->finder.getSubSequenceWithMaxSum())); |
|
| 121 | 125 | finder.remove(8); // removes the 4.0 from the second sequence, resulting again in two equal max sum sub-sequences: |
| 122 | 126 | assertEquals(11.0, finder.getMaxSum().divide(1.0), EPSILON); |
| 123 | 127 | assertTrue(finder.getStartIndexOfMaxSumSequence() == 0 || finder.getStartIndexOfMaxSumSequence() == 5); |
java/com.sap.sse.common/src/com/sap/sse/common/scalablevalue/KadaneExtremeSubsequenceFinderLinkedNodesImpl.java
| ... | ... | @@ -16,7 +16,8 @@ import com.sap.sse.common.Util; |
| 16 | 16 | * also across sub-sequences such as those extreme sum sub-sequences, a {@link #size()} as well as an |
| 17 | 17 | * {@link #isEmpty()} operation.<p> |
| 18 | 18 | * |
| 19 | - * This implementation uses a doubly-linked sequence of {@link Node}s. |
|
| 19 | + * This implementation uses a doubly-linked sequence of {@link Node}s. It is <em>not</em> thread-safe. |
|
| 20 | + * Callers must ensure that concurrent modifications are properly synchronized. |
|
| 20 | 21 | * |
| 21 | 22 | * @author Axel Uhl (D043530) |
| 22 | 23 | */ |
java/com.sap.sse.common/src/com/sap/sse/common/scalablevalue/ScalableDouble.java
| ... | ... | @@ -41,4 +41,28 @@ public class ScalableDouble implements AbstractScalarValue<Double> { |
| 41 | 41 | public int compareTo(Double o) { |
| 42 | 42 | return Double.valueOf(value).compareTo(o); |
| 43 | 43 | } |
| 44 | + |
|
| 45 | + @Override |
|
| 46 | + public int hashCode() { |
|
| 47 | + final int prime = 31; |
|
| 48 | + int result = 1; |
|
| 49 | + long temp; |
|
| 50 | + temp = Double.doubleToLongBits(value); |
|
| 51 | + result = prime * result + (int) (temp ^ (temp >>> 32)); |
|
| 52 | + return result; |
|
| 53 | + } |
|
| 54 | + |
|
| 55 | + @Override |
|
| 56 | + public boolean equals(Object obj) { |
|
| 57 | + if (this == obj) |
|
| 58 | + return true; |
|
| 59 | + if (obj == null) |
|
| 60 | + return false; |
|
| 61 | + if (getClass() != obj.getClass()) |
|
| 62 | + return false; |
|
| 63 | + ScalableDouble other = (ScalableDouble) obj; |
|
| 64 | + if (Double.doubleToLongBits(value) != Double.doubleToLongBits(other.value)) |
|
| 65 | + return false; |
|
| 66 | + return true; |
|
| 67 | + } |
|
| 44 | 68 | } |