bug6064: expose SliceNumber only on slice, not full tracked leg of competitor

This commit is contained in:
Axel Uhl
2024-11-06 14:43:55 +01:00
parent 29b01c7132
commit 0e5a2b2142
9 changed files with 769 additions and 730 deletions
@@ -25,6 +25,7 @@ import com.sap.sailing.datamining.data.HasRaceResultOfCompetitorContext;
import com.sap.sailing.datamining.data.HasTackTypeSegmentContext;
import com.sap.sailing.datamining.data.HasTrackedLegContext;
import com.sap.sailing.datamining.data.HasTrackedLegOfCompetitorContext;
import com.sap.sailing.datamining.data.HasTrackedLegSliceOfCompetitorContext;
import com.sap.sailing.datamining.data.HasTrackedRaceContext;
import com.sap.sailing.datamining.data.HasWindFixContext;
import com.sap.sailing.datamining.data.HasWindTrackContext;
@@ -107,6 +108,7 @@ public class Activator extends AbstractDataMiningActivatorWithPredefinedQueries
internalClasses.add(HasRaceResultOfCompetitorContext.class);
internalClasses.add(HasTrackedLegContext.class);
internalClasses.add(HasTrackedLegOfCompetitorContext.class);
internalClasses.add(HasTrackedLegSliceOfCompetitorContext.class);
internalClasses.add(HasGPSFixContext.class);
internalClasses.add(HasWindTrackContext.class);
internalClasses.add(HasWindFixContext.class);
@@ -0,0 +1,126 @@
package com.sap.sailing.datamining.data;
import com.sap.sailing.domain.base.Boat;
import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.tracking.TrackedLegOfCompetitor;
import com.sap.sse.common.Distance;
import com.sap.sse.common.Duration;
import com.sap.sse.common.Speed;
import com.sap.sse.datamining.annotations.Connector;
import com.sap.sse.datamining.annotations.Dimension;
import com.sap.sse.datamining.annotations.Statistic;
import com.sap.sse.datamining.shared.impl.dto.ClusterDTO;
/**
* A "slice" of a competitor's tracked leg; slicing may happen in the retriever, perhaps influenced by some
* configuration parameters, e.g., based on windward distance sailed, time spent, or rhumb line distance sailed.
* A full leg is a special case of this.
*
* @author Axel Uhl (d043530)
*
*/
public interface AbstractHasTrackedLegSliceOfCompetitorContext extends HasWindOnTrackedLeg {
@Connector(scanForStatistics=false)
public HasTrackedLegContext getTrackedLegContext();
public TrackedLegOfCompetitor getTrackedLegOfCompetitor();
@Dimension(messageKey="RelativeScoreInRaceInPercent", ordinal=12)
public ClusterDTO getPercentageClusterForRelativeScoreInRace();
@Connector(messageKey="Boat")
public Boat getBoat();
@Connector(messageKey="Competitor")
public Competitor getCompetitor();
@Statistic(messageKey="DistanceTraveled", resultDecimals=2, ordinal=0)
public Distance getDistanceTraveled();
@Statistic(messageKey="SpeedAverage", resultDecimals=2, ordinal=0)
public Double getSpeedAverageInKnots();
@Statistic(messageKey="RankGainsOrLosses", resultDecimals=2, ordinal=1)
public Integer getRankGainsOrLosses();
@Statistic(messageKey="RelativeScore", resultDecimals=2, ordinal=2)
public Double getRelativeRank();
@Statistic(messageKey="AbsoluteRank", resultDecimals=0, ordinal=3)
public Integer getRankAtSliceFinish();
@Statistic(messageKey="AbsoluteRankAfterFirstQuarter", resultDecimals=0, ordinal=4)
public Integer getRankAfterFirstQuarter();
@Statistic(messageKey="AbsoluteRankAfterSecondQuarter", resultDecimals=0, ordinal=5)
public Integer getRankAfterSecondQuarter();
@Statistic(messageKey="AbsoluteRankAfterThirdQuarter", resultDecimals=0, ordinal=6)
public Integer getRankAfterThirdQuarter();
@Statistic(messageKey="AbsoluteRankAverageAcrossFirstThreeQuarters", resultDecimals=2, ordinal=7)
public Double getRankAverageAcrossFirstThreeQuarters();
@Statistic(messageKey="AbsoluteRankRhumbLineBasedAfterFirstQuarter", resultDecimals=0, ordinal=8)
public Integer getRankRhumbLineBasedAfterFirstQuarter();
@Statistic(messageKey="AbsoluteRankRhumbLineBasedAfterSecondQuarter", resultDecimals=0, ordinal=9)
public Integer getRankRhumbLineBasedAfterSecondQuarter();
@Statistic(messageKey="AbsoluteRankRhumbLineBasedAfterThirdQuarter", resultDecimals=0, ordinal=10)
public Integer getRankRhumbLineBasedAfterThirdQuarter();
@Statistic(messageKey="AbsoluteRankRhumbLineBasedAverageAcrossFirstThreeQuarters", resultDecimals=2, ordinal=11)
public Double getRankRhumbLineBasedAverageAcrossFirstThreeQuarters();
@Statistic(messageKey="AbsoluteRankPredictionErrorAfterFirstQuarter", resultDecimals=2, ordinal=12)
public Double getRankPredictionErrorAfterFirstQuarter();
@Statistic(messageKey="AbsoluteRankPredictionErrorAfterSecondQuarter", resultDecimals=2, ordinal=13)
public Double getRankPredictionErrorAfterSecondQuarter();
@Statistic(messageKey="AbsoluteRankPredictionErrorAfterThirdQuarter", resultDecimals=2, ordinal=14)
public Double getRankPredictionErrorAfterThirdQuarter();
@Statistic(messageKey="AbsoluteRankPredictionErrorAcrossFirstThreeQuarters", resultDecimals=2, ordinal=15)
public Double getRankPredictionErrorAcrossFirstThreeQuarters();
@Statistic(messageKey="AbsoluteRankPredictionErrorRhumbLineBasedAfterFirstQuarter", resultDecimals=2, ordinal=16)
public Double getRankPredictionErrorRhumbLineBasedAfterFirstQuarter();
@Statistic(messageKey="AbsoluteRankPredictionErrorRhumbLineBasedAfterSecondQuarter", resultDecimals=2, ordinal=17)
public Double getRankPredictionErrorRhumbLineBasedAfterSecondQuarter();
@Statistic(messageKey="AbsoluteRankPredictionErrorRhumbLineBasedAfterThirdQuarter", resultDecimals=2, ordinal=18)
public Double getRankPredictionErrorRhumbLineBasedAfterThirdQuarter();
@Statistic(messageKey="AbsoluteRankPredictionErrorRhumbLineBasedAcrossFirstThreeQuarters", resultDecimals=2, ordinal=19)
public Double getRankPredictionErrorRhumbLineBasedAcrossFirstThreeQuarters();
@Statistic(messageKey="TimeSpentInSeconds", resultDecimals=0, ordinal=4)
public Long getTimeTakenInSeconds();
@Statistic(messageKey="timeSpentFoiling", resultDecimals=1)
Duration getTimeSpentFoiling();
@Statistic(messageKey="FoilingDistance", resultDecimals=1)
Distance getDistanceSpentFoiling();
@Statistic(messageKey="NumberOfManeuvers", resultDecimals=0)
public Integer getNumberOfManeuvers();
@Statistic(messageKey="NumberOfJibes", resultDecimals=0)
public Integer getNumberOfJibes();
@Statistic(messageKey="NumberOfTacks", resultDecimals=0)
public Integer getNumberOfTacks();
@Statistic(messageKey="VMG", resultDecimals=2)
public Speed getVelocityMadeGood();
@Statistic(messageKey="RatioDurationLongVsShortTack", resultDecimals=2)
public double getRatioDurationLongVsShortTack();
@Statistic(messageKey="RatioDistanceLongVsShortTack", resultDecimals=2)
public double getRatioDistanceLongVsShortTack();
}
@@ -1,5 +1,5 @@
package com.sap.sailing.datamining.data;
public interface HasTrackedLegOfCompetitor {
HasTrackedLegSliceOfCompetitorContext getTrackedLegOfCompetitorContext();
AbstractHasTrackedLegSliceOfCompetitorContext getTrackedLegOfCompetitorContext();
}
@@ -1,4 +1,4 @@
package com.sap.sailing.datamining.data;
public interface HasTrackedLegOfCompetitorContext extends HasTrackedLegSliceOfCompetitorContext {
public interface HasTrackedLegOfCompetitorContext extends AbstractHasTrackedLegSliceOfCompetitorContext {
}
@@ -1,15 +1,6 @@
package com.sap.sailing.datamining.data;
import com.sap.sailing.domain.base.Boat;
import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.tracking.TrackedLegOfCompetitor;
import com.sap.sse.common.Distance;
import com.sap.sse.common.Duration;
import com.sap.sse.common.Speed;
import com.sap.sse.datamining.annotations.Connector;
import com.sap.sse.datamining.annotations.Dimension;
import com.sap.sse.datamining.annotations.Statistic;
import com.sap.sse.datamining.shared.impl.dto.ClusterDTO;
/**
* A "slice" of a competitor's tracked leg; slicing may happen in the retriever, perhaps influenced by some
@@ -19,114 +10,10 @@ import com.sap.sse.datamining.shared.impl.dto.ClusterDTO;
* @author Axel Uhl (d043530)
*
*/
public interface HasTrackedLegSliceOfCompetitorContext extends HasWindOnTrackedLeg {
@Connector(scanForStatistics=false)
public HasTrackedLegContext getTrackedLegContext();
public TrackedLegOfCompetitor getTrackedLegOfCompetitor();
@Dimension(messageKey="RelativeScoreInRaceInPercent", ordinal=12)
public ClusterDTO getPercentageClusterForRelativeScoreInRace();
@Connector(messageKey="Boat")
public Boat getBoat();
@Connector(messageKey="Competitor")
public Competitor getCompetitor();
@Statistic(messageKey="DistanceTraveled", resultDecimals=2, ordinal=0)
public Distance getDistanceTraveled();
@Statistic(messageKey="SpeedAverage", resultDecimals=2, ordinal=0)
public Double getSpeedAverageInKnots();
@Statistic(messageKey="RankGainsOrLosses", resultDecimals=2, ordinal=1)
public Integer getRankGainsOrLosses();
@Statistic(messageKey="RelativeScore", resultDecimals=2, ordinal=2)
public Double getRelativeRank();
@Statistic(messageKey="AbsoluteRank", resultDecimals=0, ordinal=3)
public Integer getRankAtSliceFinish();
@Statistic(messageKey="AbsoluteRankAfterFirstQuarter", resultDecimals=0, ordinal=4)
public Integer getRankAfterFirstQuarter();
@Statistic(messageKey="AbsoluteRankAfterSecondQuarter", resultDecimals=0, ordinal=5)
public Integer getRankAfterSecondQuarter();
@Statistic(messageKey="AbsoluteRankAfterThirdQuarter", resultDecimals=0, ordinal=6)
public Integer getRankAfterThirdQuarter();
@Statistic(messageKey="AbsoluteRankAverageAcrossFirstThreeQuarters", resultDecimals=2, ordinal=7)
public Double getRankAverageAcrossFirstThreeQuarters();
@Statistic(messageKey="AbsoluteRankRhumbLineBasedAfterFirstQuarter", resultDecimals=0, ordinal=8)
public Integer getRankRhumbLineBasedAfterFirstQuarter();
@Statistic(messageKey="AbsoluteRankRhumbLineBasedAfterSecondQuarter", resultDecimals=0, ordinal=9)
public Integer getRankRhumbLineBasedAfterSecondQuarter();
@Statistic(messageKey="AbsoluteRankRhumbLineBasedAfterThirdQuarter", resultDecimals=0, ordinal=10)
public Integer getRankRhumbLineBasedAfterThirdQuarter();
@Statistic(messageKey="AbsoluteRankRhumbLineBasedAverageAcrossFirstThreeQuarters", resultDecimals=2, ordinal=11)
public Double getRankRhumbLineBasedAverageAcrossFirstThreeQuarters();
@Statistic(messageKey="AbsoluteRankPredictionErrorAfterFirstQuarter", resultDecimals=2, ordinal=12)
public Double getRankPredictionErrorAfterFirstQuarter();
@Statistic(messageKey="AbsoluteRankPredictionErrorAfterSecondQuarter", resultDecimals=2, ordinal=13)
public Double getRankPredictionErrorAfterSecondQuarter();
@Statistic(messageKey="AbsoluteRankPredictionErrorAfterThirdQuarter", resultDecimals=2, ordinal=14)
public Double getRankPredictionErrorAfterThirdQuarter();
@Statistic(messageKey="AbsoluteRankPredictionErrorAcrossFirstThreeQuarters", resultDecimals=2, ordinal=15)
public Double getRankPredictionErrorAcrossFirstThreeQuarters();
@Statistic(messageKey="AbsoluteRankPredictionErrorRhumbLineBasedAfterFirstQuarter", resultDecimals=2, ordinal=16)
public Double getRankPredictionErrorRhumbLineBasedAfterFirstQuarter();
@Statistic(messageKey="AbsoluteRankPredictionErrorRhumbLineBasedAfterSecondQuarter", resultDecimals=2, ordinal=17)
public Double getRankPredictionErrorRhumbLineBasedAfterSecondQuarter();
@Statistic(messageKey="AbsoluteRankPredictionErrorRhumbLineBasedAfterThirdQuarter", resultDecimals=2, ordinal=18)
public Double getRankPredictionErrorRhumbLineBasedAfterThirdQuarter();
@Statistic(messageKey="AbsoluteRankPredictionErrorRhumbLineBasedAcrossFirstThreeQuarters", resultDecimals=2, ordinal=19)
public Double getRankPredictionErrorRhumbLineBasedAcrossFirstThreeQuarters();
@Statistic(messageKey="TimeSpentInSeconds", resultDecimals=0, ordinal=4)
public Long getTimeTakenInSeconds();
@Statistic(messageKey="timeSpentFoiling", resultDecimals=1)
Duration getTimeSpentFoiling();
@Statistic(messageKey="FoilingDistance", resultDecimals=1)
Distance getDistanceSpentFoiling();
@Statistic(messageKey="NumberOfManeuvers", resultDecimals=0)
public Integer getNumberOfManeuvers();
@Statistic(messageKey="NumberOfJibes", resultDecimals=0)
public Integer getNumberOfJibes();
@Statistic(messageKey="NumberOfTacks", resultDecimals=0)
public Integer getNumberOfTacks();
@Statistic(messageKey="VMG", resultDecimals=2)
public Speed getVelocityMadeGood();
@Statistic(messageKey="RatioDurationLongVsShortTack", resultDecimals=2)
public double getRatioDurationLongVsShortTack();
@Statistic(messageKey="RatioDistanceLongVsShortTack", resultDecimals=2)
public double getRatioDistanceLongVsShortTack();
public interface HasTrackedLegSliceOfCompetitorContext extends AbstractHasTrackedLegSliceOfCompetitorContext {
/**
* one-based; e.g., if there are ten slices, they have slice numbers 1..10
*/
@Dimension(messageKey="SliceNumber")
public Integer getSliceNumber();
Integer getSliceNumber();
}
@@ -41,7 +41,7 @@ public class TrackedLegSliceOfCompetitorRetrievalProcessor extends AbstractRetri
@Override
protected Iterable<HasTrackedLegSliceOfCompetitorContext> retrieveData(HasTrackedLegOfCompetitorContext element) {
Collection<HasTrackedLegSliceOfCompetitorContext> trackedLegSlicesOfCompetitorsWithContext = new ArrayList<>();
final Collection<HasTrackedLegSliceOfCompetitorContext> trackedLegSlicesOfCompetitorsWithContext = new ArrayList<>();
for (int i=1; i<=10; i++) {
if (isAborted()) {
break;
@@ -0,0 +1,631 @@
package com.sap.sailing.datamining.impl.data;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
import com.sap.sailing.datamining.Activator;
import com.sap.sailing.datamining.SailingClusterGroups;
import com.sap.sailing.datamining.data.AbstractHasTrackedLegSliceOfCompetitorContext;
import com.sap.sailing.datamining.data.HasTackTypeSegmentContext;
import com.sap.sailing.datamining.data.HasTrackedLegContext;
import com.sap.sailing.datamining.impl.components.TackTypeSegmentRetrievalProcessor;
import com.sap.sailing.datamining.shared.TackTypeSegmentsDataMiningSettings;
import com.sap.sailing.domain.base.Boat;
import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.common.LegType;
import com.sap.sailing.domain.common.ManeuverType;
import com.sap.sailing.domain.common.NoWindException;
import com.sap.sailing.domain.common.Position;
import com.sap.sailing.domain.common.Wind;
import com.sap.sailing.domain.leaderboard.Leaderboard;
import com.sap.sailing.domain.leaderboard.caching.LeaderboardDTOCalculationReuseCache;
import com.sap.sailing.domain.tracking.BravoFixTrack;
import com.sap.sailing.domain.tracking.Maneuver;
import com.sap.sailing.domain.tracking.TrackedLeg;
import com.sap.sailing.domain.tracking.TrackedLegOfCompetitor;
import com.sap.sailing.domain.tracking.TrackedRace;
import com.sap.sailing.domain.tracking.WindLegTypeAndLegBearingAndORCPerformanceCurveCache;
import com.sap.sse.common.Distance;
import com.sap.sse.common.Duration;
import com.sap.sse.common.Speed;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.TimeRange;
import com.sap.sse.common.Util;
import com.sap.sse.datamining.data.Cluster;
import com.sap.sse.datamining.shared.impl.dto.ClusterDTO;
/**
* Equality is based on the {@link #getTrackedLegOfCompetitor()} and the {@link #sliceNumber} only. The current slicing implementation
* uses the time sailed in the leg as the basis for slicing. Other slicing criteria, such as windward distance traveled, would be much
* harder to implement because we would need to find out iteratively, between which fixes the competitor sailed, say, one tenth of the
* windward distance in the leg.
*/
public abstract class AbstractTrackedLegSliceOfCompetitorWithContext implements AbstractHasTrackedLegSliceOfCompetitorContext {
private static final long serialVersionUID = 5944904146286262768L;
private static final long DEFAULT_NUMBER_OF_SLICES = 10;
private final HasTrackedLegContext trackedLegContext;
private final TackTypeSegmentsDataMiningSettings settings;
private final TrackedLegOfCompetitor trackedLegOfCompetitor;
private final Competitor competitor;
private Integer rankAtSliceStart;
private boolean isRankAtSliceStartInitialized;
private Integer rankAtSliceFinish;
private boolean isRankAtSliceFinishInitialized;
private Wind wind;
private int sliceNumber;
public AbstractTrackedLegSliceOfCompetitorWithContext(HasTrackedLegContext trackedLegContext, TrackedLegOfCompetitor trackedLegOfCompetitor, TackTypeSegmentsDataMiningSettings settings, int sliceNumber) {
this.trackedLegContext = trackedLegContext;
this.trackedLegOfCompetitor = trackedLegOfCompetitor;
this.competitor = trackedLegOfCompetitor.getCompetitor();
this.settings = settings;
this.sliceNumber = sliceNumber;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = (prime * result + ((trackedLegOfCompetitor == null) ? 0 : trackedLegOfCompetitor.hashCode())) ^ sliceNumber;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
AbstractTrackedLegSliceOfCompetitorWithContext other = (AbstractTrackedLegSliceOfCompetitorWithContext) obj;
if (sliceNumber != other.sliceNumber) {
return false;
}
if (trackedLegOfCompetitor == null) {
if (other.trackedLegOfCompetitor != null)
return false;
} else if (!trackedLegOfCompetitor.equals(other.trackedLegOfCompetitor))
return false;
return true;
}
@Override
public HasTrackedLegContext getTrackedLegContext() {
return trackedLegContext;
}
@Override
public TrackedLegOfCompetitor getTrackedLegOfCompetitor() {
return trackedLegOfCompetitor;
}
@Override
public Competitor getCompetitor() {
return competitor;
}
@Override
public Boat getBoat() {
Boat boatOfCompetitor = getTrackedRace().getBoatOfCompetitor(getCompetitor());
return boatOfCompetitor;
}
@Override
public ClusterDTO getPercentageClusterForRelativeScoreInRace() {
final ClusterDTO result;
Double relativeScore = getTrackedLegContext().getTrackedRaceContext().getRelativeScoreForCompetitor(getCompetitor());
if (relativeScore == null) {
result = null;
} else {
SailingClusterGroups clusterGroups = Activator.getClusterGroups();
Cluster<Double> cluster = clusterGroups.getPercentageClusterGroup().getClusterFor(relativeScore);
result = new ClusterDTO(clusterGroups.getPercentageClusterFormatter().format(cluster));
}
return result;
}
protected <R> R getSomethingForLegTrackingInterval(BiFunction<TimePoint, TimePoint, R> resultSupplier) {
final TimePoint startTime = getSliceStartTime();
final TimePoint finishTime = getSliceFinishTime();
return getSomethingForInterval(resultSupplier, startTime, finishTime);
}
/**
* @return {@code null} if the competitor hasn't started the leg; otherwise, the start of the slice as split between
* the competitors leg start time point and {@link #getEffectiveEndOfLeg(TimePoint)}, into {@link #DEFAULT_NUMBER_OF_SLICES}
* parts of equal duration.
*/
protected TimePoint getSliceStartTime() {
return getTimePointAtStartOfSlice(sliceNumber-1); // sliceNumber is one-based, parameter is zero-based, so this means the beginning of the slice
}
/**
* @return {@code null} if the competitor hasn't started the leg; otherwise, the non-{@code null} end of the slice
* as split between the competitors leg start time point and {@link #getEffectiveEndOfLeg(TimePoint)}, into
* {@link #DEFAULT_NUMBER_OF_SLICES} parts of equal duration.
*/
protected TimePoint getSliceFinishTime() {
return getTimePointAtStartOfSlice(sliceNumber); // sliceNumber is one-based, parameter is zero-based, so this means the end of the slice
}
private TimePoint getTimePointAtStartOfSlice(int zeroBasedSliceNumber) {
final TimePoint legStartTime = getTrackedLegOfCompetitor().getStartTime();
final TimePoint sliceFinishTime;
if (legStartTime == null) {
sliceFinishTime = null;
} else {
final TimePoint finishTime = getTrackedLegOfCompetitor().getFinishTime();
final TimePoint effectiveEndOfLeg = getEffectiveEndOfLeg(finishTime);
sliceFinishTime = legStartTime.plus(legStartTime.until(effectiveEndOfLeg).divide(DEFAULT_NUMBER_OF_SLICES).times(zeroBasedSliceNumber)); // sliceNumber is one-based, so this means the end of the slice
}
return sliceFinishTime;
}
/**
* @return a non-{@code null} time point; {@code finishTime} if not {@code null}; otherwise the {@link TrackedRace#getEndOfRace() end of the race} if
* not {@code null}; otherwise the {@link TrackedRace#getEndOfTracking() end of tracking} if not {@code null}; otherwise the current time.
*/
protected TimePoint getEffectiveEndOfLeg(final TimePoint finishTime) {
return finishTime != null ? finishTime :
getTrackedLegContext().getTrackedLeg().getTrackedRace().getEndOfRace() != null ? getTrackedLegContext().getTrackedLeg().getTrackedRace().getEndOfRace() :
getTrackedLegContext().getTrackedLeg().getTrackedRace().getEndOfTracking() != null ? getTrackedLegContext().getTrackedLeg().getTrackedRace().getEndOfTracking() :
TimePoint.now();
}
protected <R> R getSomethingForInterval(BiFunction<TimePoint, TimePoint, R> resultSupplier,
final TimePoint startTime, final TimePoint finishTime) {
final R result;
if (startTime != null) {
final TrackedRace trackedRace = getTrackedLegContext().getTrackedLeg().getTrackedRace();
final TimePoint effectiveEndOfInterval = finishTime != null ? finishTime :
trackedRace.getEndOfTracking() != null ? trackedRace.getEndOfTracking() : TimePoint.now();
result = resultSupplier.apply(startTime, effectiveEndOfInterval);
} else {
result = null;
}
return result;
}
@Override
public Duration getTimeSpentFoiling() {
return getSomethingForLegTrackingInterval((start, end) -> {
BravoFixTrack<Competitor> bravoFixTrack = getTrackedLegContext().getTrackedLeg().getTrackedRace().getSensorTrack(getCompetitor(), BravoFixTrack.TRACK_NAME);
return bravoFixTrack == null ? Duration.NULL : bravoFixTrack.getTimeSpentFoiling(start, end);
});
}
@Override
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(start, end);
});
}
@Override
public Distance getDistanceTraveled() {
return getSomethingForLegTrackingInterval((start, end) -> {
return start == null ? null :
getTrackedLegOfCompetitor().getDistanceTraveled(end).add(getTrackedLegOfCompetitor().getDistanceTraveled(start).scale(-1));
});
}
@Override
public Double getSpeedAverageInKnots() {
final Double result;
if (getTrackedRace() == null || getSliceStartTime() == null) {
result = null;
} else {
final TrackedLegOfCompetitor trackedLegOfCompetitor = getTrackedLegOfCompetitor();
final Distance distanceTraveledInSlice = trackedLegOfCompetitor.getDistanceTraveled(getSliceFinishTime())
.add(trackedLegOfCompetitor.getDistanceTraveled(getSliceStartTime()).scale(-1));
final Duration sliceDuration = getSliceStartTime().until(getSliceFinishTime());
final Speed averageSOG = distanceTraveledInSlice.inTime(sliceDuration);
result = averageSOG == null ? null :averageSOG.getKnots();
}
return result;
}
@Override
public Integer getRankGainsOrLosses() {
Integer rankAtSliceStart = getRankAtSliceStart();
Integer rankAtSliceFinish = getRankAtSliceFinish();
return rankAtSliceStart != null && rankAtSliceFinish != null ? rankAtSliceStart - rankAtSliceFinish : null;
}
private Integer getRankAtSliceStart() {
final Integer result;
if (getTrackedLegContext().getTrackedRaceContext().getTrackedRace() != null) {
if (!isRankAtSliceStartInitialized) {
TrackedRace trackedRace = getTrackedLegContext().getTrackedRaceContext().getTrackedRace();
int rank = trackedRace.getRank(getCompetitor(), getSliceStartTime());
rankAtSliceStart = rank == 0 ? null : rank;
isRankAtSliceStartInitialized = true;
}
result = rankAtSliceStart;
} else {
result = null;
}
return result;
}
/**
* If the competitor hasn't started and finished the leg, {@code null} will be returned.
*
* @param ratioOfLegSliceTimeSpent
* the ratio of the time the competitor has spent in this slice of the leg; 0=just starting the leg; 1=just
* finished the slice of the leg
*/
private Integer getRankAtTimePercent(double ratioOfLegSliceTimeSpent) {
final Integer result;
final TrackedRace trackedRace = getTrackedLegContext().getTrackedRaceContext().getTrackedRace();
if (trackedRace != null) {
final TimePoint startTime = getSliceStartTime();
final TimePoint finishTime = getSliceFinishTime();
if (startTime != null && finishTime != null) {
final TimePoint timePoint = startTime.plus(startTime.until(finishTime).times(ratioOfLegSliceTimeSpent));
result = trackedRace.getRank(getCompetitor(), timePoint);
} else {
result = null;
}
} else {
result = null;
}
return result;
}
/**
* A "non-official" hypothetical rank for upwind and downwind legs, pretending we didn't have wind information and
* hence couldn't compute a wind-based rank. This may be used, e.g., to compare the wind-based prediction to the rhumb
* line-based prediction. If the competitor hasn't started and finished the leg, {@code null} will be returned.
*
* @param ratioOfLegTimeSpent
* the ratio of the time the competitor has spent in the leg overall; 0=just starting the leg; 1=just
* finished the leg
*/
private Integer getRhumbLineBasedRankAtTimePercent(double ratioOfLegTimeSpent) {
final Integer result;
final TrackedRace trackedRace = getTrackedLegContext().getTrackedRaceContext().getTrackedRace();
if (trackedRace != null) {
final TimePoint startTime = getSliceStartTime();
final TimePoint finishTime = getSliceFinishTime();
if (startTime != null && finishTime != null) {
final TimePoint timePoint = startTime.plus(startTime.until(finishTime).times(ratioOfLegTimeSpent));
final WindLegTypeAndLegBearingAndORCPerformanceCurveCache cache = new LeaderboardDTOCalculationReuseCache(timePoint) {
/**
* Always returns {@link LegType#REACHING} to force rhumb line-based calculation
*/
@Override
public LegType getLegType(TrackedLeg trackedLeg, TimePoint timePoint) throws NoWindException {
return LegType.REACHING;
}
};
// RaceRankComparator requires course read lock
trackedRace.getRace().getCourse().lockForRead();
final List<Competitor> rankedCompetitors = new ArrayList<>();
try {
Comparator<Competitor> comparator = trackedRace.getRankingMetric().getRaceRankingComparator(timePoint, cache);
Util.addAll(trackedRace.getRace().getCompetitors(), rankedCompetitors);
Collections.sort(rankedCompetitors, comparator);
} finally {
trackedRace.getRace().getCourse().unlockAfterRead();
}
result = rankedCompetitors.indexOf(getCompetitor()) + 1;
} else {
result = null;
}
} else {
result = null;
}
return result;
}
@Override
public Double getRelativeRank() {
Leaderboard leaderboard = getTrackedLegContext().getTrackedRaceContext().getLeaderboardContext().getLeaderboard();
double competitorCount = Util.size(leaderboard.getCompetitors());
Integer rankAtSliceFinish = getRankAtSliceFinish();
return rankAtSliceFinish == null ? null : rankAtSliceFinish / competitorCount;
}
public Integer getRankAtSliceFinish() {
if (!isRankAtSliceFinishInitialized) {
TrackedRace trackedRace = getTrackedLegContext().getTrackedRaceContext().getTrackedRace();
int rank = trackedRace.getRank(getCompetitor(), getSliceFinishTime());
rankAtSliceFinish = rank == 0 ? null : rank;
isRankAtSliceFinishInitialized = true;
}
return rankAtSliceFinish;
}
@Override
public Long getTimeTakenInSeconds() {
TimePoint startTime = getSliceStartTime();
TimePoint finishTime = getSliceFinishTime();
final Long result;
if (startTime == null || finishTime == null) {
result = null;
} else {
result = (finishTime.asMillis() - startTime.asMillis()) / 1000;
}
return result;
}
@Override
public Wind getWindInternal() {
return wind;
}
@Override
public void setWindInternal(Wind wind) {
this.wind = wind;
}
@Override
public Position getPosition() {
final TrackedLeg trackedLeg = getTrackedLegContext().getTrackedLeg();
final TrackedRace trackedRace = trackedLeg.getTrackedRace();
final TimePoint timepoint = getTimePointBetweenLegSliceStartAndLegFinish(trackedRace);
final Position result;
if (timepoint == null) {
result = null;
} else {
result = trackedLeg.getMiddleOfLeg(timepoint);
}
return result;
}
private TimePoint getTimePointBetweenLegSliceStartAndLegFinish(final TrackedRace trackedRace) {
final TimePoint competitorLegSliceStartTime = getSliceStartTime();
final TimePoint competitorLegSliceEndTime = getSliceFinishTime();
final TimePoint startTime = competitorLegSliceStartTime != null ? competitorLegSliceStartTime :
trackedRace.getStartOfRace() != null ? trackedRace.getStartOfRace() : trackedRace.getStartOfTracking();
final TimePoint endTime = competitorLegSliceEndTime != null ? competitorLegSliceEndTime :
trackedRace.getEndOfRace() != null ? trackedRace.getEndOfRace() : trackedRace.getEndOfTracking();
final TimePoint timepoint = endTime == null ? startTime : startTime == null ? null : startTime.plus(startTime.until(endTime).divide(2));
return timepoint;
}
/**
* Picks the time point that is in the middle between the time point when the competitor entered
* the leg and the time point the competitor finished the leg. If no leg start/finish time exists
* for the competitor, start/end of race and then start/end of tracking are used as fall-back values.
*/
@Override
public TimePoint getTimePoint() {
final TrackedLeg trackedLeg = getTrackedLegContext().getTrackedLeg();
final TrackedRace trackedRace = trackedLeg.getTrackedRace();
return getTimePointBetweenLegSliceStartAndLegFinish(trackedRace);
}
@Override
public AbstractHasTrackedLegSliceOfCompetitorContext getTrackedLegOfCompetitorContext() {
return this;
}
@Override
public Integer getNumberOfManeuvers() {
return getNumberOfJibes() + getNumberOfTacks();
}
@Override
public Integer getNumberOfJibes() {
return getSomethingForLegTrackingInterval((start, end) -> {
return getNumberOf(ManeuverType.JIBE, start, end);
});
}
@Override
public Integer getNumberOfTacks() {
return getSomethingForLegTrackingInterval((start, end) -> {
return getNumberOf(ManeuverType.TACK, start, end);
});
}
private int getNumberOf(ManeuverType maneuverType, TimePoint start, TimePoint end) {
TrackedRace trackedRace = getTrackedRace();
int number = 0;
if (trackedRace != null) {
for (Maneuver maneuver : trackedRace.getManeuvers(getCompetitor(), start, end, false)) {
if (maneuver.getType() == maneuverType) {
number++;
}
}
}
return number;
}
@Override
public Integer getRankAfterFirstQuarter() {
return getRankAtTimePercent(0.25);
}
@Override
public Integer getRankAfterSecondQuarter() {
return getRankAtTimePercent(0.5);
}
@Override
public Integer getRankAfterThirdQuarter() {
return getRankAtTimePercent(0.75);
}
@Override
public Double getRankAverageAcrossFirstThreeQuarters() {
return getRankAverageAcrossFirstThreeQuarters(this::getRankAtTimePercent);
}
private Double getRankAverageAcrossFirstThreeQuarters(final Function<Double, Integer> rankFunction) {
int rankSum = 0;
int count = 0;
for (double ratio=0.25; ratio<1.0; ratio+=0.25) {
final Integer rankAtRatio = rankFunction.apply(ratio);
if (rankAtRatio != null) {
rankSum += rankAtRatio;
count++;
}
}
return count == 0 ? null : (double) rankSum / (double) count;
}
@Override
public Integer getRankRhumbLineBasedAfterFirstQuarter() {
return getRhumbLineBasedRankAtTimePercent(0.25);
}
@Override
public Integer getRankRhumbLineBasedAfterSecondQuarter() {
return getRhumbLineBasedRankAtTimePercent(0.5);
}
@Override
public Integer getRankRhumbLineBasedAfterThirdQuarter() {
return getRhumbLineBasedRankAtTimePercent(0.75);
}
@Override
public Double getRankRhumbLineBasedAverageAcrossFirstThreeQuarters() {
return getRankAverageAcrossFirstThreeQuarters(this::getRhumbLineBasedRankAtTimePercent);
}
private Double getRankPredictionError(final Supplier<? extends Number> rankPredictor) {
final Integer rankAtFinish = getRankAtSliceFinish();
final Double result;
if (rankAtFinish != null) {
final Number predictiveRank = rankPredictor.get();
if (predictiveRank != null) {
final int numberOfCompetitors = Util.size(getTrackedRace().getRace().getCompetitors());
if (numberOfCompetitors != 0) {
result = Math.abs((double) rankAtFinish-predictiveRank.doubleValue()) / (double) numberOfCompetitors;
} else {
// this is a bit strange because we obviously have a non-null getCompetitor() in the race...
result = null;
}
} else {
result = null;
}
} else {
result = null;
}
return result;
}
@Override
public Double getRankPredictionErrorAfterFirstQuarter() {
return getRankPredictionError(this::getRankAfterFirstQuarter);
}
@Override
public Double getRankPredictionErrorAfterSecondQuarter() {
return getRankPredictionError(this::getRankAfterSecondQuarter);
}
@Override
public Double getRankPredictionErrorAfterThirdQuarter() {
return getRankPredictionError(this::getRankAfterThirdQuarter);
}
@Override
public Double getRankPredictionErrorAcrossFirstThreeQuarters() {
return getRankPredictionError(this::getRankAverageAcrossFirstThreeQuarters);
}
@Override
public Double getRankPredictionErrorRhumbLineBasedAfterFirstQuarter() {
return getRankPredictionError(this::getRankRhumbLineBasedAfterFirstQuarter);
}
@Override
public Double getRankPredictionErrorRhumbLineBasedAfterSecondQuarter() {
return getRankPredictionError(this::getRankRhumbLineBasedAfterSecondQuarter);
}
@Override
public Double getRankPredictionErrorRhumbLineBasedAfterThirdQuarter() {
return getRankPredictionError(this::getRankRhumbLineBasedAfterThirdQuarter);
}
@Override
public Double getRankPredictionErrorRhumbLineBasedAcrossFirstThreeQuarters() {
return getRankPredictionError(this::getRankRhumbLineBasedAverageAcrossFirstThreeQuarters);
}
@Override
public Speed getVelocityMadeGood() {
final TimePoint finishTime = getSliceFinishTime();
return getTrackedLegOfCompetitor().getAverageVelocityMadeGood(finishTime == null ? TimePoint.now() : finishTime);
}
@Override
public double getRatioDurationLongVsShortTack() {
final TackTypeRatioCollector<Duration> resultProcessor = new TackTypeRatioCollector<Duration>(Duration.NULL) {
@Override
protected Duration add(Duration a, Duration b) {
return a.plus(b);
}
@Override
protected double divide(Duration a, Duration b) {
return a.divide(b);
}
@Override
protected Duration getAddable(HasTackTypeSegmentContext element) {
return element.getDuration();
}
};
final TackTypeSegmentRetrievalProcessor tackTypeSegmentRetriever = new TackTypeSegmentRetrievalProcessor(
/* executor */ null,
Collections.emptySet(), settings, 0, "TackTypeSegments");
return Util.stream(tackTypeSegmentRetriever.retrieveData(
new RaceOfCompetitorWithContext(getTrackedLegContext().getTrackedRaceContext(), competitor, settings)))
.filter(tt->tt.getLegNumber() == getTrackedLegContext().getLegNumber()).collect(resultProcessor);
}
protected boolean isTackTypeSegmentIntersectingWithLegSlice(HasTackTypeSegmentContext tackTypeSegmentContext) {
return getSliceStartTime() != null &&
tackTypeSegmentContext.getLegNumber() == getTrackedLegContext().getLegNumber()
&& TimeRange.create(tackTypeSegmentContext.getStartOfTackTypeSegment(), tackTypeSegmentContext.getEndOfTackTypeSegment())
.intersects(TimeRange.create(getSliceStartTime(), getSliceFinishTime()));
}
@Override
public double getRatioDistanceLongVsShortTack() {
final TackTypeRatioCollector<Distance> resultProcessor = new TackTypeRatioCollector<Distance>(Distance.NULL) {
@Override
protected Distance add(Distance a, Distance b) {
return a.add(b);
}
@Override
protected double divide(Distance a, Distance b) {
return a.divide(b);
}
@Override
protected Distance getAddable(HasTackTypeSegmentContext element) {
return element.getDistance();
}
};
final TackTypeSegmentRetrievalProcessor tackTypeSegmentRetriever = new TackTypeSegmentRetrievalProcessor(
/* executor */ null,
Collections.emptySet(), settings, 0, "TackTypeSegments");
return Util.stream(tackTypeSegmentRetriever.retrieveData(
new RaceOfCompetitorWithContext(getTrackedLegContext().getTrackedRaceContext(), competitor, settings)))
.filter(tt->tt.getLegNumber() == getTrackedLegContext().getLegNumber()).collect(resultProcessor);
}
protected Integer getTheSliceNumber() {
return sliceNumber;
}
}
@@ -13,7 +13,7 @@ import com.sap.sse.common.TimePoint;
* Equality is based on the {@link #getTrackedLegOfCompetitor()} only. The {@link #getSliceNumber()} defaults to {@code 1} for this
* special case of the entire leg. Start and finish times of this "slice" equal the start/finish times of the competitor in the leg.
*/
public class TrackedLegOfCompetitorWithContext extends TrackedLegSliceOfCompetitorWithContext implements HasTrackedLegOfCompetitorContext {
public class TrackedLegOfCompetitorWithContext extends AbstractTrackedLegSliceOfCompetitorWithContext implements HasTrackedLegOfCompetitorContext {
private static final long serialVersionUID = 5944904146286262768L;
public TrackedLegOfCompetitorWithContext(HasTrackedLegContext trackedLegContext, TrackedLegOfCompetitor trackedLegOfCompetitor, TackTypeSegmentsDataMiningSettings settings) {
@@ -1,43 +1,9 @@
package com.sap.sailing.datamining.impl.data;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
import com.sap.sailing.datamining.Activator;
import com.sap.sailing.datamining.SailingClusterGroups;
import com.sap.sailing.datamining.data.HasTackTypeSegmentContext;
import com.sap.sailing.datamining.data.HasTrackedLegContext;
import com.sap.sailing.datamining.data.HasTrackedLegSliceOfCompetitorContext;
import com.sap.sailing.datamining.impl.components.TackTypeSegmentRetrievalProcessor;
import com.sap.sailing.datamining.shared.TackTypeSegmentsDataMiningSettings;
import com.sap.sailing.domain.base.Boat;
import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.common.LegType;
import com.sap.sailing.domain.common.ManeuverType;
import com.sap.sailing.domain.common.NoWindException;
import com.sap.sailing.domain.common.Position;
import com.sap.sailing.domain.common.Wind;
import com.sap.sailing.domain.leaderboard.Leaderboard;
import com.sap.sailing.domain.leaderboard.caching.LeaderboardDTOCalculationReuseCache;
import com.sap.sailing.domain.tracking.BravoFixTrack;
import com.sap.sailing.domain.tracking.Maneuver;
import com.sap.sailing.domain.tracking.TrackedLeg;
import com.sap.sailing.domain.tracking.TrackedLegOfCompetitor;
import com.sap.sailing.domain.tracking.TrackedRace;
import com.sap.sailing.domain.tracking.WindLegTypeAndLegBearingAndORCPerformanceCurveCache;
import com.sap.sse.common.Distance;
import com.sap.sse.common.Duration;
import com.sap.sse.common.Speed;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.TimeRange;
import com.sap.sse.common.Util;
import com.sap.sse.datamining.data.Cluster;
import com.sap.sse.datamining.shared.impl.dto.ClusterDTO;
/**
* Equality is based on the {@link #getTrackedLegOfCompetitor()} and the {@link #sliceNumber} only. The current slicing implementation
@@ -45,588 +11,15 @@ import com.sap.sse.datamining.shared.impl.dto.ClusterDTO;
* harder to implement because we would need to find out iteratively, between which fixes the competitor sailed, say, one tenth of the
* windward distance in the leg.
*/
public class TrackedLegSliceOfCompetitorWithContext implements HasTrackedLegSliceOfCompetitorContext {
private static final long serialVersionUID = 5944904146286262768L;
private static final long DEFAULT_NUMBER_OF_SLICES = 10;
private final HasTrackedLegContext trackedLegContext;
private final TackTypeSegmentsDataMiningSettings settings;
private final TrackedLegOfCompetitor trackedLegOfCompetitor;
private final Competitor competitor;
private Integer rankAtSliceStart;
private boolean isRankAtSliceStartInitialized;
private Integer rankAtSliceFinish;
private boolean isRankAtSliceFinishInitialized;
private Wind wind;
private int sliceNumber;
public class TrackedLegSliceOfCompetitorWithContext extends AbstractTrackedLegSliceOfCompetitorWithContext implements HasTrackedLegSliceOfCompetitorContext {
private static final long serialVersionUID = -8116119441445200029L;
public TrackedLegSliceOfCompetitorWithContext(HasTrackedLegContext trackedLegContext, TrackedLegOfCompetitor trackedLegOfCompetitor, TackTypeSegmentsDataMiningSettings settings, int sliceNumber) {
this.trackedLegContext = trackedLegContext;
this.trackedLegOfCompetitor = trackedLegOfCompetitor;
this.competitor = trackedLegOfCompetitor.getCompetitor();
this.settings = settings;
this.sliceNumber = sliceNumber;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = (prime * result + ((trackedLegOfCompetitor == null) ? 0 : trackedLegOfCompetitor.hashCode())) ^ sliceNumber;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
TrackedLegSliceOfCompetitorWithContext other = (TrackedLegSliceOfCompetitorWithContext) obj;
if (sliceNumber != other.sliceNumber) {
return false;
}
if (trackedLegOfCompetitor == null) {
if (other.trackedLegOfCompetitor != null)
return false;
} else if (!trackedLegOfCompetitor.equals(other.trackedLegOfCompetitor))
return false;
return true;
}
@Override
public HasTrackedLegContext getTrackedLegContext() {
return trackedLegContext;
}
@Override
public TrackedLegOfCompetitor getTrackedLegOfCompetitor() {
return trackedLegOfCompetitor;
}
@Override
public Competitor getCompetitor() {
return competitor;
}
@Override
public Boat getBoat() {
Boat boatOfCompetitor = getTrackedRace().getBoatOfCompetitor(getCompetitor());
return boatOfCompetitor;
}
@Override
public ClusterDTO getPercentageClusterForRelativeScoreInRace() {
final ClusterDTO result;
Double relativeScore = getTrackedLegContext().getTrackedRaceContext().getRelativeScoreForCompetitor(getCompetitor());
if (relativeScore == null) {
result = null;
} else {
SailingClusterGroups clusterGroups = Activator.getClusterGroups();
Cluster<Double> cluster = clusterGroups.getPercentageClusterGroup().getClusterFor(relativeScore);
result = new ClusterDTO(clusterGroups.getPercentageClusterFormatter().format(cluster));
}
return result;
}
protected <R> R getSomethingForLegTrackingInterval(BiFunction<TimePoint, TimePoint, R> resultSupplier) {
final TimePoint startTime = getSliceStartTime();
final TimePoint finishTime = getSliceFinishTime();
return getSomethingForInterval(resultSupplier, startTime, finishTime);
}
/**
* @return {@code null} if the competitor hasn't started the leg; otherwise, the start of the slice as split between
* the competitors leg start time point and {@link #getEffectiveEndOfLeg(TimePoint)}, into {@link #DEFAULT_NUMBER_OF_SLICES}
* parts of equal duration.
*/
protected TimePoint getSliceStartTime() {
return getTimePointAtStartOfSlice(sliceNumber-1); // sliceNumber is one-based, parameter is zero-based, so this means the beginning of the slice
}
/**
* @return {@code null} if the competitor hasn't started the leg; otherwise, the non-{@code null} end of the slice
* as split between the competitors leg start time point and {@link #getEffectiveEndOfLeg(TimePoint)}, into
* {@link #DEFAULT_NUMBER_OF_SLICES} parts of equal duration.
*/
protected TimePoint getSliceFinishTime() {
return getTimePointAtStartOfSlice(sliceNumber); // sliceNumber is one-based, parameter is zero-based, so this means the end of the slice
}
private TimePoint getTimePointAtStartOfSlice(int zeroBasedSliceNumber) {
final TimePoint legStartTime = getTrackedLegOfCompetitor().getStartTime();
final TimePoint sliceFinishTime;
if (legStartTime == null) {
sliceFinishTime = null;
} else {
final TimePoint finishTime = getTrackedLegOfCompetitor().getFinishTime();
final TimePoint effectiveEndOfLeg = getEffectiveEndOfLeg(finishTime);
sliceFinishTime = legStartTime.plus(legStartTime.until(effectiveEndOfLeg).divide(DEFAULT_NUMBER_OF_SLICES).times(zeroBasedSliceNumber)); // sliceNumber is one-based, so this means the end of the slice
}
return sliceFinishTime;
}
/**
* @return a non-{@code null} time point; {@code finishTime} if not {@code null}; otherwise the {@link TrackedRace#getEndOfRace() end of the race} if
* not {@code null}; otherwise the {@link TrackedRace#getEndOfTracking() end of tracking} if not {@code null}; otherwise the current time.
*/
protected TimePoint getEffectiveEndOfLeg(final TimePoint finishTime) {
return finishTime != null ? finishTime :
getTrackedLegContext().getTrackedLeg().getTrackedRace().getEndOfRace() != null ? getTrackedLegContext().getTrackedLeg().getTrackedRace().getEndOfRace() :
getTrackedLegContext().getTrackedLeg().getTrackedRace().getEndOfTracking() != null ? getTrackedLegContext().getTrackedLeg().getTrackedRace().getEndOfTracking() :
TimePoint.now();
}
protected <R> R getSomethingForInterval(BiFunction<TimePoint, TimePoint, R> resultSupplier,
final TimePoint startTime, final TimePoint finishTime) {
final R result;
if (startTime != null) {
final TrackedRace trackedRace = getTrackedLegContext().getTrackedLeg().getTrackedRace();
final TimePoint effectiveEndOfInterval = finishTime != null ? finishTime :
trackedRace.getEndOfTracking() != null ? trackedRace.getEndOfTracking() : TimePoint.now();
result = resultSupplier.apply(startTime, effectiveEndOfInterval);
} else {
result = null;
}
return result;
}
@Override
public Duration getTimeSpentFoiling() {
return getSomethingForLegTrackingInterval((start, end) -> {
BravoFixTrack<Competitor> bravoFixTrack = getTrackedLegContext().getTrackedLeg().getTrackedRace().getSensorTrack(getCompetitor(), BravoFixTrack.TRACK_NAME);
return bravoFixTrack == null ? Duration.NULL : bravoFixTrack.getTimeSpentFoiling(start, end);
});
}
@Override
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(start, end);
});
}
@Override
public Distance getDistanceTraveled() {
return getSomethingForLegTrackingInterval((start, end) -> {
return start == null ? null :
getTrackedLegOfCompetitor().getDistanceTraveled(end).add(getTrackedLegOfCompetitor().getDistanceTraveled(start).scale(-1));
});
}
@Override
public Double getSpeedAverageInKnots() {
final Double result;
if (getTrackedRace() == null || getSliceStartTime() == null) {
result = null;
} else {
final TrackedLegOfCompetitor trackedLegOfCompetitor = getTrackedLegOfCompetitor();
final Distance distanceTraveledInSlice = trackedLegOfCompetitor.getDistanceTraveled(getSliceFinishTime())
.add(trackedLegOfCompetitor.getDistanceTraveled(getSliceStartTime()).scale(-1));
final Duration sliceDuration = getSliceStartTime().until(getSliceFinishTime());
final Speed averageSOG = distanceTraveledInSlice.inTime(sliceDuration);
result = averageSOG == null ? null :averageSOG.getKnots();
}
return result;
}
@Override
public Integer getRankGainsOrLosses() {
Integer rankAtSliceStart = getRankAtSliceStart();
Integer rankAtSliceFinish = getRankAtSliceFinish();
return rankAtSliceStart != null && rankAtSliceFinish != null ? rankAtSliceStart - rankAtSliceFinish : null;
}
private Integer getRankAtSliceStart() {
final Integer result;
if (getTrackedLegContext().getTrackedRaceContext().getTrackedRace() != null) {
if (!isRankAtSliceStartInitialized) {
TrackedRace trackedRace = getTrackedLegContext().getTrackedRaceContext().getTrackedRace();
int rank = trackedRace.getRank(getCompetitor(), getSliceStartTime());
rankAtSliceStart = rank == 0 ? null : rank;
isRankAtSliceStartInitialized = true;
}
result = rankAtSliceStart;
} else {
result = null;
}
return result;
}
/**
* If the competitor hasn't started and finished the leg, {@code null} will be returned.
*
* @param ratioOfLegSliceTimeSpent
* the ratio of the time the competitor has spent in this slice of the leg; 0=just starting the leg; 1=just
* finished the slice of the leg
*/
private Integer getRankAtTimePercent(double ratioOfLegSliceTimeSpent) {
final Integer result;
final TrackedRace trackedRace = getTrackedLegContext().getTrackedRaceContext().getTrackedRace();
if (trackedRace != null) {
final TimePoint startTime = getSliceStartTime();
final TimePoint finishTime = getSliceFinishTime();
if (startTime != null && finishTime != null) {
final TimePoint timePoint = startTime.plus(startTime.until(finishTime).times(ratioOfLegSliceTimeSpent));
result = trackedRace.getRank(getCompetitor(), timePoint);
} else {
result = null;
}
} else {
result = null;
}
return result;
}
/**
* A "non-official" hypothetical rank for upwind and downwind legs, pretending we didn't have wind information and
* hence couldn't compute a wind-based rank. This may be used, e.g., to compare the wind-based prediction to the rhumb
* line-based prediction. If the competitor hasn't started and finished the leg, {@code null} will be returned.
*
* @param ratioOfLegTimeSpent
* the ratio of the time the competitor has spent in the leg overall; 0=just starting the leg; 1=just
* finished the leg
*/
private Integer getRhumbLineBasedRankAtTimePercent(double ratioOfLegTimeSpent) {
final Integer result;
final TrackedRace trackedRace = getTrackedLegContext().getTrackedRaceContext().getTrackedRace();
if (trackedRace != null) {
final TimePoint startTime = getSliceStartTime();
final TimePoint finishTime = getSliceFinishTime();
if (startTime != null && finishTime != null) {
final TimePoint timePoint = startTime.plus(startTime.until(finishTime).times(ratioOfLegTimeSpent));
final WindLegTypeAndLegBearingAndORCPerformanceCurveCache cache = new LeaderboardDTOCalculationReuseCache(timePoint) {
/**
* Always returns {@link LegType#REACHING} to force rhumb line-based calculation
*/
@Override
public LegType getLegType(TrackedLeg trackedLeg, TimePoint timePoint) throws NoWindException {
return LegType.REACHING;
}
};
// RaceRankComparator requires course read lock
trackedRace.getRace().getCourse().lockForRead();
final List<Competitor> rankedCompetitors = new ArrayList<>();
try {
Comparator<Competitor> comparator = trackedRace.getRankingMetric().getRaceRankingComparator(timePoint, cache);
Util.addAll(trackedRace.getRace().getCompetitors(), rankedCompetitors);
Collections.sort(rankedCompetitors, comparator);
} finally {
trackedRace.getRace().getCourse().unlockAfterRead();
}
result = rankedCompetitors.indexOf(getCompetitor()) + 1;
} else {
result = null;
}
} else {
result = null;
}
return result;
}
@Override
public Double getRelativeRank() {
Leaderboard leaderboard = getTrackedLegContext().getTrackedRaceContext().getLeaderboardContext().getLeaderboard();
double competitorCount = Util.size(leaderboard.getCompetitors());
Integer rankAtSliceFinish = getRankAtSliceFinish();
return rankAtSliceFinish == null ? null : rankAtSliceFinish / competitorCount;
}
public Integer getRankAtSliceFinish() {
if (!isRankAtSliceFinishInitialized) {
TrackedRace trackedRace = getTrackedLegContext().getTrackedRaceContext().getTrackedRace();
int rank = trackedRace.getRank(getCompetitor(), getSliceFinishTime());
rankAtSliceFinish = rank == 0 ? null : rank;
isRankAtSliceFinishInitialized = true;
}
return rankAtSliceFinish;
}
@Override
public Long getTimeTakenInSeconds() {
TimePoint startTime = getSliceStartTime();
TimePoint finishTime = getSliceFinishTime();
final Long result;
if (startTime == null || finishTime == null) {
result = null;
} else {
result = (finishTime.asMillis() - startTime.asMillis()) / 1000;
}
return result;
}
@Override
public Wind getWindInternal() {
return wind;
}
@Override
public void setWindInternal(Wind wind) {
this.wind = wind;
}
@Override
public Position getPosition() {
final TrackedLeg trackedLeg = getTrackedLegContext().getTrackedLeg();
final TrackedRace trackedRace = trackedLeg.getTrackedRace();
final TimePoint timepoint = getTimePointBetweenLegSliceStartAndLegFinish(trackedRace);
final Position result;
if (timepoint == null) {
result = null;
} else {
result = trackedLeg.getMiddleOfLeg(timepoint);
}
return result;
}
private TimePoint getTimePointBetweenLegSliceStartAndLegFinish(final TrackedRace trackedRace) {
final TimePoint competitorLegSliceStartTime = getSliceStartTime();
final TimePoint competitorLegSliceEndTime = getSliceFinishTime();
final TimePoint startTime = competitorLegSliceStartTime != null ? competitorLegSliceStartTime :
trackedRace.getStartOfRace() != null ? trackedRace.getStartOfRace() : trackedRace.getStartOfTracking();
final TimePoint endTime = competitorLegSliceEndTime != null ? competitorLegSliceEndTime :
trackedRace.getEndOfRace() != null ? trackedRace.getEndOfRace() : trackedRace.getEndOfTracking();
final TimePoint timepoint = endTime == null ? startTime : startTime == null ? null : startTime.plus(startTime.until(endTime).divide(2));
return timepoint;
}
/**
* Picks the time point that is in the middle between the time point when the competitor entered
* the leg and the time point the competitor finished the leg. If no leg start/finish time exists
* for the competitor, start/end of race and then start/end of tracking are used as fall-back values.
*/
@Override
public TimePoint getTimePoint() {
final TrackedLeg trackedLeg = getTrackedLegContext().getTrackedLeg();
final TrackedRace trackedRace = trackedLeg.getTrackedRace();
return getTimePointBetweenLegSliceStartAndLegFinish(trackedRace);
}
@Override
public HasTrackedLegSliceOfCompetitorContext getTrackedLegOfCompetitorContext() {
return this;
}
@Override
public Integer getNumberOfManeuvers() {
return getNumberOfJibes() + getNumberOfTacks();
}
@Override
public Integer getNumberOfJibes() {
return getSomethingForLegTrackingInterval((start, end) -> {
return getNumberOf(ManeuverType.JIBE, start, end);
});
}
@Override
public Integer getNumberOfTacks() {
return getSomethingForLegTrackingInterval((start, end) -> {
return getNumberOf(ManeuverType.TACK, start, end);
});
}
private int getNumberOf(ManeuverType maneuverType, TimePoint start, TimePoint end) {
TrackedRace trackedRace = getTrackedRace();
int number = 0;
if (trackedRace != null) {
for (Maneuver maneuver : trackedRace.getManeuvers(getCompetitor(), start, end, false)) {
if (maneuver.getType() == maneuverType) {
number++;
}
}
}
return number;
}
@Override
public Integer getRankAfterFirstQuarter() {
return getRankAtTimePercent(0.25);
}
@Override
public Integer getRankAfterSecondQuarter() {
return getRankAtTimePercent(0.5);
}
@Override
public Integer getRankAfterThirdQuarter() {
return getRankAtTimePercent(0.75);
}
@Override
public Double getRankAverageAcrossFirstThreeQuarters() {
return getRankAverageAcrossFirstThreeQuarters(this::getRankAtTimePercent);
}
private Double getRankAverageAcrossFirstThreeQuarters(final Function<Double, Integer> rankFunction) {
int rankSum = 0;
int count = 0;
for (double ratio=0.25; ratio<1.0; ratio+=0.25) {
final Integer rankAtRatio = rankFunction.apply(ratio);
if (rankAtRatio != null) {
rankSum += rankAtRatio;
count++;
}
}
return count == 0 ? null : (double) rankSum / (double) count;
}
@Override
public Integer getRankRhumbLineBasedAfterFirstQuarter() {
return getRhumbLineBasedRankAtTimePercent(0.25);
}
@Override
public Integer getRankRhumbLineBasedAfterSecondQuarter() {
return getRhumbLineBasedRankAtTimePercent(0.5);
}
@Override
public Integer getRankRhumbLineBasedAfterThirdQuarter() {
return getRhumbLineBasedRankAtTimePercent(0.75);
}
@Override
public Double getRankRhumbLineBasedAverageAcrossFirstThreeQuarters() {
return getRankAverageAcrossFirstThreeQuarters(this::getRhumbLineBasedRankAtTimePercent);
}
private Double getRankPredictionError(final Supplier<? extends Number> rankPredictor) {
final Integer rankAtFinish = getRankAtSliceFinish();
final Double result;
if (rankAtFinish != null) {
final Number predictiveRank = rankPredictor.get();
if (predictiveRank != null) {
final int numberOfCompetitors = Util.size(getTrackedRace().getRace().getCompetitors());
if (numberOfCompetitors != 0) {
result = Math.abs((double) rankAtFinish-predictiveRank.doubleValue()) / (double) numberOfCompetitors;
} else {
// this is a bit strange because we obviously have a non-null getCompetitor() in the race...
result = null;
}
} else {
result = null;
}
} else {
result = null;
}
return result;
}
@Override
public Double getRankPredictionErrorAfterFirstQuarter() {
return getRankPredictionError(this::getRankAfterFirstQuarter);
}
@Override
public Double getRankPredictionErrorAfterSecondQuarter() {
return getRankPredictionError(this::getRankAfterSecondQuarter);
}
@Override
public Double getRankPredictionErrorAfterThirdQuarter() {
return getRankPredictionError(this::getRankAfterThirdQuarter);
}
@Override
public Double getRankPredictionErrorAcrossFirstThreeQuarters() {
return getRankPredictionError(this::getRankAverageAcrossFirstThreeQuarters);
}
@Override
public Double getRankPredictionErrorRhumbLineBasedAfterFirstQuarter() {
return getRankPredictionError(this::getRankRhumbLineBasedAfterFirstQuarter);
}
@Override
public Double getRankPredictionErrorRhumbLineBasedAfterSecondQuarter() {
return getRankPredictionError(this::getRankRhumbLineBasedAfterSecondQuarter);
}
@Override
public Double getRankPredictionErrorRhumbLineBasedAfterThirdQuarter() {
return getRankPredictionError(this::getRankRhumbLineBasedAfterThirdQuarter);
}
@Override
public Double getRankPredictionErrorRhumbLineBasedAcrossFirstThreeQuarters() {
return getRankPredictionError(this::getRankRhumbLineBasedAverageAcrossFirstThreeQuarters);
}
@Override
public Speed getVelocityMadeGood() {
final TimePoint finishTime = getSliceFinishTime();
return getTrackedLegOfCompetitor().getAverageVelocityMadeGood(finishTime == null ? TimePoint.now() : finishTime);
}
@Override
public double getRatioDurationLongVsShortTack() {
final TackTypeRatioCollector<Duration> resultProcessor = new TackTypeRatioCollector<Duration>(Duration.NULL) {
@Override
protected Duration add(Duration a, Duration b) {
return a.plus(b);
}
@Override
protected double divide(Duration a, Duration b) {
return a.divide(b);
}
@Override
protected Duration getAddable(HasTackTypeSegmentContext element) {
return element.getDuration();
}
};
final TackTypeSegmentRetrievalProcessor tackTypeSegmentRetriever = new TackTypeSegmentRetrievalProcessor(
/* executor */ null,
Collections.emptySet(), settings, 0, "TackTypeSegments");
return Util.stream(tackTypeSegmentRetriever.retrieveData(
new RaceOfCompetitorWithContext(getTrackedLegContext().getTrackedRaceContext(), competitor, settings)))
.filter(tt->tt.getLegNumber() == getTrackedLegContext().getLegNumber()).collect(resultProcessor);
}
protected boolean isTackTypeSegmentIntersectingWithLegSlice(HasTackTypeSegmentContext tackTypeSegmentContext) {
return getSliceStartTime() != null &&
tackTypeSegmentContext.getLegNumber() == getTrackedLegContext().getLegNumber()
&& TimeRange.create(tackTypeSegmentContext.getStartOfTackTypeSegment(), tackTypeSegmentContext.getEndOfTackTypeSegment())
.intersects(TimeRange.create(getSliceStartTime(), getSliceFinishTime()));
}
@Override
public double getRatioDistanceLongVsShortTack() {
final TackTypeRatioCollector<Distance> resultProcessor = new TackTypeRatioCollector<Distance>(Distance.NULL) {
@Override
protected Distance add(Distance a, Distance b) {
return a.add(b);
}
@Override
protected double divide(Distance a, Distance b) {
return a.divide(b);
}
@Override
protected Distance getAddable(HasTackTypeSegmentContext element) {
return element.getDistance();
}
};
final TackTypeSegmentRetrievalProcessor tackTypeSegmentRetriever = new TackTypeSegmentRetrievalProcessor(
/* executor */ null,
Collections.emptySet(), settings, 0, "TackTypeSegments");
return Util.stream(tackTypeSegmentRetriever.retrieveData(
new RaceOfCompetitorWithContext(getTrackedLegContext().getTrackedRaceContext(), competitor, settings)))
.filter(tt->tt.getLegNumber() == getTrackedLegContext().getLegNumber()).collect(resultProcessor);
super(trackedLegContext, trackedLegOfCompetitor, settings, sliceNumber);
}
@Override
public Integer getSliceNumber() {
return sliceNumber;
return super.getTheSliceNumber();
}
}