Merge remote-tracking branch 'origin/master' into dataMining

This commit is contained in:
Lennart Hensler committed 2015-01-08 12:49:53 +01:00
commit 62cbabcc14
157 files changed
+6321 -1855

No files matched your search

@@ -11,13 +11,15 @@ Export-Package:
com.sap.sailing.polars.analysis,
com.sap.sailing.polars.data,
com.sap.sailing.polars.factory,
com.sap.sailing.polars.regression
com.sap.sailing.polars.regression,
com.sap.sailing.polars.windestimation
Bundle-ActivationPolicy: lazy
Require-Bundle:
com.sap.sailing.domain.common;bundle-version="1.0.0",
com.sap.sailing.domain,
com.sap.sse.datamining,
com.sap.sse.datamining.shared;bundle-version="1.0.0"
com.sap.sse.datamining.shared;bundle-version="1.0.0",
com.sap.sse.common
Import-Package:
com.sap.sailing.util,
com.sap.sse.common
+48 -38
View File
@@ -17,18 +17,18 @@ import com.sap.sailing.domain.common.Tack;
import com.sap.sailing.domain.tracking.GPSFixMoving;
import com.sap.sailing.domain.tracking.TrackedRace;
import com.sap.sailing.polars.analysis.PolarSheetAnalyzer;
import com.sap.sailing.polars.data.PolarFix;
import com.sap.sailing.polars.regression.NotEnoughDataHasBeenAddedException;
/**
* Public Facade interface allowing access to the polars of {@link BoatClass}es.
* Public Facade interface granting clients access to the polar sheets of {@link BoatClass}es. A boat's "polar sheet"
* (sometimes also referred to as a "VPP" (velocity prediction program)) makes a prediction how fast the boat will
* sail at a given true wind angle and a given true wind speed.<p>
*
* It uses a {@link PolarSheetAnalyzer} for more advanced analysis. It's methods are facaded in this interface for
* central access.
*
* The interesting methods for a user are {@link #getSpeed(BoatClass, Speed, Bearing, boolean)} if data for a specific angle is
* This service uses a {@link PolarSheetAnalyzer} for more advanced analysis. Its methods are facaded in this interface for
* central access.<p>
* The interesting methods for a client are {@link #getSpeed(BoatClass, Speed, Bearing, boolean)} if data for a specific angle is
* needed and {@link #getAverageSpeedWithBearing(BoatClass, Speed, LegType, Tack)}
* which also returns the average angle for the provided parameters.
* which also returns the average angle for the parameters provided.
*
* @author Frederik Petersen (D054528)
*
@@ -39,35 +39,37 @@ public interface PolarDataService {
*
* @param boatClass
* @param windSpeed
* @param bearingToTheWind
* Boat's direction relative to the wind. either in -180 -> +180 or 0 -> 359 degrees
* @param useLinearRegression if true uses lin. Regression for estimation in the wind interval, if false simple arithm. mean in interval
* @param trueWindAngle
* Boat's direction relative to the wind. either in -180 -> +180 or 0 -> 359 degrees interval. The true wind!
* @return The speed the boat is moving at for the specified wind and bearing according to the polar diagram.
* @throws NotEnoughDataHasBeenAddedException
*/
SpeedWithConfidence<Void> getSpeed(BoatClass boatClass, Speed windSpeed, Bearing bearingToTheWind, boolean useLinearRegression)
SpeedWithConfidence<Void> getSpeed(BoatClass boatClass, Speed windSpeed, Bearing trueWindAngle)
throws NotEnoughDataHasBeenAddedException;
/**
*
* @param boatClass
* @param windSpeed
* @param windSpeed
* @param legType
* Should be UpWind or DownWind, there is no information for other courses yet. Use getSpeed for the
* desired angle to get rawer information on other courses for now.
* Should be {@link LegType#UPWIND} or {@link LegType#DOWNWIND}, there is no information for other
* courses yet. Use getSpeed for the desired angle to get rawer information on other courses for now.
* @param tack
* Polar data can vary depending on the tack the boat is on.
* @return The estimated average speed of a boat for the supplied parameters with the estimated average bearing to
* the wind and a confidence which consists of the confidences of the wind speed, and boat speed sources (50%)
* the true wind and a confidence which consists of the confidences of the wind speed, and boat speed sources (50%)
* and a confidence calculated using the amount of underlying fixes (50%). 0 <= confidence < 1<br/>
* A value with zero confidence doesn't have any significance!<br/><br/>
*
* The bearing is somewhere between -179 to +180<br/><br/>
*
* Get the speed using returnValue.getObject()<br/><br/>
*
* A value with zero confidence doesn't have any significance!<br/>
* <br/>
*
* The bearing is somewhere between -179 to +180<br/>
* <br/>
*
* Get the speed using returnValue.getObject()<br/>
* <br/>
*
* Returns null if the leg type is not up or downwind.
*
*
* @throws NotEnoughDataHasBeenAddedException
* If there is not enough data to supply a value with some kind of significance.
*/
@@ -76,7 +78,7 @@ public interface PolarDataService {
/**
* Generates a polar sheet for geven races and settings using the provided executor for the worker threads. This
* Generates a polar sheet for given races and settings using the provided executor for the worker threads. This
* method does not access a cache for now.
*
* @param trackedRaces
@@ -90,16 +92,6 @@ public interface PolarDataService {
PolarSheetsData generatePolarSheet(Set<TrackedRace> trackedRaces, PolarSheetGenerationSettings settings,
Executor executor) throws InterruptedException, ExecutionException;
void newRaceFinishedTracking(TrackedRace trackedRace);
/**
* @param key
* The {@link BoatClass} to obtain fixes for.
* @return All raw polar fixes for the {@link BoatClass}. The implementation is responsible for deciding wether a
* cache is used or not.
*/
Set<PolarFix> getPolarFixesForBoatClass(BoatClass key);
/**
*
* @param boatClass
@@ -115,6 +107,14 @@ public interface PolarDataService {
*/
Set<BoatClass> getAllBoatClassesWithPolarSheetsAvailable();
/**
* To be called in an appropriate listener.
* Starting point for fixes entering the backend polar data mining pipeline.
*
* @param fix
* @param competitor
* @param createdTrackedRace
*/
void competitorPositionChanged(GPSFixMoving fix, Competitor competitor, TrackedRace createdTrackedRace);
/**
@@ -123,12 +123,22 @@ public interface PolarDataService {
* @param windSpeed
* @param startAngleInclusive between 0 and 359; smaller than (or equal to) endAngleExclusive
* @param endAngleExclusive between 0 and 359; bigger than startAngleInclusive
* @return array with datacount for all angles in the given area, else null
* @return array with datacount for all angles in the given area, else -1
*/
Integer[] getDataCountsForWindSpeed(BoatClass boatClass, Speed windSpeed, int startAngleInclusive, int endAngleExclusive);
int[] getDataCountsForWindSpeed(BoatClass boatClass, Speed windSpeed, int startAngleInclusive, int endAngleExclusive);
SpeedWithBearingWithConfidence<Void> getAverageSpeedWithBearing(BoatClass boatClass, Speed windSpeed,
LegType legType, Tack tack, boolean useLinReg) throws NotEnoughDataHasBeenAddedException;
/**
* From a boat's speed over ground and assuming values for <code>boatClass</code>, the <code>tack</code> the boat is
* currently sailing on, and the <code>legType</code>, this method estimates the true wind speed candidates at which
* the boat may most likely have been sailing under these conditions.
*
* The confidence of the returned candidates is derived from the average confidence of the underlying fixes,
* distance measures and the amount of underlying data.
*
* Multiple candidates are possible, because we cannot guarantee a reversible function (boatspeed over windspeed).
*
* @return set of wind candidates with confidence, empty set if no were found (due to insufficient underlying data)
*/
Set<SpeedWithBearingWithConfidence<Void>> getAverageTrueWindSpeedAndAngleCandidates(BoatClass boatClass, Speed speedOverGround, LegType legType, Tack tack);
}
@@ -2,21 +2,12 @@ package com.sap.sailing.polars.analysis;
import com.sap.sailing.domain.base.BoatClass;
import com.sap.sailing.domain.base.SpeedWithBearingWithConfidence;
import com.sap.sailing.domain.common.LegType;
import com.sap.sailing.domain.common.Speed;
import com.sap.sailing.domain.common.Tack;
import com.sap.sailing.polars.regression.NotEnoughDataHasBeenAddedException;
public interface PolarSheetAnalyzer {
SpeedWithBearingWithConfidence<Void> getAverageUpwindSpeedWithBearingOnStarboardTackFor(BoatClass boatClass,
Speed windSpeed, boolean useLinReg) throws NotEnoughDataHasBeenAddedException;
SpeedWithBearingWithConfidence<Void> getAverageDownwindSpeedWithBearingOnStarboardTackFor(BoatClass boatClass,
Speed windSpeed, boolean useLinReg) throws NotEnoughDataHasBeenAddedException;
SpeedWithBearingWithConfidence<Void> getAverageUpwindSpeedWithBearingOnPortTackFor(BoatClass boatClass,
Speed windSpeed, boolean useLinReg) throws NotEnoughDataHasBeenAddedException;
SpeedWithBearingWithConfidence<Void> getAverageDownwindSpeedWithBearingOnPortTackFor(BoatClass boatClass,
Speed windSpeed, boolean useLinReg) throws NotEnoughDataHasBeenAddedException;
SpeedWithBearingWithConfidence<Void> getAverageSpeedAndCourseOverGround(BoatClass boatClass,
Speed windSpeed, LegType legType, Tack tack) throws NotEnoughDataHasBeenAddedException;
}
@@ -1,16 +1,14 @@
package com.sap.sailing.polars.analysis.impl;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import com.sap.sailing.domain.base.BoatClass;
import com.sap.sailing.domain.base.SpeedWithBearingWithConfidence;
import com.sap.sailing.domain.base.SpeedWithConfidence;
import com.sap.sailing.domain.base.impl.SpeedWithBearingWithConfidenceImpl;
import com.sap.sailing.domain.common.Bearing;
import com.sap.sailing.domain.common.LegType;
import com.sap.sailing.domain.common.Speed;
import com.sap.sailing.domain.common.SpeedWithBearing;
import com.sap.sailing.domain.common.Tack;
import com.sap.sailing.domain.common.impl.DegreeBearingImpl;
import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl;
import com.sap.sailing.polars.PolarDataService;
@@ -26,70 +24,46 @@ import com.sap.sailing.polars.regression.NotEnoughDataHasBeenAddedException;
public class PolarSheetAnalyzerImpl implements PolarSheetAnalyzer {
private static final int MINIMUM_DATA_COUNT_FOR_ONE_ANGLE = 5;
private PolarDataService polarDataService;
private final PolarDataService polarDataService;
public PolarSheetAnalyzerImpl(PolarDataService polarDataService) {
this.polarDataService = polarDataService;
}
@Override
public SpeedWithBearingWithConfidence<Void> getAverageUpwindSpeedWithBearingOnStarboardTackFor(BoatClass boatClass, Speed windSpeed, boolean useLinReg)
public SpeedWithBearingWithConfidence<Void> getAverageSpeedAndCourseOverGround(BoatClass boatClass, Speed windSpeed, LegType legType, Tack tack)
throws NotEnoughDataHasBeenAddedException {
int startAngleInclusive = 1;
int endAngleExclusive = 90;
final int startAngleInclusive;
final int endAngleExclusive;
if (legType == LegType.UPWIND && tack == Tack.STARBOARD) {
startAngleInclusive = 0;
endAngleExclusive = 90;
} else if (legType == LegType.DOWNWIND && tack == Tack.STARBOARD) {
startAngleInclusive = 90;
endAngleExclusive = 180;
} else if (legType == LegType.DOWNWIND && tack == Tack.PORT) {
startAngleInclusive = 180;
endAngleExclusive = 270;
} else if (legType == LegType.UPWIND && tack == Tack.PORT) {
startAngleInclusive = 270;
endAngleExclusive = 360;
} else {
throw new IllegalArgumentException("Leg type must be "+LegType.UPWIND.name()+
" or "+LegType.DOWNWIND.name()+" but was "+legType.name());
}
SpeedWithBearingWithConfidence<Void> speedWithBearing = estimateAnglePeakAndAverageSpeed(boatClass, windSpeed, startAngleInclusive,
endAngleExclusive, useLinReg);
endAngleExclusive);
return speedWithBearing;
}
@Override
public SpeedWithBearingWithConfidence<Void> getAverageDownwindSpeedWithBearingOnStarboardTackFor(BoatClass boatClass, Speed windSpeed, boolean useLinReg)
throws NotEnoughDataHasBeenAddedException {
int startAngleInclusive = 91;
int endAngleExclusive = 180;
SpeedWithBearingWithConfidence<Void> speedWithBearing = estimateAnglePeakAndAverageSpeed(boatClass, windSpeed, startAngleInclusive,
endAngleExclusive, useLinReg);
return speedWithBearing;
}
@Override
public SpeedWithBearingWithConfidence<Void> getAverageDownwindSpeedWithBearingOnPortTackFor(BoatClass boatClass, Speed windSpeed, boolean useLinReg)
throws NotEnoughDataHasBeenAddedException {
int startAngleInclusive = 181;
int endAngleExclusive = 270;
SpeedWithBearingWithConfidence<Void> speedWithBearing = estimateAnglePeakAndAverageSpeed(boatClass, windSpeed, startAngleInclusive,
endAngleExclusive, useLinReg);
return speedWithBearing;
}
@Override
public SpeedWithBearingWithConfidence<Void> getAverageUpwindSpeedWithBearingOnPortTackFor(BoatClass boatClass, Speed windSpeed, boolean useLinReg)
throws NotEnoughDataHasBeenAddedException {
int startAngleInclusive = 271;
int endAngleExclusive = 360;
SpeedWithBearingWithConfidence<Void> speedWithBearing = estimateAnglePeakAndAverageSpeed(boatClass, windSpeed, startAngleInclusive,
endAngleExclusive, useLinReg);
return speedWithBearing;
}
private SpeedWithBearingWithConfidence<Void> estimateAnglePeakAndAverageSpeed(BoatClass boatClass, Speed windSpeed,
int startAngleInclusive, int endAngleExclusive, boolean useLinearRegression) throws NotEnoughDataHasBeenAddedException {
Integer[] dataCountPerAngle = getDataCountArray(boatClass, windSpeed, startAngleInclusive, endAngleExclusive);
int startAngleInclusive, int endAngleExclusive) throws NotEnoughDataHasBeenAddedException {
int[] dataCountPerAngle = getDataCountArray(boatClass, windSpeed, startAngleInclusive, endAngleExclusive);
double estimatedPeak = estimatePeak(boatClass, windSpeed, startAngleInclusive, endAngleExclusive, dataCountPerAngle);
double convertedAngleIfOver180 = convertAngleIfNecessary(estimatedPeak);
SpeedWithConfidence<Void> averagedSpeedWithConfidence = estimateSpeed(boatClass, windSpeed,
convertedAngleIfOver180, useLinearRegression);
SpeedWithConfidence<Void> averagedSpeedWithConfidence = estimateSpeed(boatClass, windSpeed, convertedAngleIfOver180);
double originConfidence = averagedSpeedWithConfidence.getConfidence();
double overallConfidence = (originConfidence + calcDataCountConfidence(dataCountPerAngle, estimatedPeak)) / 2;
Bearing bearing = new DegreeBearingImpl(convertedAngleIfOver180);
SpeedWithBearing speedWithBearing = new KnotSpeedWithBearingImpl(averagedSpeedWithConfidence.getObject().getKnots(), bearing);
return new SpeedWithBearingWithConfidenceImpl<>(speedWithBearing, overallConfidence, null);
@@ -104,14 +78,14 @@ public class PolarSheetAnalyzerImpl implements PolarSheetAnalyzer {
* @param estimatedPeak
* @return
*/
private double calcDataCountConfidence(Integer[] dataCountPerAngle, double estimatedPeak) {
private double calcDataCountConfidence(int[] dataCountPerAngle, double estimatedPeak) {
int dataCount = dataCountPerAngle[(int) Math.round(estimatedPeak)];
double confidence = 1 - Math.pow(Math.E, - dataCount / 150);
return confidence;
}
private Integer[] getDataCountArray(BoatClass boatClass, Speed windSpeed, int startAngleInclusive, int endAngleExclusive) {
Integer[] dataCountPerAngle = polarDataService.getDataCountsForWindSpeed(boatClass, windSpeed, startAngleInclusive, endAngleExclusive);
private int[] getDataCountArray(BoatClass boatClass, Speed windSpeed, int startAngleInclusive, int endAngleExclusive) {
int[] dataCountPerAngle = polarDataService.getDataCountsForWindSpeed(boatClass, windSpeed, startAngleInclusive, endAngleExclusive);
return dataCountPerAngle;
}
@@ -123,20 +97,21 @@ public class PolarSheetAnalyzerImpl implements PolarSheetAnalyzer {
return convertedAngleIfOver180;
}
private SpeedWithConfidence<Void> estimateSpeed(BoatClass boatClass, Speed windSpeed, double estimatedPeak,
boolean useLinearRegression) throws NotEnoughDataHasBeenAddedException {
private SpeedWithConfidence<Void> estimateSpeed(BoatClass boatClass, Speed windSpeed, double estimatedPeak) throws NotEnoughDataHasBeenAddedException {
SpeedWithConfidence<Void> speed = polarDataService.getSpeed(boatClass, windSpeed, new DegreeBearingImpl(
estimatedPeak), useLinearRegression);
estimatedPeak));
return speed;
}
private double estimatePeak(BoatClass boatClass, Speed windSpeed, int startAngleInclusive, int endAngleExclusive, Integer[] dataCountPerAngle)
private double estimatePeak(BoatClass boatClass, Speed windSpeed, int startAngleInclusive, int endAngleExclusive, int[] dataCountPerAngle)
throws NotEnoughDataHasBeenAddedException {
// Find peak by averaging the angles that have at least 50% of the max datacount
List<Integer> dataCountList = Arrays.asList(Arrays.copyOfRange(dataCountPerAngle, startAngleInclusive,
endAngleExclusive));
int maxDataCount = Collections.max(dataCountList);
int maxDataCount = Integer.MIN_VALUE;
for (int i = startAngleInclusive; i < endAngleExclusive; i++) {
if (dataCountPerAngle[i] > maxDataCount) {
maxDataCount = dataCountPerAngle[i];
}
}
if (maxDataCount < MINIMUM_DATA_COUNT_FOR_ONE_ANGLE) {
// The angle with the most data points doesn't have sufficient data, for the polar data to have any
// significance.
@@ -146,17 +121,13 @@ public class PolarSheetAnalyzerImpl implements PolarSheetAnalyzer {
int sumOfAllUpperHalfAngles = 0;
int numberOfAnglesAdded = 0;
for (int i = startAngleInclusive; i < endAngleExclusive; i++) {
Integer dataCount = dataCountPerAngle[i];
int dataCount = dataCountPerAngle[i];
if (dataCount > maxDataCount / 2) {
sumOfAllUpperHalfAngles = sumOfAllUpperHalfAngles + i * dataCount;
numberOfAnglesAdded = numberOfAnglesAdded + dataCount;
}
}
double estimatedPeak = (double) sumOfAllUpperHalfAngles / (double) numberOfAnglesAdded;
return estimatedPeak;
}
}
@@ -1,27 +0,0 @@
package com.sap.sailing.polars.caching;
import java.util.Set;
import com.sap.sailing.domain.tracking.TrackedRace;
import com.sap.sailing.polars.data.PolarFix;
public class NoCacheEntryException extends Exception {
private static final long serialVersionUID = 5547683568438235668L;
private final Set<TrackedRace> notCached;
private final Set<PolarFix> resultList;
public NoCacheEntryException(Set<TrackedRace> notCached, Set<PolarFix> resultList) {
this.notCached = notCached;
this.resultList = resultList;
}
public Set<TrackedRace> getNotCached() {
return notCached;
}
public Set<PolarFix> getCachedResultList() {
return resultList;
}
}
@@ -1,67 +0,0 @@
package com.sap.sailing.polars.caching;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executor;
import com.sap.sailing.domain.base.BoatClass;
import com.sap.sailing.domain.common.RegattaAndRaceIdentifier;
import com.sap.sailing.domain.tracking.TrackedRace;
import com.sap.sailing.polars.data.PolarFix;
import com.sap.sailing.util.SmartFutureCache;
public class PolarFixCache extends
SmartFutureCache<BoatClass, Map<RegattaAndRaceIdentifier, List<PolarFix>>, PolarFixCacheRaceInterval> {
private final List<PolarFixCacheUpdateDoneListener> cacheUpdateDoneListeners = new ArrayList<PolarFixCacheUpdateDoneListener>();
public PolarFixCache(Executor executor) {
super(new PolarFixCacheUpdater(executor), "polarFixCache");
}
@Override
protected void cache(BoatClass key, Map<RegattaAndRaceIdentifier, List<PolarFix>> value) {
super.cache(key, value);
synchronized (cacheUpdateDoneListeners) {
for (PolarFixCacheUpdateDoneListener listener : cacheUpdateDoneListeners) {
listener.cacheUpdateDoneForBoatClass(key);
}
}
}
public void addListener(PolarFixCacheUpdateDoneListener listener) {
synchronized (cacheUpdateDoneListeners) {
cacheUpdateDoneListeners.add(listener);
}
}
public void removeListener(PolarFixCacheUpdateDoneListener listener) {
synchronized (cacheUpdateDoneListeners) {
if (cacheUpdateDoneListeners.contains(listener)) {
cacheUpdateDoneListeners.remove(listener);
}
}
}
public Set<PolarFix> getFixesForTrackedRaces(Set<TrackedRace> trackedRaces) throws NoCacheEntryException {
Set<PolarFix> resultList = new HashSet<PolarFix>();
Set<TrackedRace> notCached = new HashSet<TrackedRace>();
for (TrackedRace trackedRace : trackedRaces) {
Map<RegattaAndRaceIdentifier, List<PolarFix>> result = get(trackedRace.getRace().getBoatClass(), false);
List<PolarFix> resultForRace = result.get(trackedRace.getRaceIdentifier());
if (resultForRace == null) {
notCached.add(trackedRace);
} else {
resultList.addAll(resultForRace);
}
}
if (notCached.size() > 0) {
throw new NoCacheEntryException(notCached, resultList);
}
return resultList;
}
}
@@ -1,97 +0,0 @@
package com.sap.sailing.polars.caching;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.tracking.TrackedRace;
import com.sap.sailing.polars.aggregation.PolarFixRaceInterval;
import com.sap.sailing.util.SmartFutureCache.UpdateInterval;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.Util.Pair;
public class PolarFixCacheRaceInterval implements UpdateInterval<PolarFixCacheRaceInterval>, PolarFixRaceInterval {
private final Map<TrackedRace, Map<Competitor, Pair<TimePoint, TimePoint>>> competitorAndTimepointsForRace;
public PolarFixCacheRaceInterval(
Map<TrackedRace, Map<Competitor, Pair<TimePoint, TimePoint>>> competitorAndTimepointsForRace) {
this.competitorAndTimepointsForRace = competitorAndTimepointsForRace;
}
@Override
public Map<TrackedRace, Map<Competitor, Pair<TimePoint, TimePoint>>> getCompetitorAndTimepointsForRace() {
return competitorAndTimepointsForRace;
}
@Override
public PolarFixCacheRaceInterval join(PolarFixCacheRaceInterval otherUpdateInterval) {
Map<TrackedRace, Map<Competitor, Pair<TimePoint, TimePoint>>> joinedInterval = new HashMap<TrackedRace, Map<Competitor, Pair<TimePoint, TimePoint>>>();
Set<TrackedRace> racesContainedInEitherMap = new HashSet<TrackedRace>();
for (TrackedRace race : competitorAndTimepointsForRace.keySet()) {
racesContainedInEitherMap.add(race);
}
Map<TrackedRace, Map<Competitor, Pair<TimePoint, TimePoint>>> otherUpdateIntervalInExplicitForm = otherUpdateInterval
.getCompetitorAndTimepointsForRace();
for (TrackedRace race : otherUpdateIntervalInExplicitForm.keySet()) {
racesContainedInEitherMap.add(race);
}
for (TrackedRace race : racesContainedInEitherMap) {
HashMap<Competitor, Pair<TimePoint, TimePoint>> joinedMap = new HashMap<Competitor, Pair<TimePoint, TimePoint>>();
Map<Competitor, Pair<TimePoint, TimePoint>> mapOfTimpointCompetitorPairs = competitorAndTimepointsForRace
.get(race);
if (mapOfTimpointCompetitorPairs != null) {
joinedMap.putAll(mapOfTimpointCompetitorPairs);
}
Map<Competitor, Pair<TimePoint, TimePoint>> mapOfOtherTimpointCompetitorPairs = otherUpdateIntervalInExplicitForm
.get(race);
if (mapOfOtherTimpointCompetitorPairs != null) {
mergeMap(joinedMap, mapOfOtherTimpointCompetitorPairs);
}
joinedInterval.put(race, joinedMap);
}
return new PolarFixCacheRaceInterval(joinedInterval);
}
private void mergeMap(HashMap<Competitor, Pair<TimePoint, TimePoint>> joinedMap,
Map<Competitor, Pair<TimePoint, TimePoint>> mapOfOtherTimpointCompetitorPairs) {
for (Entry<Competitor, Pair<TimePoint, TimePoint>> otherEntry : mapOfOtherTimpointCompetitorPairs.entrySet()) {
Competitor competitor = otherEntry.getKey();
Pair<TimePoint, TimePoint> otherValue = otherEntry.getValue();
Pair<TimePoint, TimePoint> existingValue = joinedMap.get(competitor);
if (existingValue != null) {
Pair<TimePoint, TimePoint> joinedTimepointInterval = joinTimepointIntervals(existingValue, otherValue);
joinedMap.put(competitor, joinedTimepointInterval);
} else {
joinedMap.put(competitor, otherValue);
}
}
}
/**
* This method ignores a possible gap between the two intervals.
*
* @param existingValue
* @param otherValue
* @return
*/
private Pair<TimePoint, TimePoint> joinTimepointIntervals(Pair<TimePoint, TimePoint> existingValue,
Pair<TimePoint, TimePoint> otherValue) {
TimePoint startExisting = existingValue.getA();
TimePoint endExisting = existingValue.getB();
TimePoint startOther = otherValue.getA();
TimePoint endOther = otherValue.getB();
TimePoint startNew = startExisting.before(startOther) ? startExisting : startOther;
TimePoint endNew = endExisting.after(endOther) ? endExisting : endOther;
return new Pair<TimePoint, TimePoint>(startNew, endNew);
}
}
@@ -1,9 +0,0 @@
package com.sap.sailing.polars.caching;
import com.sap.sailing.domain.base.BoatClass;
public interface PolarFixCacheUpdateDoneListener {
void cacheUpdateDoneForBoatClass(BoatClass key);
}
@@ -1,56 +0,0 @@
package com.sap.sailing.polars.caching;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.Executor;
import com.sap.sailing.domain.base.BoatClass;
import com.sap.sailing.domain.common.RegattaAndRaceIdentifier;
import com.sap.sailing.domain.common.impl.PolarSheetGenerationSettingsImpl;
import com.sap.sailing.polars.aggregation.PolarFixAggregator;
import com.sap.sailing.polars.data.PolarFix;
import com.sap.sailing.util.SmartFutureCache.CacheUpdater;
public class PolarFixCacheUpdater implements
CacheUpdater<BoatClass, Map<RegattaAndRaceIdentifier, List<PolarFix>>, PolarFixCacheRaceInterval> {
private final Executor executor;
public PolarFixCacheUpdater(Executor executor) {
this.executor = executor;
}
@Override
public Map<RegattaAndRaceIdentifier, List<PolarFix>> computeCacheUpdate(BoatClass key,
PolarFixCacheRaceInterval updateInterval) throws Exception {
PolarFixAggregator aggregator = new PolarFixAggregator(updateInterval,
PolarSheetGenerationSettingsImpl.createStandardPolarSettings(), executor);
Thread.sleep(updateInterval.getCompetitorAndTimepointsForRace().keySet().iterator().next()
.getMillisecondsOverWhichToAverageSpeed() / 2);
aggregator.startPolarFixAggregation();
return aggregator.get();
}
@Override
public Map<RegattaAndRaceIdentifier, List<PolarFix>> provideNewCacheValue(BoatClass key,
Map<RegattaAndRaceIdentifier, List<PolarFix>> oldCacheValue,
Map<RegattaAndRaceIdentifier, List<PolarFix>> computedCacheUpdate, PolarFixCacheRaceInterval updateInterval) {
Map<RegattaAndRaceIdentifier, List<PolarFix>> newCacheValue = new HashMap<RegattaAndRaceIdentifier, List<PolarFix>>();
if (oldCacheValue != null) {
newCacheValue.putAll(oldCacheValue);
}
for (Entry<RegattaAndRaceIdentifier, List<PolarFix>> newEntry : computedCacheUpdate.entrySet()) {
List<PolarFix> oldList = newCacheValue.get(newEntry.getKey());
if (oldList != null) {
oldList.addAll(newEntry.getValue());
} else {
newCacheValue.put(newEntry.getKey(), newEntry.getValue());
}
}
return newCacheValue;
}
}
@@ -1,21 +0,0 @@
package com.sap.sailing.polars.caching;
import com.sap.sailing.domain.base.BoatClass;
import com.sap.sailing.domain.common.PolarSheetsData;
import com.sap.sailing.polars.PolarDataService;
import com.sap.sailing.util.SmartFutureCache;
public class PolarSheetPerBoatClassCache extends
SmartFutureCache<BoatClass, PolarSheetsData, com.sap.sailing.util.SmartFutureCache.EmptyUpdateInterval>
implements PolarFixCacheUpdateDoneListener {
public PolarSheetPerBoatClassCache(PolarDataService polarDataService) {
super(new PolarSheetPerBoatClassCacheUpdater(polarDataService), "PolarSheetPerBoatClassCache");
}
@Override
public void cacheUpdateDoneForBoatClass(BoatClass key) {
triggerUpdate(key, new EmptyUpdateInterval());
}
}
@@ -1,36 +0,0 @@
package com.sap.sailing.polars.caching;
import com.sap.sailing.domain.base.BoatClass;
import com.sap.sailing.domain.common.PolarSheetGenerationSettings;
import com.sap.sailing.domain.common.PolarSheetsData;
import com.sap.sailing.domain.common.impl.PolarSheetGenerationSettingsImpl;
import com.sap.sailing.polars.PolarDataService;
import com.sap.sailing.polars.generation.PolarSheetGenerator;
import com.sap.sailing.util.SmartFutureCache.CacheUpdater;
import com.sap.sailing.util.SmartFutureCache.EmptyUpdateInterval;
public class PolarSheetPerBoatClassCacheUpdater implements
CacheUpdater<BoatClass, PolarSheetsData, EmptyUpdateInterval> {
private final PolarDataService polarDataService;
public PolarSheetPerBoatClassCacheUpdater(PolarDataService polarDataService) {
this.polarDataService = polarDataService;
}
@Override
public PolarSheetsData computeCacheUpdate(BoatClass key, EmptyUpdateInterval updateInterval) throws Exception {
PolarSheetGenerationSettings settings = PolarSheetGenerationSettingsImpl.createStandardPolarSettings();
PolarSheetGenerator generator = new PolarSheetGenerator(polarDataService.getPolarFixesForBoatClass(key),
settings);
PolarSheetsData generationResult = generator.generate();
return generationResult;
}
@Override
public PolarSheetsData provideNewCacheValue(BoatClass key, PolarSheetsData oldCacheValue,
PolarSheetsData computedCacheUpdate, EmptyUpdateInterval updateInterval) {
return computedCacheUpdate;
}
}
@@ -9,7 +9,7 @@ public interface PolarFix {
public abstract Speed getWindSpeed();
public abstract double getAngleToWind();
public abstract double getTrueWindAngleDeg();
public abstract String getGaugeIdString();
@@ -23,7 +23,7 @@ public class PolarFixImpl implements PolarFix {
private SpeedWithBearing boatSpeed;
private Speed windSpeed;
private double angleToWind;
private double trueWindAngleDeg;
private String gaugeIdString;
private String dayString;
@@ -53,7 +53,7 @@ public class PolarFixImpl implements PolarFix {
windEstimated = windSpeed; // maybe no upwind start; need to default to measured wind speed/direction
}
Bearing windBearing = windEstimated.getFrom();
angleToWind = bearing.getDifferenceTo(windBearing).getDegrees();
trueWindAngleDeg = bearing.getDifferenceTo(windBearing).getDegrees();
}
private String createDayString(TrackedRace race) {
@@ -105,8 +105,8 @@ public class PolarFixImpl implements PolarFix {
}
@Override
public double getAngleToWind() {
return angleToWind;
public double getTrueWindAngleDeg() {
return trueWindAngleDeg;
}
@Override
@@ -12,7 +12,7 @@ import com.sap.sailing.domain.common.PolarSheetsData;
import com.sap.sailing.domain.common.PolarSheetsHistogramData;
import com.sap.sailing.domain.common.Speed;
import com.sap.sailing.domain.common.impl.PolarSheetsDataImpl;
import com.sap.sailing.domain.common.impl.WindSteppingWithMaxDistance;
import com.sap.sailing.domain.common.impl.WindSpeedSteppingWithMaxDistance;
import com.sap.sailing.polars.data.BoatAndWindSpeedWithOriginInfo;
import com.sap.sailing.polars.data.DataPointWithOriginInfo;
import com.sap.sailing.polars.data.PolarFix;
@@ -23,7 +23,7 @@ public class PolarSheetGenerator {
private final List<List<BoatAndWindSpeedWithOriginInfo>> polarData;
private WindSteppingWithMaxDistance stepping;
private WindSpeedSteppingWithMaxDistance stepping;
private PolarSheetGenerationSettings settings;
@@ -32,7 +32,7 @@ public class PolarSheetGenerator {
public PolarSheetGenerator(Set<PolarFix> fixes, PolarSheetGenerationSettings settings) {
this.settings = settings;
this.fixes = fixes;
stepping = settings.getWindStepping();
stepping = settings.getWindSpeedStepping();
polarData = initializePolarDataContainer();
}
@@ -46,16 +46,16 @@ public class PolarSheetGenerator {
private void addPolarFix(PolarFix polarFix) {
long roundedAngle = Math.round(polarFix.getAngleToWind());
long roundedAngleDeg = Math.round(polarFix.getTrueWindAngleDeg());
Speed boatSpeed = polarFix.getBoatSpeed();
Speed windSpeed = polarFix.getWindSpeed();
int angle = (int) roundedAngle;
if (angle < 0) {
angle = (360 + angle);
int angleDeg = (int) roundedAngleDeg;
if (angleDeg < 0) {
angleDeg = (360 + angleDeg);
}
BoatAndWindSpeedWithOriginInfo speeds = new BoatAndWindSpeedWithOriginInfoImpl(boatSpeed, windSpeed,
polarFix.getGaugeIdString(), polarFix.getDayString());
polarData.get(angle).add(speeds);
polarData.get(angleDeg).add(speeds);
}
/**
@@ -270,7 +270,7 @@ public class PolarSheetGenerator {
return polarData;
}
public WindSteppingWithMaxDistance getStepping() {
public WindSpeedSteppingWithMaxDistance getStepping() {
return stepping;
}
@@ -0,0 +1,59 @@
package com.sap.sailing.polars.impl;
public class CubicEquation {
private double a;
private double b;
private double c;
private double d;
public CubicEquation(double a, double b, double c, double d) {
this.a = a;
this.b = b;
this.c = c;
this.d = d;
}
/**
* Following: http://www.1728.org/cubic2.htm
* !The complex roots are not returned!
*/
public double[] solve() {
double f = ((3 * c) / a - (b * b) / (a * a)) / 3;
double g = ((2 * b * b * b) / (a * a * a) - (9 * b * c) / (a * a) + (27 * d) / a) / 27;
double h = (g*g) / 4 + (f*f*f) / 27;
double[] result;
if (f == 0 && g == 0 && h == 0) {
// All roots equal and real
result = new double[1];
result[0] = - Math.cbrt(d/a);
} else if (h > 0) {
// Only one root is real
double r = -(g/2) + Math.sqrt(h);
double s = Math.cbrt(r);
double t = -(g/2) - Math.sqrt(h);
double u = Math.cbrt(t);
result = new double[1];
result[0] = (s + u) - (b / (3 * a));
//!!The complex roots are not returned
} else {
// All three roots are real and not equal
double i = Math.sqrt((g * g) / 4 - h);
double j = Math.cbrt(i);
double k = Math.acos(-(g/(2*i)));
double l = -1 * j;
double m = Math.cos(k / 3);
double n = Math.sqrt(3) * Math.sin(k/3);
double p = - (b/(3*a));
result = new double[3];
result[0] = 2 * j * Math.cos(k / 3) - b / (3 * a);
result[1] = l * (m + n) + p;
result[2] = l * (m - n) + p;
}
return result;
}
}
@@ -1,8 +1,6 @@
package com.sap.sailing.polars.impl;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
@@ -11,22 +9,20 @@ import com.sap.sailing.domain.base.BoatClass;
import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.base.SpeedWithBearingWithConfidence;
import com.sap.sailing.domain.base.SpeedWithConfidence;
import com.sap.sailing.domain.base.impl.SpeedWithBearingWithConfidenceImpl;
import com.sap.sailing.domain.common.Bearing;
import com.sap.sailing.domain.common.LegType;
import com.sap.sailing.domain.common.PolarSheetGenerationSettings;
import com.sap.sailing.domain.common.PolarSheetsData;
import com.sap.sailing.domain.common.RegattaAndRaceIdentifier;
import com.sap.sailing.domain.common.Speed;
import com.sap.sailing.domain.common.Tack;
import com.sap.sailing.domain.common.impl.DegreeBearingImpl;
import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl;
import com.sap.sailing.domain.tracking.GPSFixMoving;
import com.sap.sailing.domain.tracking.TrackedRace;
import com.sap.sailing.polars.PolarDataService;
import com.sap.sailing.polars.aggregation.PolarFixAggregator;
import com.sap.sailing.polars.aggregation.SimplePolarFixRaceInterval;
import com.sap.sailing.polars.analysis.PolarSheetAnalyzer;
import com.sap.sailing.polars.analysis.impl.PolarSheetAnalyzerImpl;
import com.sap.sailing.polars.caching.PolarFixCache;
import com.sap.sailing.polars.caching.PolarSheetPerBoatClassCache;
import com.sap.sailing.polars.data.PolarFix;
import com.sap.sailing.polars.generation.PolarSheetGenerator;
import com.sap.sailing.polars.mining.PolarDataMiner;
@@ -35,7 +31,7 @@ import com.sap.sailing.util.SmartFutureCache;
/**
* Uses two chained {@link SmartFutureCache}s. One to store {@link PolarFix}es extracted from {@link TrackedRace}s and
* the other one for storing one polar sheet per boat class. This allows quick access to desired measures like optimal
* the other one for storing one polar sheet per boat class. This enables quick access to desired measures like optimal
* beat angles.
*
* @author Frederik Petersen (D054528)
@@ -43,51 +39,61 @@ import com.sap.sailing.util.SmartFutureCache;
*/
public class PolarDataServiceImpl implements PolarDataService {
private final PolarFixCache polarFixCache;
private final PolarSheetPerBoatClassCache polarSheetPerBoatClassCache;
private final PolarSheetAnalyzer polarSheetAnalyzer;
private final PolarDataMiner polarDataMiner;
public PolarDataServiceImpl(Executor executor) {
this.polarFixCache = new PolarFixCache(executor);
this.polarSheetPerBoatClassCache = new PolarSheetPerBoatClassCache(this);
polarFixCache.addListener(polarSheetPerBoatClassCache);
this.polarSheetAnalyzer = new PolarSheetAnalyzerImpl(this);
this.polarDataMiner = new PolarDataMiner();
}
@Override
public SpeedWithConfidence<Void> getSpeed(BoatClass boatClass, Speed windSpeed, Bearing bearingToTheWind,
boolean useLinearRegression) throws NotEnoughDataHasBeenAddedException {
return polarDataMiner.estimateBoatSpeed(boatClass, windSpeed, bearingToTheWind, useLinearRegression);
public SpeedWithConfidence<Void> getSpeed(BoatClass boatClass, Speed windSpeed, Bearing trueWindAngle) throws NotEnoughDataHasBeenAddedException {
return polarDataMiner.estimateBoatSpeed(boatClass, windSpeed, trueWindAngle);
}
@Override
public SpeedWithBearingWithConfidence<Void> getAverageSpeedWithBearing(BoatClass boatClass, Speed windSpeed,
LegType legType, Tack tack) throws NotEnoughDataHasBeenAddedException {
return getAverageSpeedWithBearing(boatClass, windSpeed, legType, tack, true);
public Set<SpeedWithBearingWithConfidence<Void>> getAverageTrueWindSpeedAndAngleCandidates(BoatClass boatClass,
Speed speedOverGround, LegType legType, Tack tack) {
// The following is an estimation function. It only serves as a stub. It's the same for all boatclasses and returns
// default maneuver angles. This is NOT final.
// The function is able to return boat speed values for windspeed alues between 5kn and 25kn , which are some kind of realistic
// for sailing boats. They are taken from the 505 polars we gathered in the races until now.
// TODO keep a cache of sampling points over which to fit a function for every boat class and then return the
// corresponding angle
Set<SpeedWithBearingWithConfidence<Void>> resultSet = new HashSet<>();
int tackFactor = (tack.equals(Tack.PORT)) ? -1 : 1;
if (legType.equals(LegType.UPWIND)) {
CubicEquation upWindEquation = new CubicEquation(0.0002, -0.0245, 0.7602, -0.0463-speedOverGround.getKnots());
int angle = 49 * tackFactor;
solveAndAddResults(resultSet, upWindEquation, angle);
} else if (legType.equals(LegType.DOWNWIND)) {
CubicEquation downWindEquation = new CubicEquation(0.0003, -0.0373, 1.5213, -2.1309-speedOverGround.getKnots());
int angle = 30 * tackFactor;
solveAndAddResults(resultSet, downWindEquation, angle);
}
return resultSet;
//return polarDataMiner.estimateTrueWindSpeedAndAngleCandidates(boatClass, speedOverGround, legType, tack);
}
private void solveAndAddResults(Set<SpeedWithBearingWithConfidence<Void>> result, CubicEquation upWindEquation, int angle) {
double[] windSpeedCandidates = upWindEquation.solve();
for (int i = 0; i < windSpeedCandidates.length; i++) {
double windSpeedCandidateInKnots = windSpeedCandidates[i] > 0 ? windSpeedCandidates[i] : 0;
result.add(new SpeedWithBearingWithConfidenceImpl<Void>(new KnotSpeedWithBearingImpl(
windSpeedCandidateInKnots, new DegreeBearingImpl(angle)), 0.00001, null));
}
}
@Override
public SpeedWithBearingWithConfidence<Void> getAverageSpeedWithBearing(BoatClass boatClass,
Speed windSpeed, LegType legType, Tack tack, boolean useLinReg) throws NotEnoughDataHasBeenAddedException {
SpeedWithBearingWithConfidence<Void> speedWithBearing = null;
if (tack.equals(Tack.STARBOARD)) {
if (legType.equals(LegType.UPWIND)) {
speedWithBearing = polarSheetAnalyzer.getAverageUpwindSpeedWithBearingOnStarboardTackFor(boatClass, windSpeed, useLinReg);
} else if (legType.equals(LegType.DOWNWIND)) {
speedWithBearing = polarSheetAnalyzer.getAverageDownwindSpeedWithBearingOnStarboardTackFor(boatClass, windSpeed, useLinReg);
}
} else if (tack.equals(Tack.PORT)) {
if (legType.equals(LegType.UPWIND)) {
return polarSheetAnalyzer.getAverageUpwindSpeedWithBearingOnPortTackFor(boatClass, windSpeed, useLinReg);
} else if (legType.equals(LegType.DOWNWIND)) {
return polarSheetAnalyzer.getAverageDownwindSpeedWithBearingOnPortTackFor(boatClass, windSpeed, useLinReg);
}
}
return speedWithBearing;
Speed windSpeed, LegType legType, Tack tack) throws NotEnoughDataHasBeenAddedException {
return polarDataMiner.getAverageSpeedAndCourseOverGround(boatClass, windSpeed, legType, tack);
}
@@ -95,85 +101,33 @@ public class PolarDataServiceImpl implements PolarDataService {
public PolarSheetsData generatePolarSheet(Set<TrackedRace> trackedRaces, PolarSheetGenerationSettings settings,
Executor executor) throws InterruptedException, ExecutionException {
Set<PolarFix> fixes;
// If settings are default, look into cache
// if (settings.areDefault()) {
// try {
// fixes = polarFixCache.getFixesForTrackedRaces(trackedRaces);
// } catch (NoCacheEntryException e) {
// // If there is no cache entry for at least one of the races: Aggregate for non-cached races.
// fixes = e.getCachedResultList();
// PolarFixAggregator aggregator = new PolarFixAggregator(
// new SimplePolarFixRaceInterval(e.getNotCached()),
// settings, executor);
// aggregator.startPolarFixAggregation();
// fixes.addAll(aggregator.getAggregationResultAsSingleList());
// }
// } else {
PolarFixAggregator aggregator = new PolarFixAggregator(new SimplePolarFixRaceInterval(trackedRaces), settings,
executor);
aggregator.startPolarFixAggregation();
fixes = aggregator.getAggregationResultAsSingleList();
// }
PolarSheetGenerator generator = new PolarSheetGenerator(fixes, settings);
return generator.generate();
}
@Override
public void newRaceFinishedTracking(TrackedRace trackedRace) {
// TODO add a similar listening method, that deletes all cached data on removal of race
// HashSet<TrackedRace> set = new HashSet<TrackedRace>();
// set.add(trackedRace);
// polarFixCache.triggerUpdate(trackedRace.getRace().getBoatClass(), new PolarFixCacheRaceInterval(set));
}
@Override
public Set<PolarFix> getPolarFixesForBoatClass(BoatClass key) {
Map<RegattaAndRaceIdentifier, List<PolarFix>> fixesInMap = polarFixCache.get(key, false);
Set<PolarFix> resultSet = new HashSet<PolarFix>();
for (List<PolarFix> value : fixesInMap.values()) {
resultSet.addAll(value);
}
return resultSet;
}
@Override
public PolarSheetsData getPolarSheetForBoatClass(BoatClass boatClass) {
return polarDataMiner.createFullSheetForBoatClass(boatClass);
//return polarSheetPerBoatClassCache.get(boatClass, false);
}
@Override
public Set<BoatClass> getAllBoatClassesWithPolarSheetsAvailable() {
return polarDataMiner.getAvailableBoatClasses();
//return polarSheetPerBoatClassCache.keySet();
}
@Override
public void competitorPositionChanged(final GPSFixMoving fix, final Competitor competitor,
final TrackedRace createdTrackedRace) {
polarDataMiner.addFix(fix, competitor, createdTrackedRace);
// TODO build datamining pipeline for the cache
// PolarFixCacheRaceInterval interval = new PolarFixCacheRaceInterval(createIntervalSpecification(
// createdTrackedRace, competitor, timePoint));
// polarFixCache.triggerUpdate(createdTrackedRace.getRace().getBoatClass(), interval);
}
@Override
public Integer[] getDataCountsForWindSpeed(BoatClass boatClass, Speed windSpeed, int startAngleInclusive, int endAngleExclusive) {
public int[] getDataCountsForWindSpeed(BoatClass boatClass, Speed windSpeed, int startAngleInclusive, int endAngleExclusive) {
return polarDataMiner.getDataCountsForWindSpeed(boatClass, windSpeed, startAngleInclusive, endAngleExclusive);
}
// private Map<TrackedRace, Map<Competitor, Pair<TimePoint, TimePoint>>> createIntervalSpecification(
// TrackedRace createdTrackedRace, Competitor competitor, TimePoint timePoint) {
// HashMap<TrackedRace, Map<Competitor, Pair<TimePoint, TimePoint>>> result = new HashMap<TrackedRace,
// Map<Competitor, Pair<TimePoint, TimePoint>>>();
// HashMap<Competitor, Pair<TimePoint, TimePoint>> competitorMap = new HashMap<Competitor, Pair<TimePoint,
// TimePoint>>();
// competitorMap.put(competitor, new Pair<TimePoint, TimePoint>(timePoint, timePoint));
// result.put(createdTrackedRace, competitorMap);
// return result;
// }
}
@@ -0,0 +1,73 @@
package com.sap.sailing.polars.mining;
import java.util.HashMap;
import java.util.Map;
import com.sap.sailing.domain.base.BoatClass;
import com.sap.sailing.domain.common.Speed;
import com.sap.sse.datamining.data.Cluster;
import com.sap.sse.datamining.data.ClusterGroup;
public class AverageAngleContainer {
private ClusterGroup<Speed> speedClusterGroup;
private static class DataCountAngleSumAndAverage {
private int dataCount;
private double angleSumDeg;
private double averageAngleDeg;
protected DataCountAngleSumAndAverage() {
super();
this.dataCount = 0;
this.angleSumDeg = 0.0;
this.averageAngleDeg = 0.0;
}
public double getAverageAngleDeg() {
return averageAngleDeg;
}
public synchronized void add(double angleRad) {
dataCount++;
angleSumDeg += angleRad;
averageAngleDeg = angleSumDeg / (double) dataCount;
}
}
private Map<BoatClass, Map<Cluster<Speed>, DataCountAngleSumAndAverage>> dataCountAndAngleSumMap;
public AverageAngleContainer(ClusterGroup<Speed> speedClusterGroup) {
this.speedClusterGroup = speedClusterGroup;
dataCountAndAngleSumMap = new HashMap<>();
}
public void addFix(BoatClass boatClass, Speed windSpeed, int roundedAngleDeg) {
Cluster<Speed> windCluster = speedClusterGroup.getClusterFor(windSpeed);
if (!dataCountAndAngleSumMap.containsKey(boatClass)) {
dataCountAndAngleSumMap.put(boatClass, new HashMap<>());
}
Map<Cluster<Speed>, DataCountAngleSumAndAverage> boatClassMap = dataCountAndAngleSumMap.get(boatClass);
DataCountAngleSumAndAverage triple = boatClassMap.get(windCluster);
if (triple == null) {
triple = new DataCountAngleSumAndAverage();
boatClassMap.put(windCluster, triple);
}
triple.add(roundedAngleDeg);
}
/**
* @return <code>null</code> if no entry exists for the <code>windSpeed</code> requested; a valid angle in degrees
* otherwise, representing the average angle of this cluster
*/
Double getAverageAngleDeg(BoatClass boatClass, Speed windSpeed) {
Double result = null;
if (dataCountAndAngleSumMap.containsKey(boatClass)) {
Map<Cluster<Speed>, DataCountAngleSumAndAverage> boatClassMap = dataCountAndAngleSumMap.get(boatClass);
Cluster<Speed> windCluster = speedClusterGroup.getClusterFor(windSpeed);
if (boatClassMap.containsKey(windCluster)) {
DataCountAngleSumAndAverage triple = boatClassMap.get(windCluster);
result = triple.getAverageAngleDeg();
}
}
return result;
}
}
@@ -56,9 +56,9 @@ public class GPSFixMovingWithPolarContext implements PolarClusterKey {
}
@Override
public RoundedAngleToTheWind getRoundedAngleToTheWind() {
public RoundedTrueWindAngle getRoundedTrueWindAngle() {
final BearingWithConfidence<Integer> angleToTheWind = getAngleToTheWind();
return angleToTheWind == null ? null : new RoundedAngleToTheWind(angleToTheWind.getObject());
return angleToTheWind == null ? null : new RoundedTrueWindAngle(angleToTheWind.getObject());
}
public BearingWithConfidence<Integer> getAngleToTheWind() {
@@ -91,11 +91,6 @@ public class GPSFixMovingWithPolarContext implements PolarClusterKey {
// use a minimum confidence to avoid the bearing to flip to 270deg in case all is zero
track.getMillisecondsOverWhichToAverageSpeed()/2, /* minimumConfidence */ 0.00000001));
}
@Override
public BoatClass getBoatClass() {
return race.getRace().getBoatClass();
}
private Set<WindSource> collectWindSourcesToIgnoreForBearing() {
Set<WindSource> windSourcesToExclude = new HashSet<WindSource>();
@@ -148,4 +143,9 @@ public class GPSFixMovingWithPolarContext implements PolarClusterKey {
return windSourcesToExclude;
}
@Override
public BoatClass getBoatClass() {
return race.getRace().getBoatClass();
}
}
@@ -1,7 +1,9 @@
package com.sap.sailing.polars.mining;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
@@ -9,15 +11,23 @@ import java.util.logging.Logger;
import com.sap.sailing.domain.base.BoatClass;
import com.sap.sailing.domain.base.SpeedWithBearingWithConfidence;
import com.sap.sailing.domain.base.SpeedWithConfidence;
import com.sap.sailing.domain.base.impl.SpeedWithBearingWithConfidenceImpl;
import com.sap.sailing.domain.base.impl.SpeedWithConfidenceImpl;
import com.sap.sailing.domain.common.Bearing;
import com.sap.sailing.domain.common.LegType;
import com.sap.sailing.domain.common.Position;
import com.sap.sailing.domain.common.Speed;
import com.sap.sailing.domain.common.SpeedWithBearing;
import com.sap.sailing.domain.common.Tack;
import com.sap.sailing.domain.common.confidence.BearingWithConfidence;
import com.sap.sailing.domain.common.impl.DegreeBearingImpl;
import com.sap.sailing.domain.common.impl.KnotSpeedImpl;
import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl;
import com.sap.sailing.domain.tracking.WindWithConfidence;
import com.sap.sailing.polars.analysis.PolarSheetAnalyzer;
import com.sap.sailing.polars.regression.BoatSpeedEstimator;
import com.sap.sailing.polars.regression.NotEnoughDataHasBeenAddedException;
import com.sap.sailing.polars.regression.impl.WindSpeedAndAngleEstimator;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.Util.Pair;
import com.sap.sse.datamining.AdditionalResultDataBuilder;
@@ -28,18 +38,28 @@ import com.sap.sse.datamining.factories.GroupKeyFactory;
import com.sap.sse.datamining.impl.components.GroupedDataEntry;
import com.sap.sse.datamining.shared.GroupKey;
public class IncrementalRegressionProcessor implements Processor<GroupedDataEntry<GPSFixMovingWithPolarContext>,Void> {
public class IncrementalRegressionProcessor implements Processor<GroupedDataEntry<GPSFixMovingWithPolarContext>, Void>,
PolarSheetAnalyzer {
private static final Logger logger = Logger.getLogger(IncrementalRegressionProcessor.class.getName());
private final Map<GroupKey, BoatSpeedEstimator> boatSpeedEstimators = new HashMap<GroupKey, BoatSpeedEstimator>();
private final ClusterGroup<Speed> speedClusterGroup;
private final Set<BoatClass> availableBoatClasses = new HashSet<>();
private final Map<Pair<LegType, Tack>, AverageAngleContainer> averageAngleContainers;
public IncrementalRegressionProcessor(ClusterGroup<Speed> speedClusterGroup) {
this.speedClusterGroup = speedClusterGroup;
averageAngleContainers = new HashMap<>();
for (LegType legType : new LegType[] { LegType.UPWIND, LegType.DOWNWIND }) {
for (Tack tack : new Tack[] { Tack.PORT, Tack.STARBOARD }) {
averageAngleContainers.put(new Pair<LegType, Tack>(legType, tack),
new AverageAngleContainer(speedClusterGroup));
}
}
}
@Override
@@ -47,34 +67,50 @@ public class IncrementalRegressionProcessor implements Processor<GroupedDataEntr
GroupKey key = element.getKey();
BoatSpeedEstimator boatSpeedEstimator;
synchronized (boatSpeedEstimators) {
if (!boatSpeedEstimators.containsKey(key)) {
boatSpeedEstimators.put(key, new BoatSpeedEstimator());
boatSpeedEstimator = boatSpeedEstimators.get(key);
if (boatSpeedEstimator == null) {
boatSpeedEstimator = new BoatSpeedEstimator();
boatSpeedEstimators.put(key, boatSpeedEstimator);
availableBoatClasses.add(element.getDataEntry().getBoatClass());
}
boatSpeedEstimator = boatSpeedEstimators.get(key);
}
GPSFixMovingWithPolarContext fix = element.getDataEntry();
BearingWithConfidence<Integer> angleToTheWind = fix.getAngleToTheWind();
WindWithConfidence<Pair<Position, TimePoint>> windSpeed = fix.getWindSpeed();
SpeedWithBearingWithConfidence<TimePoint> boatSpeedWithConfidence = fix.getBoatSpeed();
//Only add GPS data if speeds and angles are not null, else do nothing!
// Only add GPS data if speeds and angles are not null, else do nothing!
if (angleToTheWind != null && windSpeed != null && boatSpeedWithConfidence != null) {
fillAverageAngleContainer(fix, element, windSpeed);
WindWithConfidence<Pair<Position, TimePoint>> windWithConfidenceForSpeed = windSpeed;
double confidenceForWindSpeed = windWithConfidenceForSpeed.getConfidence();
double confidenceForWindBearing = angleToTheWind.getConfidence();
double confidenceForBoatSpeed = boatSpeedWithConfidence.getConfidence();
double averagedConfidence = (confidenceForBoatSpeed + confidenceForWindBearing + confidenceForWindSpeed) / 3;
boatSpeedEstimator.addData(windWithConfidenceForSpeed.getObject().getKnots(), angleToTheWind.getObject()
.getDegrees(), boatSpeedWithConfidence.getObject().getKnots(), averagedConfidence);
}
boatSpeedEstimator.addData(boatSpeedWithConfidence.getObject(), averagedConfidence);
}
}
private void fillAverageAngleContainer(GPSFixMovingWithPolarContext fix,
GroupedDataEntry<GPSFixMovingWithPolarContext> element,
WindWithConfidence<Pair<Position, TimePoint>> windSpeed) {
final int roundedAngleDeg = fix.getRoundedTrueWindAngle().getAngleDeg();
final LegType legType;
final Tack tack;
if (roundedAngleDeg < -90) {
legType = LegType.DOWNWIND;
tack = Tack.PORT;
} else if (roundedAngleDeg < 0) {
legType = LegType.UPWIND;
tack = Tack.PORT;
} else if (roundedAngleDeg > 90) {
legType = LegType.DOWNWIND;
tack = Tack.STARBOARD;
} else { // roundedAngleDeg > 0 && roundedAngleDeg <= 90
legType = LegType.UPWIND;
tack = Tack.STARBOARD;
}
averageAngleContainers.get(new Pair<>(legType, tack)).addFix(element.getDataEntry().getBoatClass(),
windSpeed.getObject(), roundedAngleDeg);
}
@Override
@@ -93,14 +129,28 @@ public class IncrementalRegressionProcessor implements Processor<GroupedDataEntr
return null;
}
public Pair<SpeedWithConfidence<Void>, Integer> estimateBoatSpeed(final BoatClass boatClass, final Speed windSpeed,
final Bearing angleToTheWind, boolean useLinearRegression)
throws NotEnoughDataHasBeenAddedException {
int dataCount = 0;
public static class SpeedWithConfidenceAndDataCount {
private final SpeedWithConfidence<Void> speedWithConfidence;
private final int dataCount;
protected SpeedWithConfidenceAndDataCount(SpeedWithConfidence<Void> speedWithConfidence, int dataCount) {
super();
this.speedWithConfidence = speedWithConfidence;
this.dataCount = dataCount;
}
public SpeedWithConfidence<Void> getSpeedWithConfidence() {
return speedWithConfidence;
}
public int getDataCount() {
return dataCount;
}
}
SpeedWithConfidenceAndDataCount estimateBoatSpeed(final BoatClass boatClass, final Speed windSpeed,
final Bearing trueWindAngle) throws NotEnoughDataHasBeenAddedException {
PolarClusterKey key = new PolarClusterKey() {
@Override
public RoundedAngleToTheWind getRoundedAngleToTheWind() {
return new RoundedAngleToTheWind(angleToTheWind);
public RoundedTrueWindAngle getRoundedTrueWindAngle() {
return new RoundedTrueWindAngle(trueWindAngle);
}
@Override
@@ -125,12 +175,38 @@ public class IncrementalRegressionProcessor implements Processor<GroupedDataEntr
if (boatSpeedEstimator == null) {
throw new NotEnoughDataHasBeenAddedException();
}
KnotSpeedImpl speedWithoutConfidence = new KnotSpeedImpl(boatSpeedEstimator.estimateSpeed(windSpeed.getKnots(),
angleToTheWind.getDegrees(), useLinearRegression));
dataCount = boatSpeedEstimator.getDataCount();
Speed speedWithoutConfidence = boatSpeedEstimator.estimateSpeed(windSpeed.getKnots(), trueWindAngle.getDegrees());
final int dataCount = boatSpeedEstimator.getDataCount();
double confidence = boatSpeedEstimator.getConfidence();
return new Pair<SpeedWithConfidence<Void>, Integer>(new SpeedWithConfidenceImpl<Void>(
speedWithoutConfidence, confidence, null), dataCount);
return new SpeedWithConfidenceAndDataCount(new SpeedWithConfidenceImpl<Void>(speedWithoutConfidence, confidence, null), dataCount);
}
Set<SpeedWithBearingWithConfidence<Void>> estimateTrueWindSpeedAndAngleCandidates(BoatClass boatClass,
Speed speedOverGround, LegType legType, Tack tack) {
List<Pair<Speed, SpeedWithBearingWithConfidence<Void>>> averageBoatSpeedAndCourseForWindSpeed = obtainPolarSamplingPoints(
boatClass, legType, tack);
WindSpeedAndAngleEstimator windSpeedAndAngleEstimator = new WindSpeedAndAngleEstimator(averageBoatSpeedAndCourseForWindSpeed);
return windSpeedAndAngleEstimator.getAverageTrueWindSpeedAndAngleCandidates(speedOverGround);
}
private List<Pair<Speed, SpeedWithBearingWithConfidence<Void>>> obtainPolarSamplingPoints(BoatClass boatClass,
LegType legType, Tack tack) {
List<Pair<Speed, SpeedWithBearingWithConfidence<Void>>> averageBoatSpeedAndCourseForWindSpeed = new ArrayList<>();
for (int i = 0; i < 60; i++) {
Speed windSpeed = new KnotSpeedImpl(i * 0.5);
SpeedWithBearingWithConfidence<Void> averageSpeedAndCourseOverGround;
try {
averageSpeedAndCourseOverGround = getAverageSpeedAndCourseOverGround(
boatClass, windSpeed, legType, tack);
averageBoatSpeedAndCourseForWindSpeed.add(new Pair<Speed, SpeedWithBearingWithConfidence<Void>>(windSpeed,
averageSpeedAndCourseOverGround));
} catch (NotEnoughDataHasBeenAddedException e) {
// ignore this, since there is not enough data for this windspeed.
}
}
return averageBoatSpeedAndCourseForWindSpeed;
}
@Override
@@ -155,4 +231,24 @@ public class IncrementalRegressionProcessor implements Processor<GroupedDataEntr
return null;
}
@Override
public SpeedWithBearingWithConfidence<Void> getAverageSpeedAndCourseOverGround(BoatClass boatClass,
Speed windSpeed, LegType legType, Tack tack) throws NotEnoughDataHasBeenAddedException {
Double averageAngle = averageAngleContainers.get(new Pair<>(legType, tack)).getAverageAngleDeg(
boatClass, windSpeed);
return estimateSpeedForAverageAngle(boatClass, windSpeed, averageAngle);
}
private SpeedWithBearingWithConfidence<Void> estimateSpeedForAverageAngle(BoatClass boatClass, Speed windSpeed,
Double averageAngle) throws NotEnoughDataHasBeenAddedException {
if (averageAngle == null) {
throw new NotEnoughDataHasBeenAddedException();
}
DegreeBearingImpl angleToTheWind = new DegreeBearingImpl(averageAngle);
SpeedWithConfidence<Void> estimatedSpeed = estimateBoatSpeed(boatClass, windSpeed, angleToTheWind).getSpeedWithConfidence();
SpeedWithBearing speedWithBearing = new KnotSpeedWithBearingImpl(estimatedSpeed.getObject().getKnots(),
angleToTheWind);
return new SpeedWithBearingWithConfidenceImpl<Void>(speedWithBearing, estimatedSpeed.getConfidence(), null);
}
}
@@ -12,7 +12,7 @@ public interface PolarClusterKey {
* @return degrees boat to windDirection in DEG
*/
@Dimension(messageKey = "angleToWind")
RoundedAngleToTheWind getRoundedAngleToTheWind();
RoundedTrueWindAngle getRoundedTrueWindAngle();
/**
*
@@ -12,8 +12,8 @@ public class PolarDataDimensionCollectionFactory {
public static Collection<Function<?>> getClusterKeyDimensions() throws NoSuchMethodException {
Collection<Function<?>> dimensions = new ArrayList<>();
FunctionFactory functionFactory = new FunctionFactory();
Function<RoundedAngleToTheWind> angleFunction = functionFactory
.createMethodWrappingFunction(PolarClusterKey.class.getMethod("getRoundedAngleToTheWind",
Function<RoundedTrueWindAngle> angleFunction = functionFactory
.createMethodWrappingFunction(PolarClusterKey.class.getMethod("getRoundedTrueWindAngle",
new Class<?>[0]));
Function<WindSpeedLevel> windSpeedFunction = functionFactory.createMethodWrappingFunction(PolarClusterKey.class
.getMethod("getWindSpeedCluster", new Class<?>[0]));
+34 -40
View File
@@ -12,23 +12,27 @@ import java.util.concurrent.TimeUnit;
import com.sap.sailing.domain.base.BoatClass;
import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.base.SpeedWithBearingWithConfidence;
import com.sap.sailing.domain.base.SpeedWithConfidence;
import com.sap.sailing.domain.base.impl.SpeedWithConfidenceImpl;
import com.sap.sailing.domain.common.Bearing;
import com.sap.sailing.domain.common.LegType;
import com.sap.sailing.domain.common.PolarSheetGenerationSettings;
import com.sap.sailing.domain.common.PolarSheetsData;
import com.sap.sailing.domain.common.PolarSheetsHistogramData;
import com.sap.sailing.domain.common.Speed;
import com.sap.sailing.domain.common.Tack;
import com.sap.sailing.domain.common.impl.DegreeBearingImpl;
import com.sap.sailing.domain.common.impl.KnotSpeedImpl;
import com.sap.sailing.domain.common.impl.PolarSheetGenerationSettingsImpl;
import com.sap.sailing.domain.common.impl.PolarSheetsDataImpl;
import com.sap.sailing.domain.common.impl.PolarSheetsHistogramDataImpl;
import com.sap.sailing.domain.common.impl.WindSteppingWithMaxDistance;
import com.sap.sailing.domain.common.impl.WindSpeedSteppingWithMaxDistance;
import com.sap.sailing.domain.tracking.GPSFixMoving;
import com.sap.sailing.domain.tracking.TrackedRace;
import com.sap.sailing.polars.analysis.PolarSheetAnalyzer;
import com.sap.sailing.polars.mining.IncrementalRegressionProcessor.SpeedWithConfidenceAndDataCount;
import com.sap.sailing.polars.regression.NotEnoughDataHasBeenAddedException;
import com.sap.sse.common.Util.Pair;
import com.sap.sse.datamining.components.Processor;
import com.sap.sse.datamining.data.ClusterGroup;
import com.sap.sse.datamining.functions.Function;
@@ -37,9 +41,9 @@ import com.sap.sse.datamining.impl.components.GroupedDataEntry;
import com.sap.sse.datamining.impl.components.ParallelFilteringProcessor;
import com.sap.sse.datamining.impl.components.ParallelMultiDimensionsValueNestingGroupingProcessor;
public class PolarDataMiner {
public class PolarDataMiner implements PolarSheetAnalyzer {
private static final int THREAD_POOL_SIZE = Math.max(Runtime.getRuntime().availableProcessors(), 3);
private static final int THREAD_POOL_SIZE = Math.min(Runtime.getRuntime().availableProcessors(), 3);
private static final ThreadPoolExecutor executor = createExecutor();
private final PolarSheetGenerationSettings backendPolarSheetGenerationSettings;
@@ -62,11 +66,9 @@ public class PolarDataMiner {
}
}
private void setUpWorkflow() throws ClassCastException, NoSuchMethodException,
SecurityException {
WindSteppingWithMaxDistance stepping = backendPolarSheetGenerationSettings.getWindStepping();
WindSpeedSteppingWithMaxDistance stepping = backendPolarSheetGenerationSettings.getWindSpeedStepping();
final ClusterGroup<Speed> speedClusterGroup = SpeedClusterGroupFromWindSteppingCreator
.createSpeedClusterGroupFrom(stepping);
incrementalRegressionProcessor = new IncrementalRegressionProcessor(speedClusterGroup);
@@ -113,24 +115,25 @@ public class PolarDataMiner {
}
/**
*
* @param boatClass
* @param windSpeed
* @param angleToTheWind
* @param trueWindAngle
* @param useLinearRegression if true uses lin. regression in the wind interval, otherwise arithm. mean
* @return
* @throws NotEnoughDataHasBeenAddedException
*/
public SpeedWithConfidence<Void> estimateBoatSpeed(BoatClass boatClass, Speed windSpeed, Bearing angleToTheWind,
boolean useLinearRegression)
public SpeedWithConfidence<Void> estimateBoatSpeed(BoatClass boatClass, Speed windSpeed, Bearing trueWindAngle)
throws NotEnoughDataHasBeenAddedException {
return incrementalRegressionProcessor.estimateBoatSpeed(boatClass, windSpeed, angleToTheWind, useLinearRegression).getA();
return incrementalRegressionProcessor.estimateBoatSpeed(boatClass, windSpeed, trueWindAngle).getSpeedWithConfidence();
}
public Set<SpeedWithBearingWithConfidence<Void>> estimateTrueWindSpeedAndAngleCandidates(BoatClass boatClass,
Speed speedOverGround, LegType legType, Tack tack) {
return incrementalRegressionProcessor.estimateTrueWindSpeedAndAngleCandidates(boatClass, speedOverGround, legType, tack);
}
public PolarSheetsData createFullSheetForBoatClass(BoatClass boatClass) {
Double[] defaultWindSpeeds = backendPolarSheetGenerationSettings.getWindStepping().getRawStepping();
double[] defaultWindSpeeds = backendPolarSheetGenerationSettings.getWindSpeedStepping().getRawStepping();
Number[][] averagedPolarDataByWindSpeed = new Number[defaultWindSpeeds.length][360];
Map<Integer, Integer[]> dataCountPerAngleForWindspeed = new HashMap<>();
@@ -140,26 +143,25 @@ public class PolarDataMiner {
for (int windIndex = 0; windIndex < defaultWindSpeeds.length; windIndex++) {
Double windSpeed = defaultWindSpeeds[windIndex];
Integer[] perAngle = new Integer[360];
Map<Integer, PolarSheetsHistogramData> perWindSpeed = new HashMap<>();
for (int angle = 0; angle < 360; angle++) {
Pair<SpeedWithConfidence<Void>, Integer> speedWithConfidenceAndDataCount;
SpeedWithConfidenceAndDataCount speedWithConfidenceAndDataCount;
try {
int convertedAngle = convertAngleIfNecessary(angle);
speedWithConfidenceAndDataCount = incrementalRegressionProcessor.estimateBoatSpeed(boatClass,
new KnotSpeedImpl(windSpeed), new DegreeBearingImpl(convertedAngle), true);
new KnotSpeedImpl(windSpeed), new DegreeBearingImpl(convertedAngle));
} catch (NotEnoughDataHasBeenAddedException e) {
// No data so put in a 0 speed with 0 confidence
speedWithConfidenceAndDataCount = new Pair<SpeedWithConfidence<Void>, Integer>(
speedWithConfidenceAndDataCount = new SpeedWithConfidenceAndDataCount(
new SpeedWithConfidenceImpl<Void>(new KnotSpeedImpl(0), 0, null), 0);
}
averagedPolarDataByWindSpeed[windIndex][angle] = speedWithConfidenceAndDataCount.getA().getObject().getKnots();
int dataCount = speedWithConfidenceAndDataCount.getB();
averagedPolarDataByWindSpeed[windIndex][angle] = speedWithConfidenceAndDataCount.getSpeedWithConfidence().getObject().getKnots();
int dataCount = speedWithConfidenceAndDataCount.getDataCount();
totalDataCount = totalDataCount + dataCount;
//FIXME hard coded
// FIXME hard coded
double coefficiantOfVariation = 0.8;
double confidenceMeasure = 0.5;
@@ -170,15 +172,11 @@ public class PolarDataMiner {
histogramDataMap.put(windIndex, perWindSpeed);
dataCountPerAngleForWindspeed.put(windIndex, perAngle);
}
PolarSheetsData data = new PolarSheetsDataImpl(averagedPolarDataByWindSpeed, totalDataCount,
dataCountPerAngleForWindspeed, backendPolarSheetGenerationSettings.getWindStepping(), histogramDataMap);
dataCountPerAngleForWindspeed, backendPolarSheetGenerationSettings.getWindSpeedStepping(), histogramDataMap);
return data;
}
private int convertAngleIfNecessary(int angle) {
int convertedAngle = angle;
if (angle > 180) {
@@ -187,8 +185,6 @@ public class PolarDataMiner {
return convertedAngle;
}
private PolarSheetsHistogramDataImpl createEmptyHistogramData(Integer[] perAngle, int angle, int dataCount,
double coefficiantOfVariation, double confidenceMeasure) {
perAngle[angle] = dataCount;
@@ -197,38 +193,36 @@ public class PolarDataMiner {
Map<String, Integer[]> yValuesByGaugeIds = new HashMap<>();
Map<String, Integer[]> yValuesByDay = new HashMap<>();
Map<String, Integer[]> yValuesByDayAndGaugeId = new HashMap<>();
PolarSheetsHistogramDataImpl polarSheetsHistogramDataImpl = new PolarSheetsHistogramDataImpl(angle, xValues, yValues, yValuesByGaugeIds,
yValuesByDay, yValuesByDayAndGaugeId, dataCount, coefficiantOfVariation);
polarSheetsHistogramDataImpl.setConfidenceMeasure(confidenceMeasure);
return polarSheetsHistogramDataImpl;
}
public Set<BoatClass> getAvailableBoatClasses() {
return incrementalRegressionProcessor.getAvailableBoatClasses();
}
public Integer[] getDataCountsForWindSpeed(BoatClass boatClass, Speed windSpeed, int startAngleInclusive, int endAngleExclusive) {
Integer[] dataCounts = new Integer[360];
public int[] getDataCountsForWindSpeed(BoatClass boatClass, Speed windSpeed, int startAngleInclusive, int endAngleExclusive) {
int[] dataCounts = new int[360];
for (int angle = 0; angle < 360; angle++) {
if (angle >= startAngleInclusive && angle < endAngleExclusive) {
try {
dataCounts[angle] = incrementalRegressionProcessor.estimateBoatSpeed(boatClass, windSpeed,
new DegreeBearingImpl(convertAngleIfNecessary(angle)), true).getB();
new DegreeBearingImpl(convertAngleIfNecessary(angle))).getDataCount();
} catch (NotEnoughDataHasBeenAddedException e) {
dataCounts[angle] = 0;
}
} else {
dataCounts[angle] = null;
dataCounts[angle] = -1;
}
}
return dataCounts;
}
@Override
public SpeedWithBearingWithConfidence<Void> getAverageSpeedAndCourseOverGround(BoatClass boatClass,
Speed windSpeed, LegType legType, Tack tack) throws NotEnoughDataHasBeenAddedException {
return incrementalRegressionProcessor.getAverageSpeedAndCourseOverGround(boatClass, windSpeed, legType, tack);
}
}
@@ -2,24 +2,24 @@ package com.sap.sailing.polars.mining;
import com.sap.sailing.domain.common.Bearing;
public class RoundedAngleToTheWind {
public class RoundedTrueWindAngle {
private final int angle;
private final int angleDeg;
public RoundedAngleToTheWind(Bearing angleToTheWind) {
this.angle = (int) Math.round(angleToTheWind.getDegrees());
public RoundedTrueWindAngle(Bearing angleToTheWind) {
this.angleDeg = (int) Math.round(angleToTheWind.getDegrees());
}
@Override
public String toString() {
return "" + angle;
return "" + getAngleDeg();
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + angle;
result = prime * result + getAngleDeg();
return result;
}
@@ -31,10 +31,14 @@ public class RoundedAngleToTheWind {
return false;
if (getClass() != obj.getClass())
return false;
RoundedAngleToTheWind other = (RoundedAngleToTheWind) obj;
if (angle != other.angle)
RoundedTrueWindAngle other = (RoundedTrueWindAngle) obj;
if (getAngleDeg() != other.getAngleDeg())
return false;
return true;
}
public int getAngleDeg() {
return angleDeg;
}
}
@@ -1,15 +1,15 @@
package com.sap.sailing.polars.mining;
import com.sap.sailing.domain.common.Speed;
import com.sap.sailing.domain.common.impl.WindSteppingWithMaxDistance;
import com.sap.sailing.domain.common.impl.WindSpeedSteppingWithMaxDistance;
import com.sap.sailing.polars.clusters.SpeedClusterGroup;
import com.sap.sse.datamining.data.ClusterGroup;
public class SpeedClusterGroupFromWindSteppingCreator {
public static ClusterGroup<Speed> createSpeedClusterGroupFrom(WindSteppingWithMaxDistance windStepping) {
public static ClusterGroup<Speed> createSpeedClusterGroupFrom(WindSpeedSteppingWithMaxDistance windStepping) {
double maxDistance = windStepping.getMaxDistance();
Double[] rawIntegerStepping = windStepping.getRawStepping();
double[] rawIntegerStepping = windStepping.getRawStepping();
double[] rawDoubleLevelMids = new double[rawIntegerStepping.length];
for (int i = 0; i < rawIntegerStepping.length; i++) {
rawDoubleLevelMids[i] = rawIntegerStepping[i];
@@ -1,14 +1,14 @@
package com.sap.sailing.polars.mining;
import com.sap.sailing.domain.common.Speed;
import com.sap.sailing.domain.common.WindStepping;
import com.sap.sailing.domain.common.WindSpeedStepping;
public class WindSpeedLevel {
private final int level;
private final WindStepping stepping;
private final WindSpeedStepping stepping;
public WindSpeedLevel(Speed windSpeed, WindStepping stepping) {
public WindSpeedLevel(Speed windSpeed, WindSpeedStepping stepping) {
this.level = stepping.getLevelIndexForValue(windSpeed.getKnots());
this.stepping = stepping;
}
@@ -1,31 +1,36 @@
package com.sap.sailing.polars.regression;
import java.io.ObjectInputStream;
import java.io.Serializable;
import com.sap.sailing.domain.common.Speed;
import com.sap.sailing.domain.common.impl.KnotSpeedImpl;
import com.sap.sailing.polars.mining.IncrementalRegressionProcessor;
import com.sap.sailing.polars.regression.impl.IncrementalLeastSquaresProcessor;
import com.sap.sailing.util.impl.LockUtil;
import com.sap.sailing.util.impl.NamedReentrantReadWriteLock;
/**
* Combines two linear regression processors. One with the angle to the wind as the x axis and the other with the wind
* speed on the x axis. Both y axis represent the boat speed.
* Supplies incremental arithmetic mean for confidence and speed.
* <p>
*
* {@link #estimateSpeed(double, double)} returns the average of the estimation of both regressions.
* {@link #estimateSpeed(double, double)} returns the average of the speed.
* <p>
*
* Note that this class should only be used for a small interval on a polar sheet. The
* Note that this class should only be used for a small wind interval on a polar sheet. The
* {@link IncrementalRegressionProcessor} is one example for that. It has one instance of the {@link BoatSpeedEstimator}
* for each wind speed level and rounded angle combination.
* for each wind boatclass, speed level and rounded angle combination.
*
* @author Frederik Petersen (D054528)
*
*/
public class BoatSpeedEstimator {
public class BoatSpeedEstimator implements Serializable {
private IncrementalLinearRegressionProcessor angleRegression = new IncrementalLeastSquaresProcessor();
private static final long serialVersionUID = -254184914347332658L;
private IncrementalLinearRegressionProcessor windSpeedRegression = new IncrementalLeastSquaresProcessor();
private NamedReentrantReadWriteLock lock = new NamedReentrantReadWriteLock(getClass().getName(), true);
private double speedSumInKnots = 0;
private transient NamedReentrantReadWriteLock lock = createLock();
private double confidenceSum = 0;
private int dataCount = 0;
@@ -33,30 +38,22 @@ public class BoatSpeedEstimator {
/**
* @param useLinearRegression if false mean of wind interval is used, otherwise linear regression
*/
public double estimateSpeed(double windSpeed, double angleToTheWind, boolean useLinearRegression) throws NotEnoughDataHasBeenAddedException {
public Speed estimateSpeed(double windSpeed, double angleToTheWind) throws NotEnoughDataHasBeenAddedException {
LockUtil.lockForRead(lock);
double result;
if (dataCount < 1) {
throw new NotEnoughDataHasBeenAddedException();
}
try {
if (useLinearRegression) {
double angleEstimated = angleRegression.getEstimatedY(angleToTheWind);
double windSpeedEstimated = windSpeedRegression.getEstimatedY(windSpeed);
result = (angleEstimated + windSpeedEstimated) / 2.0;
} else {
//Return average for that wind interval
result = windSpeedRegression.getMeanOfY();
}
return new KnotSpeedImpl(speedSumInKnots / dataCount);
} finally {
LockUtil.unlockAfterRead(lock);
}
return result;
}
public void addData(double windSpeed, double angleToTheWind, double boatSpeed, double confidence) {
public void addData(Speed boatSpeed, double confidence) {
LockUtil.lockForWrite(lock);
try {
angleRegression.addMeasuredPoint(angleToTheWind, boatSpeed);
windSpeedRegression.addMeasuredPoint(windSpeed, boatSpeed);
speedSumInKnots = speedSumInKnots + boatSpeed.getKnots();
dataCount++;
confidenceSum = confidenceSum + confidence;
} finally {
@@ -71,5 +68,13 @@ public class BoatSpeedEstimator {
public double getConfidence() {
return confidenceSum / dataCount;
}
private void readObject(ObjectInputStream ois) {
lock = createLock();
}
private NamedReentrantReadWriteLock createLock() {
return new NamedReentrantReadWriteLock(getClass().getName(), true);
}
}
@@ -0,0 +1,201 @@
package com.sap.sailing.polars.regression.impl;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.sap.sailing.domain.base.SpeedWithBearingWithConfidence;
import com.sap.sailing.domain.base.impl.SpeedWithBearingWithConfidenceImpl;
import com.sap.sailing.domain.common.Speed;
import com.sap.sailing.domain.common.impl.DegreeBearingImpl;
import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl;
import com.sap.sse.common.Util.Pair;
public class WindSpeedAndAngleEstimator {
private static final double DEFAULT_MAX_DISTANCE_TO_SAMPLING_POINT = 0.5;
private final List<Pair<Speed, SpeedWithBearingWithConfidence<Void>>> averageBoatSpeedAndCourseForWindSpeed;
private final double maxDistanceToSamplingPointFunctionInKnots;
/**
*
* @param averageBoatSpeedAndCourseForWindSpeed
* List of sampling points. Needs to be sorted by windspeed (A of the Pair object) low to high
*/
public WindSpeedAndAngleEstimator(
List<Pair<Speed, SpeedWithBearingWithConfidence<Void>>> averageBoatSpeedAndCourseForWindSpeed) {
this(averageBoatSpeedAndCourseForWindSpeed, DEFAULT_MAX_DISTANCE_TO_SAMPLING_POINT);
}
/**
*
* @param averageBoatSpeedAndCourseForWindSpeed
* List of sampling points. Needs to be sorted by windspeed (A of the Pair object) low to high
* @param maxDistanceToSamplingPointFunctionInKnots
* If there is only a sampling point on one side, what's the maximal distance to consider valid when
* comparing?
*/
public WindSpeedAndAngleEstimator(
List<Pair<Speed, SpeedWithBearingWithConfidence<Void>>> averageBoatSpeedAndCourseForWindSpeed,
double maxDistanceToSamplingPointFunctionInKnots) {
this.averageBoatSpeedAndCourseForWindSpeed = averageBoatSpeedAndCourseForWindSpeed;
this.maxDistanceToSamplingPointFunctionInKnots = maxDistanceToSamplingPointFunctionInKnots;
}
/**
* Looks at all entries in {@link #dataCountAndAngleSumMap} and determines the wind speed and true wind angle at
* which the boat has most likely been sailing to achieve the <code>boatSpeed</code> provided.
*
* BE ADVISED: The result candidate with the highest confidence, which is returned here, is not necessarily the
* candidate which is the most likely to be correct. It really depends on the quality and quantity of gathered polar
* data.
*
* Use {@link #getAverageTrueWindSpeedAndAngleCandidates(Speed)} to get all candidates.
*
* @return null if none was found, otherwise candidate with highest confidence, found in the list returned by
* {@link #getAverageTrueWindSpeedAndAngleCandidates(Speed)}
*/
public SpeedWithBearingWithConfidence<Void> getAverageTrueWindSpeedAndAngle(Speed boatSpeed) {
Set<SpeedWithBearingWithConfidence<Void>> resultCandidates = getAverageTrueWindSpeedAndAngleCandidates(boatSpeed);
SpeedWithBearingWithConfidence<Void> resultCandidateWithHighestConfidence = null;
for (SpeedWithBearingWithConfidence<Void> resultCandidate : resultCandidates) {
if (resultCandidateWithHighestConfidence == null
|| resultCandidate.getConfidence() > resultCandidateWithHighestConfidence.getConfidence()) {
resultCandidateWithHighestConfidence = resultCandidate;
}
}
return resultCandidateWithHighestConfidence;
}
/**
* Uses the constructor-supplied sampling points to find wind and course candidates for the supplied boatSpeed.
*
* @return empty set if the underlying data was not sufficient to estimate wind. Otherwise a set of candidates
* with windspeed course of the boat relative to the wind and a confidence which was derived from the
* confidences of underlying fixes, the amount of underlying fixes and the distance between the input
* boatSpeed and the sampling points supplied by the gathered polar data.
*/
public Set<SpeedWithBearingWithConfidence<Void>> getAverageTrueWindSpeedAndAngleCandidates(Speed boatSpeed) {
double requestedBoatSpeedInKnots = boatSpeed.getKnots();
Pair<Speed, SpeedWithBearingWithConfidence<Void>> last = null;
Set<SpeedWithBearingWithConfidence<Void>> resultCandidates = new HashSet<>();
Set<SpeedWithBearingWithConfidence<Void>> resultCandidatesWithDistance = new HashSet<>();
for (Pair<Speed, SpeedWithBearingWithConfidence<Void>> averageSpeedForWindSpeed : averageBoatSpeedAndCourseForWindSpeed) {
Speed windSpeed = averageSpeedForWindSpeed.getA();
SpeedWithBearingWithConfidence<Void> averageBoatSpeedWithCourse = averageSpeedForWindSpeed.getB();
double currentBoatSpeedInKnots = averageBoatSpeedWithCourse.getObject().getKnots();
if (last != null) {
double lastBoatSpeedInKnots = last.getB().getObject().getKnots();
if (lastBoatSpeedInKnots <= requestedBoatSpeedInKnots
&& currentBoatSpeedInKnots >= requestedBoatSpeedInKnots) {
SpeedWithBearingWithConfidenceImpl<Void> resultCandidate = createWeightedResultCandidateForLowerLast(
requestedBoatSpeedInKnots, last, windSpeed, averageBoatSpeedWithCourse,
lastBoatSpeedInKnots, currentBoatSpeedInKnots);
resultCandidates.add(resultCandidate);
} else if (lastBoatSpeedInKnots >= requestedBoatSpeedInKnots
&& currentBoatSpeedInKnots <= requestedBoatSpeedInKnots) {
SpeedWithBearingWithConfidenceImpl<Void> resultCandidate = createWeightedResultCandidateForHigherLast(
requestedBoatSpeedInKnots, last, windSpeed, averageBoatSpeedWithCourse,
lastBoatSpeedInKnots, currentBoatSpeedInKnots);
resultCandidates.add(resultCandidate);
}
}
/*
* This will be run when there was no candidate found in between the two sampling points (or we are at the
* beginning) and add a Candidate if the requested point was maxDistanceToSamplingPointFunctionInKnots or
* less away from the last regarded sampling point. The candidate (if valid) is then added to the
* resultCandidatesWithDistance set. Confidences are halved and the set is only used if the standard result
* set is empty in the end
*/
if (resultCandidates.size() == 0 && last != null) {
addResultCandidateIfDistanceValid(requestedBoatSpeedInKnots, resultCandidatesWithDistance, last.getA(),
last.getB());
}
last = averageSpeedForWindSpeed;
}
// check last sampling point for distance match and return withDistance set if standard set is empty
if (last != null && resultCandidates.size() == 0) {
addResultCandidateIfDistanceValid(requestedBoatSpeedInKnots, resultCandidatesWithDistance, last.getA(),
last.getB());
resultCandidates = resultCandidatesWithDistance;
}
return resultCandidates;
}
/**
* Confidence will be halved in situations where there the requested speed does not lie between any sampling points.
* The speed and angle values are equal to the sampling point values.
*/
private void addResultCandidateIfDistanceValid(double requestedBoatSpeedInKnots,
Set<SpeedWithBearingWithConfidence<Void>> resultCandidates, Speed windSpeed,
SpeedWithBearingWithConfidence<Void> averageBoatSpeedWithCourse) {
if (Math.abs(averageBoatSpeedWithCourse.getObject().getKnots() - requestedBoatSpeedInKnots) <= maxDistanceToSamplingPointFunctionInKnots) {
SpeedWithBearingWithConfidence<Void> resultCandidate = new SpeedWithBearingWithConfidenceImpl<Void>(
new KnotSpeedWithBearingImpl(windSpeed.getKnots(), new DegreeBearingImpl(averageBoatSpeedWithCourse
.getObject().getBearing().getDegrees())), averageBoatSpeedWithCourse.getConfidence() * 0.5,
null);
resultCandidates.add(resultCandidate);
}
}
private SpeedWithBearingWithConfidenceImpl<Void> createWeightedResultCandidateForHigherLast(
double requestedBoatSpeedInKnots, Pair<Speed, SpeedWithBearingWithConfidence<Void>> last, Speed windSpeed,
SpeedWithBearingWithConfidence<Void> averageBoatSpeedWithCourse, double lastBoatSpeedInKnots,
double currentBoatSpeedInKnots) {
double differenceBetweenBothSpeedsInKnots = lastBoatSpeedInKnots - currentBoatSpeedInKnots;
double ratio;
if (differenceBetweenBothSpeedsInKnots < 0.00001) {
//Both sampling points have the same ratio. Use confidence to determine the ratio
ratio = last.getB().getConfidence() / (averageBoatSpeedWithCourse.getConfidence() + last.getB().getConfidence());
} else {
double differenceBetweenRequestedAndLower = requestedBoatSpeedInKnots - currentBoatSpeedInKnots;
ratio = differenceBetweenRequestedAndLower / differenceBetweenBothSpeedsInKnots;
}
double weightedAverageWindSpeedInKnots = (( 1 - ratio) * windSpeed.getKnots())
+ (ratio * last.getA().getKnots());
double currentCourseInDeg = averageBoatSpeedWithCourse.getObject().getBearing()
.getDegrees();
double lastCourseInDeg = last.getB().getObject().getBearing().getDegrees();
double weightedAverageAngleInDeg = (( 1 - ratio) * currentCourseInDeg) + (ratio * lastCourseInDeg);
double weightedAverageConfidence = (( 1 - ratio) * averageBoatSpeedWithCourse.getConfidence())
+ (ratio * last.getB().getConfidence());
SpeedWithBearingWithConfidenceImpl<Void> resultCandidate = new SpeedWithBearingWithConfidenceImpl<Void>(
new KnotSpeedWithBearingImpl(weightedAverageWindSpeedInKnots, new DegreeBearingImpl(
weightedAverageAngleInDeg)), weightedAverageConfidence, null);
return resultCandidate;
}
private SpeedWithBearingWithConfidenceImpl<Void> createWeightedResultCandidateForLowerLast(
double requestedBoatSpeedInKnots, Pair<Speed, SpeedWithBearingWithConfidence<Void>> last, Speed windSpeed,
SpeedWithBearingWithConfidence<Void> averageBoatSpeedWithCourse, double lastBoatSpeedInKnots,
double currentBoatSpeedInKnots) {
double differenceBetweenBothSpeedsInKnots = currentBoatSpeedInKnots - lastBoatSpeedInKnots;
double ratio;
if (differenceBetweenBothSpeedsInKnots < 0.00001) {
// Both sampling points have the same ratio. Use confidence to determine the ratio
ratio = averageBoatSpeedWithCourse.getConfidence()
/ (averageBoatSpeedWithCourse.getConfidence() + last.getB().getConfidence());
} else {
double differenceBetweenRequestedAndLower = requestedBoatSpeedInKnots - lastBoatSpeedInKnots;
ratio = differenceBetweenRequestedAndLower / differenceBetweenBothSpeedsInKnots;
}
double weightedAverageWindSpeedInKnots = (ratio * windSpeed.getKnots())
+ ((1 - ratio) * last.getA().getKnots());
double currentCourseInDeg = averageBoatSpeedWithCourse.getObject().getBearing()
.getDegrees();
double lastCourseInDeg = last.getB().getObject().getBearing().getDegrees();
double weightedAverageAngleInDeg = (ratio * currentCourseInDeg) + ((1 - ratio) * lastCourseInDeg);
double weightedAverageConfidence = (ratio * averageBoatSpeedWithCourse.getConfidence())
+ ((1 - ratio) * last.getB().getConfidence());
SpeedWithBearingWithConfidenceImpl<Void> resultCandidate = new SpeedWithBearingWithConfidenceImpl<Void>(
new KnotSpeedWithBearingImpl(weightedAverageWindSpeedInKnots, new DegreeBearingImpl(
weightedAverageAngleInDeg)), weightedAverageConfidence, null);
return resultCandidate;
}
}
@@ -0,0 +1,82 @@
package com.sap.sailing.polars.windestimation;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import com.sap.sailing.domain.base.BoatClass;
import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.common.LegType;
import com.sap.sailing.domain.common.Tack;
import com.sap.sailing.domain.tracking.Maneuver;
import com.sap.sailing.domain.tracking.TrackedRace;
import com.sap.sailing.domain.tracking.impl.WindTrackImpl;
import com.sap.sailing.polars.PolarDataService;
import com.sap.sailing.polars.regression.NotEnoughDataHasBeenAddedException;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.impl.MillisecondsTimePoint;
/**
* Implements a wind estimation based on maneuver classifications.
*
* @author Axel Uhl (D043530)
*
*/
public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl {
private static final long serialVersionUID = -764156498143531475L;
/**
* Using a fairly low base confidence for this estimation wind track. It shall only serve as a
* default and should be superseded by other wind sources easily.
*/
private static final double DEFAULT_BASE_CONFIDENCE = 0.01;
private final PolarDataService polarService;
private final TrackedRace trackedRace;
public ManeuverBasedWindEstimationTrackImpl(PolarDataService polarService, TrackedRace trackedRace, long millisecondsOverWhichToAverage)
throws NotEnoughDataHasBeenAddedException {
super(millisecondsOverWhichToAverage, DEFAULT_BASE_CONFIDENCE, /* useSpeed */ false,
/* nameForReadWriteLock */ ManeuverBasedWindEstimationTrackImpl.class.getName());
this.polarService = polarService;
this.trackedRace = trackedRace;
analyzeRace();
}
/**
* Fetches all the race's maneuvers and tries to find the tacks and jibes. For each such maneuver identified, a wind fix
* will be created based on the average COG into and out of the maneuver.
*/
private void analyzeRace() throws NotEnoughDataHasBeenAddedException {
final Map<Maneuver, BoatClass> maneuvers = getAllManeuvers();
for (final Entry<Maneuver, BoatClass> maneuverAndBoatClass : maneuvers.entrySet()) {
// Now for each maneuver's starting speed (speed into the maneuver) get the approximated
// wind speed and direction assuming average upwind / downwind performance based on the polar service.
// TODO continue here, asking the polarService for the wind speed based on boat speed, leg type and tack
for (LegType legType : new LegType[] { LegType.UPWIND, LegType.DOWNWIND }) {
for (Tack tack : new Tack[] { Tack.PORT, Tack.STARBOARD }) {
polarService.getAverageTrueWindSpeedAndAngleCandidates(maneuverAndBoatClass.getValue(),
maneuverAndBoatClass.getKey().getSpeedWithBearingBefore(),
legType, tack);
}
}
}
}
private Map<Maneuver, BoatClass> getAllManeuvers() {
Map<Maneuver, BoatClass> maneuvers = new HashMap<>();
for (Competitor competitor : trackedRace.getRace().getCompetitors()) {
final BoatClass boatClass = competitor.getBoat().getBoatClass();
final TimePoint from = trackedRace.getStartOfRace() == null ? trackedRace.getStartOfTracking()
: trackedRace.getStartOfRace();
final TimePoint to = trackedRace.getEndOfRace() == null ? trackedRace.getEndOfTracking() == null ?
MillisecondsTimePoint.now() : trackedRace.getEndOfTracking() : trackedRace.getEndOfRace();
for (Maneuver maneuver : trackedRace.getManeuvers(competitor, from, to, /* waitForLatest */ false)) {
maneuvers.put(maneuver, boatClass);
}
}
return Collections.unmodifiableMap(maneuvers);
}
}