java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/CourseChangeBasedTrackApproximationTest.java
... ...
@@ -37,7 +37,7 @@ public class CourseChangeBasedTrackApproximationTest {
37 37
final CompetitorWithBoat competitor = TrackBasedTest.createCompetitorWithBoat("Someone");
38 38
track = new DynamicGPSFixMovingTrackImpl<Competitor>(competitor,
39 39
/* millisecondsOverWhichToAverage */5000, /* lossless compaction */true);
40
- approximation = new CourseChangeBasedTrackApproximation(track, competitor.getBoat().getBoatClass());
40
+ approximation = new CourseChangeBasedTrackApproximation(track, competitor.getBoat().getBoatClass(), /* logFixes */ false);
41 41
}
42 42
43 43
@Test
java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/CourseChangeBasedTrackApproximationWithTracTracDataTest.java
... ...
@@ -8,6 +8,7 @@ import java.net.MalformedURLException;
8 8
import java.net.URI;
9 9
import java.net.URISyntaxException;
10 10
import java.net.URL;
11
+import java.util.Iterator;
11 12
import java.util.Random;
12 13
13 14
import org.junit.jupiter.api.BeforeEach;
... ...
@@ -44,7 +45,10 @@ public class CourseChangeBasedTrackApproximationWithTracTracDataTest extends Onl
44 45
assertFalse(Util.isEmpty(getTrackedRace().getRace().getCompetitors()));
45 46
do {
46 47
competitors = getTrackedRace().getRace().getCompetitors();
47
- sampleCompetitor = (CompetitorWithBoat) Util.get(competitors, new Random().nextInt(Util.size(competitors)));
48
+ // To pick a single competitor, e.g., for debugging, use the following line:
49
+ sampleCompetitor = (CompetitorWithBoat) Util.first(Util.filter(competitors, c->c.getName().equals("Dasenbrook")));
50
+ // To pick a random competitor, use the following line:
51
+// sampleCompetitor = (CompetitorWithBoat) Util.get(competitors, new Random().nextInt(Util.size(competitors)));
48 52
sampleTrack = getTrackedRace().getTrack(sampleCompetitor);
49 53
} while (sampleTrack.isEmpty());
50 54
}
... ...
@@ -68,7 +72,7 @@ public class CourseChangeBasedTrackApproximationWithTracTracDataTest extends Onl
68 72
final DynamicGPSFixTrack<Competitor, GPSFixMoving> trackCopy = new DynamicGPSFixMovingTrackImpl<Competitor>(
69 73
sampleCompetitor,
70 74
/* millisecondsOverWhichToAverage */ boatClass.getApproximateManeuverDurationInMilliseconds());
71
- final CourseChangeBasedTrackApproximation earlyInitApproximation = new CourseChangeBasedTrackApproximation(trackCopy, sampleCompetitor.getBoat().getBoatClass());
75
+ final CourseChangeBasedTrackApproximation earlyInitApproximation = new CourseChangeBasedTrackApproximation(trackCopy, sampleCompetitor.getBoat().getBoatClass(), /* logFixes */ true);
72 76
final TimePoint from = sampleTrack.getFirstRawFix().getTimePoint();
73 77
final TimePoint to = sampleTrack.getLastRawFix().getTimePoint();
74 78
sampleTrack.lockForRead();
... ...
@@ -79,11 +83,20 @@ public class CourseChangeBasedTrackApproximationWithTracTracDataTest extends Onl
79 83
} finally {
80 84
sampleTrack.unlockAfterRead();
81 85
}
82
- final CourseChangeBasedTrackApproximation lateInitApproximation = new CourseChangeBasedTrackApproximation(trackCopy, sampleCompetitor.getBoat().getBoatClass());
86
+ final CourseChangeBasedTrackApproximation lateInitApproximation = new CourseChangeBasedTrackApproximation(trackCopy, sampleCompetitor.getBoat().getBoatClass(), /* logFixes */ true);
83 87
assertEquals(earlyInitApproximation.getNumberOfFixesAdded(), lateInitApproximation.getNumberOfFixesAdded(), "Number of fixes added to approximators differs");
84 88
final Iterable<GPSFixMoving> earlyInitResult = earlyInitApproximation.approximate(from, to);
85 89
final Iterable<GPSFixMoving> lateInitResult = lateInitApproximation.approximate(from, to);
86 90
assertEquals(Util.size(earlyInitResult), Util.size(lateInitResult), "Different numbers of approximation points for competitor "+sampleCompetitor.getName());
91
+ final Iterator<GPSFixMoving> earlyIter = earlyInitResult.iterator();
92
+ final Iterator<GPSFixMoving> lateIter = lateInitResult.iterator();
93
+ int i=0;
94
+ while (earlyIter.hasNext() && lateIter.hasNext()) {
95
+ final GPSFixMoving earlyFix = earlyIter.next();
96
+ final GPSFixMoving lateFix = lateIter.next();
97
+ assertEquals(earlyFix.getTimePoint(), lateFix.getTimePoint(), "Time points of approximation fixes differ at index "+i+" for competitor "+sampleCompetitor.getName());
98
+ i++;
99
+ }
87 100
assertEquals(Util.asSet(earlyInitResult), Util.asSet(lateInitResult));
88 101
}
89 102
}
java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/TrackTest.java
... ...
@@ -357,7 +357,7 @@ public class TrackTest {
357 357
GPSFix fix4 = new GPSFixImpl(new DegreePosition(4./60., 0), new MillisecondsTimePoint(10800000)); // 1nm in one hour = 1kt
358 358
track.addGPSFix(fix4);
359 359
assertEquals(1., track.getMaximumSpeedOverGround(new MillisecondsTimePoint(0), new MillisecondsTimePoint(3600000)).
360
- getB().getKnots(), 0.001);
360
+ getB().getKnots(), 0.01);
361 361
}
362 362
363 363
@Test
... ...
@@ -372,7 +372,7 @@ public class TrackTest {
372 372
GPSFix fix4 = new GPSFixImpl(new DegreePosition(4./60., 0), new MillisecondsTimePoint(10800000)); // 1nm in one hour = 1kt
373 373
track.addGPSFix(fix4);
374 374
assertEquals(2., track.getMaximumSpeedOverGround(new MillisecondsTimePoint(0), new MillisecondsTimePoint(10800000)).
375
- getB().getKnots(), 0.001);
375
+ getB().getKnots(), 0.01);
376 376
}
377 377
378 378
@Test
java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/CourseChangeBasedTrackApproximation.java
... ...
@@ -135,7 +135,10 @@ public class CourseChangeBasedTrackApproximation implements Serializable, GPSTra
135 135
private final double maneuverAngleInDegreesThreshold;
136 136
private Duration windowDuration;
137 137
138
- FixWindow() {
138
+ private final boolean logFixes;
139
+
140
+ FixWindow(boolean logFixes) {
141
+ this.logFixes = logFixes;
139 142
this.window = new LinkedList<>();
140 143
this.queueOfNewFixes = new LinkedList<>();
141 144
this.speedForFixesInWindow = new LinkedList<>();
... ...
@@ -237,11 +240,21 @@ public class CourseChangeBasedTrackApproximation implements Serializable, GPSTra
237 240
}
238 241
return result;
239 242
}
240
-
243
+
241 244
private GPSFixMoving addOldEnoughFix(GPSFixMoving next) {
242 245
assert window.isEmpty() || !next.getTimePoint().before(window.peekFirst().getTimePoint());
243 246
final GPSFixMoving result;
247
+ final boolean validityCached = next.isValidityCached();
248
+ final boolean validity = validityCached ? next.isValidCached() : track.isValid(next);
244 249
final SpeedWithBearing nextSpeed = next.isEstimatedSpeedCached() ? next.getCachedEstimatedSpeed() : track.getEstimatedSpeed(next.getTimePoint());
250
+ if (logFixes) {
251
+ // CSV logging: approxId, fixIndex, fixTimeMillis, validityCached, speedCached, COG, SOG
252
+ System.out.println(System.identityHashCode(this) + "," + next.getTimePoint().asMillis() + ","
253
+ + next.isValidityCached() + "," + next.isEstimatedSpeedCached() + ","
254
+ + (nextSpeed == null ? "null" : nextSpeed.getBearing().getDegrees()) + ","
255
+ + (nextSpeed == null ? "null" : nextSpeed.getKnots()) + ","
256
+ + validityCached + "," + validity);
257
+ }
245 258
if (nextSpeed != null) {
246 259
numberOfFixesAdded++;
247 260
int insertPosition = window.size();
... ...
@@ -398,15 +411,15 @@ public class CourseChangeBasedTrackApproximation implements Serializable, GPSTra
398 411
}
399 412
}
400 413
401
- public CourseChangeBasedTrackApproximation(GPSFixTrack<Competitor, GPSFixMoving> track, BoatClass boatClass) {
414
+ public CourseChangeBasedTrackApproximation(GPSFixTrack<Competitor, GPSFixMoving> track, BoatClass boatClass, boolean logFixes) {
402 415
this.track = track;
403 416
this.boatClass = boatClass;
404
- this.fixWindow = new FixWindow();
417
+ this.fixWindow = new FixWindow(logFixes);
405 418
this.maneuverCandidates = new TreeSet<>(TimedComparator.INSTANCE);
406 419
track.addListener(this);
407 420
addAllFixesOfTrack();
408 421
}
409
-
422
+
410 423
/**
411 424
* Defined only in order to make it {@code synchronized} so that data will be written to the output stream
412 425
* consistently.
... ...
@@ -459,7 +472,7 @@ public class CourseChangeBasedTrackApproximation implements Serializable, GPSTra
459 472
if (fixWindow.isAtOrAfterFirst(fix.getTimePoint())) {
460 473
addFix(fix);
461 474
} else {
462
- final FixWindow outOfOrderWindow = new FixWindow();
475
+ final FixWindow outOfOrderWindow = new FixWindow(/* logFixes */ false);
463 476
final Duration maximumWindowLength = outOfOrderWindow.getMaximumWindowLength();
464 477
// fix is an out-of-order delivery; construct a new FixWindow and analyze the track around the new fix.
465 478
// A time range around the fix is constructed that will be re-scanned. The time range covers at least
java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/GPSFixTrackImpl.java
... ...
@@ -631,11 +631,15 @@ public abstract class GPSFixTrackImpl<ItemType, FixType extends GPSFix> extends
631 631
ConfidenceFactory.INSTANCE.createExponentialTimeDifferenceWeigher(
632 632
// use a minimum confidence to avoid the bearing to flip to 270deg in case all is zero
633 633
getMillisecondsOverWhichToAverageSpeed() / 2, /* minimumConfidence */ 0.00000001)); // half confidence if half averaging interval apart
634
- result = estimatedSpeed == null ? null : estimatedSpeed.getObject();
635 634
if (estimatedSpeed != null) {
636 635
if (ceil != null && ceil.getTimePoint().equals(at)) {
637
- ceil.cacheEstimatedSpeed(result);
636
+ ceil.cacheEstimatedSpeed(estimatedSpeed.getObject());
637
+ result = ceil.getCachedEstimatedSpeed(); // this way, should the fix apply compaction, we will still return consistent (compacted) results
638
+ } else {
639
+ result = estimatedSpeed.getObject();
638 640
}
641
+ } else {
642
+ result = null;
639 643
}
640 644
}
641 645
if (logger.isLoggable(Level.FINEST) && (estimatedSpeedCacheHits + estimatedSpeedCacheMisses) % 1000 == 0) {
... ...
@@ -1022,6 +1026,11 @@ public abstract class GPSFixTrackImpl<ItemType, FixType extends GPSFix> extends
1022 1026
* point of the <code>gpsFix</code> "upwards." However, if the adjacent earlier fixes have changed their validity by
1023 1027
* the addition of <code>gpsFix</code>, the distance cache must be invalidated starting with the first fix whose
1024 1028
* validity changed.
1029
+ * <p>
1030
+ *
1031
+ * When a fix's validity changes, the set of fixes returned by {@link #getInternalFixes()} changes. Since
1032
+ * {@link #getEstimatedSpeed(TimePoint)} uses {@link #getInternalFixes()} for its calculation, the estimated
1033
+ * speed caches of fixes whose speed calculation might be affected need to be invalidated as well.
1025 1034
*/
1026 1035
private void invalidateValidityAndEstimatedSpeedAndDistanceCaches(FixType gpsFix) {
1027 1036
assertWriteLock();
... ...
@@ -1047,14 +1056,42 @@ public abstract class GPSFixTrackImpl<ItemType, FixType extends GPSFix> extends
1047 1056
boolean lowerWasValid = isValid(getRawFixes(), lower);
1048 1057
lower.invalidateCache();
1049 1058
boolean lowerIsValid = isValid(getRawFixes(), lower);
1050
- if (lowerIsValid != lowerWasValid && lower.getTimePoint().before(distanceCacheInvalidationStart)) {
1051
- distanceCacheInvalidationStart = lower.getTimePoint();
1059
+ if (lowerIsValid != lowerWasValid) {
1060
+ if (lower.getTimePoint().before(distanceCacheInvalidationStart)) {
1061
+ distanceCacheInvalidationStart = lower.getTimePoint();
1062
+ }
1063
+ invalidateEstimatedSpeedCachesAffectedByValidityChange(lower);
1052 1064
}
1053 1065
}
1054 1066
getDistanceCache().invalidateAllAtOrLaterThan(distanceCacheInvalidationStart);
1055 1067
Iterable<FixType> highers = getLaterFixesWhoseValidityMayBeAffected(gpsFix);
1056 1068
for (FixType higher : highers) {
1069
+ boolean higherWasValid = isValid(getRawFixes(), higher);
1057 1070
higher.invalidateCache();
1071
+ boolean higherIsValid = isValid(getRawFixes(), higher);
1072
+ if (higherIsValid != higherWasValid) {
1073
+ invalidateEstimatedSpeedCachesAffectedByValidityChange(higher);
1074
+ }
1075
+ }
1076
+ }
1077
+
1078
+ /**
1079
+ * When the validity of {@code fixWithChangedValidity} changes, the set of fixes returned by
1080
+ * {@link #getInternalFixes()} changes. Since {@link #getEstimatedSpeed(TimePoint)} uses
1081
+ * {@link #getInternalFixes()} for its calculation, this method invalidates the estimated speed caches
1082
+ * of all fixes whose speed estimation might be affected by this change.
1083
+ * <p>
1084
+ *
1085
+ * The affected fixes are those within the time interval returned by
1086
+ * {@link #getTimeIntervalWhoseEstimatedSpeedMayHaveChangedAfterAddingFix(GPSFix)}, since a validity
1087
+ * change has the same effect on speed estimation as adding or removing a fix.
1088
+ */
1089
+ private void invalidateEstimatedSpeedCachesAffectedByValidityChange(FixType fixWithChangedValidity) {
1090
+ TimeRange affectedInterval = getTimeIntervalWhoseEstimatedSpeedMayHaveChangedAfterAddingFix(fixWithChangedValidity);
1091
+ for (FixType affectedFix : getInternalRawFixes().subSet(
1092
+ createDummyGPSFix(affectedInterval.from()), true,
1093
+ createDummyGPSFix(affectedInterval.to()), true)) {
1094
+ affectedFix.invalidateEstimatedSpeedCache();
1058 1095
}
1059 1096
}
1060 1097
java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java
... ...
@@ -590,6 +590,7 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl
590 590
markPassingsForCompetitor.put(competitor, new ConcurrentSkipListSet<MarkPassing>(MarkPassingByTimeComparator.INSTANCE));
591 591
final DynamicGPSFixMovingTrackImpl<Competitor> track = new DynamicGPSFixMovingTrackImpl<Competitor>(competitor, millisecondsOverWhichToAverageSpeed);
592 592
tracks.put(competitor, track);
593
+ maneuverApproximators.put(competitor, new CourseChangeBasedTrackApproximation(track, race.getBoatOfCompetitor(competitor).getBoatClass(), /* logFixes */ false));
593 594
}
594 595
markPassingsForWaypoint = new ConcurrentHashMap<Waypoint, NavigableSet<MarkPassing>>();
595 596
for (Waypoint waypoint : race.getCourse().getWaypoints()) {