mirror of
https://github.com/eclipse-sailing-analytics/sailing-analytics.git
synced 2026-10-05 20:03:52 +00:00
started to add wind estimation caching; added test case which still fails sporadically
This commit is contained in:
1 parent
83aade65bd
commit
2a9687ac14
13 files changed
+520
-268
No files matched your search
+1
-1
@@ -62,7 +62,7 @@ public class FetchTracksAndStoreLocallyTest extends OnlineTracTracBasedTest {
|
||||
track.addGPSFix((GPSFixMoving) fix);
|
||||
}
|
||||
@Override
|
||||
public void markPassingReceived(MarkPassing markPassing) {
|
||||
public void markPassingReceived(MarkPassing oldMarkPassing, MarkPassing markPassing) {
|
||||
}
|
||||
@Override
|
||||
public void windDataReceived(Wind wind) {
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ public class ReceiveTrackingDataTest extends AbstractTracTracLiveTest {
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void markPassingReceived(MarkPassing markPassing) {
|
||||
public void markPassingReceived(MarkPassing oldMarkPassing, MarkPassing markPassing) {
|
||||
}
|
||||
@Override
|
||||
public void windDataReceived(Wind wind) {
|
||||
|
||||
+51
-4
@@ -1,18 +1,24 @@
|
||||
package com.sap.sailing.domain.test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import com.sap.sailing.domain.base.Competitor;
|
||||
import com.sap.sailing.domain.base.TimePoint;
|
||||
import com.sap.sailing.domain.base.Waypoint;
|
||||
import com.sap.sailing.domain.base.impl.BoatClassImpl;
|
||||
import com.sap.sailing.domain.base.impl.BoatImpl;
|
||||
@@ -31,6 +37,7 @@ import com.sap.sailing.domain.tracking.NoWindException;
|
||||
import com.sap.sailing.domain.tracking.Wind;
|
||||
import com.sap.sailing.domain.tracking.impl.GPSFixMovingImpl;
|
||||
import com.sap.sailing.domain.tracking.impl.MarkPassingImpl;
|
||||
import com.sap.sailing.domain.tracking.impl.TrackBasedEstimationWindTrackImpl;
|
||||
|
||||
public class WindEstimationOnConstructedTracksTest extends StoredTrackBasedTest {
|
||||
private List<Competitor> competitors;
|
||||
@@ -72,17 +79,57 @@ public class WindEstimationOnConstructedTracksTest extends StoredTrackBasedTest
|
||||
private CompetitorImpl createCompetitor(String competitorName) {
|
||||
return new CompetitorImpl(123, competitorName, new TeamImpl("STG", Collections.singleton(
|
||||
new PersonImpl(competitorName, new NationalityImpl("Germany", "GER"),
|
||||
/* dateOfBirth */ null, "This is famous "+competitorName)),
|
||||
new PersonImpl("Rigo van Maas", new NationalityImpl("The Netherlands", "NED"),
|
||||
/* dateOfBirth */ null, "This is Rigo, the coach")), new BoatImpl(competitorName+"'s boat", new BoatClassImpl("505", /* typicallyStartsUpwind */ true), null));
|
||||
/* dateOfBirth */null, "This is famous " + competitorName)), new PersonImpl("Rigo van Maas",
|
||||
new NationalityImpl("The Netherlands", "NED"),
|
||||
/* dateOfBirth */null, "This is Rigo, the coach")), new BoatImpl(competitorName + "'s boat",
|
||||
new BoatClassImpl("505", /* typicallyStartsUpwind */true), null));
|
||||
}
|
||||
|
||||
private void setBearingForCompetitor(Competitor competitor, MillisecondsTimePoint timePoint, double bearingDeg) {
|
||||
private void setBearingForCompetitor(Competitor competitor, TimePoint timePoint, double bearingDeg) {
|
||||
DynamicGPSFixTrack<Competitor, GPSFixMoving> hungersTrack = getTrackedRace().getTrack(competitor);
|
||||
hungersTrack.addGPSFix(new GPSFixMovingImpl(new DegreePosition(54.4680424, 10.234451), timePoint,
|
||||
new KnotSpeedWithBearingImpl(10, new DegreeBearingImpl(bearingDeg))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWindEstimationCaching() {
|
||||
initRace(2, new int[] { 1, 1 });
|
||||
TimePoint now = getTrackedRace()
|
||||
.getMarkPassingsInOrder(getTrackedRace().getRace().getCourse().getFirstWaypoint()).iterator().next()
|
||||
.getTimePoint();
|
||||
setBearingForCompetitor(competitors.get(0), now, 320);
|
||||
setBearingForCompetitor(competitors.get(1), now, 50);
|
||||
final Map<TimePoint, Wind> cachedFixes = new HashMap<TimePoint, Wind>();
|
||||
TrackBasedEstimationWindTrackImpl track = new TrackBasedEstimationWindTrackImpl(
|
||||
getTrackedRace(), /* millisecondsOverWhichToAverage */ 30000) {
|
||||
@Override
|
||||
protected void cache(TimePoint timePoint, Wind fix) {
|
||||
super.cache(timePoint, fix);
|
||||
cachedFixes.put(timePoint, fix);
|
||||
}
|
||||
};
|
||||
Wind estimatedWindDirection = track.getEstimatedWind(/* position */ null, now);
|
||||
assertNotNull(estimatedWindDirection);
|
||||
assertEquals(185., estimatedWindDirection.getBearing().getDegrees(), 0.00000001);
|
||||
assertFalse(cachedFixes.isEmpty());
|
||||
assertEquals(185., cachedFixes.values().iterator().next().getBearing().getDegrees(), 0.00000001);
|
||||
// now clear set of cached fixes, ask again and ensure nothing is cached again:
|
||||
cachedFixes.clear();
|
||||
Wind estimatedWindDirectionCached = track.getEstimatedWind(/* position */ null, now);
|
||||
assertTrue(cachedFixes.isEmpty());
|
||||
assertNotNull(estimatedWindDirectionCached);
|
||||
assertEquals(185., estimatedWindDirectionCached.getBearing().getDegrees(), 0.00000001);
|
||||
// now add a GPS fix and make sure the cache is invalidated
|
||||
now = MillisecondsTimePoint.now();
|
||||
setBearingForCompetitor(competitors.get(0), now, 330);
|
||||
Wind estimatedWindDirectionNew = track.getEstimatedWind(/* position */ null, now);
|
||||
assertFalse(cachedFixes.isEmpty());
|
||||
assertNotNull(estimatedWindDirectionNew);
|
||||
assertTrue("Expected estimated wind direction to now be greater than 185 degrees but was "
|
||||
+ estimatedWindDirectionCached.getBearing().getDegrees(), estimatedWindDirectionCached.getBearing()
|
||||
.getDegrees() < 185.); // remember: bearing is opposite of from; boats start with upwind
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWindEstimationForSimpleTracks() throws NoWindException {
|
||||
initRace(2, new int[] { 1, 1 });
|
||||
|
||||
+10
-2
@@ -1,17 +1,25 @@
|
||||
package com.sap.sailing.domain.base.impl;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
|
||||
import com.sap.sailing.domain.base.TimePoint;
|
||||
|
||||
public abstract class AbstractTimePoint implements TimePoint {
|
||||
private static SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
|
||||
|
||||
public static Comparator<TimePoint> TIMEPOINT_COMPARATOR = new Comparator<TimePoint>() {
|
||||
@Override
|
||||
public int compare(TimePoint o1, TimePoint o2) {
|
||||
long milliDiff = o1.asMillis() - o2.asMillis();
|
||||
return milliDiff<0 ? -1 : milliDiff == 0 ? 0 : 1;
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public int compareTo(TimePoint o) {
|
||||
long milliDiff = asMillis() - o.asMillis();
|
||||
return milliDiff<0 ? -1 : milliDiff == 0 ? 0 : 1;
|
||||
return TIMEPOINT_COMPARATOR.compare(this, o);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
-2
@@ -20,8 +20,6 @@ public interface DynamicTrackedRace extends TrackedRace {
|
||||
*/
|
||||
DynamicGPSFixTrack<Buoy, GPSFix> getOrCreateTrack(Buoy buoy);
|
||||
|
||||
void addListener(RaceChangeListener<Competitor> listener);
|
||||
|
||||
// TODO need another listener protocol for general changes in ranking and leg completion
|
||||
|
||||
/**
|
||||
|
||||
+9
-1
@@ -4,7 +4,15 @@ package com.sap.sailing.domain.tracking;
|
||||
public interface RaceChangeListener<ItemType> extends WindListener {
|
||||
void gpsFixReceived(GPSFix fix, ItemType competitor);
|
||||
|
||||
void markPassingReceived(MarkPassing markPassing);
|
||||
/**
|
||||
* Invoked after the mark passings have been updated in the {@link TrackedRace}.
|
||||
*
|
||||
* @param oldMarkPassing
|
||||
* the mark passing replaced by <code>markPassing</code> or <code>null</code> if for the mark passing's
|
||||
* waypoint no previous {@link MarkPassing} was recorded for the {@link MarkPassing#getCompetitor()
|
||||
* competitor}.
|
||||
*/
|
||||
void markPassingReceived(MarkPassing oldMarkPassing, MarkPassing markPassing);
|
||||
|
||||
void speedAveragingChanged(long oldMillisecondsOverWhichToAverage, long newMillisecondsOverWhichToAverage);
|
||||
|
||||
|
||||
@@ -256,4 +256,6 @@ public interface TrackedRace {
|
||||
*/
|
||||
boolean raceIsKnownToStartUpwind();
|
||||
|
||||
void addListener(RaceChangeListener<Competitor> listener);
|
||||
|
||||
}
|
||||
+16
-10
@@ -1,8 +1,10 @@
|
||||
package com.sap.sailing.domain.tracking.impl;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.NavigableSet;
|
||||
import java.util.Set;
|
||||
import java.util.logging.Level;
|
||||
@@ -61,11 +63,6 @@ public class DynamicTrackedRaceImpl extends TrackedRaceImpl implements
|
||||
public synchronized void recordFix(Competitor competitor, GPSFixMoving fix) {
|
||||
DynamicGPSFixTrack<Competitor, GPSFixMoving> track = getTrack(competitor);
|
||||
track.addGPSFix(fix); // the track notifies this tracked race which in turn notifies its listeners
|
||||
if (getStart() == null || getStart().compareTo(fix.getTimePoint())>0) {
|
||||
// infer race start time from fix; earliest fix received defines start if earlier than assumed start so far
|
||||
setStartTimeReceived(fix.getTimePoint());
|
||||
}
|
||||
updated(fix.getTimePoint());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -166,10 +163,10 @@ public class DynamicTrackedRaceImpl extends TrackedRaceImpl implements
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyListeners(MarkPassing markPassing) {
|
||||
private void notifyListeners(MarkPassing oldMarkPassing, MarkPassing markPassing) {
|
||||
for (RaceChangeListener<Competitor> listener : getListeners()) {
|
||||
try {
|
||||
listener.markPassingReceived(markPassing);
|
||||
listener.markPassingReceived(oldMarkPassing, markPassing);
|
||||
} catch (Throwable t) {
|
||||
logger.log(Level.SEVERE, "RaceChangeListener "+listener+" threw exception "+t.getMessage());
|
||||
logger.throwing(DynamicTrackedRaceImpl.class.getName(), "notifyListeners(MarkPassing)", t);
|
||||
@@ -179,7 +176,11 @@ public class DynamicTrackedRaceImpl extends TrackedRaceImpl implements
|
||||
|
||||
@Override
|
||||
public void updateMarkPassings(Competitor competitor, Iterable<MarkPassing> markPassings) {
|
||||
Map<Waypoint, MarkPassing> oldMarkPassings = new HashMap<Waypoint, MarkPassing>();
|
||||
synchronized (this) {
|
||||
for (MarkPassing oldMarkPassing : getMarkPassings(competitor)) {
|
||||
oldMarkPassings.put(oldMarkPassing.getWaypoint(), oldMarkPassing);
|
||||
}
|
||||
clearMarkPassings(competitor);
|
||||
NavigableSet<MarkPassing> competitorMarkPassings = getMarkPassings(competitor);
|
||||
TimePoint timePointOfLatestEvent = new MillisecondsTimePoint(0);
|
||||
@@ -194,7 +195,7 @@ public class DynamicTrackedRaceImpl extends TrackedRaceImpl implements
|
||||
}
|
||||
// notify *after* all mark passings have been re-established; should avoid flicker
|
||||
for (MarkPassing markPassing : markPassings) {
|
||||
notifyListeners(markPassing);
|
||||
notifyListeners(oldMarkPassings.get(markPassing.getWaypoint()), markPassing);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,6 +232,11 @@ public class DynamicTrackedRaceImpl extends TrackedRaceImpl implements
|
||||
|
||||
@Override
|
||||
public void gpsFixReceived(GPSFix fix, Competitor competitor) {
|
||||
if (getStart() == null || getStart().compareTo(fix.getTimePoint())>0) {
|
||||
// infer race start time from fix; earliest fix received defines start if earlier than assumed start so far
|
||||
setStartTimeReceived(fix.getTimePoint());
|
||||
}
|
||||
updated(fix.getTimePoint());
|
||||
notifyListeners(fix, competitor);
|
||||
}
|
||||
|
||||
@@ -245,8 +251,8 @@ public class DynamicTrackedRaceImpl extends TrackedRaceImpl implements
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markPassingReceived(MarkPassing markPassing) {
|
||||
notifyListeners(markPassing);
|
||||
public void markPassingReceived(MarkPassing oldMarkPassing, MarkPassing markPassing) {
|
||||
notifyListeners(oldMarkPassing, markPassing);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+267
@@ -0,0 +1,267 @@
|
||||
package com.sap.sailing.domain.tracking.impl;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.Iterator;
|
||||
import java.util.NavigableSet;
|
||||
import java.util.SortedSet;
|
||||
|
||||
import com.sap.sailing.domain.base.TimePoint;
|
||||
import com.sap.sailing.domain.base.impl.MillisecondsTimePoint;
|
||||
import com.sap.sailing.domain.tracking.TrackedRace;
|
||||
import com.sap.sailing.domain.tracking.Wind;
|
||||
import com.sap.sailing.domain.tracking.impl.WindTrackImpl.DummyWind;
|
||||
import com.sap.sailing.util.impl.AbstractUnmodifiableNavigableSet;
|
||||
import com.sap.sailing.util.impl.DescendingNavigableSet;
|
||||
|
||||
/**
|
||||
* Emulates a collection of {@link Wind} fixes for a {@link TrackedRace}, computed using
|
||||
* {@link TrackedRace#getEstimatedWindDirection(com.sap.sailing.domain.base.Position, TimePoint)}. If not contrained
|
||||
* by a {@link #from} and/or a {@link #to} time point, an equidistant time field is assumed, starting at
|
||||
* {@link TrackedRace#getStart()} and leading up to {@link TrackedRace#getTimePointOfNewestEvent()}. If
|
||||
* {@link TrackedRace#getStart()} returns <code>null</code>, {@link Long#MAX_VALUE} is used as the {@link #from}
|
||||
* time point, pushing the start to the more or less infinite future ("end of the universe"). If no event was
|
||||
* received yet and hence {@link TrackedRace#getTimePointOfNewestEvent()} returns <code>null</code>, the {@link #to}
|
||||
* end is assumed to be the beginning of the epoch (1970-01-01T00:00:00).
|
||||
*
|
||||
* @author Axel Uhl (d043530)
|
||||
*
|
||||
*/
|
||||
public class EstimatedWindFixesAsNavigableSet extends AbstractUnmodifiableNavigableSet<Wind> {
|
||||
/**
|
||||
* The time resolution is one second.
|
||||
*/
|
||||
private static final long RESOLUTION_IN_MILLISECONDS = 1000l;
|
||||
|
||||
private final TrackedRace trackedRace;
|
||||
|
||||
private final TimePoint from;
|
||||
|
||||
private final TimePoint to;
|
||||
|
||||
private final TrackBasedEstimationWindTrackImpl track;
|
||||
|
||||
public EstimatedWindFixesAsNavigableSet(TrackBasedEstimationWindTrackImpl track, TrackedRace trackedRace) {
|
||||
this(track, trackedRace, null, null);
|
||||
}
|
||||
|
||||
private EstimatedWindFixesAsNavigableSet(TrackBasedEstimationWindTrackImpl track, TrackedRace trackedRace,
|
||||
TimePoint from, TimePoint to) {
|
||||
this.track = track;
|
||||
this.trackedRace = trackedRace;
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
}
|
||||
|
||||
private TimePoint lowerToResolution(Wind w) {
|
||||
return new MillisecondsTimePoint((w.getTimePoint().asMillis() - 1) / RESOLUTION_IN_MILLISECONDS
|
||||
* RESOLUTION_IN_MILLISECONDS);
|
||||
}
|
||||
|
||||
private TimePoint floorToResolution(Wind w) {
|
||||
return new MillisecondsTimePoint(w.getTimePoint().asMillis() / RESOLUTION_IN_MILLISECONDS
|
||||
* RESOLUTION_IN_MILLISECONDS);
|
||||
}
|
||||
|
||||
private TimePoint ceilingToResolution(Wind w) {
|
||||
return new MillisecondsTimePoint(((w.getTimePoint().asMillis() - 1) / RESOLUTION_IN_MILLISECONDS + 1)
|
||||
* RESOLUTION_IN_MILLISECONDS);
|
||||
}
|
||||
|
||||
private TimePoint higherToResolution(Wind w) {
|
||||
return new MillisecondsTimePoint((w.getTimePoint().asMillis() / RESOLUTION_IN_MILLISECONDS + 1)
|
||||
* RESOLUTION_IN_MILLISECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* The time point starting from and including which the GPS fixes are considered in the race's tracks. Returns the
|
||||
* value of {@link #from} unless it is <code>null</code>. In this case, the time point of the
|
||||
* {@link TrackedRace#getStart() race start}, {@link #floorToResolution(Wind) floored to the resolution of this set}
|
||||
* will be returned instead. If no valid start time can be obtained from the race, <code>Long.MAX_VALUE</code> is
|
||||
* returned instead.
|
||||
*/
|
||||
private TimePoint getFrom() {
|
||||
return from == null ? trackedRace.getStart() == null ? new MillisecondsTimePoint(Long.MAX_VALUE)
|
||||
: floorToResolution(new DummyWind(trackedRace.getStart())) : from;
|
||||
}
|
||||
|
||||
/**
|
||||
* Time point up to and including which the GPS fixes are considered in the race's tracks. Returns the value of
|
||||
* {@link #to} unless it is <code>null</code>. In this case, the time point of the
|
||||
* {@link TrackedRace#getTimePointOfNewestEvent() time point of the newest event},
|
||||
* {@link #ceilingToResolution(Wind) ceiled to the resolution of this set} will be returned instead. If no valid
|
||||
* time of a newest event can be obtained from the race, <code>MillisecondsTimePoint(1)</code> is returned instead.
|
||||
*/
|
||||
private TimePoint getTo() {
|
||||
return to == null ? trackedRace.getTimePointOfNewestEvent() == null ? new MillisecondsTimePoint(1)
|
||||
: ceilingToResolution(new DummyWind(trackedRace.getTimePointOfNewestEvent())) : to;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Wind lower(Wind w) {
|
||||
TimePoint timePoint = lowerToResolution(w);
|
||||
return timePoint.compareTo(getFrom()) < 0 ? null : track.getEstimatedWindDirection(w.getPosition(), timePoint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Wind floor(Wind w) {
|
||||
TimePoint timePoint = floorToResolution(w);
|
||||
return timePoint.compareTo(getFrom()) < 0 ? null : track.getEstimatedWindDirection(w.getPosition(), timePoint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Wind ceiling(Wind w) {
|
||||
TimePoint timePoint = ceilingToResolution(w);
|
||||
return timePoint.compareTo(getTo()) > 0 ? null : track.getEstimatedWindDirection(w.getPosition(), timePoint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Wind higher(Wind w) {
|
||||
TimePoint timePoint = higherToResolution(w);
|
||||
return timePoint.compareTo(getTo()) > 0 ? null : track.getEstimatedWindDirection(w.getPosition(), timePoint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<Wind> iterator() {
|
||||
return new Iterator<Wind>() {
|
||||
private TimePoint timePoint = getFrom();
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return timePoint.compareTo(getTo()) <= 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Wind next() {
|
||||
Wind result = floor(new DummyWind(timePoint));
|
||||
timePoint = new MillisecondsTimePoint(timePoint.asMillis() + RESOLUTION_IN_MILLISECONDS);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public NavigableSet<Wind> descendingSet() {
|
||||
return new DescendingNavigableSet<Wind>(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<Wind> descendingIterator() {
|
||||
return new Iterator<Wind>() {
|
||||
private TimePoint timePoint = lowerToResolution(new DummyWind(getTo()));
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return timePoint.compareTo(getFrom()) >= 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Wind next() {
|
||||
Wind result = floor(new DummyWind(timePoint));
|
||||
timePoint = new MillisecondsTimePoint(timePoint.asMillis() - RESOLUTION_IN_MILLISECONDS);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public NavigableSet<Wind> subSet(Wind fromElement, boolean fromInclusive, Wind toElement, boolean toInclusive) {
|
||||
return new EstimatedWindFixesAsNavigableSet(track, trackedRace, fromInclusive ? ceilingToResolution(fromElement)
|
||||
: higherToResolution(fromElement), toInclusive ? floorToResolution(toElement)
|
||||
: lowerToResolution(toElement));
|
||||
}
|
||||
|
||||
@Override
|
||||
public NavigableSet<Wind> headSet(Wind toElement, boolean inclusive) {
|
||||
return new EstimatedWindFixesAsNavigableSet(track, trackedRace, /* from */ null,
|
||||
inclusive ? ceilingToResolution(toElement) : lowerToResolution(toElement));
|
||||
}
|
||||
|
||||
@Override
|
||||
public NavigableSet<Wind> tailSet(Wind fromElement, boolean inclusive) {
|
||||
return new EstimatedWindFixesAsNavigableSet(track, trackedRace, inclusive ? floorToResolution(fromElement)
|
||||
: higherToResolution(fromElement),
|
||||
/* to */ null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SortedSet<Wind> subSet(Wind fromElement, Wind toElement) {
|
||||
return subSet(fromElement, true, toElement, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SortedSet<Wind> headSet(Wind toElement) {
|
||||
return headSet(toElement, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SortedSet<Wind> tailSet(Wind fromElement) {
|
||||
return tailSet(fromElement, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Comparator<? super Wind> comparator() {
|
||||
return WindComparator.INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Wind first() {
|
||||
return floor(new DummyWind(getFrom()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Wind last() {
|
||||
return ceiling(new DummyWind(getTo()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return (int) ((getTo().asMillis() - getFrom().asMillis()) / RESOLUTION_IN_MILLISECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(Object o) {
|
||||
boolean result = false;
|
||||
if (o instanceof Wind) {
|
||||
Wind wind = (Wind) o;
|
||||
result = wind.getTimePoint().asMillis() % RESOLUTION_IN_MILLISECONDS == 0
|
||||
&& wind.getTimePoint().compareTo(getFrom()) >= 0 && wind.getTimePoint().compareTo(getTo()) < 0;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object[] toArray() {
|
||||
Object[] result = new Object[size()];
|
||||
int i = 0;
|
||||
for (Wind w : this) {
|
||||
result[i++] = w;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T[] toArray(T[] a) {
|
||||
Object[] result = a;
|
||||
if (result.length < size()) {
|
||||
result = new Object[size()];
|
||||
}
|
||||
int i = 0;
|
||||
for (Wind w : this) {
|
||||
result[i++] = w;
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
T[] tResult = (T[]) result;
|
||||
return tResult;
|
||||
}
|
||||
}
|
||||
|
||||
+154
-242
@@ -1,51 +1,140 @@
|
||||
package com.sap.sailing.domain.tracking.impl;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.NavigableSet;
|
||||
import java.util.SortedSet;
|
||||
|
||||
import com.sap.sailing.domain.base.Competitor;
|
||||
import com.sap.sailing.domain.base.Position;
|
||||
import com.sap.sailing.domain.base.TimePoint;
|
||||
import com.sap.sailing.domain.base.impl.AbstractTimePoint;
|
||||
import com.sap.sailing.domain.base.impl.MillisecondsTimePoint;
|
||||
import com.sap.sailing.domain.tracking.GPSFix;
|
||||
import com.sap.sailing.domain.tracking.MarkPassing;
|
||||
import com.sap.sailing.domain.tracking.RaceChangeListener;
|
||||
import com.sap.sailing.domain.tracking.TrackedRace;
|
||||
import com.sap.sailing.domain.tracking.Wind;
|
||||
import com.sap.sailing.util.impl.AbstractUnmodifiableNavigableSet;
|
||||
import com.sap.sailing.util.impl.DescendingNavigableSet;
|
||||
import com.sap.sailing.util.impl.ArrayListNavigableSet;
|
||||
|
||||
/**
|
||||
* A virtual wind track that computes the wind bearing based on the boat tracks recorded in the tracked
|
||||
* race for which this wind track is constructed. It has a fixed time resolution as defined by the constant
|
||||
* {@link #RESOLUTION_IN_MILLISECONDS}. When asked for the wind at a time at which the wind cannot be estimated,
|
||||
* the raw fixes will have <code>null</code> as value for this time. These <code>null</code> "fixes" are at the
|
||||
* same time considered "outliers" by the {@link #getInternalFixes()} operation which filters them from the
|
||||
* "smoothened" view. With this, the view with "outliers" removed contains all those fixes for which the wind
|
||||
* bearing was successfully computed from the tracked race's boat tracks.
|
||||
* A virtual wind track that computes and caches the wind bearing based on the boat tracks recorded in the tracked race
|
||||
* for which this wind track is constructed. It has a fixed time resolution as defined by the constant
|
||||
* {@link #RESOLUTION_IN_MILLISECONDS}. When asked for the wind at a time at which the wind cannot be estimated, the raw
|
||||
* fixes will have <code>null</code> as value for this time. These <code>null</code> "fixes" are at the same time
|
||||
* considered "outliers" by the {@link #getInternalFixes()} operation which filters them from the "smoothened" view.
|
||||
* With this, the view with "outliers" removed contains all those fixes for which the wind bearing was successfully
|
||||
* computed from the tracked race's boat tracks.
|
||||
* <p>
|
||||
*
|
||||
* Caching is done using the base class's {@link TrackImpl#fixes} field which is made accessible through
|
||||
* {@link #getCachedFixes()}. This track observes the {@link TrackedRace} for which it provides wind estimations.
|
||||
* Whenever a change occurs, all fixes whose derivation is potentially affected by the change are removed from the
|
||||
* cache. For new GPS fixes arriving this is the time span used for
|
||||
* {@link TrackedRace#getMillisecondsOverWhichToAverageSpeed() averaging speeds}. For a new mark passing, all fixes
|
||||
* between the old and new mark passing times as well as those
|
||||
* {@link TrackedRace#getMillisecondsOverWhichToAverageSpeed()} before and after this time period are removed from the
|
||||
* cache. If the {@link #speedAveragingChanged(long, long) speed averaging changes}, the entire cache is cleared.
|
||||
*
|
||||
*
|
||||
* @author Axel Uhl (d043530)
|
||||
*
|
||||
*
|
||||
*/
|
||||
public class TrackBasedEstimationWindTrackImpl extends WindTrackImpl {
|
||||
/**
|
||||
* The time resolution is one second.
|
||||
*/
|
||||
private static final long RESOLUTION_IN_MILLISECONDS = 1000l;
|
||||
|
||||
public class TrackBasedEstimationWindTrackImpl extends WindTrackImpl implements RaceChangeListener<Competitor> {
|
||||
private final EstimatedWindFixesAsNavigableSet virtualInternalRawFixes;
|
||||
|
||||
|
||||
private final TrackedRace trackedRace;
|
||||
|
||||
private final NavigableSet<TimePoint> timePointsWithCachedNullResult;
|
||||
|
||||
public TrackBasedEstimationWindTrackImpl(TrackedRace trackedRace, long millisecondsOverWhichToAverage) {
|
||||
super(millisecondsOverWhichToAverage);
|
||||
this.trackedRace = trackedRace;
|
||||
this.virtualInternalRawFixes = new EstimatedWindFixesAsNavigableSet(trackedRace);
|
||||
trackedRace.addListener(this);
|
||||
this.virtualInternalRawFixes = new EstimatedWindFixesAsNavigableSet(this, trackedRace);
|
||||
this.timePointsWithCachedNullResult = new ArrayListNavigableSet<TimePoint>(AbstractTimePoint.TIMEPOINT_COMPARATOR);
|
||||
}
|
||||
|
||||
private NavigableSet<Wind> getCachedFixes() {
|
||||
return super.getInternalRawFixes();
|
||||
}
|
||||
|
||||
private NavigableSet<TimePoint> getTimePointsWithCachedNullResult() {
|
||||
return timePointsWithCachedNullResult;
|
||||
}
|
||||
|
||||
protected synchronized void cache(TimePoint timePoint, Wind fix) {
|
||||
if (fix == null) {
|
||||
getTimePointsWithCachedNullResult().add(timePoint);
|
||||
} else {
|
||||
getCachedFixes().add(fix);
|
||||
}
|
||||
}
|
||||
|
||||
protected synchronized void cacheNull(TimePoint timePoint) {
|
||||
timePointsWithCachedNullResult.add(timePoint);
|
||||
}
|
||||
|
||||
private synchronized void invalidateCache(Wind startOfInvalidation, TimePoint endOfInvalidation) {
|
||||
NavigableSet<Wind> cache = getCachedFixes();
|
||||
Iterator<Wind> iter = cache.tailSet(startOfInvalidation, /* inclusive */ true).iterator();
|
||||
while (iter.hasNext()) {
|
||||
Wind next = iter.next();
|
||||
if (next.getTimePoint().compareTo(endOfInvalidation) < 0) {
|
||||
iter.remove();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Iterator<TimePoint> nullIter = getTimePointsWithCachedNullResult().tailSet(startOfInvalidation.getTimePoint(), /* inclusive */
|
||||
true).iterator();
|
||||
while (nullIter.hasNext()) {
|
||||
TimePoint next = nullIter.next();
|
||||
if (next.compareTo(endOfInvalidation) < 0) {
|
||||
nullIter.remove();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void clearCache() {
|
||||
getCachedFixes().clear();
|
||||
getTimePointsWithCachedNullResult().clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up wind data in the {@link #getCachedFixes() cache} and the {@link #getTimePointsWithCachedNullResult()
|
||||
* null store} first. Only if nothing is found for the time point requested, the
|
||||
* {@link TrackedRace#getEstimatedWindDirection(Position, TimePoint) wind estimation algorithm} is used to compute
|
||||
* it. The result will then be added to the cache.
|
||||
*/
|
||||
protected synchronized Wind getEstimatedWindDirection(Position p, TimePoint timePoint) {
|
||||
Wind result;
|
||||
NavigableSet<Wind> cache = getCachedFixes();
|
||||
Wind cachedFix = cache.floor(getDummyFix(timePoint));
|
||||
if (cachedFix == null) {
|
||||
if (getTimePointsWithCachedNullResult().contains(timePoint)) {
|
||||
result = null;
|
||||
} else {
|
||||
result = trackedRace.getEstimatedWindDirection(p, timePoint);
|
||||
cache(timePoint, result);
|
||||
}
|
||||
} else {
|
||||
if (cachedFix.getTimePoint().equals(timePoint)) {
|
||||
result = cachedFix;
|
||||
} else {
|
||||
result = trackedRace.getEstimatedWindDirection(p, timePoint);
|
||||
cache(timePoint, result);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected NavigableSet<Wind> getInternalRawFixes() {
|
||||
return virtualInternalRawFixes;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected NavigableSet<Wind> getInternalFixes() {
|
||||
return new PartialNavigableSetView<Wind>(getInternalRawFixes()) {
|
||||
@@ -95,226 +184,49 @@ public class TrackBasedEstimationWindTrackImpl extends WindTrackImpl {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emulates a collection of {@link Wind} fixes for a {@link TrackedRace}, computed using
|
||||
* {@link TrackedRace#getEstimatedWindDirection(com.sap.sailing.domain.base.Position, TimePoint)}. If not contrained
|
||||
* by a {@link #from} and/or a {@link #to} time point, an equidistant time field is assumed, starting at
|
||||
* {@link TrackedRace#getStart()} and leading up to {@link TrackedRace#getTimePointOfNewestEvent()}. If
|
||||
* {@link TrackedRace#getStart()} returns <code>null</code>, {@link Long#MAX_VALUE} is used as the {@link #from}
|
||||
* time point, pushing the start to the more or less infinite future ("end of the universe"). If no event was
|
||||
* received yet and hence {@link TrackedRace#getTimePointOfNewestEvent()} returns <code>null</code>, the
|
||||
* {@link #to} end is assumed to be the beginning of the epoch (1970-01-01T00:00:00).
|
||||
*
|
||||
* @author Axel Uhl (d043530)
|
||||
*
|
||||
*/
|
||||
private static class EstimatedWindFixesAsNavigableSet extends AbstractUnmodifiableNavigableSet<Wind> {
|
||||
private final TrackedRace trackedRace;
|
||||
|
||||
private final TimePoint from;
|
||||
|
||||
private final TimePoint to;
|
||||
|
||||
public EstimatedWindFixesAsNavigableSet(TrackedRace trackedRace) {
|
||||
this(trackedRace, null, null);
|
||||
}
|
||||
|
||||
private EstimatedWindFixesAsNavigableSet(TrackedRace trackedRace, TimePoint from, TimePoint to) {
|
||||
this.trackedRace = trackedRace;
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
}
|
||||
|
||||
private TimePoint lowerToResolution(Wind w) {
|
||||
return new MillisecondsTimePoint((w.getTimePoint().asMillis()-1) / RESOLUTION_IN_MILLISECONDS * RESOLUTION_IN_MILLISECONDS);
|
||||
}
|
||||
|
||||
private TimePoint floorToResolution(Wind w) {
|
||||
return new MillisecondsTimePoint(w.getTimePoint().asMillis() / RESOLUTION_IN_MILLISECONDS * RESOLUTION_IN_MILLISECONDS);
|
||||
}
|
||||
|
||||
private TimePoint ceilingToResolution(Wind w) {
|
||||
return new MillisecondsTimePoint(((w.getTimePoint().asMillis()-1) / RESOLUTION_IN_MILLISECONDS + 1) * RESOLUTION_IN_MILLISECONDS);
|
||||
}
|
||||
|
||||
private TimePoint higherToResolution(Wind w) {
|
||||
return new MillisecondsTimePoint((w.getTimePoint().asMillis() / RESOLUTION_IN_MILLISECONDS + 1) * RESOLUTION_IN_MILLISECONDS);
|
||||
}
|
||||
|
||||
private TimePoint getFrom() {
|
||||
return from == null ? trackedRace.getStart() == null ? new MillisecondsTimePoint(Long.MAX_VALUE)
|
||||
: trackedRace.getStart() : from;
|
||||
}
|
||||
|
||||
private TimePoint getTo() {
|
||||
return to == null ? trackedRace.getTimePointOfNewestEvent() == null ? new MillisecondsTimePoint(1)
|
||||
: trackedRace.getTimePointOfNewestEvent() : to;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Wind lower(Wind w) {
|
||||
TimePoint timePoint = lowerToResolution(w);
|
||||
return timePoint.compareTo(getFrom()) < 0 ? null : trackedRace.getEstimatedWindDirection(w.getPosition(), timePoint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Wind floor(Wind w) {
|
||||
TimePoint timePoint = floorToResolution(w);
|
||||
return timePoint.compareTo(getFrom()) < 0 ? null : trackedRace.getEstimatedWindDirection(w.getPosition(), timePoint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Wind ceiling(Wind w) {
|
||||
TimePoint timePoint = ceilingToResolution(w);
|
||||
return timePoint.compareTo(getTo()) > 0 ? null : trackedRace.getEstimatedWindDirection(w.getPosition(), timePoint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Wind higher(Wind w) {
|
||||
TimePoint timePoint = higherToResolution(w);
|
||||
return timePoint.compareTo(getTo()) > 0 ? null : trackedRace.getEstimatedWindDirection(w.getPosition(), timePoint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<Wind> iterator() {
|
||||
return new Iterator<Wind>() {
|
||||
private TimePoint timePoint = getFrom();
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return timePoint.compareTo(getTo()) < 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Wind next() {
|
||||
Wind result = floor(new DummyWind(timePoint));
|
||||
timePoint = new MillisecondsTimePoint(timePoint.asMillis()+RESOLUTION_IN_MILLISECONDS);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public NavigableSet<Wind> descendingSet() {
|
||||
return new DescendingNavigableSet<Wind>(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<Wind> descendingIterator() {
|
||||
return new Iterator<Wind>() {
|
||||
private TimePoint timePoint = lowerToResolution(new DummyWind(getTo()));
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return timePoint.compareTo(getFrom()) >= 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Wind next() {
|
||||
Wind result = floor(new DummyWind(timePoint));
|
||||
timePoint = new MillisecondsTimePoint(timePoint.asMillis()-RESOLUTION_IN_MILLISECONDS);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public NavigableSet<Wind> subSet(Wind fromElement, boolean fromInclusive, Wind toElement, boolean toInclusive) {
|
||||
return new EstimatedWindFixesAsNavigableSet(trackedRace, fromInclusive?ceilingToResolution(fromElement):higherToResolution(fromElement),
|
||||
toInclusive?floorToResolution(toElement):lowerToResolution(toElement));
|
||||
}
|
||||
|
||||
@Override
|
||||
public NavigableSet<Wind> headSet(Wind toElement, boolean inclusive) {
|
||||
return new EstimatedWindFixesAsNavigableSet(trackedRace, /* from */ null,
|
||||
inclusive?floorToResolution(toElement):lowerToResolution(toElement));
|
||||
}
|
||||
|
||||
@Override
|
||||
public NavigableSet<Wind> tailSet(Wind fromElement, boolean inclusive) {
|
||||
return new EstimatedWindFixesAsNavigableSet(trackedRace, inclusive?ceilingToResolution(fromElement):higherToResolution(fromElement),
|
||||
/* to */ null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SortedSet<Wind> subSet(Wind fromElement, Wind toElement) {
|
||||
return subSet(fromElement, true, toElement, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SortedSet<Wind> headSet(Wind toElement) {
|
||||
return headSet(toElement, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SortedSet<Wind> tailSet(Wind fromElement) {
|
||||
return tailSet(fromElement, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Comparator<? super Wind> comparator() {
|
||||
return WindComparator.INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Wind first() {
|
||||
return floor(new DummyWind(getFrom()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Wind last() {
|
||||
return ceiling(new DummyWind(getTo()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return (int) ((getTo().asMillis()-getFrom().asMillis())/RESOLUTION_IN_MILLISECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(Object o) {
|
||||
boolean result = false;
|
||||
if (o instanceof Wind) {
|
||||
Wind wind = (Wind) o;
|
||||
result = wind.getTimePoint().asMillis() % RESOLUTION_IN_MILLISECONDS == 0
|
||||
&& wind.getTimePoint().compareTo(getFrom()) >= 0 && wind.getTimePoint().compareTo(getTo()) < 0;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object[] toArray() {
|
||||
Object[] result = new Object[size()];
|
||||
int i=0;
|
||||
for (Wind w : this) {
|
||||
result[i++] = w;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T[] toArray(T[] a) {
|
||||
Object[] result = a;
|
||||
if (result.length < size()) {
|
||||
result = new Object[size()];
|
||||
}
|
||||
int i=0;
|
||||
for (Wind w : this) {
|
||||
result[i++] = w;
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
T[] tResult = (T[]) result;
|
||||
return tResult;
|
||||
}
|
||||
@Override
|
||||
public void windDataReceived(Wind wind) {
|
||||
// we ignore wind updates for cache invalidations; however, TODO we'd like to invalidate in case a leg changes its type
|
||||
}
|
||||
|
||||
@Override
|
||||
public void windDataRemoved(Wind wind) {
|
||||
// we ignore wind updates for cache invalidations; however, TODO we'd like to invalidate in case a leg changes its type
|
||||
}
|
||||
|
||||
@Override
|
||||
public void windAveragingChanged(long oldMillisecondsOverWhichToAverage, long newMillisecondsOverWhichToAverage) {
|
||||
// we ignore wind updates for cache invalidations; however, TODO we'd like to invalidate in case a leg changes its type
|
||||
}
|
||||
|
||||
@Override
|
||||
public void gpsFixReceived(GPSFix fix, Competitor competitor) {
|
||||
long averagingInterval = trackedRace.getMillisecondsOverWhichToAverageSpeed();
|
||||
Wind startOfInvalidation = getDummyFix(new MillisecondsTimePoint(fix.getTimePoint().asMillis()-averagingInterval));
|
||||
TimePoint endOfInvalidation = new MillisecondsTimePoint(fix.getTimePoint().asMillis()+averagingInterval);
|
||||
invalidateCache(startOfInvalidation, endOfInvalidation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markPassingReceived(MarkPassing oldMarkPassing, MarkPassing markPassing) {
|
||||
long averagingInterval = trackedRace.getMillisecondsOverWhichToAverageSpeed();
|
||||
Wind startOfInvalidation;
|
||||
TimePoint endOfInvalidation;
|
||||
if (oldMarkPassing == null) {
|
||||
startOfInvalidation = getDummyFix(new MillisecondsTimePoint(markPassing.getTimePoint().asMillis()-averagingInterval));
|
||||
endOfInvalidation = new MillisecondsTimePoint(markPassing.getTimePoint().asMillis()+averagingInterval);
|
||||
} else {
|
||||
TimePoint[] interval = new TimePoint[] { oldMarkPassing.getTimePoint(), markPassing.getTimePoint() };
|
||||
Arrays.sort(interval);
|
||||
startOfInvalidation = getDummyFix(new MillisecondsTimePoint(interval[0].asMillis()-averagingInterval));
|
||||
endOfInvalidation = new MillisecondsTimePoint(interval[1].asMillis()+averagingInterval);
|
||||
}
|
||||
invalidateCache(startOfInvalidation, endOfInvalidation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void speedAveragingChanged(long oldMillisecondsOverWhichToAverage, long newMillisecondsOverWhichToAverage) {
|
||||
clearCache();
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -192,7 +192,7 @@ public class TrackedLegImpl implements TrackedLeg, RaceChangeListener<Competitor
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markPassingReceived(MarkPassing markPassing) {
|
||||
public void markPassingReceived(MarkPassing oldMarkPassing, MarkPassing markPassing) {
|
||||
clearCaches();
|
||||
}
|
||||
|
||||
|
||||
+7
-3
@@ -118,7 +118,7 @@ public class WindTrackImpl extends TrackImpl<Wind> implements WindTrack {
|
||||
*/
|
||||
protected Wind getEstimatedWindUnsynchronized(Position p, TimePoint at) {
|
||||
DummyWind atTimed = new DummyWind(at);
|
||||
NavigableSet<Wind> beforeSet = getInternalFixes().headSet(atTimed, /* inclusive */ true);
|
||||
NavigableSet<Wind> beforeSet = getInternalFixes().headSet(atTimed, /* inclusive */ false);
|
||||
NavigableSet<Wind> afterSet = getInternalFixes().tailSet(atTimed, /* inclusive */ true);
|
||||
Iterator<Wind> beforeIter = beforeSet.descendingIterator();
|
||||
Iterator<Wind> afterIter = afterSet.iterator();
|
||||
@@ -188,8 +188,12 @@ public class WindTrackImpl extends TrackImpl<Wind> implements WindTrack {
|
||||
result.append(wind);
|
||||
result.append(" avg(");
|
||||
result.append(millisecondsOverWhichToAverage);
|
||||
result.append("ms): ");
|
||||
result.append(getEstimatedWind(wind.getPosition(), wind.getTimePoint()));
|
||||
if (wind == null) {
|
||||
result.append("ms)");
|
||||
} else {
|
||||
result.append("ms): ");
|
||||
result.append(getEstimatedWind(wind.getPosition(), wind.getTimePoint()));
|
||||
}
|
||||
result.append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -273,7 +273,7 @@ public class ArrayListNavigableSet<E> implements NavigableSet<E> {
|
||||
}
|
||||
@Override
|
||||
public void remove() {
|
||||
list.remove(i);
|
||||
list.remove(--i);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user