mirror of
https://github.com/eclipse-sailing-analytics/sailing-analytics.git
synced 2026-10-18 02:00:58 +00:00
Merge branch 'master' into bug4365
This commit is contained in:
commit
a40e19aed7
41 files changed
+1008
-496
No files matched your search
+2
-2
@@ -56,7 +56,7 @@ public class BravoFixTrackWithContext implements HasBravoFixTrackContext {
|
||||
end = endOfRace;
|
||||
}
|
||||
return bravoFixTrack.getDistanceSpentFoiling(
|
||||
getRaceOfCompetitorContext().getTrackedRaceContext().getTrackedRace().getTrack(getRaceOfCompetitorContext().getCompetitor()),
|
||||
startOfRace, end);
|
||||
startOfRace,
|
||||
end);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+6
-2
@@ -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),
|
||||
|
||||
+43
-2
@@ -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()) {
|
||||
|
||||
+41
-3
@@ -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;
|
||||
}
|
||||
|
||||
+14
@@ -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;
|
||||
|
||||
+48
-86
@@ -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
|
||||
|
||||
+2
-1
@@ -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
|
||||
|
||||
+2
-1
@@ -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
|
||||
|
||||
+2
-1
@@ -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
|
||||
|
||||
+8
@@ -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);
|
||||
}
|
||||
}
|
||||
+27
@@ -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();
|
||||
|
||||
}
|
||||
+221
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+55
@@ -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());
|
||||
}
|
||||
|
||||
+10
@@ -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) {
|
||||
|
||||
+11
@@ -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
|
||||
|
||||
+35
-5
@@ -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}.
|
||||
|
||||
+5
-1
@@ -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);
|
||||
|
||||
|
||||
@@ -612,7 +612,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.
|
||||
@@ -624,6 +624,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)}
|
||||
*/
|
||||
|
||||
+149
-51
@@ -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
|
||||
|
||||
-237
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+18
-60
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+32
-1
@@ -711,7 +711,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);
|
||||
@@ -721,6 +721,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));
|
||||
|
||||
+45
-7
@@ -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;
|
||||
|
||||
+11
@@ -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;
|
||||
|
||||
+5
@@ -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>
|
||||
|
||||
+3
-3
@@ -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>
|
||||
|
||||
+3
-3
@@ -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<>();
|
||||
|
||||
+30
-2
@@ -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:
|
||||
|
||||
+8
@@ -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();
|
||||
}
|
||||
+9
-1
@@ -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
|
||||
+11
-3
@@ -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
|
||||
+56
-2
@@ -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,
|
||||
|
||||
+19
@@ -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));
|
||||
|
||||
+6
@@ -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,
|
||||
|
||||
+2
@@ -6700,6 +6700,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);
|
||||
}
|
||||
|
||||
+11
@@ -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) {
|
||||
|
||||
+6
@@ -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">
|
||||
|
||||
Reference in new issue
Block a user