java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/CourseChangeBasedTrackApproximationTest.java
... ...
@@ -4,18 +4,13 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
4 4
import static org.junit.jupiter.api.Assertions.assertFalse;
5 5
import static org.junit.jupiter.api.Assertions.assertTrue;
6 6
7
-import java.io.File;
8
-import java.net.MalformedURLException;
9
-import java.net.URI;
10
-import java.net.URISyntaxException;
11
-import java.net.URL;
12
-import java.util.Random;
13
-
14 7
import org.junit.jupiter.api.BeforeEach;
15 8
import org.junit.jupiter.api.Test;
16 9
10
+import com.sap.sailing.domain.base.BoatClass;
17 11
import com.sap.sailing.domain.base.Competitor;
18 12
import com.sap.sailing.domain.base.CompetitorWithBoat;
13
+import com.sap.sailing.domain.base.impl.BoatClassImpl;
19 14
import com.sap.sailing.domain.common.impl.DegreePosition;
20 15
import com.sap.sailing.domain.common.impl.KnotSpeedImpl;
21 16
import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl;
... ...
@@ -24,81 +19,27 @@ import com.sap.sailing.domain.common.tracking.impl.GPSFixMovingImpl;
24 19
import com.sap.sailing.domain.tracking.DynamicGPSFixTrack;
25 20
import com.sap.sailing.domain.tracking.impl.CourseChangeBasedTrackApproximation;
26 21
import com.sap.sailing.domain.tracking.impl.DynamicGPSFixMovingTrackImpl;
27
-import com.sap.sailing.domain.tractracadapter.ReceiverType;
22
+import com.sap.sse.common.Duration;
23
+import com.sap.sse.common.Speed;
28 24
import com.sap.sse.common.TimePoint;
29 25
import com.sap.sse.common.Util;
30 26
import com.sap.sse.common.impl.DegreeBearingImpl;
31 27
import com.sap.sse.common.impl.MillisecondsDurationImpl;
32 28
import com.sap.sse.common.impl.MillisecondsTimePoint;
33 29
34
-public class CourseChangeBasedTrackApproximationTest extends OnlineTracTracBasedTest {
30
+public class CourseChangeBasedTrackApproximationTest {
35 31
private DynamicGPSFixTrack<Competitor, GPSFixMoving> track;
36 32
private CourseChangeBasedTrackApproximation approximation;
37
- private Iterable<Competitor> competitors;
38
- private CompetitorWithBoat sampleCompetitor;
39
- private DynamicGPSFixTrack<Competitor, GPSFixMoving> sampleTrack;
33
+ private final static BoatClass boatClass = new BoatClassImpl("505", /* typicallyStartsUpwind */true);
40 34
41
- public CourseChangeBasedTrackApproximationTest() throws MalformedURLException, URISyntaxException {
42
- super();
43
- }
44
-
45 35
@BeforeEach
46 36
public void setUp() throws Exception {
47
- super.setUp();
48
- URI storedUri = new URI("file:///" + new File("resources/event_20110609_KielerWoch-505_Race_2.mtb").getCanonicalPath().replace('\\', '/'));
49
- super.setUp(
50
- new URL("file:///" + new File("resources/event_20110609_KielerWoch-505_Race_2.txt").getCanonicalPath()),
51
- /* liveUri */ null, /* storedUri */ storedUri,
52
- new ReceiverType[] { ReceiverType.RACECOURSE, ReceiverType.RAWPOSITIONS });
53
- getTrackedRace().waitUntilNotLoading();
54
- assertFalse(Util.isEmpty(getTrackedRace().getRace().getCompetitors()));
55
- do {
56
- competitors = getTrackedRace().getRace().getCompetitors();
57
- sampleCompetitor = (CompetitorWithBoat) Util.get(competitors, new Random().nextInt(Util.size(competitors)));
58
- sampleTrack = getTrackedRace().getTrack(sampleCompetitor);
59
- } while (sampleTrack.isEmpty());
60 37
final CompetitorWithBoat competitor = TrackBasedTest.createCompetitorWithBoat("Someone");
61 38
track = new DynamicGPSFixMovingTrackImpl<Competitor>(competitor,
62 39
/* millisecondsOverWhichToAverage */5000, /* lossless compaction */true);
63 40
approximation = new CourseChangeBasedTrackApproximation(track, competitor.getBoat().getBoatClass());
64 41
}
65 42
66
- /**
67
- * During the work on bug5959 (https://bugzilla.sapsailing.com/bugzilla/show_bug.cgi?id=5959) we identified an issue
68
- * with early vs. late initialization of the approximation. When initializing the
69
- * {@link CourseChangeBasedTrackApproximation} objects before adding any fixes to the track, we received different
70
- * results than when initializing it after adding fixes and first trying to compute maneuvers. This goes against the
71
- * specification that the approximation should be independent of when it is initialized.<p>
72
- *
73
- * To conduct the test, we take the fixes from a loaded competitor track, create a new {@link DynamicGPSFixMovingTrackImpl},
74
- * construct a {@link CourseChangeBasedTrackApproximation} based on this track (early initialization), then copy all fixes
75
- * from the loaded competitor track to the new test track (which adds these fixes to the approximation), then create a second
76
- * {@link CourseChangeBasedTrackApproximation} which will then add all fixes in one sweep. Then, we compare the results of
77
- * {@link CourseChangeBasedTrackApproximation#approximate(TimePoint, TimePoint)} for the duration of the entire track,
78
- * expecting them to be equal.
79
- */
80
- @Test
81
- public void testNoDiffBetweenEarlyAndLateInitialization() {
82
- final DynamicGPSFixTrack<Competitor, GPSFixMoving> trackCopy = new DynamicGPSFixMovingTrackImpl<Competitor>(sampleCompetitor, /* millisecondsOverWhichToAverage */ 15000);
83
- final CourseChangeBasedTrackApproximation earlyInitApproximation = new CourseChangeBasedTrackApproximation(trackCopy, sampleCompetitor.getBoat().getBoatClass());
84
- final TimePoint from = sampleTrack.getFirstRawFix().getTimePoint();
85
- final TimePoint to = sampleTrack.getLastRawFix().getTimePoint();
86
- sampleTrack.lockForRead();
87
- try {
88
- for (final GPSFixMoving fix : sampleTrack.getRawFixes()) {
89
- trackCopy.add(fix);
90
- }
91
- } finally {
92
- sampleTrack.unlockAfterRead();
93
- }
94
- final CourseChangeBasedTrackApproximation lateInitApproximation = new CourseChangeBasedTrackApproximation(trackCopy, sampleCompetitor.getBoat().getBoatClass());
95
- assertEquals(earlyInitApproximation.getNumberOfFixesAdded(), lateInitApproximation.getNumberOfFixesAdded(), "Number of fixes added to approximators differs");
96
- final Iterable<GPSFixMoving> earlyInitResult = earlyInitApproximation.approximate(from, to);
97
- final Iterable<GPSFixMoving> lateInitResult = lateInitApproximation.approximate(from, to);
98
- assertEquals(Util.size(earlyInitResult), Util.size(lateInitResult), "Different numbers of approximation points for competitor "+sampleCompetitor.getName());
99
- assertEquals(Util.asSet(earlyInitResult), Util.asSet(lateInitResult));
100
- }
101
-
102 43
@Test
103 44
public void simpleTackRecognition() {
104 45
final GPSFixMoving start = fix(10000l, 0, 0, 5, 0);
... ...
@@ -125,6 +66,62 @@ public class CourseChangeBasedTrackApproximationTest extends OnlineTracTracBased
125 66
}
126 67
}
127 68
69
+ @Test
70
+ public void testDirectionChangeJustAboveThreshold() {
71
+ final Duration samplingInterval = Duration.ONE_SECOND;
72
+ final double aBitOverMinimumManeuverAngleDegrees = boatClass.getManeuverDegreeAngleThreshold() * 1.2;
73
+ final TimePoint start = TimePoint.of(10000l);
74
+ final Speed speed = new KnotSpeedImpl(5.0);
75
+ GPSFixMoving next = fix(start.asMillis(), 0, 0, speed.getKnots(), 0);
76
+ track.add(next);
77
+ // perform aBitOverMinimumManeuverAngleDegrees within five fixes:
78
+ final int NUMBER_OF_FIXES_FOR_MANEUVER = 5;
79
+ for (int i=0; i<NUMBER_OF_FIXES_FOR_MANEUVER; i++) {
80
+ next = travel(next, samplingInterval.asMillis(), speed.getKnots(), ((double) i+1.0)/((double) NUMBER_OF_FIXES_FOR_MANEUVER) * aBitOverMinimumManeuverAngleDegrees);
81
+ track.add(next);
82
+ }
83
+ final Iterable<GPSFixMoving> oneManeuverCandidate = approximation.approximate(start, start.plus(samplingInterval.times(NUMBER_OF_FIXES_FOR_MANEUVER)));
84
+ assertFalse(Util.isEmpty(oneManeuverCandidate));
85
+ assertEquals(1, Util.size(oneManeuverCandidate));
86
+ }
87
+
88
+ /**
89
+ * See also https://bugzilla.sapsailing.com/bugzilla/show_bug.cgi?id=6209#c5 which talks about FixWindow contents
90
+ * that describe COG changes in different directions across the FixWindow's duration. This way, a COG change just
91
+ * barely exceeding the threshold wouldn't be recognized/emitted as an approximated fix if it is preceded by a COG
92
+ * change in the other direction that does not by itself pass the threshold.
93
+ */
94
+ @Test
95
+ public void testTurningDirectionChangeInSameWindow() {
96
+ final Duration samplingInterval = Duration.ONE_SECOND;
97
+ final double halfMinimumManeuverAngleDegrees = boatClass.getManeuverDegreeAngleThreshold() / 2.0;
98
+ final TimePoint start = TimePoint.of(10000l);
99
+ final Speed speed = new KnotSpeedImpl(5.0);
100
+ double cog = 0.0;
101
+ GPSFixMoving next = fix(start.asMillis(), 0, 0, speed.getKnots(), cog);
102
+ track.add(next);
103
+ // perform halfMinimumManeuverAngleDegrees within five fixes:
104
+ final int NUMBER_OF_FIXES_FOR_NON_MANEUVER = 5;
105
+ for (int i=0; i<NUMBER_OF_FIXES_FOR_NON_MANEUVER; i++) {
106
+ cog -= 1.0/((double) NUMBER_OF_FIXES_FOR_NON_MANEUVER) * halfMinimumManeuverAngleDegrees;
107
+ next = travel(next, samplingInterval.asMillis(), speed.getKnots(), cog);
108
+ track.add(next);
109
+ }
110
+ final Iterable<GPSFixMoving> emptyManeuverCandidates = approximation.approximate(start, start.plus(samplingInterval.times(NUMBER_OF_FIXES_FOR_NON_MANEUVER)));
111
+ assertTrue(Util.isEmpty(emptyManeuverCandidates));
112
+ final double aBitOverMinimumManeuverAngleDegrees = boatClass.getManeuverDegreeAngleThreshold() * 1.2;
113
+ // perform aBitOverMinimumManeuverAngleDegrees within five fixes:
114
+ final int NUMBER_OF_FIXES_FOR_MANEUVER = 5;
115
+ for (int i=0; i<NUMBER_OF_FIXES_FOR_MANEUVER; i++) {
116
+ cog += 1.0/((double) NUMBER_OF_FIXES_FOR_MANEUVER) * aBitOverMinimumManeuverAngleDegrees;
117
+ next = travel(next, samplingInterval.asMillis(), speed.getKnots(), cog);
118
+ track.add(next);
119
+ }
120
+ final Iterable<GPSFixMoving> oneManeuverCandidate = approximation.approximate(start, start.plus(samplingInterval.times(NUMBER_OF_FIXES_FOR_NON_MANEUVER+NUMBER_OF_FIXES_FOR_MANEUVER)));
121
+ assertFalse(Util.isEmpty(oneManeuverCandidate));
122
+ assertEquals(1, Util.size(oneManeuverCandidate));
123
+ }
124
+
128 125
private GPSFixMoving fix(long timepoint, double lat, double lon, double speedInKnots, double cogDeg) {
129 126
return new GPSFixMovingImpl(new DegreePosition(lat, lon), new MillisecondsTimePoint(timepoint), new KnotSpeedWithBearingImpl(speedInKnots, new DegreeBearingImpl(cogDeg)), /* optionalTrueHeading */ null);
130 127
}
java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/CourseChangeBasedTrackApproximationWithTracTracDataTest.java
... ...
@@ -0,0 +1,87 @@
1
+package com.sap.sailing.domain.test;
2
+
3
+import static org.junit.jupiter.api.Assertions.assertEquals;
4
+import static org.junit.jupiter.api.Assertions.assertFalse;
5
+
6
+import java.io.File;
7
+import java.net.MalformedURLException;
8
+import java.net.URI;
9
+import java.net.URISyntaxException;
10
+import java.net.URL;
11
+import java.util.Random;
12
+
13
+import org.junit.jupiter.api.BeforeEach;
14
+import org.junit.jupiter.api.Test;
15
+
16
+import com.sap.sailing.domain.base.Competitor;
17
+import com.sap.sailing.domain.base.CompetitorWithBoat;
18
+import com.sap.sailing.domain.common.tracking.GPSFixMoving;
19
+import com.sap.sailing.domain.tracking.DynamicGPSFixTrack;
20
+import com.sap.sailing.domain.tracking.impl.CourseChangeBasedTrackApproximation;
21
+import com.sap.sailing.domain.tracking.impl.DynamicGPSFixMovingTrackImpl;
22
+import com.sap.sailing.domain.tractracadapter.ReceiverType;
23
+import com.sap.sse.common.TimePoint;
24
+import com.sap.sse.common.Util;
25
+
26
+public class CourseChangeBasedTrackApproximationWithTracTracDataTest extends OnlineTracTracBasedTest {
27
+ private Iterable<Competitor> competitors;
28
+ private CompetitorWithBoat sampleCompetitor;
29
+ private DynamicGPSFixTrack<Competitor, GPSFixMoving> sampleTrack;
30
+
31
+ public CourseChangeBasedTrackApproximationWithTracTracDataTest() throws MalformedURLException, URISyntaxException {
32
+ super();
33
+ }
34
+
35
+ @BeforeEach
36
+ public void setUp() throws Exception {
37
+ super.setUp();
38
+ URI storedUri = new URI("file:///" + new File("resources/event_20110609_KielerWoch-505_Race_2.mtb").getCanonicalPath().replace('\\', '/'));
39
+ super.setUp(
40
+ new URL("file:///" + new File("resources/event_20110609_KielerWoch-505_Race_2.txt").getCanonicalPath()),
41
+ /* liveUri */ null, /* storedUri */ storedUri,
42
+ new ReceiverType[] { ReceiverType.RACECOURSE, ReceiverType.RAWPOSITIONS });
43
+ getTrackedRace().waitUntilNotLoading();
44
+ assertFalse(Util.isEmpty(getTrackedRace().getRace().getCompetitors()));
45
+ do {
46
+ competitors = getTrackedRace().getRace().getCompetitors();
47
+ sampleCompetitor = (CompetitorWithBoat) Util.get(competitors, new Random().nextInt(Util.size(competitors)));
48
+ sampleTrack = getTrackedRace().getTrack(sampleCompetitor);
49
+ } while (sampleTrack.isEmpty());
50
+ }
51
+
52
+ /**
53
+ * During the work on bug5959 (https://bugzilla.sapsailing.com/bugzilla/show_bug.cgi?id=5959) we identified an issue
54
+ * with early vs. late initialization of the approximation. When initializing the
55
+ * {@link CourseChangeBasedTrackApproximation} objects before adding any fixes to the track, we received different
56
+ * results than when initializing it after adding fixes and first trying to compute maneuvers. This goes against the
57
+ * specification that the approximation should be independent of when it is initialized.<p>
58
+ *
59
+ * To conduct the test, we take the fixes from a loaded competitor track, create a new {@link DynamicGPSFixMovingTrackImpl},
60
+ * construct a {@link CourseChangeBasedTrackApproximation} based on this track (early initialization), then copy all fixes
61
+ * from the loaded competitor track to the new test track (which adds these fixes to the approximation), then create a second
62
+ * {@link CourseChangeBasedTrackApproximation} which will then add all fixes in one sweep. Then, we compare the results of
63
+ * {@link CourseChangeBasedTrackApproximation#approximate(TimePoint, TimePoint)} for the duration of the entire track,
64
+ * expecting them to be equal.
65
+ */
66
+ @Test
67
+ public void testNoDiffBetweenEarlyAndLateInitialization() {
68
+ final DynamicGPSFixTrack<Competitor, GPSFixMoving> trackCopy = new DynamicGPSFixMovingTrackImpl<Competitor>(sampleCompetitor, /* millisecondsOverWhichToAverage */ 15000);
69
+ final CourseChangeBasedTrackApproximation earlyInitApproximation = new CourseChangeBasedTrackApproximation(trackCopy, sampleCompetitor.getBoat().getBoatClass());
70
+ final TimePoint from = sampleTrack.getFirstRawFix().getTimePoint();
71
+ final TimePoint to = sampleTrack.getLastRawFix().getTimePoint();
72
+ sampleTrack.lockForRead();
73
+ try {
74
+ for (final GPSFixMoving fix : sampleTrack.getRawFixes()) {
75
+ trackCopy.add(fix);
76
+ }
77
+ } finally {
78
+ sampleTrack.unlockAfterRead();
79
+ }
80
+ final CourseChangeBasedTrackApproximation lateInitApproximation = new CourseChangeBasedTrackApproximation(trackCopy, sampleCompetitor.getBoat().getBoatClass());
81
+ assertEquals(earlyInitApproximation.getNumberOfFixesAdded(), lateInitApproximation.getNumberOfFixesAdded(), "Number of fixes added to approximators differs");
82
+ final Iterable<GPSFixMoving> earlyInitResult = earlyInitApproximation.approximate(from, to);
83
+ final Iterable<GPSFixMoving> lateInitResult = lateInitApproximation.approximate(from, to);
84
+ assertEquals(Util.size(earlyInitResult), Util.size(lateInitResult), "Different numbers of approximation points for competitor "+sampleCompetitor.getName());
85
+ assertEquals(Util.asSet(earlyInitResult), Util.asSet(lateInitResult));
86
+ }
87
+}
java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/CourseChangeBasedTrackApproximation.java
... ...
@@ -14,6 +14,7 @@ import java.util.TreeSet;
14 14
15 15
import com.sap.sailing.domain.base.BoatClass;
16 16
import com.sap.sailing.domain.base.Competitor;
17
+import com.sap.sailing.domain.common.NauticalSide;
17 18
import com.sap.sailing.domain.common.SpeedWithBearing;
18 19
import com.sap.sailing.domain.common.tracking.GPSFixMoving;
19 20
import com.sap.sailing.domain.common.tracking.impl.GPSFixMovingImpl;
... ...
@@ -28,8 +29,25 @@ import com.sap.sse.common.impl.TimeRangeImpl;
28 29
29 30
/**
30 31
* Given a {@link GPSFixTrack} containing {@link GPSFixMoving}, an instance of this class finds areas on the track where
31
- * the course changes sufficiently quickly to indicate a relevant maneuver. The basis for this is the
32
- * {@link BoatClass}'s {@link BoatClass#getApproximateManeuverDuration() maneuver duration}.
32
+ * the course changes sufficiently quickly and far to indicate a relevant maneuver. The basis for the duration to
33
+ * consider is the {@link BoatClass}'s {@link BoatClass#getApproximateManeuverDuration() maneuver duration}. The basis
34
+ * for the angular threshold is {@link BoatClass#getManeuverDegreeAngleThreshold()}.
35
+ * <p>
36
+ *
37
+ * Analyzing the track works with a {@link FixWindow analysis window} that contains up to a certain duration worth of
38
+ * GPS fixes, keeping track of their respective {@link SpeedWithBearing} COG/SOG vectors as well as the cumulative
39
+ * course change counted from the beginning of the window up to each of the fixes stored in the window. Course changes
40
+ * within the window are all in the same {@link NauticalSide direction} (so either all to {@link NauticalSide#PORT PORT}
41
+ * or all to {@link NauticalSide#STARBOARD STARBOARD}). When a fix addition leads to a maximum cumulative course change
42
+ * exceeding the threshold, a maneuver candidate is added to {@link #maneuverCandidates}, and the fixes from the start
43
+ * of the window up to the fix where the
44
+ * <p>
45
+ *
46
+ * When a fix is added to the window that at the insertion point leads to a change in COG change direction, all fixes
47
+ * from the beginning of the window up to the fix preceding the one added are removed, so the window now starts with a
48
+ * course change in the other direction. If the fix insert position was not at the end of the window, the course change
49
+ * direction from the fix added to the next fix is also checked for consistency with the first course change direction
50
+ * of the window.
33 51
* <p>
34 52
*
35 53
* Upon construction, all maneuver candidates for all fixes already contained by the {@link GPSFixTrack} are determined.
... ...
@@ -197,6 +215,7 @@ public class CourseChangeBasedTrackApproximation implements Serializable, GPSTra
197 215
absoluteMaximumTotalCourseChangeFromBeginningOfWindowInDegrees = Math.abs(totalCourseChangeFromBeginningOfWindowForCurrentFix);
198 216
indexOfMaximumTotalCourseChange = insertPosition-1;
199 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
200 219
}
201 220
if (windowDuration.compareTo(getMaximumWindowLength()) > 0) {
202 221
result = tryToExtractManeuverCandidate();