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
}