d6126636a0026b897a0f9302c0fc23ead04b75e2
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()) { |