diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/CombinedWindAsNavigableSet.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/CombinedWindAsNavigableSet.java index 333ace6b6b0..cc8e2e40052 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/CombinedWindAsNavigableSet.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/CombinedWindAsNavigableSet.java @@ -16,8 +16,7 @@ import com.sap.sailing.domain.tracking.WindTrack; */ public class CombinedWindAsNavigableSet extends VirtualWindFixesAsNavigableSet { - public CombinedWindAsNavigableSet(WindTrack track, TrackedRace trackedRace, - long resolutionInMilliseconds) { + public CombinedWindAsNavigableSet(WindTrack track, TrackedRace trackedRace, long resolutionInMilliseconds) { super(track, trackedRace, resolutionInMilliseconds); } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java index 0806a427bb7..d311813d7f2 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java @@ -77,74 +77,75 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { private static final Logger logger = Logger.getLogger(TrackedRaceImpl.class.getName()); private static final double PENALTY_CIRCLE_DEGREES_THRESHOLD = 320; - + /** * Used in {@link #getStartFinishPlacemarks()} to calculate the radius for the * {@link ReverseGeocoder#getPlacemarksNear(Position, double) GetPlacemarksNear-Service}. */ private static final double GEONAMES_RADIUS_CACLCULATION_FACTOR = 10.0; - // TODO observe the race course; if it changes, update leg structures; consider fine-grained update events that tell what changed + // TODO observe the race course; if it changes, update leg structures; consider fine-grained update events that tell + // what changed private final RaceDefinition race; - + private final TrackedEvent trackedEvent; - + /** - * Keeps the oldest timestamp that is fed into this tracked race, either from a boat fix, a buoy - * fix, a race start/finish or a coarse definition. + * Keeps the oldest timestamp that is fed into this tracked race, either from a boat fix, a buoy fix, a race + * start/finish or a coarse definition. */ private TimePoint startOfTracking; - + /** * Race start time as announced by the tracking infrastructure */ private TimePoint startTimeReceived; - + private TimePoint timePointOfNewestEvent; private TimePoint timePointOfLastEvent; private long updateCount; - - private final Map> competitorRankings; - + + private final Map> competitorRankings; + /** * legs appear in the order in which they appear in the race's course */ private final LinkedHashMap trackedLegs; - + private final Map> tracks; private final Map> markPassingsForCompetitor; - + /** * The mark passing sets used as values are ordered by time stamp. */ private final Map> markPassingsForWaypoint; - + /** * Values are the from and to time points between which the maneuvers have been previously - * computed. Clients wanting to know maneuvers for the competitor outside of this time interval need to (re-)compute them. + * computed. Clients wanting to know maneuvers for the competitor outside of this time interval need to (re-)compute + * them. */ private final Map>> maneuverCache; - + /** - * A tracked race can maintain a number of sources for wind information from which a client - * can select. As all intra-leg computations are done dynamically based on wind information, - * selecting a different wind information source can alter the intra-leg results. See - * {@link #currentWindSource}. + * A tracked race can maintain a number of sources for wind information from which a client can select. As all + * intra-leg computations are done dynamically based on wind information, selecting a different wind information + * source can alter the intra-leg results. See {@link #currentWindSource}. */ private final Map windTracks; - + private Wind directionFromStartToNextMarkCache; - + private final Map> buoyTracks; - + protected long millisecondsOverWhichToAverageSpeed; private final Map startToNextMarkCacheInvalidationListeners; protected long millisecondsOverWhichToAverageWind; - + private final WindStore windStore; - + public TrackedRaceImpl(TrackedEvent trackedEvent, RaceDefinition race, WindStore windStore, long millisecondsOverWhichToAverageWind, long millisecondsOverWhichToAverageSpeed) { super(); @@ -154,14 +155,15 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { this.millisecondsOverWhichToAverageSpeed = millisecondsOverWhichToAverageSpeed; this.millisecondsOverWhichToAverageWind = millisecondsOverWhichToAverageWind; this.startToNextMarkCacheInvalidationListeners = new HashMap(); - this.maneuverCache = new HashMap>>(); + this.maneuverCache = new HashMap>>(); this.buoyTracks = new HashMap>(); int i = 0; for (Waypoint waypoint : race.getCourse().getWaypoints()) { for (Buoy buoy : waypoint.getBuoys()) { getOrCreateTrack(buoy); - if (i<2) { - // add cache invalidation listeners for first and second waypoint's buoys for directionFromStartToNextMarkCache + if (i < 2) { + // add cache invalidation listeners for first and second waypoint's buoys for + // directionFromStartToNextMarkCache addStartToNextMarkCacheInvalidationListener(buoy); } } @@ -177,44 +179,50 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { markPassingsForCompetitor = new HashMap>(); tracks = new HashMap>(); for (Competitor competitor : race.getCompetitors()) { - markPassingsForCompetitor.put(competitor, new ConcurrentSkipListSet(MarkPassingByTimeComparator.INSTANCE)); - tracks.put(competitor, new DynamicGPSFixMovingTrackImpl(competitor, millisecondsOverWhichToAverageSpeed)); + markPassingsForCompetitor.put(competitor, new ConcurrentSkipListSet( + MarkPassingByTimeComparator.INSTANCE)); + tracks.put(competitor, new DynamicGPSFixMovingTrackImpl(competitor, + millisecondsOverWhichToAverageSpeed)); } markPassingsForWaypoint = new HashMap>(); for (Waypoint waypoint : race.getCourse().getWaypoints()) { - markPassingsForWaypoint.put(waypoint, new ConcurrentSkipListSet(MarkPassingByTimeComparator.INSTANCE)); + markPassingsForWaypoint.put(waypoint, new ConcurrentSkipListSet( + MarkPassingByTimeComparator.INSTANCE)); } windTracks = new HashMap(); windTracks.putAll(windStore.loadWindTracks(trackedEvent, this, millisecondsOverWhichToAverageWind)); // by default, a tracked race offers one course-based wind estimation, one track-based wind estimation track and - // one "WEB" track for manual or REST-based wind reception; other wind tracks may be added as fixes are received for them. + // one "WEB" track for manual or REST-based wind reception; other wind tracks may be added as fixes are received + // for them. WindSource courseBasedWindSource = new WindSourceImpl(WindSourceType.COURSE_BASED); - windTracks.put(courseBasedWindSource, windStore.getWindTrack(trackedEvent, this, courseBasedWindSource, millisecondsOverWhichToAverageWind)); + windTracks.put(courseBasedWindSource, + windStore.getWindTrack(trackedEvent, this, courseBasedWindSource, millisecondsOverWhichToAverageWind)); WindSource trackBasedWindSource = new WindSourceImpl(WindSourceType.TRACK_BASED_ESTIMATION); - windTracks.put(trackBasedWindSource, windStore.getWindTrack(trackedEvent, this, trackBasedWindSource, millisecondsOverWhichToAverageWind)); + windTracks.put(trackBasedWindSource, + windStore.getWindTrack(trackedEvent, this, trackBasedWindSource, millisecondsOverWhichToAverageWind)); this.trackedEvent = trackedEvent; competitorRankings = new HashMap>(); } - + /** * Precondition: race has already been set, e.g., in constructor before this methocd is called */ abstract protected TrackedLeg createTrackedLeg(Leg leg); - + public RaceIdentifier getRaceIdentifier() { return new EventNameAndRaceName(getTrackedEvent().getEvent().getName(), getRace().getName()); } - + @Override public NavigableSet getMarkPassings(Competitor competitor) { return markPassingsForCompetitor.get(competitor); } - + @Override public Iterable getMarkPassingsInOrder(Waypoint waypoint) { return markPassingsForWaypoint.get(waypoint); } - + @Override public TimePoint getStartOfTracking() { return startOfTracking; @@ -234,10 +242,11 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { if (firstMarkPassingFirstMark != null) { TimePoint timeOfFirstMarkPassingFirstMark = firstMarkPassingFirstMark.getTimePoint(); if (startTimeReceived != null) { - long startTimeReceived2timeOfFirstMarkPassingFirstMark = timeOfFirstMarkPassingFirstMark.asMillis() - - startTimeReceived.asMillis(); + long startTimeReceived2timeOfFirstMarkPassingFirstMark = timeOfFirstMarkPassingFirstMark.asMillis() + - startTimeReceived.asMillis(); if (startTimeReceived2timeOfFirstMarkPassingFirstMark > MAX_TIME_BETWEEN_START_AND_FIRST_MARK_PASSING_IN_MILLISECONDS) { - result = new MillisecondsTimePoint(timeOfFirstMarkPassingFirstMark.asMillis() - MAX_TIME_BETWEEN_START_AND_FIRST_MARK_PASSING_IN_MILLISECONDS); + result = new MillisecondsTimePoint(timeOfFirstMarkPassingFirstMark.asMillis() + - MAX_TIME_BETWEEN_START_AND_FIRST_MARK_PASSING_IN_MILLISECONDS); } else { result = startTimeReceived; } @@ -249,7 +258,7 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { } return result; } - + @Override public TimePoint getAssumedEnd() { TimePoint result = null; @@ -288,8 +297,8 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { return null; } else { TimePoint end = timePoint; - if (markPassings.last().getWaypoint() == getRace().getCourse().getLastWaypoint() && - timePoint.compareTo(markPassings.last().getTimePoint()) > 0) { + if (markPassings.last().getWaypoint() == getRace().getCourse().getLastWaypoint() + && timePoint.compareTo(markPassings.last().getTimePoint()) > 0) { // competitor has finished race; use time point of crossing the finish line end = markPassings.last().getTimePoint(); } @@ -301,25 +310,27 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { public GPSFixTrack getTrack(Competitor competitor) { return tracks.get(competitor); } - + @Override public TrackedLeg getTrackedLegFinishingAt(Waypoint endOfLeg) { int indexOfWaypoint = getRace().getCourse().getIndexOfWaypoint(endOfLeg); if (indexOfWaypoint == -1) { - throw new IllegalArgumentException("Waypoint "+endOfLeg+" not found in "+getRace().getCourse()); + throw new IllegalArgumentException("Waypoint " + endOfLeg + " not found in " + getRace().getCourse()); } else if (indexOfWaypoint == 0) { - throw new IllegalArgumentException("Waypoint "+endOfLeg+" isn't start of any leg in "+getRace().getCourse()); + throw new IllegalArgumentException("Waypoint " + endOfLeg + " isn't start of any leg in " + + getRace().getCourse()); } - return trackedLegs.get(race.getCourse().getLegs().get(indexOfWaypoint-1)); + return trackedLegs.get(race.getCourse().getLegs().get(indexOfWaypoint - 1)); } @Override public TrackedLeg getTrackedLegStartingAt(Waypoint startOfLeg) { int indexOfWaypoint = getRace().getCourse().getIndexOfWaypoint(startOfLeg); if (indexOfWaypoint == -1) { - throw new IllegalArgumentException("Waypoint "+startOfLeg+" not found in "+getRace().getCourse()); - } else if (indexOfWaypoint == Util.size(getRace().getCourse().getWaypoints())-1) { - throw new IllegalArgumentException("Waypoint "+startOfLeg+" isn't start of any leg in "+getRace().getCourse()); + throw new IllegalArgumentException("Waypoint " + startOfLeg + " not found in " + getRace().getCourse()); + } else if (indexOfWaypoint == Util.size(getRace().getCourse().getWaypoints()) - 1) { + throw new IllegalArgumentException("Waypoint " + startOfLeg + " isn't start of any leg in " + + getRace().getCourse()); } return trackedLegs.get(race.getCourse().getLegs().get(indexOfWaypoint)); } @@ -356,7 +367,7 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { } return result; } - + public TrackedLeg getTrackedLeg(Leg leg) { return trackedLegs.get(leg); } @@ -384,7 +395,7 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { int currentRank = getTrackedLeg(competitor, leg).getRank(timePoint); return currentRank - previousRank; } - + @Override public synchronized int getRank(Competitor competitor) throws NoWindException { return getRank(competitor, MillisecondsTimePoint.now()); @@ -404,7 +415,7 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { Collections.sort(rankedCompetitors, comparator); competitorRankings.put(timePoint, rankedCompetitors); } - return rankedCompetitors.indexOf(competitor)+1; + return rankedCompetitors.indexOf(competitor) + 1; } } catch (NoWindError e) { throw e.getCause(); @@ -428,7 +439,7 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { } return result; } - + @Override public TrackedLeg getCurrentLeg(TimePoint timePoint) { Waypoint lastWaypointPassed = null; @@ -489,32 +500,46 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { public Position getApproximatePosition(Waypoint waypoint, TimePoint timePoint) { Position result = null; for (Buoy buoy : waypoint.getBuoys()) { - Position nextPos = getOrCreateTrack(buoy).getEstimatedPosition(timePoint, /* extrapolate */ false); + Position nextPos = getOrCreateTrack(buoy).getEstimatedPosition(timePoint, /* extrapolate */false); if (result == null) { result = nextPos; } else { - result = result.translateGreatCircle(result.getBearingGreatCircle(nextPos), result.getDistance(nextPos).scale(0.5)); + result = result.translateGreatCircle(result.getBearingGreatCircle(nextPos), result.getDistance(nextPos) + .scale(0.5)); } } return result; } + /** + * For wind sources of the special type {@link WindSourceType#COMBINED}, emits a new {@link CombinedWindTrackImpl} + * which will not be added to {@link #windTracks} and will not lead to the wind source being listed in + * {@link #getWindSources()} or {@link #getWindSources(WindSourceType)}. For all other wind sources, checks + * {@link #windTracks} for the respective source. If found, it's returned; otherwise the wind track is created + * through the {@link #windStore} using {@link #createWindTrack(WindSource)} and added to {@link #windTracks} before + * being returned. + */ @Override public WindTrack getOrCreateWindTrack(WindSource windSource) { WindTrack result; - synchronized (windTracks) { - result = windTracks.get(windSource); - if (result == null) { - result = createWindTrack(windSource); - windTracks.put(windSource, result); + if (windSource.getType() == WindSourceType.COMBINED) { + result = new CombinedWindTrackImpl(this, getMillisecondsOverWhichToAverageWind()); + } else { + synchronized (windTracks) { + result = windTracks.get(windSource); + if (result == null) { + result = createWindTrack(windSource); + windTracks.put(windSource, result); + } } } return result; } - + /** - * Creates a wind track for the windSource specified and stores it in {@link #windTracks}. The averaging interval is - * set according to the averaging interval set for all other wind sources, or the default if no other wind source exists yet. + * Creates a wind track for the windSource specified and stores it in {@link #windTracks}. The + * averaging interval is set according to the averaging interval set for all other wind sources, or the default if + * no other wind source exists yet. */ protected WindTrack createWindTrack(WindSource windSource) { return windStore.getWindTrack(trackedEvent, this, windSource, millisecondsOverWhichToAverageWind); @@ -528,26 +553,30 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { @Override public Wind getWind(Position p, TimePoint at, Iterable windSourcesToExclude) { - final WindWithConfidence> windWithConfidence = getWindWithConfidence(p, at, windSourcesToExclude); + final WindWithConfidence> windWithConfidence = getWindWithConfidence(p, at, + windSourcesToExclude); return windWithConfidence == null ? null : windWithConfidence.getObject(); } - + @Override - public WindWithConfidence> getWindWithConfidence(Position p, TimePoint at, Iterable windSourcesToExclude) { - final Weigher weigher = ConfidenceFactory.INSTANCE.createHyperbolicTimeDifferenceWeigher(/*halfConfidenceAfterMilliseconds*/ 10000l); - Weigher> timeWeigherThatPretendsToAlsoWeighPositions = new Weigher>() { + public WindWithConfidence> getWindWithConfidence(Position p, TimePoint at, + Iterable windSourcesToExclude) { + final Weigher weigher = ConfidenceFactory.INSTANCE + .createHyperbolicTimeDifferenceWeigher(/* halfConfidenceAfterMilliseconds */10000l); + Weigher> timeWeigherThatPretendsToAlsoWeighPositions = new Weigher>() { @Override public double getConfidence(Pair fix, Pair request) { return weigher.getConfidence(fix.getB(), request.getB()); } }; - ConfidenceBasedAverager> averager = - ConfidenceFactory.INSTANCE.createAverager(timeWeigherThatPretendsToAlsoWeighPositions); + ConfidenceBasedAverager> averager = ConfidenceFactory.INSTANCE + .createAverager(timeWeigherThatPretendsToAlsoWeighPositions); List>> windFixesWithConfidences = new ArrayList>>(); for (WindSource windSource : getWindSources()) { if (!Util.contains(windSourcesToExclude, windSource)) { WindTrack track = getOrCreateWindTrack(windSource); - WindWithConfidence> windWithConfidence = track.getAveragedWindWithConfidence(p, at); + WindWithConfidence> windWithConfidence = track.getAveragedWindWithConfidence( + p, at); if (windWithConfidence != null) { windFixesWithConfidences.add(windWithConfidence); } @@ -555,7 +584,7 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { } HasConfidence> average = averager.getAverage( windFixesWithConfidences, new Pair(p, at)); - WindWithConfidence> result = new WindWithConfidenceImpl>( + WindWithConfidence> result = new WindWithConfidenceImpl>( average.getObject(), average.getConfidence(), new Pair(p, at)); return result; } @@ -578,19 +607,20 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { } return result; } - + @Override public TimePoint getTimePointOfNewestEvent() { return timePointOfNewestEvent; } - + @Override public TimePoint getTimePointOfLastEvent() { return timePointOfLastEvent; } /** - * @param timeOfEvent may be null meaning to only unblock waiters but not update any time points + * @param timeOfEvent + * may be null meaning to only unblock waiters but not update any time points */ protected synchronized void updated(TimePoint timeOfEvent) { updateCount++; @@ -622,12 +652,12 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { wait(); // ...until updated(...) notifies us } } - @Override public synchronized void waypointAdded(int zeroBasedIndex, Waypoint waypointThatGotAdded) { updateStartToNextMarkCacheInvalidationCacheListenersAfterWaypointAdded(zeroBasedIndex, waypointThatGotAdded); - markPassingsForWaypoint.put(waypointThatGotAdded, new ConcurrentSkipListSet(MarkPassingByTimeComparator.INSTANCE)); + markPassingsForWaypoint.put(waypointThatGotAdded, new ConcurrentSkipListSet( + MarkPassingByTimeComparator.INSTANCE)); for (Buoy buoy : waypointThatGotAdded.getBuoys()) { getOrCreateTrack(buoy); } @@ -648,7 +678,7 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { for (Map.Entry entry : reorderedTrackedLegs.entrySet()) { trackedLegs.put(entry.getKey(), entry.getValue()); } - updated(/* time point*/ null); + updated(/* time point */null); } private void updateStartToNextMarkCacheInvalidationCacheListenersAfterWaypointAdded(int zeroBasedIndex, @@ -669,7 +699,7 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { } addStartToNextMarkCacheInvalidationListener(waypointThatGotAdded); } - + private void addStartToNextMarkCacheInvalidationListener(Waypoint waypoint) { for (Buoy buoy : waypoint.getBuoys()) { addStartToNextMarkCacheInvalidationListener(buoy); @@ -702,7 +732,7 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { updateStartToNextMarkCacheInvalidationCacheListenersAfterWaypointRemoved(zeroBasedIndex, waypointThatGotRemoved); Leg toRemove = null; Leg last = null; - int i=0; + int i = 0; for (Map.Entry e : trackedLegs.entrySet()) { last = e.getKey(); if (i == zeroBasedIndex) { @@ -717,10 +747,10 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { } if (toRemove != null) { trackedLegs.remove(toRemove); - updated(/* time point*/ null); + updated(/* time point */null); } } - + private void updateStartToNextMarkCacheInvalidationCacheListenersAfterWaypointRemoved(int zeroBasedIndex, Waypoint waypointThatGotRemoved) { if (zeroBasedIndex < 2) { @@ -751,8 +781,8 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { Weigher weigher = ConfidenceFactory.INSTANCE.createExponentialTimeDifferenceWeigher( // use a minimum confidence to avoid the bearing to flip to 270deg in case all is zero getMillisecondsOverWhichToAverageSpeed()); - Map> bearings = clusterBearingsByLegType(timePoint, - position, dummyMarkPassingForNow, weigher); + Map> bearings = clusterBearingsByLegType(timePoint, position, + dummyMarkPassingForNow, weigher); // use the minimum confidence of the four "quadrants" as the result's confidence BearingWithConfidenceImpl reversedUpwindAverage = null; int upwindNumberOfRelevantBoats = 0; @@ -763,8 +793,8 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { BearingWithConfidence average0 = bearingClustersUpwind[0].getAverage(timePoint); BearingWithConfidence average1 = bearingClustersUpwind[1].getAverage(timePoint); confidence = Math.min(average0.getConfidence(), average1.getConfidence()); - reversedUpwindAverage = new BearingWithConfidenceImpl(average0.getObject().middle(average1.getObject()).reverse(), - confidence, timePoint); + reversedUpwindAverage = new BearingWithConfidenceImpl(average0.getObject() + .middle(average1.getObject()).reverse(), confidence, timePoint); upwindNumberOfRelevantBoats = Math.min(bearingClustersUpwind[0].size(), bearingClustersUpwind[1].size()); } BearingWithConfidenceImpl downwindAverage = null; @@ -776,8 +806,10 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { BearingWithConfidence average1 = bearingClustersDownwind[1].getAverage(timePoint); double downwindConfidence = Math.min(average0.getConfidence(), average1.getConfidence()); confidence = Math.min(confidence, downwindConfidence); - downwindAverage = new BearingWithConfidenceImpl(average0.getObject().middle(average1.getObject()), downwindConfidence, timePoint); - downwindNumberOfRelevantBoats = Math.min(bearingClustersDownwind[0].size(), bearingClustersDownwind[1].size()); + downwindAverage = new BearingWithConfidenceImpl(average0.getObject() + .middle(average1.getObject()), downwindConfidence, timePoint); + downwindNumberOfRelevantBoats = Math.min(bearingClustersDownwind[0].size(), + bearingClustersDownwind[1].size()); } int numberOfBoatsRelevantForEstimate = upwindNumberOfRelevantBoats + downwindNumberOfRelevantBoats; BearingWithConfidenceCluster resultCluster = new BearingWithConfidenceCluster(weigher); @@ -790,14 +822,16 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { resultCluster.add(downwindAverage); } BearingWithConfidence resultBearing = resultCluster.getAverage(timePoint); - return resultBearing == null ? null : new WindImpl(null, timePoint, - new KnotSpeedWithBearingImpl(/* speedInKnots */ numberOfBoatsRelevantForEstimate, resultBearing.getObject())); + return resultBearing == null ? null : new WindImpl(null, timePoint, new KnotSpeedWithBearingImpl( + /* speedInKnots */numberOfBoatsRelevantForEstimate, resultBearing.getObject())); } - // TODO confidences need to be computed not only based on timePoint but also on position: boats far away don't contribute as confidently as boats close by + // TODO confidences need to be computed not only based on timePoint but also on position: boats far away don't + // contribute as confidently as boats close by private Map> clusterBearingsByLegType(TimePoint timePoint, Position position, DummyMarkPassingWithTimePointOnly dummyMarkPassingForNow, Weigher weigher) { - Weigher weigherForMarkPassingProximity = new HyperbolicTimeDifferenceWeigher(getMillisecondsOverWhichToAverageSpeed()*5); + Weigher weigherForMarkPassingProximity = new HyperbolicTimeDifferenceWeigher( + getMillisecondsOverWhichToAverageSpeed() * 5); Map> bearings = new HashMap>(); for (LegType legType : LegType.values()) { bearings.put(legType, new BearingWithConfidenceCluster(weigher)); @@ -810,16 +844,22 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { try { legType = trackedLeg.getLegType(timePoint); } catch (NoWindException e) { - logger.warning("Unable to determine leg type for race "+getRace().getName()+" while trying to estimate wind"); + logger.warning("Unable to determine leg type for race " + getRace().getName() + + " while trying to estimate wind"); return null; } if (legType != LegType.REACHING) { GPSFixTrack track = getTrack(competitor); if (!track.hasDirectionChange(timePoint, getManeuverDegreeAngleThreshold())) { - SpeedWithBearingWithConfidence estimatedSpeedWithConfidence = track.getEstimatedSpeed(timePoint, weigher); - if (estimatedSpeedWithConfidence != null && estimatedSpeedWithConfidence.getObject() != null && - // Mark passings may be missing or far off. This can lead to boats apparently going "backwards" regarding the leg's direction; ignore those - isNavigatingForward(estimatedSpeedWithConfidence.getObject().getBearing(), trackedLeg, timePoint)) { + SpeedWithBearingWithConfidence estimatedSpeedWithConfidence = track + .getEstimatedSpeed(timePoint, weigher); + if (estimatedSpeedWithConfidence != null + && estimatedSpeedWithConfidence.getObject() != null + && + // Mark passings may be missing or far off. This can lead to boats apparently going + // "backwards" regarding the leg's direction; ignore those + isNavigatingForward(estimatedSpeedWithConfidence.getObject().getBearing(), trackedLeg, + timePoint)) { // additionally to generally excluding maneuvers, reduce confidence around mark passings: NavigableSet markPassings = getMarkPassings(competitor); double markPassingProximityConfidenceReduction = 1.0; @@ -854,10 +894,10 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { } return bearings; } - + /** - * Checks if the bearing generally moves in the direction that the trackedLeg has at time point - * at. + * Checks if the bearing generally moves in the direction that the trackedLeg has at time + * point at. */ private boolean isNavigatingForward(Bearing bearing, TrackedLeg trackedLeg, TimePoint at) { Bearing legBearing = trackedLeg.getLegBearing(at); @@ -878,11 +918,14 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { * * In this case, the boat's sails will be on the starboard side, so the result has to be {@link Tack#STARBOARD}. The * angle between the boat's heading (which we can only approximate by the boat's bearing) and the wind bearing in - * this case is 90 degrees. wind.{@link Bearing#getDifferenceTo(Bearing) getDifferenceTo}(boat) - * in this case will return a bearing representing -90 degrees.

+ * this case is 90 degrees. wind.{@link Bearing#getDifferenceTo(Bearing) getDifferenceTo}(boat) in this + * case will return a bearing representing -90 degrees. + *

* - * If the wind is blowing the other way, the angle returned by {@link Bearing#getDifferenceTo(Bearing)} will correspond - * to +90 degrees. In other words, a negative angle means starboard tack, a positive angle represents port tack.

+ * If the wind is blowing the other way, the angle returned by {@link Bearing#getDifferenceTo(Bearing)} will + * correspond to +90 degrees. In other words, a negative angle means starboard tack, a positive angle represents + * port tack. + *

* * For the unlikely case of 0 degrees difference, {@link Tack#STARBOARD} will result. */ @@ -891,10 +934,10 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { return getTack(getTrack(competitor).getEstimatedPosition(timePoint, /* extrapolate */false), timePoint, getTrack(competitor).getEstimatedSpeed(timePoint).getBearing()); } - + /** - * Based on the wind direction at timePoint and at position where, compares the boatBearing - * to the wind's bearing at that time and place and determined the tack. + * Based on the wind direction at timePoint and at position where, compares the + * boatBearing to the wind's bearing at that time and place and determined the tack. */ private Tack getTack(Position where, TimePoint timePoint, Bearing boatBearing) { Bearing wind = getWind(where, timePoint).getBearing(); @@ -904,7 +947,7 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { @Override public String toString() { - return "TrackedRace for "+getRace(); + return "TrackedRace for " + getRace(); } @Override @@ -913,16 +956,17 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { getTrack(competitor)); return douglasPeucker.approximate(maxDistance, from, to); } - + /** * Caches results in {@link #maneuverCache}. The cache is {@link #clearAllCaches() invalidated} by any * {@link #updated(TimePoint) update}. Therefore, it is mainly useful for completed races. The cache tries to grow * the time interval for which the maneuvers of a competitor have been computed. If from and * to are within an interval already cached, the interval requested is - * {@link #extractInterval(TimePoint, TimePoint, List) extracted} from the maneuver list cached. Otherwise, - * the cached interval (empty in case no maneuvers were cached for competitor yet) is extended to - * include from..to by computing and caching the maneuvers for the new, extended interval. From the resulting - * extended maneuver list the interval requested is then {@link #extractInterval(TimePoint, TimePoint, List) extracted}. + * {@link #extractInterval(TimePoint, TimePoint, List) extracted} from the maneuver list cached. Otherwise, the + * cached interval (empty in case no maneuvers were cached for competitor yet) is extended to include + * from..to by computing and caching the maneuvers for the new, extended interval. From the resulting + * extended maneuver list the interval requested is then {@link #extractInterval(TimePoint, TimePoint, List) + * extracted}. */ @Override public List getManeuvers(Competitor competitor, TimePoint from, TimePoint to) throws NoWindException { @@ -940,7 +984,8 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { : from.compareTo(fromToAndManeuvers.getA()) <= 0 ? from : fromToAndManeuvers.getA(); TimePoint extendedTo = fromToAndManeuvers == null ? to : to.compareTo(fromToAndManeuvers.getB()) >= 0 ? to : fromToAndManeuvers.getB(); - List extendedResultForCache = detectManeuvers(competitor, + List extendedResultForCache = detectManeuvers( + competitor, approximate(competitor, getRace().getBoatClass().getMaximumDistanceForCourseApproximation(), extendedFrom, extendedTo)); result = extractInterval(from, to, extendedResultForCache); @@ -962,7 +1007,7 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { } return result; } - + /** * Tries to detect maneuvers on the competitor's track based on a number of approximating fixes. The * fixes contain bearing information, but this is not the bearing leading to the next approximation fix but the @@ -980,7 +1025,8 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { * @return an empty list if no maneuver is detected for competitor between from and * to, or else the list of maneuvers detected. */ - private List detectManeuvers(Competitor competitor, List approximatingFixesToAnalyze) throws NoWindException { + private List detectManeuvers(Competitor competitor, List approximatingFixesToAnalyze) + throws NoWindException { List result = new ArrayList(); if (approximatingFixesToAnalyze.size() > 2) { List> courseChangeSequenceInSameDirection = new ArrayList>(); @@ -988,22 +1034,27 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { GPSFixMoving previous = approximationPointsIter.next(); GPSFixMoving current = approximationPointsIter.next(); // the bearings in these variables are between approximation points - SpeedWithBearing speedWithBearingOnApproximationFromPreviousToCurrent = previous.getSpeedAndBearingRequiredToReach(current); + SpeedWithBearing speedWithBearingOnApproximationFromPreviousToCurrent = previous + .getSpeedAndBearingRequiredToReach(current); SpeedWithBearing speedWithBearingOnApproximationAtBeginningOfUnidirectionalCourseChanges = speedWithBearingOnApproximationFromPreviousToCurrent; - SpeedWithBearing speedWithBearingOnApproximationFromCurrentToNext; // will certainly be assigned because iter's collection's size > 2 + SpeedWithBearing speedWithBearingOnApproximationFromCurrentToNext; // will certainly be assigned because + // iter's collection's size > 2 do { GPSFixMoving next = approximationPointsIter.next(); speedWithBearingOnApproximationFromCurrentToNext = current.getSpeedAndBearingRequiredToReach(next); // compute course change on "approximation track" CourseChange courseChange = speedWithBearingOnApproximationFromPreviousToCurrent .getCourseChangeRequiredToReach(speedWithBearingOnApproximationFromCurrentToNext); - Pair courseChangeAtFix = new Pair(current, courseChange); - if (!courseChangeSequenceInSameDirection.isEmpty() && - Math.signum(courseChangeSequenceInSameDirection.get(0).getB().getCourseChangeInDegrees()) != - Math.signum(courseChange.getCourseChangeInDegrees())) { - // course change in different direction; cluster the course changes in same direction so far, then start new list + Pair courseChangeAtFix = new Pair(current, + courseChange); + if (!courseChangeSequenceInSameDirection.isEmpty() + && Math.signum(courseChangeSequenceInSameDirection.get(0).getB().getCourseChangeInDegrees()) != Math + .signum(courseChange.getCourseChangeInDegrees())) { + // course change in different direction; cluster the course changes in same direction so far, then + // start new list List maneuvers = groupChangesInSameDirectionIntoManeuvers(competitor, - speedWithBearingOnApproximationAtBeginningOfUnidirectionalCourseChanges, courseChangeSequenceInSameDirection); + speedWithBearingOnApproximationAtBeginningOfUnidirectionalCourseChanges, + courseChangeSequenceInSameDirection); result.addAll(maneuvers); courseChangeSequenceInSameDirection.clear(); speedWithBearingOnApproximationAtBeginningOfUnidirectionalCourseChanges = speedWithBearingOnApproximationFromPreviousToCurrent; @@ -1014,7 +1065,8 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { speedWithBearingOnApproximationFromPreviousToCurrent = speedWithBearingOnApproximationFromCurrentToNext; } while (approximationPointsIter.hasNext()); if (!courseChangeSequenceInSameDirection.isEmpty()) { - result.addAll(groupChangesInSameDirectionIntoManeuvers(competitor, speedWithBearingOnApproximationAtBeginningOfUnidirectionalCourseChanges, + result.addAll(groupChangesInSameDirectionIntoManeuvers(competitor, + speedWithBearingOnApproximationAtBeginningOfUnidirectionalCourseChanges, courseChangeSequenceInSameDirection)); } } @@ -1046,8 +1098,11 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { List> group = new ArrayList>(); if (!courseChangeSequenceInSameDirection.isEmpty()) { Distance twoHullLengths = competitor.getBoat().getBoatClass().getHullLength().scale(2); - SpeedWithBearing beforeGroupOnApproximation = speedWithBearingOnApproximationAtBeginning; // speed/bearing before group - SpeedWithBearing beforeCurrentCourseChangeOnApproximation = beforeGroupOnApproximation; // speed/bearing before current course change + SpeedWithBearing beforeGroupOnApproximation = speedWithBearingOnApproximationAtBeginning; // speed/bearing + // before group + SpeedWithBearing beforeCurrentCourseChangeOnApproximation = beforeGroupOnApproximation; // speed/bearing + // before current + // course change Iterator> iter = courseChangeSequenceInSameDirection.iterator(); double totalCourseChangeInDegrees = 0.0; long totalMilliseconds = 0l; @@ -1058,8 +1113,9 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { if (!group.isEmpty() && currentFixAndCourseChange.getA().getTimePoint().asMillis() - group.get(group.size() - 1).getA().getTimePoint().asMillis() > getApproximateManeuverDurationInMilliseconds() - && currentFixAndCourseChange.getA().getPosition().getDistance( - group.get(group.size() - 1).getA().getPosition()).compareTo(twoHullLengths) > 0) { + && currentFixAndCourseChange.getA().getPosition() + .getDistance(group.get(group.size() - 1).getA().getPosition()) + .compareTo(twoHullLengths) > 0) { // if next is more then approximate maneuver duration later or further apart than two hull lengths, // turn the current group into a maneuver and add to result Maneuver maneuver = createManeuverFromGroupOfCourseChanges(competitor, beforeGroupOnApproximation, @@ -1070,15 +1126,17 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { totalMilliseconds = 0l; beforeGroupOnApproximation = beforeCurrentCourseChangeOnApproximation; } - afterCurrentCourseChange = beforeCurrentCourseChangeOnApproximation.applyCourseChange(currentFixAndCourseChange.getB()); + afterCurrentCourseChange = beforeCurrentCourseChangeOnApproximation + .applyCourseChange(currentFixAndCourseChange.getB()); totalMilliseconds += currentFixAndCourseChange.getA().getTimePoint().asMillis(); totalCourseChangeInDegrees += currentFixAndCourseChange.getB().getCourseChangeInDegrees(); group.add(currentFixAndCourseChange); - beforeCurrentCourseChangeOnApproximation = afterCurrentCourseChange; // speed/bearing after course change + beforeCurrentCourseChangeOnApproximation = afterCurrentCourseChange; // speed/bearing after course + // change } while (iter.hasNext()); if (!group.isEmpty()) { - result.add(createManeuverFromGroupOfCourseChanges(competitor, beforeGroupOnApproximation, - group, afterCurrentCourseChange, totalCourseChangeInDegrees, totalMilliseconds)); + result.add(createManeuverFromGroupOfCourseChanges(competitor, beforeGroupOnApproximation, group, + afterCurrentCourseChange, totalCourseChangeInDegrees, totalMilliseconds)); } } return result; @@ -1086,31 +1144,38 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { private Maneuver createManeuverFromGroupOfCourseChanges(Competitor competitor, SpeedWithBearing speedWithBearingOnApproximationAtBeginning, List> group, - SpeedWithBearing speedWithBearingOnApproximationAtEnd, double totalCourseChangeInDegrees, long totalMilliseconds) - throws NoWindException { - TimePoint maneuverTimePoint = new MillisecondsTimePoint(totalMilliseconds/group.size()); - Position maneuverPosition = getTrack(competitor).getEstimatedPosition(maneuverTimePoint, /* extrapolate */ false); + SpeedWithBearing speedWithBearingOnApproximationAtEnd, double totalCourseChangeInDegrees, + long totalMilliseconds) throws NoWindException { + TimePoint maneuverTimePoint = new MillisecondsTimePoint(totalMilliseconds / group.size()); + Position maneuverPosition = getTrack(competitor) + .getEstimatedPosition(maneuverTimePoint, /* extrapolate */false); MillisecondsTimePoint timePointBeforeManeuver = new MillisecondsTimePoint(group.get(0).getA().getTimePoint() - .asMillis() - getApproximateManeuverDurationInMilliseconds()/2); + .asMillis() + - getApproximateManeuverDurationInMilliseconds() / 2); MillisecondsTimePoint timePointAfterManeuver = new MillisecondsTimePoint(group.get(group.size() - 1).getA() - .getTimePoint().asMillis() + getApproximateManeuverDurationInMilliseconds()/2); - Tack tackBeforeManeuver = getTack(maneuverPosition, timePointBeforeManeuver, speedWithBearingOnApproximationAtBeginning.getBearing()); - Tack tackAfterManeuver = getTack(maneuverPosition, timePointAfterManeuver, speedWithBearingOnApproximationAtEnd.getBearing()); + .getTimePoint().asMillis() + + getApproximateManeuverDurationInMilliseconds() / 2); + Tack tackBeforeManeuver = getTack(maneuverPosition, timePointBeforeManeuver, + speedWithBearingOnApproximationAtBeginning.getBearing()); + Tack tackAfterManeuver = getTack(maneuverPosition, timePointAfterManeuver, + speedWithBearingOnApproximationAtEnd.getBearing()); // the TrackedLegOfCompetitor variables may be null, e.g., in case the time points are before or after the race TrackedLegOfCompetitor legBeforeManeuver = getTrackedLeg(competitor, timePointBeforeManeuver); TrackedLegOfCompetitor legAfterManeuver = getTrackedLeg(competitor, timePointAfterManeuver); ManeuverType maneuverType; if (Math.abs(totalCourseChangeInDegrees) > PENALTY_CIRCLE_DEGREES_THRESHOLD) { maneuverType = ManeuverType.PENALTY_CIRCLE; - } else if (legBeforeManeuver != legAfterManeuver && + } else if (legBeforeManeuver != legAfterManeuver + && // a maneuver at the start line is not to be considered a MARK_PASSING maneuver; show a tack as a tack - legAfterManeuver != null && legAfterManeuver.getLeg().getFrom() != getRace().getCourse().getFirstWaypoint()) { + legAfterManeuver != null + && legAfterManeuver.getLeg().getFrom() != getRace().getCourse().getFirstWaypoint()) { maneuverType = ManeuverType.MARK_PASSING; } else { if (tackBeforeManeuver != tackAfterManeuver) { - LegType legType = legBeforeManeuver!=null ? - getTrackedLeg(legBeforeManeuver.getLeg()).getLegType(timePointBeforeManeuver) : - legAfterManeuver!=null ? getTrackedLeg(legAfterManeuver.getLeg()).getLegType(timePointAfterManeuver) : null; + LegType legType = legBeforeManeuver != null ? getTrackedLeg(legBeforeManeuver.getLeg()).getLegType( + timePointBeforeManeuver) : legAfterManeuver != null ? getTrackedLeg(legAfterManeuver.getLeg()) + .getLegType(timePointAfterManeuver) : null; if (legType != null) { // tack or jibe switch (legType) { @@ -1124,7 +1189,9 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { maneuverType = ManeuverType.UNKNOWN; if (logger.isLoggable(Level.FINE)) { logger.fine("Unknown maneuver for " - + competitor + " at " + maneuverTimePoint + + competitor + + " at " + + maneuverTimePoint + (legBeforeManeuver != null ? " on reaching leg " + legBeforeManeuver.getLeg() : " before start")); } @@ -1132,22 +1199,25 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { } } else { maneuverType = ManeuverType.UNKNOWN; - logger.fine("Can't determine leg type because tracked legs for competitor "+competitor+ - " cannot be determined for time points "+timePointBeforeManeuver+" and "+ - timePointAfterManeuver); + logger.fine("Can't determine leg type because tracked legs for competitor " + competitor + + " cannot be determined for time points " + timePointBeforeManeuver + " and " + + timePointAfterManeuver); } } else { // heading up or bearing away Wind wind = getWind(maneuverPosition, maneuverTimePoint); Bearing windBearing = wind.getBearing(); - Bearing toWindBeforeManeuver = windBearing.getDifferenceTo(speedWithBearingOnApproximationAtBeginning.getBearing()); - Bearing toWindAfterManeuver = windBearing.getDifferenceTo(speedWithBearingOnApproximationAtEnd.getBearing()); - maneuverType = Math.abs(toWindBeforeManeuver.getDegrees()) < Math.abs(toWindAfterManeuver.getDegrees()) ? - ManeuverType.HEAD_UP : ManeuverType.BEAR_AWAY; + Bearing toWindBeforeManeuver = windBearing.getDifferenceTo(speedWithBearingOnApproximationAtBeginning + .getBearing()); + Bearing toWindAfterManeuver = windBearing.getDifferenceTo(speedWithBearingOnApproximationAtEnd + .getBearing()); + maneuverType = Math.abs(toWindBeforeManeuver.getDegrees()) < Math.abs(toWindAfterManeuver.getDegrees()) ? ManeuverType.HEAD_UP + : ManeuverType.BEAR_AWAY; } } - Maneuver maneuver = new ManeuverImpl(maneuverType, tackAfterManeuver, maneuverPosition, maneuverTimePoint, speedWithBearingOnApproximationAtBeginning, - speedWithBearingOnApproximationAtEnd, totalCourseChangeInDegrees); + Maneuver maneuver = new ManeuverImpl(maneuverType, tackAfterManeuver, maneuverPosition, maneuverTimePoint, + speedWithBearingOnApproximationAtBeginning, speedWithBearingOnApproximationAtEnd, + totalCourseChangeInDegrees); return maneuver; } @@ -1165,22 +1235,22 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { private double getMinimumAngleBetweenDifferentTacksUpwind() { return getRace().getBoatClass().getMinimumAngleBetweenDifferentTacksUpwind(); } - + private long getApproximateManeuverDurationInMilliseconds() { return getRace().getBoatClass().getApproximateManeuverDurationInMilliseconds(); } private class StartToNextMarkCacheInvalidationListener implements GPSTrackListener { private final GPSFixTrack listeningTo; - + public StartToNextMarkCacheInvalidationListener(GPSFixTrack listeningTo) { this.listeningTo = listeningTo; } - + public void stopListening() { listeningTo.removeListener(this); } - + @Override public void gpsFixReceived(GPSFix fix, Buoy buoy) { directionFromStartToNextMarkCache = null; @@ -1190,13 +1260,13 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { public void speedAveragingChanged(long oldMillisecondsOverWhichToAverage, long newMillisecondsOverWhichToAverage) { } } - + @Override public Pair getStartFinishPlacemarks() { Pair placemarks = new Pair(null, null); Placemark startBest = null; Placemark finishBest = null; - + // Get start postition Iterator startBuoys = getRace().getCourse().getFirstWaypoint().getBuoys().iterator(); GPSFix startBuoyFix = startBuoys.hasNext() ? getOrCreateTrack(startBuoys.next()).getLastRawFix() : null; @@ -1224,8 +1294,7 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { Iterator finishBuoys = getRace().getCourse().getFirstWaypoint().getBuoys().iterator(); GPSFix finishBuoyFix = finishBuoys.hasNext() ? getOrCreateTrack(finishBuoys.next()).getLastRawFix() : null; Position finishPosition = finishBuoyFix != null ? finishBuoyFix.getPosition() : null; - if (startPosition != null - && finishPosition != null) { + if (startPosition != null && finishPosition != null) { if (startPosition.getDistance(finishPosition).getKilometers() <= ReverseGeocoder.POSITION_CACHE_DISTANCE_LIMIT_IN_KM) { finishBest = startBest; } else { @@ -1245,7 +1314,7 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { } } } - + if (startBest != null) { placemarks.setA(startBest); } @@ -1254,9 +1323,10 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { } return placemarks; } - + @Override - public Distance getWindwardDistanceToOverallLeader(Competitor competitor, TimePoint timePoint) throws NoWindException { + public Distance getWindwardDistanceToOverallLeader(Competitor competitor, TimePoint timePoint) + throws NoWindException { final TrackedLegOfCompetitor trackedLeg = getTrackedLeg(competitor, timePoint); return trackedLeg == null ? null : trackedLeg.getWindwardDistanceToOverallLeader(timePoint); } @@ -1271,10 +1341,10 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { } return result; } - + @Override public Iterable getWindSources() { return windTracks.keySet(); } - + } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index 78164f4c60d..83f6672add4 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -609,7 +609,10 @@ public class SailingServiceImpl extends RemoteServiceServlet implements SailingS result.raceIsKnownToStartUpwind = trackedRace.raceIsKnownToStartUpwind(); Map windTrackInfoDTOs = new HashMap(); result.windTrackInfoByWindSource = windTrackInfoDTOs; - for (WindSource windSource : trackedRace.getWindSources()) { + List windSourcesToDeliver = new ArrayList(); + Util.addAll(trackedRace.getWindSources(), windSourcesToDeliver); + windSourcesToDeliver.add(new WindSourceImpl(WindSourceType.COMBINED)); + for (WindSource windSource : windSourcesToDeliver) { if (windSourceTypeNames == null || windSourceTypeNames.contains(windSource.getType().name())) { TimePoint fromTimePoint = new MillisecondsTimePoint(from); WindTrackInfoDTO windTrackInfoDTO = new WindTrackInfoDTO();