started to add wind estimation caching; added test case which still fails sporadically

This commit is contained in:
Axel Uhl committed 2011-12-27 16:02:41 +01:00
1 parent 83aade65bd
commit 2a9687ac14
13 files changed
+520 -268

No files matched your search

@@ -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) {
@@ -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) {
@@ -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 });
@@ -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
@@ -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
/**
@@ -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);
}
@@ -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
@@ -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;
}
}
@@ -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();
}
}
@@ -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();
}
@@ -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");
}
}
@@ -273,7 +273,7 @@ public class ArrayListNavigableSet<E> implements NavigableSet<E> {
}
@Override
public void remove() {
list.remove(i);
list.remove(--i);
}
};
}