Merge branch 'master' into bug3742

This commit is contained in:
Axel Uhl committed 2017-11-21 16:26:40 +01:00
commit 9b2ae09a11
41 files changed
+1008 -496

No files matched your search

@@ -56,7 +56,7 @@ public class BravoFixTrackWithContext implements HasBravoFixTrackContext {
end = endOfRace;
}
return bravoFixTrack.getDistanceSpentFoiling(
getRaceOfCompetitorContext().getTrackedRaceContext().getTrackedRace().getTrack(getRaceOfCompetitorContext().getCompetitor()),
startOfRace, end);
startOfRace,
end);
}
}
@@ -96,7 +96,7 @@ public class TrackedLegOfCompetitorWithContext implements HasTrackedLegOfCompeti
public Distance getDistanceSpentFoiling() {
return getSomethingForLegTrackingInterval((start, end) -> {
BravoFixTrack<Competitor> bravoFixTrack = getTrackedLegContext().getTrackedLeg().getTrackedRace().getSensorTrack(getCompetitor(), BravoFixTrack.TRACK_NAME);
return bravoFixTrack == null ? Distance.NULL : bravoFixTrack.getDistanceSpentFoiling(getTrackedLegContext().getTrackedLeg().getTrackedRace().getTrack(getCompetitor()), start, end);
return bravoFixTrack == null ? Distance.NULL : bravoFixTrack.getDistanceSpentFoiling(start, end);
});
}
@@ -25,6 +25,8 @@ public enum DetailType implements Serializable {
CURRENT_HEEL_IN_DEGREES(2,DESCENDING),
CURRENT_PITCH_IN_DEGREES(2,DESCENDING),
CURRENT_RIDE_HEIGHT_IN_METERS(2,DESCENDING),
CURRENT_DISTANCE_FOILED_IN_METERS(0, DESCENDING),
CURRENT_DURATION_FOILED_IN_SECONDS(0, DESCENDING),
CURRENT_PORT_DAGGERBOARD_RAKE(2,DESCENDING),
CURRENT_STBD_DAGGERBOARD_RAKE(2,DESCENDING),
CURRENT_PORT_RUDDER_RAKE(2,DESCENDING),
@@ -64,8 +66,10 @@ public enum DetailType implements Serializable {
MAXIMUM_SPEED_OVER_GROUND_IN_KNOTS(1, DESCENDING),
TIME_ON_TIME_FACTOR(4, DESCENDING), TIME_ON_DISTANCE_ALLOWANCE_IN_SECONDS_PER_NAUTICAL_MILE(0, ASCENDING),
TOTAL_DISTANCE_TRAVELED(0, ASCENDING), TOTAL_AVERAGE_SPEED_OVER_GROUND(2, DESCENDING),
TOTAL_TIME_SAILED_IN_SECONDS(1, ASCENDING), RACE_CURRENT_SPEED_OVER_GROUND_IN_KNOTS(2,
DESCENDING), RACE_CURRENT_RIDE_HEIGHT_IN_METERS(2, DESCENDING),
TOTAL_TIME_SAILED_IN_SECONDS(1, ASCENDING),
TOTAL_DURATION_FOILED_IN_SECONDS(0, DESCENDING), TOTAL_DISTANCE_FOILED_IN_METERS(0, DESCENDING),
RACE_CURRENT_SPEED_OVER_GROUND_IN_KNOTS(2, DESCENDING), RACE_CURRENT_RIDE_HEIGHT_IN_METERS(2, DESCENDING),
RACE_CURRENT_DISTANCE_FOILED_IN_METERS(0, DESCENDING), RACE_CURRENT_DURATION_FOILED_IN_SECONDS(0, DESCENDING),
RACE_NET_POINTS(2, ASCENDING), REGATTA_NET_POINTS(2, ASCENDING), REGATTA_NET_POINTS_SUM(2, ASCENDING),
RACE_RATIO_BETWEEN_TIME_SINCE_LAST_POSITION_FIX_AND_AVERAGE_SAMPLING_INTERVAL(1, ASCENDING), RACE_DISTANCE_TO_START_FIVE_SECONDS_BEFORE_RACE_START(1, ASCENDING),
RACE_SPEED_OVER_GROUND_FIVE_SECONDS_BEFORE_START(2, DESCENDING),
@@ -138,7 +138,7 @@ public class LeaderboardEntryDTO implements Serializable {
return totalPointsCorrected || (reasonForMaxPoints != null && reasonForMaxPoints != MaxPointsReason.NONE);
}
public Duration getTimeSailedInMillis() {
public Duration getTimeSailed() {
final Duration result;
if (legDetails != null) {
long timeInMilliseconds = 0;
@@ -159,7 +159,7 @@ public class LeaderboardEntryDTO implements Serializable {
return result;
}
public Distance getDistanceTraveledInMeters() {
public Distance getDistanceTraveled() {
Distance result = null;
if (legDetails != null) {
for (LegEntryDTO legDetail : legDetails) {
@@ -176,6 +176,47 @@ public class LeaderboardEntryDTO implements Serializable {
return result;
}
public Duration getDurationFoiled() {
final Duration result;
if (legDetails != null) {
Double acc = null;
for (LegEntryDTO legDetail : legDetails) {
if (legDetail != null) {
if (legDetail.currentDurationFoiledInSeconds != null) {
if (acc == null) {
acc = 0.0;
}
acc += legDetail.currentDurationFoiledInSeconds;
} else {
acc = null;
break;
}
}
}
result = acc == null ? null : new MillisecondsDurationImpl((long) (acc*1000.));
} else {
result = null;
}
return result;
}
public Distance getDistanceFoiled() {
Distance result = null;
if (legDetails != null) {
for (LegEntryDTO legDetail : legDetails) {
if (legDetail != null) {
if (legDetail.currentDistanceFoiledInMeters != null) {
if (result == null) {
result = Distance.NULL;
}
result = result.add(new MeterDistance(legDetail.currentDistanceFoiledInMeters));
}
}
}
}
return result;
}
public int getOneBasedCurrentLegNumber() {
int result = 0;
if (legDetails != null && !legDetails.isEmpty()) {
@@ -26,6 +26,8 @@ public class LeaderboardRowDTO implements Serializable {
public Double totalTimeSailedUpwindInSeconds;
public Double totalTimeSailedReachingInSeconds;
public Double totalDistanceTraveledInMeters;
public Double totalDurationFoiledInSeconds;
public Double totalDistanceFoiledInMeters;
@Override
public int hashCode() {
@@ -38,6 +40,10 @@ public class LeaderboardRowDTO implements Serializable {
+ ((maximumSpeedOverGroundInKnots == null) ? 0 : maximumSpeedOverGroundInKnots.hashCode());
result = prime * result
+ ((totalDistanceTraveledInMeters == null) ? 0 : totalDistanceTraveledInMeters.hashCode());
result = prime * result
+ ((totalDurationFoiledInSeconds == null) ? 0 : totalDurationFoiledInSeconds.hashCode());
result = prime * result
+ ((totalDistanceFoiledInMeters == null) ? 0 : totalDistanceFoiledInMeters.hashCode());
result = prime * result + ((netPoints == null) ? 0 : netPoints.hashCode());
result = prime * result
+ ((totalTimeSailedDownwindInSeconds == null) ? 0 : totalTimeSailedDownwindInSeconds.hashCode());
@@ -87,6 +93,16 @@ public class LeaderboardRowDTO implements Serializable {
return false;
} else if (!totalDistanceTraveledInMeters.equals(other.totalDistanceTraveledInMeters))
return false;
if (totalDurationFoiledInSeconds == null) {
if (other.totalDurationFoiledInSeconds != null)
return false;
} else if (!totalDurationFoiledInSeconds.equals(other.totalDurationFoiledInSeconds))
return false;
if (totalDistanceFoiledInMeters == null) {
if (other.totalDistanceFoiledInMeters != null)
return false;
} else if (!totalDistanceFoiledInMeters.equals(other.totalDistanceFoiledInMeters))
return false;
if (netPoints == null) {
if (other.netPoints != null)
return false;
@@ -120,11 +136,11 @@ public class LeaderboardRowDTO implements Serializable {
return true;
}
public Distance getDistanceTraveledInMeters(String raceColumnName) {
public Distance getDistanceTraveled(String raceColumnName) {
final Distance result;
LeaderboardEntryDTO fieldsForRace = fieldsByRaceColumnName.get(raceColumnName);
if (fieldsForRace != null) {
result = fieldsForRace.getDistanceTraveledInMeters();
result = fieldsForRace.getDistanceTraveled();
} else {
result = null;
}
@@ -135,7 +151,29 @@ public class LeaderboardRowDTO implements Serializable {
final Duration result;
LeaderboardEntryDTO fieldsForRace = fieldsByRaceColumnName.get(raceColumnName);
if (fieldsForRace != null) {
result = fieldsForRace.getTimeSailedInMillis();
result = fieldsForRace.getTimeSailed();
} else {
result = null;
}
return result;
}
public Distance getDistanceFoiled(String raceColumnName) {
final Distance result;
LeaderboardEntryDTO fieldsForRace = fieldsByRaceColumnName.get(raceColumnName);
if (fieldsForRace != null) {
result = fieldsForRace.getDistanceFoiled();
} else {
result = null;
}
return result;
}
public Duration getDurationFoiled(String raceColumnName) {
final Duration result;
LeaderboardEntryDTO fieldsForRace = fieldsByRaceColumnName.get(raceColumnName);
if (fieldsForRace != null) {
result = fieldsForRace.getDurationFoiled();
} else {
result = null;
}
@@ -44,6 +44,8 @@ public class LegEntryDTO implements Serializable {
public Double currentHeelInDegrees;
public Double currentPitchInDegrees;
public Double currentDistanceFoiledInMeters;
public Double currentDurationFoiledInSeconds;
/**
* If the competitor has already finished the leg at the time point for which this entry was created, contains the
@@ -81,6 +83,8 @@ public class LegEntryDTO implements Serializable {
result = prime * result + ((currentHeelInDegrees == null) ? 0 : currentHeelInDegrees.hashCode());
result = prime * result + ((currentPitchInDegrees == null) ? 0 : currentPitchInDegrees.hashCode());
result = prime * result + ((currentRideHeightInMeters == null) ? 0 : currentRideHeightInMeters.hashCode());
result = prime * result + ((currentDurationFoiledInSeconds == null) ? 0 : currentDurationFoiledInSeconds.hashCode());
result = prime * result + ((currentDistanceFoiledInMeters == null) ? 0 : currentDistanceFoiledInMeters.hashCode());
result = prime * result
+ ((currentSpeedOverGroundInKnots == null) ? 0 : currentSpeedOverGroundInKnots.hashCode());
result = prime * result + ((distanceTraveledInMeters == null) ? 0 : distanceTraveledInMeters.hashCode());
@@ -150,6 +154,16 @@ public class LegEntryDTO implements Serializable {
return false;
} else if (!currentRideHeightInMeters.equals(other.currentRideHeightInMeters))
return false;
if (currentDurationFoiledInSeconds == null) {
if (other.currentDurationFoiledInSeconds != null)
return false;
} else if (!currentDurationFoiledInSeconds.equals(other.currentDurationFoiledInSeconds))
return false;
if (currentDistanceFoiledInMeters == null) {
if (other.currentDistanceFoiledInMeters != null)
return false;
} else if (!currentDistanceFoiledInMeters.equals(other.currentDistanceFoiledInMeters))
return false;
if (currentSpeedOverGroundInKnots == null) {
if (other.currentSpeedOverGroundInKnots != null)
return false;
@@ -56,6 +56,8 @@ import com.sap.sailing.domain.base.impl.RegattaImpl;
import com.sap.sailing.domain.base.impl.WaypointImpl;
import com.sap.sailing.domain.common.DeviceIdentifier;
import com.sap.sailing.domain.common.Distance;
import com.sap.sailing.domain.common.Position;
import com.sap.sailing.domain.common.SpeedWithBearing;
import com.sap.sailing.domain.common.TrackedRaceStatusEnum;
import com.sap.sailing.domain.common.impl.DegreeBearingImpl;
import com.sap.sailing.domain.common.impl.DegreePosition;
@@ -81,6 +83,7 @@ import com.sap.sailing.domain.racelogtracking.impl.fixtracker.FixLoaderAndTracke
import com.sap.sailing.domain.ranking.OneDesignRankingMetric;
import com.sap.sailing.domain.regattalog.impl.EmptyRegattaLogStore;
import com.sap.sailing.domain.tracking.BravoFixTrack;
import com.sap.sailing.domain.tracking.DynamicGPSFixTrack;
import com.sap.sailing.domain.tracking.DynamicSensorFixTrack;
import com.sap.sailing.domain.tracking.DynamicTrack;
import com.sap.sailing.domain.tracking.DynamicTrackedRace;
@@ -92,6 +95,7 @@ import com.sap.sailing.domain.tracking.impl.DynamicTrackedRaceImpl;
import com.sap.sailing.domain.tracking.impl.DynamicTrackedRegattaImpl;
import com.sap.sailing.domain.tracking.impl.EmptyWindStore;
import com.sap.sailing.domain.tracking.impl.SensorFixTrackImpl;
import com.sap.sse.common.Duration;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.Timed;
import com.sap.sse.common.Util.Pair;
@@ -178,18 +182,12 @@ public class SensorFixStoreAndLoadTest {
public void testLoadAlreadyAddedFixes() throws InterruptedException {
regattaLog.add(new RegattaLogDeviceCompetitorBravoMappingEventImpl(new MillisecondsTimePoint(3), author, comp,
device, new MillisecondsTimePoint(START_OF_TRACKING), new MillisecondsTimePoint(MID_OF_TRACKING)));
addBravoFixes();
FixLoaderAndTracker fixLoaderAndTracker = createFixLoaderAndTracker();
trackedRace.attachRaceLog(raceLog);
trackedRace.attachRegattaLog(regattaLog);
trackedRace.waitForLoadingToFinish();
testNumberOfRawFixes(trackedRace.getSensorTrack(comp, BravoFixTrack.TRACK_NAME), 2);
fixLoaderAndTracker.stop(true, /* willBeRemoved */ false);
}
@@ -197,18 +195,12 @@ public class SensorFixStoreAndLoadTest {
public void testAddFixesWhileTracking() throws InterruptedException {
regattaLog.add(new RegattaLogDeviceCompetitorBravoMappingEventImpl(new MillisecondsTimePoint(3), author, comp,
device, new MillisecondsTimePoint(START_OF_TRACKING), new MillisecondsTimePoint(MID_OF_TRACKING)));
FixLoaderAndTracker fixLoaderAndTracker = createFixLoaderAndTracker();
trackedRace.attachRaceLog(raceLog);
trackedRace.attachRegattaLog(regattaLog);
addBravoFixes();
trackedRace.waitForLoadingToFinish();
testNumberOfRawFixes(trackedRace.getSensorTrack(comp, BravoFixTrack.TRACK_NAME), 2);
fixLoaderAndTracker.stop(true, /* willBeRemoved */ false);
}
@@ -216,18 +208,12 @@ public class SensorFixStoreAndLoadTest {
public void testNoFixesAreLoadedIfNoStoredFixIsInTimeRange() throws InterruptedException {
regattaLog.add(new RegattaLogDeviceCompetitorBravoMappingEventImpl(new MillisecondsTimePoint(3), author, comp,
device, new MillisecondsTimePoint(AFTER_LAST_FIX), new MillisecondsTimePoint(END_OF_TRACKING)));
addBravoFixes();
FixLoaderAndTracker fixLoaderAndTracker = createFixLoaderAndTracker();
trackedRace.attachRaceLog(raceLog);
trackedRace.attachRegattaLog(regattaLog);
trackedRace.waitForLoadingToFinish();
testNumberOfRawFixes(trackedRace.getSensorTrack(comp, BravoFixTrack.TRACK_NAME), 0);
fixLoaderAndTracker.stop(true, /* willBeRemoved */ false);
}
@@ -235,18 +221,12 @@ public class SensorFixStoreAndLoadTest {
public void testFixesNotInMappedTimeRangeAreIgnoredWhileTracking() throws InterruptedException {
regattaLog.add(new RegattaLogDeviceCompetitorBravoMappingEventImpl(new MillisecondsTimePoint(3), author, comp,
device, new MillisecondsTimePoint(AFTER_LAST_FIX), new MillisecondsTimePoint(END_OF_TRACKING)));
FixLoaderAndTracker fixLoaderAndTracker = createFixLoaderAndTracker();
trackedRace.attachRaceLog(raceLog);
trackedRace.attachRegattaLog(regattaLog);
addBravoFixes();
trackedRace.waitForLoadingToFinish();
testNumberOfRawFixes(trackedRace.getSensorTrack(comp, BravoFixTrack.TRACK_NAME), 0);
fixLoaderAndTracker.stop(true, /* willBeRemoved */ false);
}
@@ -254,18 +234,12 @@ public class SensorFixStoreAndLoadTest {
public void testCompetitorWithoutMappingHasNoTrack() throws InterruptedException {
regattaLog.add(new RegattaLogDeviceCompetitorBravoMappingEventImpl(new MillisecondsTimePoint(3), author, comp,
device, new MillisecondsTimePoint(START_OF_TRACKING), new MillisecondsTimePoint(MID_OF_TRACKING)));
addBravoFixes();
FixLoaderAndTracker fixLoaderAndTracker = createFixLoaderAndTracker();
trackedRace.attachRaceLog(raceLog);
trackedRace.attachRegattaLog(regattaLog);
trackedRace.waitForLoadingToFinish();
assertNull(trackedRace.getSensorTrack(comp2, BravoFixTrack.TRACK_NAME));
fixLoaderAndTracker.stop(true, /* willBeRemoved */ false);
}
@@ -275,19 +249,13 @@ public class SensorFixStoreAndLoadTest {
device, new MillisecondsTimePoint(START_OF_TRACKING), new MillisecondsTimePoint(MID_OF_TRACKING)));
regattaLog.add(new RegattaLogDeviceCompetitorBravoMappingEventImpl(new MillisecondsTimePoint(3), author, comp2,
device, new MillisecondsTimePoint(MID_OF_TRACKING + 1), new MillisecondsTimePoint(END_OF_TRACKING)));
addBravoFixes();
FixLoaderAndTracker fixLoaderAndTracker = createFixLoaderAndTracker();
trackedRace.attachRaceLog(raceLog);
trackedRace.attachRegattaLog(regattaLog);
trackedRace.waitForLoadingToFinish();
testNumberOfRawFixes(trackedRace.getSensorTrack(comp, BravoFixTrack.TRACK_NAME), 2);
testNumberOfRawFixes(trackedRace.getSensorTrack(comp2, BravoFixTrack.TRACK_NAME), 1);
fixLoaderAndTracker.stop(true, /* willBeRemoved */ false);
}
@@ -297,18 +265,12 @@ public class SensorFixStoreAndLoadTest {
device, new MillisecondsTimePoint(START_OF_TRACKING), new MillisecondsTimePoint(MID_OF_TRACKING)));
regattaLog.add(new RegattaLogDeviceCompetitorBravoMappingEventImpl(new MillisecondsTimePoint(3), author, comp,
device, new MillisecondsTimePoint(MID_OF_TRACKING + 1), new MillisecondsTimePoint(END_OF_TRACKING)));
addBravoFixes();
FixLoaderAndTracker fixLoaderAndTracker = createFixLoaderAndTracker();
trackedRace.attachRaceLog(raceLog);
trackedRace.attachRegattaLog(regattaLog);
trackedRace.waitForLoadingToFinish();
testNumberOfRawFixes(trackedRace.getSensorTrack(comp, BravoFixTrack.TRACK_NAME), 3);
fixLoaderAndTracker.stop(true, /* willBeRemoved */ false);
}
@@ -319,18 +281,12 @@ public class SensorFixStoreAndLoadTest {
new MillisecondsTimePoint(MID_OF_TRACKING));
regattaLog.add(mappingEvent);
regattaLog.add(new RegattaLogRevokeEventImpl(author, mappingEvent, "Test purposes"));
FixLoaderAndTracker fixLoaderAndTracker = createFixLoaderAndTracker();
trackedRace.attachRaceLog(raceLog);
trackedRace.attachRegattaLog(regattaLog);
addBravoFixes();
trackedRace.waitForLoadingToFinish();
assertNull(trackedRace.getSensorTrack(comp, BravoFixTrack.TRACK_NAME));
fixLoaderAndTracker.stop(true, /* willBeRemoved */ false);
}
@@ -342,23 +298,16 @@ public class SensorFixStoreAndLoadTest {
new MillisecondsTimePoint(END_OF_TRACKING));
regattaLog.add(mappingEvent);
regattaLog.add(new RegattaLogRevokeEventImpl(author, mappingEvent, "Test purposes"));
// non-revoked mapping timestamp 100-200
regattaLog.add(new RegattaLogDeviceCompetitorBravoMappingEventImpl(new MillisecondsTimePoint(3), author, comp,
device, new MillisecondsTimePoint(START_OF_TRACKING), new MillisecondsTimePoint(MID_OF_TRACKING)));
FixLoaderAndTracker fixLoaderAndTracker = createFixLoaderAndTracker();
trackedRace.attachRaceLog(raceLog);
trackedRace.attachRegattaLog(regattaLog);
addBravoFixes();
trackedRace.waitForLoadingToFinish();
// Only Fixes from 100 to 200 may be included
testNumberOfRawFixes(trackedRace.getSensorTrack(comp, BravoFixTrack.TRACK_NAME), 2);
fixLoaderAndTracker.stop(true, /* willBeRemoved */ false);
}
@@ -371,25 +320,16 @@ public class SensorFixStoreAndLoadTest {
RegattaLogCloseOpenEndedDeviceMappingEventImpl closeEvent = new RegattaLogCloseOpenEndedDeviceMappingEventImpl(
new MillisecondsTimePoint(4), author, mappingEventId, new MillisecondsTimePoint(MID_OF_TRACKING));
regattaLog.add(closeEvent);
addBravoFixes();
FixLoaderAndTracker fixLoaderAndTracker = createFixLoaderAndTracker();
trackedRace.attachRaceLog(raceLog);
trackedRace.attachRegattaLog(regattaLog);
trackedRace.waitForLoadingToFinish();
BravoFixTrack<Competitor> bravoFixTrack = trackedRace.getSensorTrack(comp, BravoFixTrack.TRACK_NAME);
testNumberOfRawFixes(bravoFixTrack, 2);
regattaLog.add(new RegattaLogRevokeEventImpl(author, closeEvent, "Test purposes"));
trackedRace.waitForLoadingToFinish();
testNumberOfRawFixes(bravoFixTrack, 3);
fixLoaderAndTracker.stop(true, /* willBeRemoved */ false);
}
@@ -414,20 +354,14 @@ public class SensorFixStoreAndLoadTest {
deviceTest, new MillisecondsTimePoint(START_OF_TRACKING), new MillisecondsTimePoint(MID_OF_TRACKING)));
regattaLog.add(new RegattaLogDeviceCompetitorBravoMappingEventImpl(new MillisecondsTimePoint(3), author, comp,
device, new MillisecondsTimePoint(START_OF_TRACKING), new MillisecondsTimePoint(MID_OF_TRACKING)));
addTestFixes();
addBravoFixes();
FixLoaderAndTracker fixLoaderAndTracker = createFixLoaderAndTracker();
trackedRace.attachRaceLog(raceLog);
trackedRace.attachRegattaLog(regattaLog);
trackedRace.waitForLoadingToFinish();
testNumberOfRawFixes(trackedRace.getSensorTrack(comp, BravoFixTrack.TRACK_NAME), 2);
testNumberOfRawFixes(trackedRace.getSensorTrack(comp, TestFixTrackImpl.TRACK_NAME), 1);
fixLoaderAndTracker.stop(true, /* willBeRemoved */ false);
}
@@ -437,21 +371,14 @@ public class SensorFixStoreAndLoadTest {
deviceTest, new MillisecondsTimePoint(START_OF_TRACKING), new MillisecondsTimePoint(MID_OF_TRACKING)));
regattaLog.add(new RegattaLogDeviceCompetitorBravoMappingEventImpl(new MillisecondsTimePoint(3), author, comp,
device, new MillisecondsTimePoint(START_OF_TRACKING), new MillisecondsTimePoint(MID_OF_TRACKING)));
FixLoaderAndTracker fixLoaderAndTracker = createFixLoaderAndTracker();
trackedRace.attachRaceLog(raceLog);
trackedRace.attachRegattaLog(regattaLog);
addTestFixes();
addBravoFixes();
trackedRace.waitForLoadingToFinish();
testNumberOfRawFixes(trackedRace.getSensorTrack(comp, TestFixTrackImpl.TRACK_NAME), 1);
testNumberOfRawFixes(trackedRace.getSensorTrack(comp, BravoFixTrack.TRACK_NAME), 2);
fixLoaderAndTracker.stop(true, /* willBeRemoved */ false);
}
@@ -461,18 +388,12 @@ public class SensorFixStoreAndLoadTest {
deviceTest, new MillisecondsTimePoint(START_OF_TRACKING), new MillisecondsTimePoint(END_OF_TRACKING)));
regattaLog.add(new RegattaLogDeviceCompetitorBravoMappingEventImpl(new MillisecondsTimePoint(3), author, comp,
device, new MillisecondsTimePoint(START_OF_TRACKING), new MillisecondsTimePoint(END_OF_TRACKING)));
FixLoaderAndTracker fixLoaderAndTracker = createFixLoaderAndTracker();
trackedRace.attachRaceLog(raceLog);
trackedRace.attachRegattaLog(regattaLog);
addTestFixes();
addBravoFixes();
trackedRace.waitForLoadingToFinish();
BravoFixTrack<Competitor> bravoFixTrack = trackedRace.getSensorTrack(comp, BravoFixTrack.TRACK_NAME);
assertEquals(FIX_RIDE_HEIGHT,
bravoFixTrack.getFirstFixAtOrAfter(new MillisecondsTimePoint(FIX_TIMESTAMP)).getRideHeight());
@@ -480,11 +401,9 @@ public class SensorFixStoreAndLoadTest {
bravoFixTrack.getFirstFixAtOrAfter(new MillisecondsTimePoint(FIX_TIMESTAMP2)).getRideHeight());
assertEquals(FIX_RIDE_HEIGHT3,
bravoFixTrack.getFirstFixAtOrAfter(new MillisecondsTimePoint(FIX_TIMESTAMP3)).getRideHeight());
TestFixTrackImpl<Competitor> testFixTrack = trackedRace.getSensorTrack(comp, TestFixTrackImpl.TRACK_NAME);
assertEquals(FIX_TEST_VALUE,
testFixTrack.getFirstFixAtOrAfter(new MillisecondsTimePoint(FIX_TIMESTAMP)).getTestValue());
fixLoaderAndTracker.stop(true, /* willBeRemoved */ false);
}
@@ -501,7 +420,6 @@ public class SensorFixStoreAndLoadTest {
}
private FixLoaderAndTracker createFixLoaderAndTracker() {
return new FixLoaderAndTracker(trackedRace, store, new SensorFixMapperFactory() {
private TestDataFixMapper testDataFixMapper = new TestDataFixMapper();
private BravoDataFixMapper bravoDataFixMapper = new BravoDataFixMapper();
@@ -747,6 +665,50 @@ public class SensorFixStoreAndLoadTest {
// only the initial 3 fixes are available
testNumberOfRawFixes(trackedRace.getSensorTrack(comp, BravoFixTrack.TRACK_NAME), 3);
fixLoaderAndTracker.stop(true, /* willBeRemoved */ false);
final DynamicGPSFixTrack<Competitor, GPSFixMoving> gpsFixTrack = trackedRace.getTrack(comp);
final DegreePosition pos1 = new DegreePosition(0, 0);
final DegreeBearingImpl course = new DegreeBearingImpl(0);
final KnotSpeedWithBearingImpl speed = new KnotSpeedWithBearingImpl(10, course);
final MillisecondsTimePoint timePoint = new MillisecondsTimePoint(FIX_TIMESTAMP);
final MillisecondsTimePoint timePoint2 = new MillisecondsTimePoint(FIX_TIMESTAMP2);
final MillisecondsTimePoint timePoint3 = new MillisecondsTimePoint(FIX_TIMESTAMP3);
final TimePoint timePointBetween2And3 = new MillisecondsTimePoint((FIX_TIMESTAMP2+FIX_TIMESTAMP3)/2);
final GPSFixMoving fix1 = new GPSFixMovingImpl(pos1, timePoint, speed);
final Position pos2 = pos1.translateGreatCircle(course, speed.travel(timePoint.until(timePoint2)));
final GPSFixMoving fix2 = new GPSFixMovingImpl(pos2, timePoint2, speed);
final Position pos3 = pos2.translateGreatCircle(course, speed.travel(timePoint2.until(timePoint3)));
final GPSFixMoving fix3 = new GPSFixMovingImpl(pos3, timePoint3, speed);
gpsFixTrack.add(fix1);
gpsFixTrack.add(fix2);
gpsFixTrack.add(fix3);
final BravoFixTrack<Competitor> bravoFixTrack = trackedRace.getSensorTrack(comp, BravoFixTrack.TRACK_NAME);
final Distance foilingDistance = bravoFixTrack.getDistanceSpentFoiling(timePoint, timePoint3);
final Distance distanceTraveled = gpsFixTrack.getDistanceTraveled(timePoint, timePoint3);
assertEquals(distanceTraveled.getMeters(), foilingDistance.getMeters(), 0.001);
final Duration foilingDuration = bravoFixTrack.getTimeSpentFoiling(timePoint, timePoint3);
assertEquals(timePoint.until(timePoint3).asMillis(), foilingDuration.asMillis());
// now insert a GPS fix which leads to an extended distance traveled as it is not exactly on the previous track
final DegreeBearingImpl temporaryCourse = new DegreeBearingImpl(90);
final GPSFixMoving fixBetween2And3 = new GPSFixMovingImpl(
pos2.translateGreatCircle(temporaryCourse, speed.add(new KnotSpeedWithBearingImpl(5, temporaryCourse)).travel(timePoint2.until(timePointBetween2And3))),
timePointBetween2And3, speed);
gpsFixTrack.add(fixBetween2And3);
final Distance distanceTraveledDifferently = gpsFixTrack.getDistanceTraveled(timePoint, timePoint3);
assertTrue(distanceTraveledDifferently.compareTo(distanceTraveled) > 0);
final Distance foilingDistanceDifferent = bravoFixTrack.getDistanceSpentFoiling(timePoint, timePoint3);
assertEquals(distanceTraveledDifferently.getMeters(), foilingDistanceDifferent.getMeters(), 0.001);
// now overwrite fix3 by a new one with increased speed, therefore extended distance:
final SpeedWithBearing doubledSpeed = speed.add(speed);
final GPSFixMoving fix3Faster = new GPSFixMovingImpl(
pos2.translateGreatCircle(course, doubledSpeed.travel(timePoint2.until(timePoint3))),
timePoint3, doubledSpeed);
gpsFixTrack.add(fix3Faster, /* replace */ true);
final Distance distanceTraveledFaster = gpsFixTrack.getDistanceTraveled(timePoint, timePoint3);
assertTrue(distanceTraveledFaster.compareTo(distanceTraveled) > 0);
final Distance foilingDistanceFaster = bravoFixTrack.getDistanceSpentFoiling(timePoint, timePoint3);
assertEquals(distanceTraveledFaster.getMeters(), foilingDistanceFaster.getMeters(), 0.001);
}
@Test
@@ -20,7 +20,8 @@ public class BravoDataFixMapper implements SensorFixMapper<BravoFix, DynamicSens
@Override
public DynamicSensorFixTrack<Competitor, BravoFix> getTrack(DynamicTrackedRace race, Competitor key) {
return race.getOrCreateSensorTrack(key, BravoFixTrack.TRACK_NAME,
() -> new BravoFixTrackImpl<Competitor>(key, BravoFixTrack.TRACK_NAME, /* hasExtendedFixes */ false));
() -> new BravoFixTrackImpl<Competitor>(key, BravoFixTrack.TRACK_NAME, /* hasExtendedFixes */ false,
race.getTrack(key)));
}
@Override
@@ -21,7 +21,8 @@ public class BravoExtendedDataFixMapper implements SensorFixMapper<BravoFix, Dyn
@Override
public DynamicSensorFixTrack<Competitor, BravoFix> getTrack(DynamicTrackedRace race, Competitor key) {
return race.getOrCreateSensorTrack(key, BravoFixTrack.TRACK_NAME,
() -> new BravoFixTrackImpl<Competitor>(key, BravoFixTrack.TRACK_NAME, /* hasExtendedFixes */ true));
() -> new BravoFixTrackImpl<Competitor>(key, BravoFixTrack.TRACK_NAME, /* hasExtendedFixes */ true,
race.getTrack(key)));
}
@Override
@@ -25,7 +25,8 @@ public class ExpeditionExtendedDataFixMapper implements SensorFixMapper<BravoFix
@Override
public DynamicSensorFixTrack<Competitor, BravoFix> getTrack(DynamicTrackedRace race, Competitor key) {
return race.getOrCreateSensorTrack(key, BravoFixTrack.TRACK_NAME,
() -> new BravoFixTrackImpl<Competitor>(key, BravoFixTrack.TRACK_NAME, /* hasExtendedFixes */ true));
() -> new BravoFixTrackImpl<Competitor>(key, BravoFixTrack.TRACK_NAME, /* hasExtendedFixes */ true,
race.getTrack(key)));
}
@Override
@@ -15,6 +15,7 @@ import com.sap.sailing.domain.abstractlog.race.RaceLog;
import com.sap.sailing.domain.abstractlog.race.RaceLogEvent;
import com.sap.sailing.domain.abstractlog.race.RaceLogEventVisitor;
import com.sap.sailing.domain.common.abstractlog.NotRevokableException;
import com.sap.sailing.domain.tracking.impl.TimeRangeCache;
import com.sap.sse.common.Duration;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.Util.Function;
@@ -263,4 +264,11 @@ public class NoAddingRaceLogWrapper implements RaceLog {
public int size() {
return innerRaceLog.size();
}
@Override
public <T> T getValueSum(TimePoint from, TimePoint to, T nullElement,
com.sap.sailing.domain.tracking.Track.Adder<T> adder, TimeRangeCache<T> cache,
com.sap.sailing.domain.tracking.Track.TimeRangeValueCalculator<T> valueCalculator) {
return innerRaceLog.getValueSum(from, to, nullElement, adder, cache, valueCalculator);
}
}
@@ -5,6 +5,7 @@ import java.util.Iterator;
import java.util.concurrent.locks.ReadWriteLock;
import com.sap.sailing.domain.common.tracking.GPSFixMoving;
import com.sap.sailing.domain.tracking.impl.TimeRangeCache;
import com.sap.sse.common.Duration;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.Timed;
@@ -31,6 +32,29 @@ import com.sap.sse.common.scalablevalue.ScalableValue;
* @author Axel Uhl (d043530)
*/
public interface Track<FixType extends Timed> extends Serializable {
/**
* An adding function to be used together with {@link Track#getValueSum(TimePoint, TimePoint, Object, Adder, TimeRangeCache, TimeRangeValueCalculator)}.
*
* @author Axel Uhl (D043530)
*
* @param <T>
*/
static interface Adder<T> {
/**
* Adds two elements of type {@code T}. Neither argument must be {@code null}.
*/
T add(T t1, T t2);
}
static interface TimeRangeValueCalculator<T> {
/**
* Calculates a value for fixes across a time range. When the method is called,
* a read lock will previously have been {@link Track#lockForRead obtained} before,
* so an implementing class does not need to worry about acquiring the lock.
*/
T calculate(TimePoint from, TimePoint to);
}
/**
* Locks this track for reading by the calling thread. If the thread already holds the lock for this track,
* the hold count will be incremented. Make sure to call {@link #unlockAfterRead()} in a <code>finally</code>
@@ -209,6 +233,8 @@ public interface Track<FixType extends Timed> extends Serializable {
*/
Duration getAverageIntervalBetweenRawFixes();
<T> T getValueSum(TimePoint from, TimePoint to, T nullElement, Adder<T> adder, TimeRangeCache<T> cache, TimeRangeValueCalculator<T> valueCalculator);
/**
* @return the number of raw fixes contained in the Track.
*/
@@ -218,4 +244,5 @@ public interface Track<FixType extends Timed> extends Serializable {
* Tells whether the collection of {@link #getRawFixes() raw fixes} (no outliers removed) is empty
*/
boolean isEmpty();
}
@@ -0,0 +1,221 @@
package com.sap.sailing.domain.tracking.impl;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map.Entry;
import java.util.NavigableSet;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.Util;
import com.sap.sse.common.Util.Pair;
import com.sap.sse.concurrent.LockUtil;
import com.sap.sse.concurrent.NamedReentrantReadWriteLock;
import com.sap.sse.util.impl.ArrayListNavigableSet;
/**
* This cache looks "backwards." It contains pairs whose first component represents a <code>to</code> parameter used in
* a calculation for a time range. It is ordered by this component. The second component is a navigable, ordered set of
* pairs where the first pair component represents a <code>from</code> parameter used in the calculation's time range
* and the second pair component represents the result of the calculation for this parameter combination.
* <p>
*
* For implementation efficiency in combination with using an {@link ArrayListNavigableSet} for the values and in order
* to be able to efficiently extend a cache entry for a single <code>to</code> fix, the navigable sets containing the
* <code>from</code> fixes and results are ordered such that earlier fixes come later in the set. This way, extending
* the cache entry for a <code>to</code> fix to an earlier <code>from</code> fix only requires appending to the set.
* <p>
*
* <b>Invalidation</b>: When a new fix is added to the track, all cache entries for fixes at or later than the new fix's
* time point are removed from this cache. Additionally, the fix insertion may have an impact on the
* {@link #getEarliestFromAndResultAtOrAfterFrom(TimePoint, TimePoint) previous fix's} validity (track smoothing) and
* therefore on its selection for result aggregation. Therefore, if fix addition turned the previous fix invalid, the
* cache entries for the time points at or after the previous fix also need to be removed.
* <p>
*
* <b>Cache use</b>: When a result across a time range is to be computed the calculating method should first look for a
* cache entry for the <code>to</code> parameter. If one is found, the earliest entry in the navigable set for the
* navigable set of <code>from</code> and result values that is at or after the requested <code>from</code> time point
* is determined. If such an entry exists, the result is remembered and the algorithm is repeated recursively, using the
* <code>from</code> value found in the cache as the new <code>to</code> value, and the <code>from</code> value
* originally passed to the calculating method as <code>from</code> again. If no entry is found in the cache entry for
* <code>to</code> that is at or after the requested <code>from</code> time, the result has to be computed "from
* scratch."
* <p>
*
* If a cache entry for <code>to</code> is not found, the latest cache entry before it is looked up. If one is found,
* the result for the time range between the <code>to</code> time point requested and the <code>to</code> time point
* found in the cache is computed by iterating the smoothened fixes for this interval. If none is found, the result is
* computed by iterating backwards all the way to <code>from</code>.
* <p>
*
* Once the calculating method has computed its value, it should {@link #cache(TimePoint, TimePoint, Object) add} the
* result to the cache.
*
* @author Axel Uhl (D043530)
*/
public class TimeRangeCache<T> {
public static final int MAX_SIZE = 100;
private final NavigableSet<Util.Pair<TimePoint, NavigableSet<Util.Pair<TimePoint, T>>>> timeRangeCache;
/**
* The cache is to have limited size. Eviction shall happen based on a least-recently-used strategy. Usage is
* defined as having been returned by {@link #getEarliestFromAndResultAtOrAfterFrom(TimePoint, TimePoint)} or
* having been added by {@link #cache(TimePoint, TimePoint, Object)}.
* <p>
*
* When an eldest entry is asked to be expunged from this map and the map has more than {@link #MAX_SIZE} elements,
* the expunging will be admitted, and the entry is removed from the {@link #timeRangeCache} core structure. Reading
* and writing this structure must happen under the {@link #lock write lock} because also reading the linked hash
* map that counts access as "use" has a modifying effect on its internal structures.
* <p>
*
* The key pairs are from/to pairs. Note that this is in some sense "the opposite direction" compared to the
* alignment of the {@link #timeRangeCache} structure which has as its outer keys the "to" time point.<p>
*
* Read access is to be <code>synchronized<code> using this field's mutex; write access only happens under the
* {@link #lock write lock} and therefore will have no contenders.
*/
private final LinkedHashMap<Util.Pair<TimePoint, TimePoint>, Void> lruCache;
private final NamedReentrantReadWriteLock lock;
private static final Comparator<Util.Pair<TimePoint, ?>> timePointInPairComparator = new Comparator<Util.Pair<TimePoint, ?>>() {
@Override
public int compare(Util.Pair<TimePoint, ?> o1, Util.Pair<TimePoint, ?> o2) {
return o1.getA().compareTo(o2.getA());
}
};
public TimeRangeCache(String nameForLockLogging) {
lock = new NamedReentrantReadWriteLock("lock for TimeRangeCache for "+nameForLockLogging, /* fair */ true);
this.timeRangeCache = new ArrayListNavigableSet<Util.Pair<TimePoint, NavigableSet<Util.Pair<TimePoint, T>>>>(timePointInPairComparator);
this.lruCache = new LinkedHashMap<Util.Pair<TimePoint, TimePoint>, Void>(/* initial capacity */ 10, /* load factor */ 0.75f,
/* access-based ordering */ true) {
private static final long serialVersionUID = -6568235517111733193L;
@Override
protected boolean removeEldestEntry(Entry<Pair<TimePoint, TimePoint>, Void> eldest) {
final boolean expunge = size() > MAX_SIZE;
if (expunge) {
removeCacheEntry(eldest.getKey().getA(), eldest.getKey().getB());
}
return expunge;
}
};
}
public int size() {
return lruCache.size();
}
private void removeCacheEntry(TimePoint from, TimePoint to) {
assert lock.getWriteHoldCount() == 1; // we can be sure we are alone here; this only happens when adding a new entry, holding the write lock
Util.Pair<TimePoint, NavigableSet<Util.Pair<TimePoint, T>>> entryForTo = timeRangeCache.floor(createDummy(to));
if (entryForTo.getA().equals(to)) {
Pair<TimePoint, T> entryForFrom = entryForTo.getB().ceiling(new Util.Pair<TimePoint, T>(from, null));
if (entryForFrom.getA().equals(from)) {
entryForTo.getB().remove(entryForFrom);
if (entryForTo.getB().isEmpty()) {
timeRangeCache.remove(entryForTo);
}
}
}
}
/**
* Looks up the entry closest to but no later than <code>to</code>. If not found, <code>null</code> is returned. If
* found, the earliest pair of from/result that is at or after <code>from</code> will be returned, together with
* the <code>to</code> value of the entry. If there is no entry that is at or after <code>from</code>,
* <code>null</code> is returned.
*/
public Util.Pair<TimePoint, Util.Pair<TimePoint, T>> getEarliestFromAndResultAtOrAfterFrom(TimePoint from, TimePoint to) {
LockUtil.lockForRead(lock);
try {
Util.Pair<TimePoint, Util.Pair<TimePoint, T>> result = null;
Util.Pair<TimePoint, NavigableSet<Util.Pair<TimePoint, T>>> entryForTo = timeRangeCache.floor(createDummy(to));
if (entryForTo != null) {
final Util.Pair<TimePoint, T> fromCeiling = entryForTo.getB().ceiling(new Util.Pair<TimePoint, T>(from, null));
if (fromCeiling != null) {
result = new Util.Pair<TimePoint, Util.Pair<TimePoint, T>>(entryForTo.getA(), fromCeiling);
}
}
// no writer can be active because we're holding the read lock; read access on the lruCache is synchronized using
// the lruCache's mutex; this is necessary because we're using access-based LRU pinging where even getting an entry
// modifies the internal parts of the data structure which is not thread safe.
synchronized (lruCache) {
lruCache.get(new Util.Pair<TimePoint, TimePoint>(from, to));
}
return result;
} finally {
LockUtil.unlockAfterRead(lock);
}
}
/**
* Removes all cache entries that have a <code>to</code> time point that is at or after <code>timePoint</code>.
*/
public void invalidateAllAtOrLaterThan(TimePoint timePoint) {
LockUtil.lockForWrite(lock);
try {
Util.Pair<TimePoint, NavigableSet<Util.Pair<TimePoint, T>>> dummy = createDummy(timePoint);
NavigableSet<Util.Pair<TimePoint, NavigableSet<Util.Pair<TimePoint, T>>>> toRemove = timeRangeCache.tailSet(dummy, /* inclusive */ true);
for (Iterator<Util.Pair<TimePoint, NavigableSet<Util.Pair<TimePoint, T>>>> i=toRemove.iterator(); i.hasNext(); ) {
Util.Pair<TimePoint, NavigableSet<Util.Pair<TimePoint, T>>> entryToRemove = i.next();
assert entryToRemove.getA().compareTo(timePoint) >= 0;
for (Pair<TimePoint, T> fromAndResult : entryToRemove.getB()) {
lruCache.remove(new Util.Pair<>(fromAndResult.getA(), entryToRemove.getA()));
}
i.remove();
}
} finally {
LockUtil.unlockAfterWrite(lock);
}
}
private NavigableSet<Util.Pair<TimePoint, T>> getEntryForTo(TimePoint to) {
NavigableSet<Util.Pair<TimePoint, T>> result = null;
Util.Pair<TimePoint, NavigableSet<Util.Pair<TimePoint, T>>> dummyForTo = createDummy(to);
Util.Pair<TimePoint, NavigableSet<Util.Pair<TimePoint, T>>> entryForTo = timeRangeCache.floor(dummyForTo);
if (entryForTo != null && entryForTo.getA().equals(to)) {
result = entryForTo.getB();
}
return result;
}
public void cache(TimePoint from, TimePoint to, T result) {
LockUtil.lockForWrite(lock);
try {
NavigableSet<Util.Pair<TimePoint, T>> entryForTo = getEntryForTo(to);
if (entryForTo == null) {
entryForTo = new ArrayListNavigableSet<Util.Pair<TimePoint, T>>(timePointInPairComparator);
Util.Pair<TimePoint, NavigableSet<Util.Pair<TimePoint, T>>> pairForTo = new Util.Pair<TimePoint, NavigableSet<Util.Pair<TimePoint, T>>>(
to, entryForTo);
timeRangeCache.add(pairForTo);
}
entryForTo.add(new Util.Pair<TimePoint, T>(from, result));
lruCache.put(new Util.Pair<TimePoint, TimePoint>(from, to), null);
} finally {
LockUtil.unlockAfterWrite(lock);
}
}
private Util.Pair<TimePoint, NavigableSet<Util.Pair<TimePoint, T>>> createDummy(TimePoint to) {
return new Util.Pair<TimePoint, NavigableSet<Util.Pair<TimePoint, T>>>(to, null);
}
/**
* Removes all contents from this cache
*/
public void clear() {
LockUtil.lockForWrite(lock);
try {
timeRangeCache.clear();
lruCache.clear();
} finally {
LockUtil.unlockAfterWrite(lock);
}
}
}
@@ -11,6 +11,7 @@ import com.sap.sailing.domain.tracking.Track;
import com.sap.sse.common.Duration;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.Timed;
import com.sap.sse.common.Util;
import com.sap.sse.common.Util.Function;
import com.sap.sse.common.Util.Pair;
import com.sap.sse.common.scalablevalue.ScalableValue;
@@ -459,6 +460,60 @@ public class TrackImpl<FixType extends Timed> implements Track<FixType> {
}
}
@Override
public <T> T getValueSum(TimePoint from, TimePoint to, T nullElement, Adder<T> adder, TimeRangeCache<T> cache, TimeRangeValueCalculator<T> valueCalculator) {
return getValueSumRecursively(from, to, /* recursionLevel */ 0, nullElement, adder, cache, valueCalculator);
}
private <T> T getValueSumRecursively(TimePoint from, TimePoint to, int recursionDepth, T nullElement,
Adder<T> adder, TimeRangeCache<T> cache, TimeRangeValueCalculator<T> valueCalculator) {
T result;
if (!from.before(to)) {
result = nullElement;
} else {
boolean perfectCacheHit = false;
lockForRead();
try {
Util.Pair<TimePoint, Util.Pair<TimePoint, T>> bestCacheEntry = cache.getEarliestFromAndResultAtOrAfterFrom(from, to);
if (bestCacheEntry != null) {
perfectCacheHit = true; // potentially a cache hit; but if it doesn't span the full interval, it's not perfect; see below
// compute the missing stretches between best cache entry's "from" and our "from" and the cache
// entry's "to" and our "to"
T distanceFromFromToBeginningOfCacheEntry = nullElement;
T distanceFromEndOfCacheEntryToTo = nullElement;
if (!bestCacheEntry.getB().getA().equals(from)) {
assert bestCacheEntry.getB().getA().after(from);
perfectCacheHit = false;
distanceFromFromToBeginningOfCacheEntry = getValueSumRecursively(from, bestCacheEntry
.getB().getA(), recursionDepth + 1, nullElement, adder, cache, valueCalculator);
}
if (!bestCacheEntry.getA().equals(to)) {
assert bestCacheEntry.getA().before(to);
perfectCacheHit = false;
distanceFromEndOfCacheEntryToTo = getValueSumRecursively(bestCacheEntry.getA(), to,
recursionDepth + 1, nullElement, adder, cache, valueCalculator);
}
result = adder.add(adder.add(distanceFromFromToBeginningOfCacheEntry, bestCacheEntry.getB().getB()),
distanceFromEndOfCacheEntryToTo);
} else {
if (from.compareTo(to) < 0) {
result = valueCalculator.calculate(from, to);
} else {
result = nullElement;
}
}
} finally {
unlockAfterRead();
}
if (!perfectCacheHit && recursionDepth == 0) {
cache.cache(from, to, result);
}
}
return result;
}
@Override
public int size() {
return fixes.size();
@@ -51,7 +51,7 @@ import com.sap.sailing.domain.common.tracking.impl.GPSFixMovingImpl;
import com.sap.sailing.domain.common.tracking.impl.VeryCompactGPSFixMovingImpl;
import com.sap.sailing.domain.tracking.DynamicGPSFixTrack;
import com.sap.sailing.domain.tracking.GPSFixTrack;
import com.sap.sailing.domain.tracking.impl.DistanceCache;
import com.sap.sailing.domain.tracking.impl.TimeRangeCache;
import com.sap.sailing.domain.tracking.impl.DynamicGPSFixMovingTrackImpl;
import com.sap.sailing.domain.tracking.impl.DynamicGPSFixTrackImpl;
import com.sap.sailing.domain.tracking.impl.GPSFixTrackImpl;
@@ -442,7 +442,7 @@ public class TrackTest {
@Test
public void testDistanceTraveledBackwardsQuery() {
final Set<com.sap.sse.common.Util.Triple<TimePoint, TimePoint, Distance>> cacheEntries = new HashSet<>();
final DistanceCache distanceCache = new DistanceCache("test-DistanceCache") {
final TimeRangeCache<Distance> distanceCache = new TimeRangeCache<Distance>("test-DistanceCache") {
@Override
public void cache(TimePoint from, TimePoint to, Distance distance) {
super.cache(from, to, distance);
@@ -453,7 +453,7 @@ public class TrackTest {
DynamicGPSFixTrack<Object, GPSFix> track = new DynamicGPSFixTrackImpl<Object>(new Object(), /* millisecondsOverWhichToAverage */ 30000l) {
private static final long serialVersionUID = -7277196393160609503L;
@Override
protected DistanceCache getDistanceCache() {
protected TimeRangeCache<Distance> getDistanceCache() {
return distanceCache;
}
};
@@ -779,7 +779,7 @@ public class TrackTest {
@Test
public void testDistanceTraveledOnSmoothenedTrackThenAddingOutlier() {
final Set<TimePoint> invalidationCalls = new HashSet<TimePoint>();
final DistanceCache distanceCache = new DistanceCache("test-DistanceCache") {
final TimeRangeCache<Distance> distanceCache = new TimeRangeCache<Distance>("test-DistanceCache") {
@Override
public void invalidateAllAtOrLaterThan(TimePoint timePoint) {
super.invalidateAllAtOrLaterThan(timePoint);
@@ -789,7 +789,7 @@ public class TrackTest {
DynamicGPSFixTrack<Object, GPSFix> track = new DynamicGPSFixTrackImpl<Object>(new Object(), /* millisecondsOverWhichToAverage */ 30000l) {
private static final long serialVersionUID = -7277196393160609503L;
@Override
protected DistanceCache getDistanceCache() {
protected TimeRangeCache<Distance> getDistanceCache() {
return distanceCache;
}
};
@@ -811,7 +811,7 @@ public class TrackTest {
}
invalidationCalls.clear();
assertEquals(speed.getMetersPerSecond()*(steps-1), track.getDistanceTraveled(now, start).getMeters(), 0.02);
final com.sap.sse.common.Util.Pair<TimePoint, com.sap.sse.common.Util.Pair<TimePoint, Distance>> fullIntervalCacheEntry = distanceCache.getEarliestFromAndDistanceAtOrAfterFrom(now, start);
final com.sap.sse.common.Util.Pair<TimePoint, com.sap.sse.common.Util.Pair<TimePoint, Distance>> fullIntervalCacheEntry = distanceCache.getEarliestFromAndResultAtOrAfterFrom(now, start);
assertNotNull(fullIntervalCacheEntry); // no more entry for "to"-value start in cache
assertEquals(start, fullIntervalCacheEntry.getA());
assertEquals(now, fullIntervalCacheEntry.getB().getA());
@@ -822,13 +822,13 @@ public class TrackTest {
assertEquals(1, invalidationCalls.size());
TimePoint timePointOfLastFixBeforeOutlier = track.getLastFixBefore(timePointForOutlier).getTimePoint();
assertTrue(invalidationCalls.iterator().next().after(timePointOfLastFixBeforeOutlier)); // outlier doesn't turn its preceding element into an outlier
assertNull(distanceCache.getEarliestFromAndDistanceAtOrAfterFrom(now, start)); // no more entry for "to"-value start in cache
assertNull(distanceCache.getEarliestFromAndResultAtOrAfterFrom(now, start)); // no more entry for "to"-value start in cache
invalidationCalls.clear();
final TimePoint timePointOfLastOriginalFix = track.getLastRawFix().getTimePoint();
assertEquals(speed.getMetersPerSecond() * (steps - 1),
track.getDistanceTraveled(now, timePointOfLastOriginalFix).getMeters(), 0.02);
final com.sap.sse.common.Util.Pair<TimePoint, com.sap.sse.common.Util.Pair<TimePoint, Distance>> newFullIntervalCacheEntry = distanceCache
.getEarliestFromAndDistanceAtOrAfterFrom(now, timePointOfLastOriginalFix);
.getEarliestFromAndResultAtOrAfterFrom(now, timePointOfLastOriginalFix);
assertNotNull(newFullIntervalCacheEntry); // no more entry for "to"-value start in cache
assertEquals(timePointOfLastOriginalFix, newFullIntervalCacheEntry.getA());
assertEquals(now, newFullIntervalCacheEntry.getB().getA());
@@ -845,7 +845,7 @@ public class TrackTest {
invalidationCalls.clear();
// expect the invalidation to have started after the single cache entry, so the cache entry still has to be there:
final com.sap.sse.common.Util.Pair<TimePoint, com.sap.sse.common.Util.Pair<TimePoint, Distance>> stillPresentFullIntervalCacheEntry = distanceCache
.getEarliestFromAndDistanceAtOrAfterFrom(now, timePointOfLastOriginalFix);
.getEarliestFromAndResultAtOrAfterFrom(now, timePointOfLastOriginalFix);
assertNull(stillPresentFullIntervalCacheEntry); // no more entry for "to"-value start in cache because new temporary outlier lies in previously cached interval
GPSFix polishedLastFix = track.getLastFixBefore(new MillisecondsTimePoint(Long.MAX_VALUE)); // get the last smoothened fix...
// ...which now is expected to be two fixes before lateOutlier because lateOutlier has previous fixes within the time range
@@ -870,7 +870,7 @@ public class TrackTest {
assertTrue(timePointForLateOutlier.compareTo(fix.getTimePoint()) < 0);
// expect the invalidation to have started at the fix before the outlier, leaving the previous result ending at the fix right before the outlier intact
final com.sap.sse.common.Util.Pair<TimePoint, com.sap.sse.common.Util.Pair<TimePoint, Distance>> stillStillPresentFullIntervalCacheEntry = distanceCache
.getEarliestFromAndDistanceAtOrAfterFrom(now, timePointOfLastOriginalFix);
.getEarliestFromAndResultAtOrAfterFrom(now, timePointOfLastOriginalFix);
assertNull(stillStillPresentFullIntervalCacheEntry); // still nothing in the cache
track.lockForRead();
try {
@@ -955,11 +955,11 @@ public class TrackTest {
final Duration raceDuration = Duration.ONE_HOUR.times(48);
final Speed speed = new KnotSpeedImpl(12);
final Distance distancePerSample = speed.travel(fixTime, fixTime.plus(samplingInterval));
final DistanceCache distanceCache = new DistanceCache("test-DistanceCache");
final TimeRangeCache<Distance> distanceCache = new TimeRangeCache<>("test-DistanceCache");
DynamicGPSFixTrack<Object, GPSFix> track = new DynamicGPSFixTrackImpl<Object>(new Object(), /* millisecondsOverWhichToAverage */ 30000l) {
private static final long serialVersionUID = -7277196393160609503L;
@Override
protected DistanceCache getDistanceCache() {
protected TimeRangeCache<Distance> getDistanceCache() {
return distanceCache;
}
};
@@ -980,13 +980,13 @@ public class TrackTest {
queryTime = queryTime.plus(queryInterval);
}
}
assertTrue(distanceCache.size() <= DistanceCache.MAX_SIZE);
assertTrue(distanceCache.size() <= TimeRangeCache.MAX_SIZE);
}
@Test
public void testDistanceCacheAccessForPartialStrip() {
final Set<TimePoint> invalidationCalls = new HashSet<TimePoint>();
final DistanceCache distanceCache = new DistanceCache("test-DistanceCache") {
final TimeRangeCache<Distance> distanceCache = new TimeRangeCache<Distance>("test-DistanceCache") {
@Override
public void invalidateAllAtOrLaterThan(TimePoint timePoint) {
super.invalidateAllAtOrLaterThan(timePoint);
@@ -996,7 +996,7 @@ public class TrackTest {
DynamicGPSFixTrack<Object, GPSFix> track = new DynamicGPSFixTrackImpl<Object>(new Object(), /* millisecondsOverWhichToAverage */ 30000l) {
private static final long serialVersionUID = -7277196393160609503L;
@Override
protected DistanceCache getDistanceCache() {
protected TimeRangeCache<Distance> getDistanceCache() {
return distanceCache;
}
};
@@ -1024,11 +1024,11 @@ public class TrackTest {
assertEquals(speed.getMetersPerSecond()*(steps-1), track.getDistanceTraveled(now, start).getMeters(), 0.02);
assertTrue(invalidationCalls.isEmpty());
// expect a cache entry exactly for the strip's boundaries
com.sap.sse.common.Util.Pair<TimePoint, com.sap.sse.common.Util.Pair<TimePoint, Distance>> stripCacheEntry = distanceCache.getEarliestFromAndDistanceAtOrAfterFrom(stripFrom, stripTo);
com.sap.sse.common.Util.Pair<TimePoint, com.sap.sse.common.Util.Pair<TimePoint, Distance>> stripCacheEntry = distanceCache.getEarliestFromAndResultAtOrAfterFrom(stripFrom, stripTo);
assertEquals(stripTo, stripCacheEntry.getA());
assertEquals(stripFrom, stripCacheEntry.getB().getA());
// expect a cache entry exactly for the full boundaries
com.sap.sse.common.Util.Pair<TimePoint, com.sap.sse.common.Util.Pair<TimePoint, Distance>> fullCacheEntry = distanceCache.getEarliestFromAndDistanceAtOrAfterFrom(now, start);
com.sap.sse.common.Util.Pair<TimePoint, com.sap.sse.common.Util.Pair<TimePoint, Distance>> fullCacheEntry = distanceCache.getEarliestFromAndResultAtOrAfterFrom(now, start);
assertEquals(start, fullCacheEntry.getA());
assertEquals(now, fullCacheEntry.getB().getA());
}
@@ -674,6 +674,16 @@ public class MockedTrackedRace implements DynamicTrackedRace {
return null;
}
@Override
public Distance getDistanceFoiled(Competitor competitor, TimePoint timePoint) {
return null;
}
@Override
public Duration getDurationFoiled(Competitor competitor, TimePoint timePoint) {
return null;
}
@Override
public Distance getWindwardDistanceToCompetitorFarthestAhead(Competitor competitor, TimePoint timePoint,
WindPositionMode windPositionMode) {
@@ -60,6 +60,7 @@ import com.sap.sailing.domain.tracking.WindStore;
import com.sap.sailing.domain.tracking.WindTrack;
import com.sap.sailing.domain.tracking.WindWithConfidence;
import com.sap.sailing.domain.tracking.impl.DynamicTrackedRegattaImpl;
import com.sap.sse.common.Duration;
import com.sap.sse.common.IsManagedByCache;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.Util;
@@ -360,6 +361,16 @@ public class MockedTrackedRaceWithStartTimeAndRanks implements TrackedRace {
return null;
}
@Override
public Distance getDistanceFoiled(Competitor competitor, TimePoint timePoint) {
return null;
}
@Override
public Duration getDurationFoiled(Competitor competitor, TimePoint timePoint) {
return null;
}
@Override
public Distance getWindwardDistanceToCompetitorFarthestAhead(Competitor competitor, TimePoint timePoint, WindPositionMode windPositionMode) {
return null;
@@ -481,6 +481,26 @@ public interface Leaderboard extends LeaderboardBase, HasRaceColumns {
* leaderboard
*/
Distance getTotalDistanceTraveled(Competitor competitor, TimePoint timePoint);
/**
* Computes the distance the <code>competitor</code> has been foiling in the tracked races in this leaderboard, starting
* to count in each race when the competitor passes the start line, aggregating up to <code>timePoint</code> or the
* end of the last race, whichever is first.
*
* @return <code>null</code> if the <code>competitor</code> hasn't foiled any distance in any tracked race in this
* leaderboard
*/
Distance getTotalDistanceFoiled(Competitor competitor, TimePoint timePoint);
/**
* Computes the duration the <code>competitor</code> has foiled in the tracked races in this leaderboard, starting
* to count in each race when the competitor passes the start line, aggregating up to <code>timePoint</code> or the
* end of the last race, whichever is first.
*
* @return <code>null</code> if the <code>competitor</code> hasn't foiled at any time in any tracked race in this
* leaderboard
*/
Duration getTotalDurationFoiled(Competitor competitor, TimePoint timePoint);
/**
* Same as {@link #getNetPoints(Competitor, RaceColumn, TimePoint)}, only that for determining the discarded
@@ -19,6 +19,7 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.RunnableFuture;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -642,8 +643,12 @@ public abstract class AbstractLeaderboardWithCache implements Leaderboard {
row.totalTimeSailedReachingInSeconds = totalTimeSailedReaching==null?null:totalTimeSailedReaching.asSeconds();
final Duration totalTimeSailed = this.getTotalTimeSailed(competitor, timePoint);
row.totalTimeSailedInSeconds = totalTimeSailed==null?null:totalTimeSailed.asSeconds();
final Distance totalDistanceTraveledInMeters = this.getTotalDistanceTraveled(competitor, timePoint);
row.totalDistanceTraveledInMeters = totalDistanceTraveledInMeters==null?null:totalDistanceTraveledInMeters.getMeters();
final Distance totalDistanceTraveled = this.getTotalDistanceTraveled(competitor, timePoint);
row.totalDistanceTraveledInMeters = totalDistanceTraveled==null?null:totalDistanceTraveled.getMeters();
final Distance totalDistanceFoiled = this.getTotalDistanceFoiled(competitor, timePoint);
row.totalDistanceFoiledInMeters = totalDistanceFoiled==null?null:totalDistanceFoiled.getMeters();
final Duration totalDurationFoiled = this.getTotalDurationFoiled(competitor, timePoint);
row.totalDurationFoiledInSeconds = totalDurationFoiled==null?null:totalDurationFoiled.asSeconds();
}
private LeaderboardDTOCache getLeaderboardDTOCache() {
@@ -821,6 +826,12 @@ public abstract class AbstractLeaderboardWithCache implements Leaderboard {
result.currentHeelInDegrees = heel == null ? null : heel.getDegrees();
Bearing pitch = trackedLeg.getPitch(timePoint);
result.currentPitchInDegrees = pitch == null ? null : pitch.getDegrees();
Distance distanceFoiled = trackedLeg.getDistanceFoiled(timePoint);
result.currentDistanceFoiledInMeters = distanceFoiled == null ? null : distanceFoiled.getMeters();
Duration durationFoiled = trackedLeg.getDurationFoiled(timePoint);
result.currentDurationFoiledInSeconds = durationFoiled == null ? null : durationFoiled.asSeconds();
result.currentSpeedOverGroundInKnots = speedOverGroundInKnots == null ? null : speedOverGroundInKnots;
Distance distanceTraveled = trackedLeg.getDistanceTraveled(timePoint);
result.distanceTraveledInMeters = distanceTraveled == null ? null : distanceTraveled.getMeters();
@@ -1082,18 +1093,37 @@ public abstract class AbstractLeaderboardWithCache implements Leaderboard {
@Override
public Distance getTotalDistanceTraveled(Competitor competitor, TimePoint timePoint) {
Distance result = null;
return getTotals(competitor, timePoint, Distance::add, TrackedRace::getDistanceTraveled);
}
@Override
public Distance getTotalDistanceFoiled(Competitor competitor, TimePoint timePoint) {
return getTotals(competitor, timePoint, Distance::add, TrackedRace::getDistanceFoiled);
}
@Override
public Duration getTotalDurationFoiled(Competitor competitor, TimePoint timePoint) {
return getTotals(competitor, timePoint, Duration::plus, TrackedRace::getDurationFoiled);
}
@FunctionalInterface
private static interface ValueFromRaceGetter<T> {
T get(TrackedRace trackedRace, Competitor competitor, TimePoint timePoint);
}
private <T> T getTotals(Competitor competitor, TimePoint timePoint, BiFunction<T, T, T> adder, ValueFromRaceGetter<T> valueGetter) {
T result = null;
for (TrackedRace trackedRace : getTrackedRaces()) {
TimePoint startOfRace;
if (Util.contains(trackedRace.getRace().getCompetitors(), competitor) &&
(startOfRace=trackedRace.getStartOfRace()) != null &&
!startOfRace.after(timePoint)) {
Distance distanceSailedInRace = trackedRace.getDistanceTraveled(competitor, timePoint);
T distanceSailedInRace = valueGetter.get(trackedRace, competitor, timePoint);
if (distanceSailedInRace != null) {
if (result == null) {
result = distanceSailedInRace;
} else {
result = result.add(distanceSailedInRace);
result = adder.apply(result, distanceSailedInRace);
}
} else {
// if competitor has not finished one single race in the whole
@@ -2,12 +2,10 @@ package com.sap.sailing.domain.tracking;
import java.io.Serializable;
import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.common.Bearing;
import com.sap.sailing.domain.common.Distance;
import com.sap.sailing.domain.common.tracking.BravoExtendedFix;
import com.sap.sailing.domain.common.tracking.BravoFix;
import com.sap.sailing.domain.common.tracking.GPSFixMoving;
import com.sap.sse.common.Duration;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.WithID;
@@ -73,7 +71,7 @@ public interface BravoFixTrack<ItemType extends WithID & Serializable> extends S
/**
* The distance sailed foiling during the time range provided
*/
Distance getDistanceSpentFoiling(GPSFixTrack<Competitor, GPSFixMoving> gpsFixTrack, TimePoint from, TimePoint to);
Distance getDistanceSpentFoiling(TimePoint from, TimePoint to);
/**
* Returns {@code true}, if the track contains {@link BravoExtendedFix} instances instead of simple {@link BravoFix BravoFixes}.
@@ -241,6 +241,11 @@ public interface TrackedLegOfCompetitor extends Serializable {
Bearing getHeel(TimePoint at);
Bearing getPitch(TimePoint at);
Distance getDistanceFoiled(TimePoint at);
Duration getDurationFoiled(TimePoint at);
/**
* Computes the distance along the wind track to the wind-projected position of the race's overall leader. If leader
* and competitor are in the same leg, this is simply the windward distance. If the leader is already one or more
@@ -261,7 +266,6 @@ public interface TrackedLegOfCompetitor extends Serializable {
/**
* Same as {@link #getWindwardDistanceToCompetitorFarthestAhead(TimePoint, WindPositionMode, RankingInfo)}, only that a cache for leg type
* calculation is passed.
* @param rankingInfo TODO
*/
Distance getWindwardDistanceToCompetitorFarthestAhead(TimePoint timePoint, WindPositionMode windPositionMode, RankingInfo rankingInfo, WindLegTypeAndLegBearingCache cache);
@@ -611,7 +611,7 @@ public interface TrackedRace extends Serializable, IsManagedByCache<SharedDomain
boolean notifyAboutGPSFixesAlreadyLoaded);
void removeListener(RaceChangeListener listener);
/**
* @return <code>null</code> if there are no mark passings for the <code>competitor</code> in this race
* or if the competitor has not finished one of the legs in the race.
@@ -623,6 +623,18 @@ public interface TrackedRace extends Serializable, IsManagedByCache<SharedDomain
*/
Distance getDistanceTraveledIncludingGateStart(Competitor competitor, TimePoint timePoint);
/**
* @return <code>null</code> if there are no mark passings for the <code>competitor</code> in this race
* or if the competitor has not finished one of the legs in the race.
*/
Distance getDistanceFoiled(Competitor competitor, TimePoint timePoint);
/**
* @return <code>null</code> if there are no mark passings for the <code>competitor</code> in this race
* or if the competitor has not finished one of the legs in the race.
*/
Duration getDurationFoiled(Competitor competitor, TimePoint timePoint);
/**
* See {@link TrackedLegOfCompetitor#getWindwardDistanceToCompetitorFarthestAhead(TimePoint, WindPositionMode, RankingInfo)}
*/
@@ -1,9 +1,10 @@
package com.sap.sailing.domain.tracking.impl;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.function.Function;
import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.common.Bearing;
import com.sap.sailing.domain.common.Distance;
import com.sap.sailing.domain.common.confidence.impl.ScalableDouble;
@@ -15,6 +16,8 @@ import com.sap.sailing.domain.common.tracking.GPSFixMoving;
import com.sap.sailing.domain.tracking.BravoFixTrack;
import com.sap.sailing.domain.tracking.DynamicBravoFixTrack;
import com.sap.sailing.domain.tracking.GPSFixTrack;
import com.sap.sailing.domain.tracking.GPSTrackListener;
import com.sap.sailing.domain.tracking.Track;
import com.sap.sailing.domain.tracking.TrackedRace;
import com.sap.sse.common.Duration;
import com.sap.sse.common.TimePoint;
@@ -31,6 +34,37 @@ public class BravoFixTrackImpl<ItemType extends WithID & Serializable> extends S
private static final long serialVersionUID = 460944392510182976L;
private final boolean hasExtendedFixes;
private transient TimeRangeCache<Duration> foilingTimeCache;
private transient TimeRangeCache<Distance> foilingDistanceCache;
private transient TimeRangeCache<Distance> averageRideHeightCache;
/**
* If a GPS track was provided at construction time, remember it non-transiently. It is needed when restoring
* the object after de-serialization, so the cache invalidation listener can be re-established.
*/
private GPSFixTrack<ItemType, GPSFixMoving> gpsTrack;
private class CacheInvalidationGpsTrackListener implements GPSTrackListener<ItemType, GPSFixMoving> {
private static final long serialVersionUID = 6395529765232404414L;
@Override
public boolean isTransient() {
return true;
}
@Override
public void gpsFixReceived(GPSFixMoving fix, ItemType item, boolean firstFixInTrack) {
assert item == getTrackedItem();
foilingDistanceCache.invalidateAllAtOrLaterThan(fix.getTimePoint());
}
@Override
public void speedAveragingChanged(long oldMillisecondsOverWhichToAverage,
long newMillisecondsOverWhichToAverage) {
}
}
/**
* @param trackedItem
@@ -39,8 +73,55 @@ public class BravoFixTrackImpl<ItemType extends WithID & Serializable> extends S
* the name of the track by which it can be obtained from the {@link TrackedRace}.
*/
public BravoFixTrackImpl(ItemType trackedItem, String trackName, boolean hasExtendedFixes) {
this(trackedItem, trackName, hasExtendedFixes, /* listen to GPS track for distance cache invalidation */ null);
}
/**
* @param gpsTrack
* if not {@code null}, this track will listen on that track for changes in order to invalidate this
* track's caches; for example, if a GPS fix is added, this track may need to invalidate its
* {@link #foilingDistanceCache} accordingly because the distance traveled between the fixes may have
* changed.
*/
public BravoFixTrackImpl(ItemType trackedItem, String trackName, boolean hasExtendedFixes, GPSFixTrack<ItemType, GPSFixMoving> gpsTrack) {
super(trackedItem, trackName, BravoFixTrack.TRACK_NAME + " for " + trackedItem);
this.hasExtendedFixes = hasExtendedFixes;
this.foilingTimeCache = createFoilingTimeCache(trackedItem);
this.foilingDistanceCache = createFoilingDistanceCache(trackedItem);
this.averageRideHeightCache = createAverageRideHeightCache(trackedItem);
this.gpsTrack = gpsTrack;
if (gpsTrack != null) {
gpsTrack.addListener(new CacheInvalidationGpsTrackListener());
}
}
private TimeRangeCache<Distance> createAverageRideHeightCache(ItemType trackedItem) {
return createTimeRangeCache(trackedItem, "averageRideHeightCache");
}
private <T> TimeRangeCache<T> createTimeRangeCache(ItemType trackedItem, final String cacheName) {
return new TimeRangeCache<>(cacheName+" for "+trackedItem);
}
private TimeRangeCache<Distance> createFoilingDistanceCache(ItemType trackedItem) {
return createTimeRangeCache(trackedItem, "foilingDistanceCache");
}
private TimeRangeCache<Duration> createFoilingTimeCache(ItemType trackedItem) {
return createTimeRangeCache(trackedItem, "foilingTimeCache");
}
/**
* After reading this object from an {@link ObjectInputStream}, initialize the caches properly.
*/
private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException {
ois.defaultReadObject();
this.foilingTimeCache = createFoilingTimeCache(getTrackedItem());
this.foilingDistanceCache = createFoilingDistanceCache(getTrackedItem());
this.averageRideHeightCache = createAverageRideHeightCache(getTrackedItem());
if (gpsTrack != null) {
gpsTrack.addListener(new CacheInvalidationGpsTrackListener());
}
}
@Override
@@ -67,29 +148,43 @@ public class BravoFixTrackImpl<ItemType extends WithID & Serializable> extends S
return rideHeight != null && rideHeight.compareTo(BravoFix.MIN_FOILING_HEIGHT_THRESHOLD) >= 0;
}
@Override
public boolean add(BravoFix fix, boolean replace) {
final boolean added = super.add(fix, replace);
if (added) {
final TimePoint fixTimePoint = fix.getTimePoint();
averageRideHeightCache.invalidateAllAtOrLaterThan(fixTimePoint);
foilingDistanceCache.invalidateAllAtOrLaterThan(fixTimePoint);
foilingTimeCache.invalidateAllAtOrLaterThan(fixTimePoint);
}
return added;
}
@Override
public Distance getAverageRideHeight(TimePoint from, TimePoint to) {
final Distance result;
lockForRead();
try {
Distance sum = Distance.NULL;
int count = 0;
for (final BravoFix fix : getFixes(from, true, to, true)) {
final Distance rideHeight = fix.getRideHeight();
if (rideHeight != null) {
sum = sum.add(rideHeight);
count++;
return getValueSum(from, to, /* nullElement */ Distance.NULL, Distance::add, averageRideHeightCache,
/* valueCalculator */ new Track.TimeRangeValueCalculator<Distance>() {
@Override
public Distance calculate(TimePoint from, TimePoint to) {
Distance result;
Distance sum = Distance.NULL;
int count = 0;
for (final BravoFix fix : getFixes(from, true, to, true)) {
final Distance rideHeight = fix.getRideHeight();
if (rideHeight != null) {
sum = sum.add(rideHeight);
count++;
}
}
if (count > 0) {
result = sum.scale(1./count);
} else {
result = null;
}
return result;
}
if (count > 0) {
result = sum.scale(1./count);
} else {
result = null;
}
} finally {
unlockAfterRead();
}
return result;
});
}
private boolean isFoiling(BravoFix fix) {
@@ -98,44 +193,47 @@ public class BravoFixTrackImpl<ItemType extends WithID & Serializable> extends S
@Override
public Duration getTimeSpentFoiling(TimePoint from, TimePoint to) {
Duration result = Duration.NULL;
lockForRead();
try {
TimePoint last = from;
boolean isFoiling = false;
for (final BravoFix fix : getFixes(from, true, to, true)) {
final boolean fixFoils = isFoiling(fix);
if (isFoiling && fixFoils) {
result = result.plus(last.until(fix.getTimePoint()));
return getValueSum(from, to, /* nullElement */ Duration.NULL, Duration::plus, foilingTimeCache,
/* valueCalculator */ new Track.TimeRangeValueCalculator<Duration>() {
@Override
public Duration calculate(TimePoint from, TimePoint to) {
Duration result = Duration.NULL;
TimePoint last = from;
boolean isFoiling = false;
for (final BravoFix fix : getFixes(from, true, to, true)) {
final boolean fixFoils = isFoiling(fix);
if (isFoiling && fixFoils) {
result = result.plus(last.until(fix.getTimePoint()));
}
last = fix.getTimePoint();
isFoiling = fixFoils;
}
last = fix.getTimePoint();
isFoiling = fixFoils;
return result;
}
} finally {
unlockAfterRead();
}
return result;
});
}
@Override
public Distance getDistanceSpentFoiling(GPSFixTrack<Competitor, GPSFixMoving> gpsFixTrack, TimePoint from, TimePoint to) {
Distance result = Distance.NULL;
lockForRead();
try {
TimePoint last = from;
boolean isFoiling = false;
for (final BravoFix fix : getFixes(from, true, to, true)) {
final boolean fixFoils = isFoiling(fix);
if (isFoiling && fixFoils) {
result = result.add(gpsFixTrack.getDistanceTraveled(last, fix.getTimePoint()));
public Distance getDistanceSpentFoiling(TimePoint from, TimePoint to) {
assert gpsTrack != null;
return getValueSum(from, to, /* nullElement */ Distance.NULL, Distance::add, foilingDistanceCache,
/* valueCalculator */ new Track.TimeRangeValueCalculator<Distance>() {
@Override
public Distance calculate(TimePoint from, TimePoint to) {
Distance result = Distance.NULL;
TimePoint last = from;
boolean isFoiling = false;
for (final BravoFix fix : getFixes(from, true, to, true)) {
final boolean fixFoils = isFoiling(fix);
if (isFoiling && fixFoils) {
result = result.add(gpsTrack.getDistanceTraveled(last, fix.getTimePoint()));
}
last = fix.getTimePoint();
isFoiling = fixFoils;
}
last = fix.getTimePoint();
isFoiling = fixFoils;
return result;
}
} finally {
unlockAfterRead();
}
return result;
});
}
@Override
@@ -1,237 +0,0 @@
package com.sap.sailing.domain.tracking.impl;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map.Entry;
import java.util.NavigableSet;
import com.sap.sailing.domain.common.Distance;
import com.sap.sailing.domain.common.tracking.GPSFix;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.Util;
import com.sap.sse.common.Util.Pair;
import com.sap.sse.concurrent.LockUtil;
import com.sap.sse.concurrent.NamedReentrantReadWriteLock;
import com.sap.sse.util.impl.ArrayListNavigableSet;
/**
* Computing {@link #getDistanceTraveled(TimePoint, TimePoint)} is more expensive the longer the track is and the
* higher the tracking frequency. It is called regularly, usually for the same start time. Keeping to re-evaluate
* all distances between all GPS fixes, even if they remain constant, is a waste of resources, particularly when we
* assume that usually GPS fixes are appended to a track and not usually randomly inserted, even though this can
* happen.
* <p>
*
* This cache looks "backwards." It contains pairs whose first component represents a <code>to</code> parameter used
* in {@link #getDistanceTraveled(TimePoint, TimePoint)}. It is ordered by this component. The second component is a
* navigable, ordered set of pairs where the first pair component represents a <code>from</code> parameter used in
* {@link #getDistanceTraveled(TimePoint, TimePoint)} and the second pair component represents the result of
* {@link #getDistanceTraveled(TimePoint, TimePoint)} for this parameter combination. Note that the cache does only
* store distances for <code>from</code> time points actually passed to
* {@link #getDistanceTraveled(TimePoint, TimePoint)} and does not cache all interim combinations collected while
* computing a result. As such, it differs from a typical "dynamic programming" algorithm which would store all
* in-between values too. Assuming that <code>from</code> values are sparse (leg start, race start, ...), there
* doesn't seem to be much use in storing all the other in-between values too.
* <p>
*
* For implementation efficiency in combination with using a {@link ArrayListNavigableSet} for the values and in
* order to be able to efficiently extend a cache entry for a single <code>to</code> fix, the navigable sets
* containing the <code>from</code> fixes and distances are ordered such that earlier fixes come later in the set.
* This way, extending the cache entry for a <code>to</code> fix to an earlier <code>from</code> fix only requires
* appending to the set.
* <p>
*
* <b>Invalidation</b>: When a new fix is added to this track, all distance cache entries for fixes at or later than
* the new fix's time point are removed from this cache. Additionally, the fix insertion may have an impact on the
* {@link #getEarlierFixesWhoseValidityMayBeAffected(GPSFix) previous fix's} validity (track smoothing) and
* therefore on its selection for distance aggregation. Therefore, if fix addition turned the previous fix invalid,
* the cache entries for the time points at or after the previous fix also need to be removed.
* <p>
*
* <b>Cache use</b>: When {@link #getDistanceTraveled(TimePoint, TimePoint)} is called, it first looks for a cache
* entry for the <code>to</code> parameter. If one is found, the earliest entry in the navigable set for the
* navigable set of <code>from</code> and distance values that is at or after the requested <code>from</code> time
* point is determined. If such an entry exists, the distance is remembered and the algorithm is repeated
* recursively, using the <code>from</code> value found in the cache as the new <code>to</code> value, and the
* <code>from</code> value originally passed to {@link #getDistanceTraveled(TimePoint, TimePoint)} as
* <code>from</code> again. If no entry is found in the cache entry for <code>to</code> that is at or after the
* requested <code>from</code> time, the distance is computed by iterating the smoothened fixes.
* <p>
*
* If a cache entry for <code>to</code> is not found, the latest cache entry before it is looked up. If one is
* found, the distance between the <code>to</code> time point requested and the <code>to</code> time point found in
* the cache is computed by iterating the smoothened fixes for this interval. If none is found, the distance is
* computed by iterating backwards all the way to <code>from</code>.
* <p>
*
* Once the {@link #getDistanceTraveled(TimePoint, TimePoint)} has computed its value, it adds the result to the
* cache.
*
* @author Axel Uhl (D043530)
*/
public class DistanceCache {
public static final int MAX_SIZE = 100;
private final NavigableSet<Util.Pair<TimePoint, NavigableSet<Util.Pair<TimePoint, Distance>>>> distanceCache;
/**
* The cache is to have limited size. Eviction shall happen based on a least-recently-used strategy. Usage is
* defined as having been returned by {@link #getEarliestFromAndDistanceAtOrAfterFrom(TimePoint, TimePoint)} or
* having been added by {@link #cache(TimePoint, TimePoint, Distance)}.
* <p>
*
* When an eldest entry is asked to be expunged from this map and the map has more than {@link #MAX_SIZE} elements,
* the expunging will be admitted, and the entry is removed from the {@link #distanceCache} core structure. Reading
* and writing this structure must happen under the {@link #lock write lock} because also reading the linked hash
* map that counts access as "use" has a modifying effect on its internal structures.
* <p>
*
* The key pairs are from/to pairs. Note that this is in some sense "the opposite direction" compared to the
* alignment of the {@link #distanceCache} structure which has as its outer keys the "to" time point.<p>
*
* Read access is to be <code>synchronized<code> using this field's mutex; write access only happens under the
* {@link #lock write lock} and therefore will have no contenders.
*/
private final LinkedHashMap<Util.Pair<TimePoint, TimePoint>, Void> lruCache;
private final NamedReentrantReadWriteLock lock;
private static final Comparator<Util.Pair<TimePoint, ?>> timePointInPairComparator = new Comparator<Util.Pair<TimePoint, ?>>() {
@Override
public int compare(Util.Pair<TimePoint, ?> o1, Util.Pair<TimePoint, ?> o2) {
return o1.getA().compareTo(o2.getA());
}
};
public DistanceCache(String nameForLockLogging) {
lock = new NamedReentrantReadWriteLock("lock for DistanceCache for "+nameForLockLogging, /* fair */ true);
this.distanceCache = new ArrayListNavigableSet<Util.Pair<TimePoint, NavigableSet<Util.Pair<TimePoint, Distance>>>>(timePointInPairComparator);
this.lruCache = new LinkedHashMap<Util.Pair<TimePoint, TimePoint>, Void>(/* initial capacity */ 10, /* load factor */ 0.75f,
/* access-based ordering */ true) {
private static final long serialVersionUID = -6568235517111733193L;
@Override
protected boolean removeEldestEntry(Entry<Pair<TimePoint, TimePoint>, Void> eldest) {
final boolean expunge = size() > MAX_SIZE;
if (expunge) {
removeCacheEntry(eldest.getKey().getA(), eldest.getKey().getB());
}
return expunge;
}
};
}
public int size() {
return lruCache.size();
}
private void removeCacheEntry(TimePoint from, TimePoint to) {
assert lock.getWriteHoldCount() == 1; // we can be sure we are alone here; this only happens when adding a new entry, holding the write lock
Util.Pair<TimePoint, NavigableSet<Util.Pair<TimePoint, Distance>>> entryForTo = distanceCache.floor(createDummy(to));
if (entryForTo.getA().equals(to)) {
Pair<TimePoint, Distance> entryForFrom = entryForTo.getB().ceiling(new Util.Pair<TimePoint, Distance>(from, null));
if (entryForFrom.getA().equals(from)) {
entryForTo.getB().remove(entryForFrom);
if (entryForTo.getB().isEmpty()) {
distanceCache.remove(entryForTo);
}
}
}
}
/**
* Looks up the entry closest to but no later than <code>to</code>. If not found, <code>null</code> is returned. If
* found, the earliest pair of from/distance that is at or after <code>from</code> will be returned, together with
* the <code>to</code> value of the entry. If there is no entry that is at or after <code>from</code>,
* <code>null</code> is returned.
*/
public Util.Pair<TimePoint, Util.Pair<TimePoint, Distance>> getEarliestFromAndDistanceAtOrAfterFrom(TimePoint from, TimePoint to) {
LockUtil.lockForRead(lock);
try {
Util.Pair<TimePoint, Util.Pair<TimePoint, Distance>> result = null;
Util.Pair<TimePoint, NavigableSet<Util.Pair<TimePoint, Distance>>> entryForTo = distanceCache.floor(createDummy(to));
if (entryForTo != null) {
final Util.Pair<TimePoint, Distance> fromCeiling = entryForTo.getB().ceiling(new Util.Pair<TimePoint, Distance>(from, null));
if (fromCeiling != null) {
result = new Util.Pair<TimePoint, Util.Pair<TimePoint, Distance>>(entryForTo.getA(), fromCeiling);
}
}
// no writer can be active because we're holding the read lock; read access on the lruCache is synchronized using
// the lruCache's mutex; this is necessary because we're using access-based LRU pinging where even getting an entry
// modifies the internal parts of the data structure which is not thread safe.
synchronized (lruCache) {
lruCache.get(new Util.Pair<TimePoint, TimePoint>(from, to));
}
return result;
} finally {
LockUtil.unlockAfterRead(lock);
}
}
/**
* Removes all cache entries that have a <code>to</code> time point that is at or after <code>timePoint</code>.
*/
public void invalidateAllAtOrLaterThan(TimePoint timePoint) {
LockUtil.lockForWrite(lock);
try {
Util.Pair<TimePoint, NavigableSet<Util.Pair<TimePoint, Distance>>> dummy = createDummy(timePoint);
NavigableSet<Util.Pair<TimePoint, NavigableSet<Util.Pair<TimePoint, Distance>>>> toRemove = distanceCache.tailSet(dummy, /* inclusive */ true);
for (Iterator<Util.Pair<TimePoint, NavigableSet<Util.Pair<TimePoint, Distance>>>> i=toRemove.iterator(); i.hasNext(); ) {
Util.Pair<TimePoint, NavigableSet<Util.Pair<TimePoint, Distance>>> entryToRemove = i.next();
assert entryToRemove.getA().compareTo(timePoint) >= 0;
for (Pair<TimePoint, Distance> fromAndDistance : entryToRemove.getB()) {
lruCache.remove(new Util.Pair<>(fromAndDistance.getA(), entryToRemove.getA()));
}
i.remove();
}
} finally {
LockUtil.unlockAfterWrite(lock);
}
}
private NavigableSet<Util.Pair<TimePoint, Distance>> getEntryForTo(TimePoint to) {
NavigableSet<Util.Pair<TimePoint, Distance>> result = null;
Util.Pair<TimePoint, NavigableSet<Util.Pair<TimePoint, Distance>>> dummyForTo = createDummy(to);
Util.Pair<TimePoint, NavigableSet<Util.Pair<TimePoint, Distance>>> entryForTo = distanceCache.floor(dummyForTo);
if (entryForTo != null && entryForTo.getA().equals(to)) {
result = entryForTo.getB();
}
return result;
}
public void cache(TimePoint from, TimePoint to, Distance distance) {
LockUtil.lockForWrite(lock);
try {
NavigableSet<Util.Pair<TimePoint, Distance>> entryForTo = getEntryForTo(to);
if (entryForTo == null) {
entryForTo = new ArrayListNavigableSet<Util.Pair<TimePoint, Distance>>(timePointInPairComparator);
Util.Pair<TimePoint, NavigableSet<Util.Pair<TimePoint, Distance>>> pairForTo = new Util.Pair<TimePoint, NavigableSet<Util.Pair<TimePoint, Distance>>>(
to, entryForTo);
distanceCache.add(pairForTo);
}
entryForTo.add(new Util.Pair<TimePoint, Distance>(from, distance));
lruCache.put(new Util.Pair<TimePoint, TimePoint>(from, to), null);
} finally {
LockUtil.unlockAfterWrite(lock);
}
}
private Util.Pair<TimePoint, NavigableSet<Util.Pair<TimePoint, Distance>>> createDummy(TimePoint to) {
return new Util.Pair<TimePoint, NavigableSet<Util.Pair<TimePoint, Distance>>>(to, null);
}
/**
* Removes all contents from this cache
*/
public void clear() {
LockUtil.lockForWrite(lock);
try {
distanceCache.clear();
lruCache.clear();
} finally {
LockUtil.unlockAfterWrite(lock);
}
}
}
@@ -38,6 +38,7 @@ import com.sap.sailing.domain.common.tracking.WithValidityCache;
import com.sap.sailing.domain.common.tracking.impl.CompactPositionHelper;
import com.sap.sailing.domain.tracking.GPSFixTrack;
import com.sap.sailing.domain.tracking.GPSTrackListener;
import com.sap.sailing.domain.tracking.Track;
import com.sap.sailing.domain.tracking.TrackedRace;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.TimeRange;
@@ -116,7 +117,7 @@ public abstract class GPSFixTrackImpl<ItemType, FixType extends GPSFix> extends
* Once the {@link #getDistanceTraveled(TimePoint, TimePoint)} has computed its value, it adds the result to the
* cache.
*/
private transient DistanceCache distanceCache;
private transient TimeRangeCache<Distance> distanceCache;
private transient MaxSpeedCache<ItemType, FixType> maxSpeedCache;
@@ -155,7 +156,7 @@ public abstract class GPSFixTrackImpl<ItemType, FixType extends GPSFix> extends
this.millisecondsOverWhichToAverage = millisecondsOverWhichToAverage;
this.maxSpeedForSmoothing = maxSpeedForSmoothening;
this.listeners = new TrackListenerCollection<ItemType, FixType, GPSTrackListener<ItemType,FixType>>();
this.distanceCache = new DistanceCache(trackedItem==null?"null":trackedItem.toString());
this.distanceCache = new TimeRangeCache<>(trackedItem==null?"null":trackedItem.toString());
this.maxSpeedCache = createMaxSpeedCache();
this.validityCachingSuspended = false;
this.losslessCompaction = losslessCompaction;
@@ -190,7 +191,7 @@ public abstract class GPSFixTrackImpl<ItemType, FixType extends GPSFix> extends
private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException {
ois.defaultReadObject();
distanceCache = new DistanceCache(getTrackedItem().toString());
distanceCache = new TimeRangeCache<>(getTrackedItem().toString());
maxSpeedCache = createMaxSpeedCache();
}
@@ -506,76 +507,33 @@ public abstract class GPSFixTrackImpl<ItemType, FixType extends GPSFix> extends
@Override
public Distance getDistanceTraveled(TimePoint from, TimePoint to) {
return getDistanceTraveledRecursively(from, to, 0);
}
private Distance getDistanceTraveledRecursively(TimePoint from, TimePoint to, int recursionDepth) {
Distance result;
if (!from.before(to)) {
result = Distance.NULL;
} else {
boolean perfectCacheHit = false;
lockForRead();
try {
Util.Pair<TimePoint, Util.Pair<TimePoint, Distance>> bestCacheEntry = getDistanceCache()
.getEarliestFromAndDistanceAtOrAfterFrom(from, to);
if (bestCacheEntry != null) {
perfectCacheHit = true; // potentially a cache hit; but if it doesn't span the full interval, it's not perfect; see below
// compute the missing stretches between best cache entry's "from" and our "from" and the cache
// entry's "to" and our "to"
Distance distanceFromFromToBeginningOfCacheEntry = Distance.NULL;
Distance distanceFromEndOfCacheEntryToTo = Distance.NULL;
if (!bestCacheEntry.getB().getA().equals(from)) {
assert bestCacheEntry.getB().getA().after(from);
perfectCacheHit = false;
distanceFromFromToBeginningOfCacheEntry = getDistanceTraveledRecursively(from, bestCacheEntry
.getB().getA(), recursionDepth + 1);
}
if (!bestCacheEntry.getA().equals(to)) {
assert bestCacheEntry.getA().before(to);
perfectCacheHit = false;
distanceFromEndOfCacheEntryToTo = getDistanceTraveledRecursively(bestCacheEntry.getA(), to,
recursionDepth + 1);
}
result = distanceFromFromToBeginningOfCacheEntry.add(bestCacheEntry.getB().getB()).add(
distanceFromEndOfCacheEntryToTo);
} else {
double distanceInNauticalMiles = 0;
if (from.compareTo(to) < 0) {
return getValueSum(from, to, /* nullElement */ Distance.NULL, Distance::add, getDistanceCache(),
/* valueCalculator */ new Track.TimeRangeValueCalculator<Distance>() {
@Override
public Distance calculate(TimePoint from, TimePoint to) {
// getEstimatedPosition's current implementation returns a position equal to that of a fix at
// "from" if there is one with exactly that time stamp
Distance distance = Distance.NULL;
Position fromPos = getEstimatedPosition(from, /* extrapolate */false);
if (fromPos == null) {
result = Distance.NULL;
} else {
if (fromPos != null) {
// TODO bug 4122: idea: use the subset to determine fromPos and toPos in one go, eliminating two expensive getEstimatedPosition calls
NavigableSet<GPSFix> subset = getGPSFixes().subSet(new DummyGPSFix(from),
/* fromInclusive */false, new DummyGPSFix(to),
/* toInclusive */false);
for (GPSFix fix : subset) {
double distanceBetweenAdjacentFixesInNauticalMiles = fromPos.getDistance(
fix.getPosition()).getNauticalMiles();
distanceInNauticalMiles += distanceBetweenAdjacentFixesInNauticalMiles;
Distance distanceBetweenAdjacentFixes = fromPos.getDistance(
fix.getPosition());
distance = distance.add(distanceBetweenAdjacentFixes);
fromPos = fix.getPosition();
}
Position toPos = getEstimatedPosition(to, false);
distanceInNauticalMiles += fromPos.getDistance(toPos).getNauticalMiles();
result = new NauticalMileDistance(distanceInNauticalMiles);
distance = distance.add(fromPos.getDistance(toPos));
}
} else {
result = Distance.NULL;
return distance;
}
}
} finally {
unlockAfterRead();
}
if (!perfectCacheHit && recursionDepth == 0) {
getDistanceCache().cache(from, to, result);
}
}
return result;
});
}
@Override
public Distance getRawDistanceTraveled(TimePoint from, TimePoint to) {
lockForRead();
@@ -1065,7 +1023,7 @@ public abstract class GPSFixTrackImpl<ItemType, FixType extends GPSFix> extends
}
}
protected DistanceCache getDistanceCache() {
protected TimeRangeCache<Distance> getDistanceCache() {
return distanceCache;
}
@@ -709,7 +709,7 @@ public class TrackedLegOfCompetitorImpl implements TrackedLegOfCompetitor {
public Distance getRideHeight(TimePoint at) {
final Distance result;
if (hasStartedLeg(at)) {
TimePoint timePoint =hasFinishedLeg(at) ? getMarkPassingForLegEnd().getTimePoint() : at;
TimePoint timePoint = hasFinishedLeg(at) ? getMarkPassingForLegEnd().getTimePoint() : at;
BravoFixTrack<Competitor> track = getTrackedRace()
.<BravoFix, BravoFixTrack<Competitor>> getSensorTrack(competitor, BravoFixTrack.TRACK_NAME);
result = track == null ? null : track.getRideHeight(timePoint);
@@ -719,6 +719,37 @@ public class TrackedLegOfCompetitorImpl implements TrackedLegOfCompetitor {
return result;
}
@Override
public Distance getDistanceFoiled(TimePoint at) {
final Distance result;
if (hasStartedLeg(at)) {
TimePoint timePoint = hasFinishedLeg(at) ? getMarkPassingForLegEnd().getTimePoint() : at;
BravoFixTrack<Competitor> track = getTrackedRace()
.<BravoFix, BravoFixTrack<Competitor>> getSensorTrack(competitor, BravoFixTrack.TRACK_NAME);
result = track == null ? null
: track.getDistanceSpentFoiling(getMarkPassingForLegStart().getTimePoint(),
timePoint);
} else {
result = null;
}
return result;
}
@Override
public Duration getDurationFoiled(TimePoint at) {
final Duration result;
if (hasStartedLeg(at)) {
TimePoint timePoint = hasFinishedLeg(at) ? getMarkPassingForLegEnd().getTimePoint() : at;
BravoFixTrack<Competitor> track = getTrackedRace()
.<BravoFix, BravoFixTrack<Competitor>> getSensorTrack(competitor, BravoFixTrack.TRACK_NAME);
result = track == null ? null
: track.getTimeSpentFoiling(getMarkPassingForLegStart().getTimePoint(), timePoint);
} else {
result = null;
}
return result;
}
@Override
public Duration getEstimatedTimeToNextMark(TimePoint timePoint, WindPositionMode windPositionMode) {
return getEstimatedTimeToNextMark(timePoint, windPositionMode, new LeaderboardDTOCalculationReuseCache(timePoint));
@@ -1212,7 +1212,50 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl
}
private Distance getDistanceTraveled(Competitor competitor, TimePoint timePoint, boolean considerGateStart) {
final Distance result;
return getValueFromStartToTimePointOrEnd(competitor, timePoint,
(from, to)->{
final Distance result;
final Distance preResult = getTrack(competitor).getDistanceTraveled(from, to);
if (considerGateStart && preResult != null) {
result = preResult.add(getAdditionalGateStartDistance(competitor, timePoint));
} else {
result = preResult;
}
return result;
});
}
@Override
public Distance getDistanceFoiled(Competitor competitor, TimePoint timePoint) {
return getBravoValue(competitor, timePoint, BravoFixTrack::getDistanceSpentFoiling);
}
@Override
public Duration getDurationFoiled(Competitor competitor, TimePoint timePoint) {
return getBravoValue(competitor, timePoint, BravoFixTrack::getTimeSpentFoiling);
}
@FunctionalInterface
private static interface BravoFromToValueCalculator<T> {
T getValue(BravoFixTrack<Competitor> bravoFixTrack, TimePoint from, TimePoint to);
}
private <T> T getBravoValue(Competitor competitor, TimePoint timePoint, BravoFromToValueCalculator<T> bravoValueCalculator) {
return getValueFromStartToTimePointOrEnd(competitor, timePoint,
(from, to)->{
final T result;
final BravoFixTrack<Competitor> bravoFixTrack = getSensorTrack(competitor, BravoFixTrack.TRACK_NAME);
if (bravoFixTrack != null) {
result = bravoValueCalculator.getValue(bravoFixTrack, from, to);
} else {
result = null;
}
return result;
});
}
private <T> T getValueFromStartToTimePointOrEnd(Competitor competitor, TimePoint timePoint, Track.TimeRangeValueCalculator<T> valueCalculator) {
final T result;
NavigableSet<MarkPassing> markPassings = getMarkPassings(competitor);
try {
lockForRead(markPassings);
@@ -1239,12 +1282,7 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl
if (end == null) {
result = null;
} else {
final Distance preResult = getTrack(competitor).getDistanceTraveled(markPassings.first().getTimePoint(), end);
if (considerGateStart && preResult != null) {
result = preResult.add(getAdditionalGateStartDistance(competitor, timePoint));
} else {
result = preResult;
}
result = valueCalculator.calculate(markPassings.first().getTimePoint(), end);
}
}
return result;
@@ -70,6 +70,7 @@ import com.sap.sailing.domain.tracking.WindStore;
import com.sap.sailing.domain.tracking.WindTrack;
import com.sap.sailing.domain.tracking.WindWithConfidence;
import com.sap.sailing.domain.tracking.impl.WindTrackImpl;
import com.sap.sse.common.Duration;
import com.sap.sse.common.IsManagedByCache;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.Util;
@@ -406,6 +407,16 @@ public class MockedTrackedRace implements DynamicTrackedRace {
return null;
}
@Override
public Distance getDistanceFoiled(Competitor competitor, TimePoint timePoint) {
return null;
}
@Override
public Duration getDurationFoiled(Competitor competitor, TimePoint timePoint) {
return null;
}
@Override
public Distance getWindwardDistanceToCompetitorFarthestAhead(Competitor competitor, TimePoint timePoint, WindPositionMode windPositionMode) {
return null;
@@ -13,6 +13,11 @@
state of the currently opened page to get lost.
After a sign out (and another page reload) the language preference will stop to take effect.
</li>
<li>There are additional column types in the leaderboard that show per leg, per race and overall statistics
for distance foiled and duration foiled. This allows for quick competitor comparisons regarding these
figures which are so important for any foiling boat class. Note, that this feature is also available
on overall leaderboards, providing foiling stats across an entire season or event series.
</li>
</ul>
<h5 class="articleSubheadline">October 2017</h5>
@@ -6,13 +6,13 @@
padding: 0 1em !important;
max-width: 80em;
}
h2, .container > div, p > span:first-child {
.container > h2, .container > div, .container > p > span:first-child {
text-transform: uppercase;
}
.container > div, p > span:first-child {
.container > div, .container > p > span:first-child {
text-decoration: underline;
}
p > span:first-child:after {
.container > p > span:first-child:after {
content: ":"
}
</ui:style>
@@ -46,17 +46,17 @@ public abstract class LeaderboardSettings extends AbstractGenericSerializableSet
@Override
protected void addChildSettings() {
isShowCompetitorNationality = new BooleanSetting("showCompetitorNationality", this, false);
List<DetailType> maneuverDetails = new ArrayList<DetailType>();
List<DetailType> maneuverDetails = new ArrayList<>();
maneuverDetails.add(DetailType.TACK);
maneuverDetails.add(DetailType.JIBE);
maneuverDetails.add(DetailType.PENALTY_CIRCLE);
maneuverDetailsToShow = new EnumSetSetting<>("maneuverDetailsToShow", this, maneuverDetails, DetailType::valueOf);
List<DetailType> legDetails = new ArrayList<DetailType>();
List<DetailType> legDetails = new ArrayList<>();
legDetails.add(DetailType.DISTANCE_TRAVELED);
legDetails.add(DetailType.AVERAGE_SPEED_OVER_GROUND_IN_KNOTS);
legDetails.add(DetailType.RANK_GAIN);
legDetailsToShow = new EnumSetSetting<>("legDetailsToShow", this, legDetails, DetailType::valueOf);
List<DetailType> raceDetails = new ArrayList<DetailType>();
List<DetailType> raceDetails = new ArrayList<>();
raceDetails.add(DetailType.DISPLAY_LEGS);
raceDetailsToShow = new EnumSetSetting<>("raceDetailsToShow", this, raceDetails, DetailType::valueOf);
List<DetailType> overallDetails = new ArrayList<>();
@@ -40,12 +40,20 @@ public class DetailTypeFormatter {
return stringMessages.currentSpeedOverGroundInKnots();
case CURRENT_RIDE_HEIGHT_IN_METERS:
return stringMessages.currentOrAverageRideHeightInMeters();
case CURRENT_DISTANCE_FOILED_IN_METERS:
return stringMessages.currentDistanceFoiledInMeters();
case CURRENT_DURATION_FOILED_IN_SECONDS:
return stringMessages.currentDurationFoiled();
case CURRENT_HEEL_IN_DEGREES:
return stringMessages.currentHeelInDegree();
case CURRENT_PITCH_IN_DEGREES:
return stringMessages.currentPitchInDegree();
case RACE_CURRENT_RIDE_HEIGHT_IN_METERS:
return stringMessages.currentRideHeightInMeters();
case RACE_CURRENT_DURATION_FOILED_IN_SECONDS:
return stringMessages.currentDurationFoiled();
case RACE_CURRENT_DISTANCE_FOILED_IN_METERS:
return stringMessages.currentDistanceFoiledInMeters();
case CURRENT_PORT_DAGGERBOARD_RAKE:
return stringMessages.currentPortDaggerboardRake();
case CURRENT_STBD_DAGGERBOARD_RAKE:
@@ -158,6 +166,10 @@ public class DetailTypeFormatter {
return stringMessages.totalTimeSailedInSeconds();
case TOTAL_AVERAGE_SPEED_OVER_GROUND:
return stringMessages.totalAverageSpeedOverGround();
case TOTAL_DURATION_FOILED_IN_SECONDS:
return stringMessages.totalDurationFoiledInSeconds();
case TOTAL_DISTANCE_FOILED_IN_METERS:
return stringMessages.totalDistanceFoiledInMeters();
case AVERAGE_MANEUVER_LOSS_IN_METERS:
return stringMessages.averageManeuverLossInMeters();
case AVERAGE_TACK_LOSS_IN_METERS:
@@ -188,8 +200,6 @@ public class DetailTypeFormatter {
return stringMessages.beatAngle();
case COURSE_OVER_GROUND_TRUE_DEGREES:
return stringMessages.courseOverGroundTrueDegrees();
default:
break;
}
return null;
}
@@ -241,7 +251,10 @@ public class DetailTypeFormatter {
case DISTANCE_TO_START_LINE:
case CURRENT_RIDE_HEIGHT_IN_METERS:
case RACE_CURRENT_RIDE_HEIGHT_IN_METERS:
case RACE_CURRENT_DISTANCE_FOILED_IN_METERS:
case CURRENT_DEPTH_IN_METERS:
case CURRENT_DISTANCE_FOILED_IN_METERS:
case TOTAL_DISTANCE_FOILED_IN_METERS:
return stringMessages.metersUnit();
case CURRENT_DEFLECTOR_IN_MILLIMETERS:
@@ -296,6 +309,9 @@ public class DetailTypeFormatter {
case RACE_TIME_TRAVELED_REACHING:
case RACE_TIME_TRAVELED_UPWIND:
case TIME_TRAVELED:
case CURRENT_DURATION_FOILED_IN_SECONDS:
case RACE_CURRENT_DURATION_FOILED_IN_SECONDS:
case TOTAL_DURATION_FOILED_IN_SECONDS:
return stringMessages.hhmmssUnit();
// Cases for detail types without unit, so that an empty string is returned.
@@ -385,8 +401,16 @@ public class DetailTypeFormatter {
return stringMessages.currentSpeedOverGroundInKnotsTooltip();
case CURRENT_RIDE_HEIGHT_IN_METERS:
return stringMessages.currentOrAverageRideHeightInMetersTooltip();
case CURRENT_DISTANCE_FOILED_IN_METERS:
return stringMessages.currentDistanceFoiledInMetersTooltip();
case CURRENT_DURATION_FOILED_IN_SECONDS:
return stringMessages.currentDurationFoiledTooltip();
case RACE_CURRENT_RIDE_HEIGHT_IN_METERS:
return stringMessages.currentRideHeightInMetersTooltip();
case RACE_CURRENT_DURATION_FOILED_IN_SECONDS:
return stringMessages.currentDurationFoiledTooltip();
case RACE_CURRENT_DISTANCE_FOILED_IN_METERS:
return stringMessages.currentDistanceFoiledInMetersTooltip();
case CURRENT_PORT_DAGGERBOARD_RAKE:
return stringMessages.currentPortDaggerboardRakeTooltip();
case CURRENT_STBD_DAGGERBOARD_RAKE:
@@ -483,6 +507,10 @@ public class DetailTypeFormatter {
return stringMessages.totalDistanceTraveledTooltip();
case TOTAL_AVERAGE_SPEED_OVER_GROUND:
return stringMessages.totalAverageSpeedOverGroundTooltip();
case TOTAL_DURATION_FOILED_IN_SECONDS:
return stringMessages.totalDurationFoiledInSecondsTooltip();
case TOTAL_DISTANCE_FOILED_IN_METERS:
return stringMessages.totalDistanceFoiledInMetersTooltip();
case TIME_ON_TIME_FACTOR:
return stringMessages.timeOnTimeFactorTooltip();
case TIME_ON_DISTANCE_ALLOWANCE_IN_SECONDS_PER_NAUTICAL_MILE:
@@ -1972,4 +1972,12 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages,
String currentDriftInDegreesTooltip();
String currentDepthInMetersTooltip();
String tonsUnit();
String currentDistanceFoiledInMeters();
String currentDurationFoiled();
String currentDistanceFoiledInMetersTooltip();
String currentDurationFoiledTooltip();
String totalDurationFoiledInSeconds();
String totalDistanceFoiledInMeters();
String totalDurationFoiledInSecondsTooltip();
String totalDistanceFoiledInMetersTooltip();
}
@@ -1956,4 +1956,12 @@ currentRudderInDegreesTooltip=Rudder angle; negative values mean to port, positi
currentLeewayInDegreesTooltip=Leeway
currentDriftInDegreesTooltip=Angle between heading and course over ground
currentDepthInMetersTooltip=Water depth
tonsUnit=t
tonsUnit=t
currentDistanceFoiledInMeters=Distance Foiled
currentDurationFoiled=Time Foiled
currentDistanceFoiledInMetersTooltip=Distance spent foiling in meters
currentDurationFoiledTooltip=Time spent foiling
totalDurationFoiledInSeconds=Total Time Foiled
totalDistanceFoiledInMeters=Total Distance Foiled
totalDurationFoiledInSecondsTooltip=Total time foiled, across the entire leaderboard
totalDistanceFoiledInMetersTooltip=Total distance foiled, across the entire leaderboard
@@ -1942,12 +1942,20 @@ currentTargetBoatspeedPercentageTooltip=Anteil der erreichten berechneten Zielge
currentRakeInDegreesTooltip=Mastneigung
currentForestayLoadTooltip=Gemessene Vorstagsspannung
currentForestayPressureTooltip=Druck am Vorstag
currentDeflectorPercentageTooltip=Prozentualer Anteil Abweiser (???)
currentDeflectorInMillimetersTooltip=Abweiser Abstand (???)
currentDeflectorPercentageTooltip=Prozentualer Anteil Deflektor
currentDeflectorInMillimetersTooltip=Deflektor-Abstand
currentTackAngleInDegreesTooltip=Berechneter optimaler Wendewinkel
currentSetTooltip=Set (???)
currentRudderInDegreesTooltip=Ruderwinkel; negative Werte bedeuten Ruder nach Backbord, positive Ruder nach Steuerbord
currentLeewayInDegreesTooltip=Leeway (???)
currentDriftInDegreesTooltip=Winkel zwischen Kurs durchs Wasser und Kurs über Grund
currentDepthInMetersTooltip=Wassertiefe
tonsUnit=t
tonsUnit=t
currentDistanceFoiledInMeters=Gefoilte Strecke
currentDurationFoiled=Gefoilte Zeit
currentDistanceFoiledInMetersTooltip=Gefoilte Strecke in Metern
currentDurationFoiledTooltip=Gefoilte Zeit
totalDurationFoiledInSeconds=Gesamt gefoilte Zeit
totalDistanceFoiledInMeters=Gesamt gefoilte Strecke
totalDurationFoiledInSecondsTooltip=Insgesamt gefoilte Zeit, über die gesamte Ergebnisliste kumuliert
totalDistanceFoiledInMetersTooltip=Insgesamt gefoilte Strecke, über die gesamte Ergebnisliste kumuliert
@@ -1118,6 +1118,7 @@ public abstract class LeaderboardPanel<LS extends LeaderboardSettings> extends A
DetailType.RACE_CALCULATED_TIME_TRAVELED,
DetailType.RACE_CALCULATED_TIME_AT_ESTIMATED_ARRIVAL_AT_COMPETITOR_FARTHEST_AHEAD,
DetailType.RACE_CURRENT_SPEED_OVER_GROUND_IN_KNOTS, DetailType.RACE_CURRENT_RIDE_HEIGHT_IN_METERS,
DetailType.RACE_CURRENT_DISTANCE_FOILED_IN_METERS, DetailType.RACE_CURRENT_DURATION_FOILED_IN_SECONDS,
DetailType.RACE_DISTANCE_TO_COMPETITOR_FARTHEST_AHEAD_IN_METERS, DetailType.NUMBER_OF_MANEUVERS,
DetailType.DISPLAY_LEGS, DetailType.CURRENT_LEG,
DetailType.RACE_AVERAGE_ABSOLUTE_CROSS_TRACK_ERROR_IN_METERS,
@@ -1138,6 +1139,7 @@ public abstract class LeaderboardPanel<LS extends LeaderboardSettings> extends A
public static DetailType[] getAvailableOverallDetailColumnTypes() {
return new DetailType[] { DetailType.REGATTA_RANK, DetailType.TOTAL_DISTANCE_TRAVELED,
DetailType.TOTAL_AVERAGE_SPEED_OVER_GROUND, DetailType.TOTAL_TIME_SAILED_IN_SECONDS,
DetailType.TOTAL_DURATION_FOILED_IN_SECONDS, DetailType.TOTAL_DISTANCE_FOILED_IN_METERS,
DetailType.MAXIMUM_SPEED_OVER_GROUND_IN_KNOTS, DetailType.TIME_ON_TIME_FACTOR,
DetailType.TIME_ON_DISTANCE_ALLOWANCE_IN_SECONDS_PER_NAUTICAL_MILE };
}
@@ -1206,6 +1208,14 @@ public abstract class LeaderboardPanel<LS extends LeaderboardSettings> extends A
new RideHeightColumn(DetailType.RACE_CURRENT_RIDE_HEIGHT_IN_METERS,
new RaceCurrentRideHeightInMeters(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE,
LeaderboardPanel.this));
result.put(DetailType.RACE_CURRENT_DISTANCE_FOILED_IN_METERS,
new FormattedDoubleDetailTypeColumn(DetailType.RACE_CURRENT_DISTANCE_FOILED_IN_METERS,
new RaceDistanceFoiledInMeters(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE,
LeaderboardPanel.this));
result.put(DetailType.RACE_CURRENT_DURATION_FOILED_IN_SECONDS,
new TotalTimeColumn(DetailType.RACE_CURRENT_DURATION_FOILED_IN_SECONDS,
new RaceDurationFoiledInSeconds(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE,
LeaderboardPanel.this));
result.put(DetailType.RACE_DISTANCE_TO_COMPETITOR_FARTHEST_AHEAD_IN_METERS,
new FormattedDoubleDetailTypeColumn(DetailType.RACE_DISTANCE_TO_COMPETITOR_FARTHEST_AHEAD_IN_METERS,
new RaceDistanceToCompetitorFarthestAheadInMeters(), LEG_COLUMN_HEADER_STYLE,
@@ -1347,7 +1357,7 @@ public abstract class LeaderboardPanel<LS extends LeaderboardSettings> extends A
private class RaceAverageSpeedInKnots implements LegDetailField<Double> {
@Override
public Double get(LeaderboardRowDTO row) {
final Distance distanceTraveledInMeters = row.getDistanceTraveledInMeters(getRaceColumnName());
final Distance distanceTraveledInMeters = row.getDistanceTraveled(getRaceColumnName());
final Duration time = row.getTimeSailed(getRaceColumnName());
final Double result;
if (distanceTraveledInMeters != null && time != null) {
@@ -1537,11 +1547,37 @@ public abstract class LeaderboardPanel<LS extends LeaderboardSettings> extends A
private class RaceDistanceTraveledInMeters implements LegDetailField<Double> {
@Override
public Double get(LeaderboardRowDTO row) {
Distance distanceForRaceColumn = row.getDistanceTraveledInMeters(getRaceColumnName());
Distance distanceForRaceColumn = row.getDistanceTraveled(getRaceColumnName());
return distanceForRaceColumn == null ? null : distanceForRaceColumn.getMeters();
}
}
/**
* Accumulates the distance foiled over all legs of a race
*
* @author Axel Uhl (D043530)
*/
private class RaceDistanceFoiledInMeters implements LegDetailField<Double> {
@Override
public Double get(LeaderboardRowDTO row) {
Distance distanceForRaceColumn = row.getDistanceFoiled(getRaceColumnName());
return distanceForRaceColumn == null ? null : distanceForRaceColumn.getMeters();
}
}
/**
* Accumulates the duration foiled over all legs of a race
*
* @author Axel Uhl (D043530)
*/
private class RaceDurationFoiledInSeconds implements LegDetailField<Double> {
@Override
public Double get(LeaderboardRowDTO row) {
Duration durationForRaceColumn = row.getDurationFoiled(getRaceColumnName());
return durationForRaceColumn == null ? null : durationForRaceColumn.asSeconds();
}
}
private class RaceCalculatedTimeTraveledInSeconds implements LegDetailField<Double> {
@Override
public Double get(LeaderboardRowDTO row) {
@@ -2055,6 +2091,20 @@ public abstract class LeaderboardPanel<LS extends LeaderboardSettings> extends A
}
}
private static class TotalDistanceFoiledInMetersField implements LegDetailField<Double> {
@Override
public Double get(LeaderboardRowDTO row) {
return row.totalDistanceFoiledInMeters;
}
}
private static class TotalDurationFoiledInSecondsField implements LegDetailField<Double> {
@Override
public Double get(LeaderboardRowDTO row) {
return row.totalDurationFoiledInSeconds;
}
}
private static class TimeOnTimeFactorColumn implements LegDetailField<Double> {
@Override
public Double get(LeaderboardRowDTO row) {
@@ -2096,6 +2146,10 @@ public abstract class LeaderboardPanel<LS extends LeaderboardSettings> extends A
result.put(DetailType.MAXIMUM_SPEED_OVER_GROUND_IN_KNOTS,
new MaxSpeedOverallColumn(RACE_COLUMN_HEADER_STYLE, RACE_COLUMN_STYLE, this));
result.put(DetailType.TOTAL_TIME_SAILED_IN_SECONDS, createOverallTimeTraveledColumn());
result.put(DetailType.TOTAL_DURATION_FOILED_IN_SECONDS, new TotalTimeColumn(DetailType.TOTAL_DURATION_FOILED_IN_SECONDS,
new TotalDurationFoiledInSecondsField(), RACE_COLUMN_HEADER_STYLE, RACE_COLUMN_STYLE, this));
result.put(DetailType.TOTAL_DISTANCE_FOILED_IN_METERS, new FormattedDoubleDetailTypeColumn(DetailType.TOTAL_DISTANCE_FOILED_IN_METERS,
new TotalDistanceFoiledInMetersField(), RACE_COLUMN_HEADER_STYLE, RACE_COLUMN_STYLE, this));
result.put(DetailType.TIME_ON_TIME_FACTOR, new FormattedDoubleDetailTypeColumn(DetailType.TIME_ON_TIME_FACTOR,
new TimeOnTimeFactorColumn(), RACE_COLUMN_HEADER_STYLE, RACE_COLUMN_STYLE, this));
result.put(DetailType.TIME_ON_DISTANCE_ALLOWANCE_IN_SECONDS_PER_NAUTICAL_MILE,
@@ -122,6 +122,20 @@ public class LegColumn extends ExpandableSortableColumn<String> {
}
}
private class CurrentDistanceFoiledInMeters extends AbstractLegDetailField<Double> {
@Override
protected Double getFromNonNullEntry(LegEntryDTO entry) {
return entry.currentDistanceFoiledInMeters;
}
}
private class CurrentDurationFoiled extends AbstractLegDetailField<Double> {
@Override
protected Double getFromNonNullEntry(LegEntryDTO entry) {
return entry.currentDurationFoiledInSeconds;
}
}
private class EstimatedTimeToNextWaypointInSeconds extends AbstractLegDetailField<Double> {
@Override
protected Double getFromNonNullEntry(LegEntryDTO entry) {
@@ -272,6 +286,7 @@ public class LegColumn extends ExpandableSortableColumn<String> {
DetailType.GAP_CHANGE_SINCE_LEG_START_IN_SECONDS, DetailType.SIDE_TO_WHICH_MARK_AT_LEG_START_WAS_ROUNDED,
DetailType.CURRENT_SPEED_OVER_GROUND_IN_KNOTS, DetailType.CURRENT_HEEL_IN_DEGREES,
DetailType.CURRENT_PITCH_IN_DEGREES, DetailType.CURRENT_RIDE_HEIGHT_IN_METERS,
DetailType.CURRENT_DISTANCE_FOILED_IN_METERS, DetailType.CURRENT_DURATION_FOILED_IN_SECONDS,
DetailType.WINDWARD_DISTANCE_TO_GO_IN_METERS, DetailType.NUMBER_OF_MANEUVERS,
DetailType.ESTIMATED_TIME_TO_NEXT_WAYPOINT_IN_SECONDS, DetailType.VELOCITY_MADE_GOOD_IN_KNOTS,
DetailType.TIME_TRAVELED, DetailType.CORRECTED_TIME_TRAVELED, DetailType.AVERAGE_ABSOLUTE_CROSS_TRACK_ERROR_IN_METERS,
@@ -299,6 +314,10 @@ public class LegColumn extends ExpandableSortableColumn<String> {
detailHeaderStyle, detailColumnStyle, leaderboardPanel));
result.put(DetailType.CURRENT_RIDE_HEIGHT_IN_METERS, new RideHeightColumn(DetailType.CURRENT_RIDE_HEIGHT_IN_METERS,
new CurrentRideHeightInMeters(), detailHeaderStyle, detailColumnStyle, leaderboardPanel));
result.put(DetailType.CURRENT_DISTANCE_FOILED_IN_METERS, new FormattedDoubleDetailTypeColumn(DetailType.CURRENT_DISTANCE_FOILED_IN_METERS,
new CurrentDistanceFoiledInMeters(), detailHeaderStyle, detailColumnStyle, leaderboardPanel));
result.put(DetailType.CURRENT_DURATION_FOILED_IN_SECONDS, new TotalTimeColumn(DetailType.CURRENT_DURATION_FOILED_IN_SECONDS,
new CurrentDurationFoiled(), detailHeaderStyle, detailColumnStyle, leaderboardPanel));
result.put(DetailType.ESTIMATED_TIME_TO_NEXT_WAYPOINT_IN_SECONDS,
new FormattedDoubleDetailTypeColumn(DetailType.ESTIMATED_TIME_TO_NEXT_WAYPOINT_IN_SECONDS, new EstimatedTimeToNextWaypointInSeconds(),
detailHeaderStyle, detailColumnStyle, leaderboardPanel));
@@ -4,6 +4,12 @@ import com.sap.sailing.domain.common.DetailType;
import com.sap.sailing.domain.common.dto.LeaderboardRowDTO;
import com.sap.sailing.gwt.ui.client.DurationAsHoursMinutesSecondsFormatter;
/**
* Displays a {@code double} value that is assumed to represent seconds as a time in hours, minutes and seconds.
*
* @author Axel Uhl (D043530)
*
*/
public class TotalTimeColumn extends FormattedDoubleDetailTypeColumn {
protected TotalTimeColumn(DetailType detailType, LegDetailField<Double> field, String headerStyle,
@@ -6696,6 +6696,8 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
}
if (hasBravoTrack) {
availableDetailsTypes.add(DetailType.RACE_CURRENT_RIDE_HEIGHT_IN_METERS);
availableDetailsTypes.add(DetailType.RACE_CURRENT_DURATION_FOILED_IN_SECONDS);
availableDetailsTypes.add(DetailType.RACE_CURRENT_DISTANCE_FOILED_IN_METERS);
availableDetailsTypes.add(DetailType.CURRENT_HEEL_IN_DEGREES);
availableDetailsTypes.add(DetailType.CURRENT_PITCH_IN_DEGREES);
}
@@ -59,6 +59,7 @@ import com.sap.sailing.domain.tracking.WindStore;
import com.sap.sailing.domain.tracking.WindWithConfidence;
import com.sap.sailing.domain.tracking.impl.DynamicTrackedRegattaImpl;
import com.sap.sailing.domain.tracking.impl.EmptyWindStore;
import com.sap.sse.common.Duration;
import com.sap.sse.common.IsManagedByCache;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.Util;
@@ -359,6 +360,16 @@ public class DummyTrackedRace extends TrackedRaceWithWindEssentials {
return null;
}
@Override
public Distance getDistanceFoiled(Competitor competitor, TimePoint timePoint) {
return null;
}
@Override
public Duration getDurationFoiled(Competitor competitor, TimePoint timePoint) {
return null;
}
@Override
public Distance getWindwardDistanceToCompetitorFarthestAhead(Competitor competitor, TimePoint timePoint,
WindPositionMode windPositionMode) {
@@ -29,6 +29,12 @@
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZlcnNpb249JzEuMScgaGVpZ2h0PScxOHB4JyB3aWR0aD0nMTZweCc+PHRleHQgeD0nMCcgeT0nMTUnIGZpbGw9JyNlZmFhMDAnIGZvbnQtc2l6ZT0nMTYnPiYjOTY2MDs8L3RleHQ+PC9zdmc+");
background-position: right center;
background-repeat: no-repeat;
/* Fix for old firefox versions to make the native arrow disappear: */
text-overflow: "";
text-indent: 0.01px;
}
.languageSelection::-ms-expand {
display: none;
}
</ui:style>
<g:FlowPanel tag="span">