bug6064: implemented slicing of tracked legs of competitors in data mining

This commit is contained in:
Axel Uhl
2024-11-06 13:02:23 +01:00
parent 890ba4acfb
commit 29b01c7132
6 changed files with 180 additions and 77 deletions
@@ -25,6 +25,7 @@ SearchTag=Search Tag
LeaderboardSailingDomainRetrieverChain=Leaderboards
RaceSailingDomainRetrieverChain=Races
LegOfCompetitorSailingDomainRetrieverChain=Legs of Competitors
LegOfCompetitorSliceSailingDomainRetrieverChain=Slices of Legs of Competitors
GPSFixSailingDomainRetrieverChain=GPS Fixes
BravoFixSailingDomainRetrieverChain=Bravo Fixes
BravoFixTrackSailingDomainRetrieverChain=Bravo Tracks
@@ -22,6 +22,7 @@ Race=Rennen
LeaderboardSailingDomainRetrieverChain=Leaderboards
RaceSailingDomainRetrieverChain=Rennen
LegOfCompetitorSailingDomainRetrieverChain=Bahnschenkel eines Teilnehmers
LegOfCompetitorSliceSailingDomainRetrieverChain=Segmente der Bahnschenkel eines Teilnehmers
GPSFixSailingDomainRetrieverChain=GPS-Fixes
BravoFixSailingDomainRetrieverChain=Bravo-Fixes
BravoFixTrackSailingDomainRetrieverChain=Bravo Messreihen
@@ -38,7 +38,7 @@ public interface HasTrackedLegSliceOfCompetitorContext extends HasWindOnTrackedL
public Distance getDistanceTraveled();
@Statistic(messageKey="SpeedAverage", resultDecimals=2, ordinal=0)
public Double getSpeedAverage();
public Double getSpeedAverageInKnots();
@Statistic(messageKey="RankGainsOrLosses", resultDecimals=2, ordinal=1)
public Integer getRankGainsOrLosses();
@@ -47,7 +47,7 @@ public interface HasTrackedLegSliceOfCompetitorContext extends HasWindOnTrackedL
public Double getRelativeRank();
@Statistic(messageKey="AbsoluteRank", resultDecimals=0, ordinal=3)
public Integer getRankAtFinish();
public Integer getRankAtSliceFinish();
@Statistic(messageKey="AbsoluteRankAfterFirstQuarter", resultDecimals=0, ordinal=4)
public Integer getRankAfterFirstQuarter();
@@ -12,9 +12,13 @@ import com.sap.sse.datamining.components.Processor;
import com.sap.sse.datamining.impl.components.AbstractRetrievalProcessor;
/**
* Produces "slices" of a {@link HasTrackedLegOfCompetitorContext} object, splitting up the leg in multiple consecutive parts. The parts
* are numbered. Slicing currently happens based on wind-based distance traveled in the leg, into ten slices. Future extensions, based on
* settings, may offer more sophisticated ways of slicing. See also bug 6064.
* Produces "slices" of a {@link HasTrackedLegOfCompetitorContext} object, splitting up the leg in multiple consecutive
* parts. The parts are numbered. Slicing currently happens based on the time sailed in the leg, into ten equally long
* slices. If the competitor hasn't finished the leg (yet), the duration sailed so far will be divided up into the slices.
* This makes this fact type less useful for live situation as the slices "grow" while the competitor continues to sail
* in the leg.<p>
*
* Future extensions, based on settings, may offer more sophisticated ways of slicing. See also bug 6064.
*
* @author Axel Uhl (d043530)
*
@@ -1,12 +1,17 @@
package com.sap.sailing.datamining.impl.data;
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.shared.TackTypeSegmentsDataMiningSettings;
import com.sap.sailing.domain.tracking.TrackedLegOfCompetitor;
import com.sap.sse.common.Distance;
import com.sap.sse.common.Speed;
import com.sap.sse.common.TimePoint;
/**
* Equality is based on the {@link #getTrackedLegOfCompetitor()} only.
* 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 {
private static final long serialVersionUID = 5944904146286262768L;
@@ -14,4 +19,50 @@ public class TrackedLegOfCompetitorWithContext extends TrackedLegSliceOfCompetit
public TrackedLegOfCompetitorWithContext(HasTrackedLegContext trackedLegContext, TrackedLegOfCompetitor trackedLegOfCompetitor, TackTypeSegmentsDataMiningSettings settings) {
super(trackedLegContext, trackedLegOfCompetitor, settings, 1);
}
protected TimePoint getSliceStartTime() {
return getTrackedLegOfCompetitor().getStartTime();
}
protected TimePoint getSliceFinishTime() {
return getEffectiveEndOfLeg(getTrackedLegOfCompetitor().getFinishTime());
}
/**
* For the whole leg the distance traveled can be calculated more easily, without subtracting the distance traveled in the leg from
* the beginning of the slice.
*/
@Override
public Distance getDistanceTraveled() {
return getSliceStartTime() == null ? null : getTrackedLegOfCompetitor().getDistanceTraveled(getSliceFinishTime());
}
/**
* Can use {@link TrackedLegOfCompetitor#getAverageSpeedOverGround(TimePoint)} which computes efficiently
* from the competitor's leg start time point.
*/
@Override
public Double getSpeedAverageInKnots() {
final Double result;
if (getTrackedRace() == null) {
result = null;
} else {
if (getSliceStartTime() == null) {
result = null;
} else {
final Speed averageSOG = getTrackedLegOfCompetitor().getAverageSpeedOverGround(getSliceFinishTime());
result = averageSOG == null ? null : averageSOG.getKnots();
}
}
return result;
}
/**
* Shortcut for full leg: only the tack type segment's {@link HasTackTypeSegmentContext#getLegNumber() leg number}
* must match this leg's leg number.
*/
@Override
protected boolean isTackTypeSegmentIntersectingWithLegSlice(HasTackTypeSegmentContext tackTypeSegmentContext) {
return tackTypeSegmentContext.getLegNumber() == getTrackedLegContext().getLegNumber();
}
}
@@ -17,7 +17,6 @@ import com.sap.sailing.datamining.impl.components.TackTypeSegmentRetrievalProces
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.base.Leg;
import com.sap.sailing.domain.common.LegType;
import com.sap.sailing.domain.common.ManeuverType;
import com.sap.sailing.domain.common.NoWindException;
@@ -35,27 +34,32 @@ 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.common.impl.MillisecondsTimePoint;
import com.sap.sse.datamining.data.Cluster;
import com.sap.sse.datamining.shared.impl.dto.ClusterDTO;
/**
* Equality is based on the {@link #getTrackedLegOfCompetitor()} only.
* 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 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 rankAtStart;
private boolean isRankAtStartInitialized;
private Integer rankAtFinish;
private boolean isRankAtFinishInitialized;
private Integer rankAtSliceStart;
private boolean isRankAtSliceStartInitialized;
private Integer rankAtSliceFinish;
private boolean isRankAtSliceFinishInitialized;
private Wind wind;
private int sliceNumber;
@@ -131,18 +135,60 @@ public class TrackedLegSliceOfCompetitorWithContext implements HasTrackedLegSlic
}
protected <R> R getSomethingForLegTrackingInterval(BiFunction<TimePoint, TimePoint, R> resultSupplier) {
final TimePoint startTime = getTrackedLegOfCompetitor().getStartTime();
final TimePoint finishTime = getTrackedLegOfCompetitor().getFinishTime();
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() : MillisecondsTimePoint.now();
trackedRace.getEndOfTracking() != null ? trackedRace.getEndOfTracking() : TimePoint.now();
result = resultSupplier.apply(startTime, effectiveEndOfInterval);
} else {
result = null;
@@ -168,52 +214,45 @@ public class TrackedLegSliceOfCompetitorWithContext implements HasTrackedLegSlic
@Override
public Distance getDistanceTraveled() {
final Distance result;
final TrackedRace trackedRace = getTrackedLegContext().getTrackedRaceContext().getTrackedRace();
if (trackedRace != null) {
final TimePoint now = TimePoint.now();
final TimePoint endTimePoint = trackedRace.getEndOfRace() != null ? trackedRace.getEndOfRace()
: trackedRace.getEndOfTracking() != null ? trackedRace.getEndOfTracking() : now;
result = getTrackedLegOfCompetitor().getDistanceTraveled(endTimePoint.after(now) ? now : endTimePoint);
} else {
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 Double getSpeedAverage() {
if (getTrackedRace() == null) {
return null;
} else {
final List<Leg> legs = getTrackedRace().getRace().getCourse().getLegs();
if (legs.isEmpty()) {
return null;
} else {
final Leg lastLeg = legs.get(legs.size() - 1);
TimePoint finished = getTrackedRace().getTrackedLeg(getCompetitor(), lastLeg).getFinishTime();
return getTrackedRace().getAverageSpeedOverGround(getCompetitor(), finished).getKnots();
}
}
}
@Override
public Integer getRankGainsOrLosses() {
Integer rankAtStart = getRankAtStart();
Integer rankAtFinish = getRankAtFinish();
return rankAtStart != null && rankAtFinish != null ? rankAtStart - rankAtFinish : null;
Integer rankAtSliceStart = getRankAtSliceStart();
Integer rankAtSliceFinish = getRankAtSliceFinish();
return rankAtSliceStart != null && rankAtSliceFinish != null ? rankAtSliceStart - rankAtSliceFinish : null;
}
private Integer getRankAtStart() {
private Integer getRankAtSliceStart() {
final Integer result;
if (getTrackedLegContext().getTrackedRaceContext().getTrackedRace() != null) {
if (!isRankAtStartInitialized) {
if (!isRankAtSliceStartInitialized) {
TrackedRace trackedRace = getTrackedLegContext().getTrackedRaceContext().getTrackedRace();
int rank = trackedRace.getRank(getCompetitor(), getTrackedLegOfCompetitor().getStartTime());
rankAtStart = rank == 0 ? null : rank;
isRankAtStartInitialized = true;
int rank = trackedRace.getRank(getCompetitor(), getSliceStartTime());
rankAtSliceStart = rank == 0 ? null : rank;
isRankAtSliceStartInitialized = true;
}
result = rankAtStart;
result = rankAtSliceStart;
} else {
result = null;
}
@@ -223,18 +262,18 @@ public class TrackedLegSliceOfCompetitorWithContext implements HasTrackedLegSlic
/**
* 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
* @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 ratioOfLegTimeSpent) {
private Integer getRankAtTimePercent(double ratioOfLegSliceTimeSpent) {
final Integer result;
final TrackedRace trackedRace = getTrackedLegContext().getTrackedRaceContext().getTrackedRace();
if (trackedRace != null) {
final TimePoint startTime = getTrackedLegOfCompetitor().getStartTime();
final TimePoint finishTime = getTrackedLegOfCompetitor().getFinishTime();
final TimePoint startTime = getSliceStartTime();
final TimePoint finishTime = getSliceFinishTime();
if (startTime != null && finishTime != null) {
final TimePoint timePoint = startTime.plus(startTime.until(finishTime).times(ratioOfLegTimeSpent));
final TimePoint timePoint = startTime.plus(startTime.until(finishTime).times(ratioOfLegSliceTimeSpent));
result = trackedRace.getRank(getCompetitor(), timePoint);
} else {
result = null;
@@ -258,8 +297,8 @@ public class TrackedLegSliceOfCompetitorWithContext implements HasTrackedLegSlic
final Integer result;
final TrackedRace trackedRace = getTrackedLegContext().getTrackedRaceContext().getTrackedRace();
if (trackedRace != null) {
final TimePoint startTime = getTrackedLegOfCompetitor().getStartTime();
final TimePoint finishTime = getTrackedLegOfCompetitor().getFinishTime();
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) {
@@ -295,24 +334,24 @@ public class TrackedLegSliceOfCompetitorWithContext implements HasTrackedLegSlic
public Double getRelativeRank() {
Leaderboard leaderboard = getTrackedLegContext().getTrackedRaceContext().getLeaderboardContext().getLeaderboard();
double competitorCount = Util.size(leaderboard.getCompetitors());
Integer rankAtFinish = getRankAtFinish();
return rankAtFinish == null ? null : rankAtFinish / competitorCount;
Integer rankAtSliceFinish = getRankAtSliceFinish();
return rankAtSliceFinish == null ? null : rankAtSliceFinish / competitorCount;
}
public Integer getRankAtFinish() {
if (!isRankAtFinishInitialized) {
public Integer getRankAtSliceFinish() {
if (!isRankAtSliceFinishInitialized) {
TrackedRace trackedRace = getTrackedLegContext().getTrackedRaceContext().getTrackedRace();
int rank = trackedRace.getRank(getCompetitor(), getTrackedLegOfCompetitor().getFinishTime());
rankAtFinish = rank == 0 ? null : rank;
isRankAtFinishInitialized = true;
int rank = trackedRace.getRank(getCompetitor(), getSliceFinishTime());
rankAtSliceFinish = rank == 0 ? null : rank;
isRankAtSliceFinishInitialized = true;
}
return rankAtFinish;
return rankAtSliceFinish;
}
@Override
public Long getTimeTakenInSeconds() {
TimePoint startTime = getTrackedLegOfCompetitor().getStartTime();
TimePoint finishTime = getTrackedLegOfCompetitor().getFinishTime();
TimePoint startTime = getSliceStartTime();
TimePoint finishTime = getSliceFinishTime();
final Long result;
if (startTime == null || finishTime == null) {
result = null;
@@ -336,7 +375,7 @@ public class TrackedLegSliceOfCompetitorWithContext implements HasTrackedLegSlic
public Position getPosition() {
final TrackedLeg trackedLeg = getTrackedLegContext().getTrackedLeg();
final TrackedRace trackedRace = trackedLeg.getTrackedRace();
final TimePoint timepoint = getTimePointBetweenLegStartAndLegFinish(trackedRace);
final TimePoint timepoint = getTimePointBetweenLegSliceStartAndLegFinish(trackedRace);
final Position result;
if (timepoint == null) {
result = null;
@@ -346,12 +385,12 @@ public class TrackedLegSliceOfCompetitorWithContext implements HasTrackedLegSlic
return result;
}
private TimePoint getTimePointBetweenLegStartAndLegFinish(final TrackedRace trackedRace) {
final TimePoint competitorLegStartTime = getTrackedLegOfCompetitor().getStartTime();
final TimePoint competitorLegEndTime = getTrackedLegOfCompetitor().getFinishTime();
final TimePoint startTime = competitorLegStartTime != null ? competitorLegStartTime :
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 = competitorLegEndTime != null ? competitorLegEndTime :
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;
@@ -366,7 +405,7 @@ public class TrackedLegSliceOfCompetitorWithContext implements HasTrackedLegSlic
public TimePoint getTimePoint() {
final TrackedLeg trackedLeg = getTrackedLegContext().getTrackedLeg();
final TrackedRace trackedRace = trackedLeg.getTrackedRace();
return getTimePointBetweenLegStartAndLegFinish(trackedRace);
return getTimePointBetweenLegSliceStartAndLegFinish(trackedRace);
}
@Override
@@ -460,7 +499,7 @@ public class TrackedLegSliceOfCompetitorWithContext implements HasTrackedLegSlic
}
private Double getRankPredictionError(final Supplier<? extends Number> rankPredictor) {
final Integer rankAtFinish = getRankAtFinish();
final Integer rankAtFinish = getRankAtSliceFinish();
final Double result;
if (rankAtFinish != null) {
final Number predictiveRank = rankPredictor.get();
@@ -523,7 +562,7 @@ public class TrackedLegSliceOfCompetitorWithContext implements HasTrackedLegSlic
@Override
public Speed getVelocityMadeGood() {
final TimePoint finishTime = getTrackedLegOfCompetitor().getFinishTime();
final TimePoint finishTime = getSliceFinishTime();
return getTrackedLegOfCompetitor().getAverageVelocityMadeGood(finishTime == null ? TimePoint.now() : finishTime);
}
@@ -552,6 +591,13 @@ public class TrackedLegSliceOfCompetitorWithContext implements HasTrackedLegSlic
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() {