diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/FetchTracksAndStoreLocallyTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/FetchTracksAndStoreLocallyTest.java index 131047955d9..4574847d135 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/FetchTracksAndStoreLocallyTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/FetchTracksAndStoreLocallyTest.java @@ -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) { diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/ReceiveTrackingDataTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/ReceiveTrackingDataTest.java index 54b1f89c02a..31b5363c5f0 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/ReceiveTrackingDataTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/ReceiveTrackingDataTest.java @@ -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) { diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/WindEstimationOnConstructedTracksTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/WindEstimationOnConstructedTracksTest.java index 1dba0a912e0..0899eb33d27 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/WindEstimationOnConstructedTracksTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/WindEstimationOnConstructedTracksTest.java @@ -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 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 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 cachedFixes = new HashMap(); + 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 }); diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/AbstractTimePoint.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/AbstractTimePoint.java index 8e97371d16f..c87f7dca24a 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/AbstractTimePoint.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/AbstractTimePoint.java @@ -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_COMPARATOR = new Comparator() { + @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 diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/DynamicTrackedRace.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/DynamicTrackedRace.java index e518ae0c4f5..615ceb75190 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/DynamicTrackedRace.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/DynamicTrackedRace.java @@ -20,8 +20,6 @@ public interface DynamicTrackedRace extends TrackedRace { */ DynamicGPSFixTrack getOrCreateTrack(Buoy buoy); - void addListener(RaceChangeListener listener); - // TODO need another listener protocol for general changes in ranking and leg completion /** diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/RaceChangeListener.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/RaceChangeListener.java index 89f92e750b0..a9571bd70d1 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/RaceChangeListener.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/RaceChangeListener.java @@ -4,7 +4,15 @@ package com.sap.sailing.domain.tracking; public interface RaceChangeListener 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 markPassing or null 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); diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java index 61f6bea80dd..3f0946102a0 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java @@ -256,4 +256,6 @@ public interface TrackedRace { */ boolean raceIsKnownToStartUpwind(); + void addListener(RaceChangeListener listener); + } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DynamicTrackedRaceImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DynamicTrackedRaceImpl.java index 2c6e9d42c8d..ef06ed5b59e 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DynamicTrackedRaceImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DynamicTrackedRaceImpl.java @@ -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 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 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 markPassings) { + Map oldMarkPassings = new HashMap(); synchronized (this) { + for (MarkPassing oldMarkPassing : getMarkPassings(competitor)) { + oldMarkPassings.put(oldMarkPassing.getWaypoint(), oldMarkPassing); + } clearMarkPassings(competitor); NavigableSet 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 diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/EstimatedWindFixesAsNavigableSet.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/EstimatedWindFixesAsNavigableSet.java new file mode 100755 index 00000000000..7834dc879a2 --- /dev/null +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/EstimatedWindFixesAsNavigableSet.java @@ -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 null, {@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 null, 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 { + /** + * 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 null. 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, Long.MAX_VALUE 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 null. 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, MillisecondsTimePoint(1) 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 iterator() { + return new Iterator() { + 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 descendingSet() { + return new DescendingNavigableSet(this); + } + + @Override + public Iterator descendingIterator() { + return new Iterator() { + 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 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 headSet(Wind toElement, boolean inclusive) { + return new EstimatedWindFixesAsNavigableSet(track, trackedRace, /* from */ null, + inclusive ? ceilingToResolution(toElement) : lowerToResolution(toElement)); + } + + @Override + public NavigableSet tailSet(Wind fromElement, boolean inclusive) { + return new EstimatedWindFixesAsNavigableSet(track, trackedRace, inclusive ? floorToResolution(fromElement) + : higherToResolution(fromElement), + /* to */ null); + } + + @Override + public SortedSet subSet(Wind fromElement, Wind toElement) { + return subSet(fromElement, true, toElement, false); + } + + @Override + public SortedSet headSet(Wind toElement) { + return headSet(toElement, false); + } + + @Override + public SortedSet tailSet(Wind fromElement) { + return tailSet(fromElement, true); + } + + @Override + public Comparator 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[] 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; + } +} + diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackBasedEstimationWindTrackImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackBasedEstimationWindTrackImpl.java index 91357799502..31f963d1aaa 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackBasedEstimationWindTrackImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackBasedEstimationWindTrackImpl.java @@ -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 null as value for this time. These null "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 null as value for this time. These null "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. + *

+ * + * 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 { private final EstimatedWindFixesAsNavigableSet virtualInternalRawFixes; - + private final TrackedRace trackedRace; + private final NavigableSet 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(AbstractTimePoint.TIMEPOINT_COMPARATOR); + } + + private NavigableSet getCachedFixes() { + return super.getInternalRawFixes(); } + private NavigableSet 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 cache = getCachedFixes(); + Iterator 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 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 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 getInternalRawFixes() { return virtualInternalRawFixes; } - + @Override protected NavigableSet getInternalFixes() { return new PartialNavigableSetView(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 null, {@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 null, 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 { - 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 iterator() { - return new Iterator() { - 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 descendingSet() { - return new DescendingNavigableSet(this); - } - - @Override - public Iterator descendingIterator() { - return new Iterator() { - 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 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 headSet(Wind toElement, boolean inclusive) { - return new EstimatedWindFixesAsNavigableSet(trackedRace, /* from */ null, - inclusive?floorToResolution(toElement):lowerToResolution(toElement)); - } - - @Override - public NavigableSet tailSet(Wind fromElement, boolean inclusive) { - return new EstimatedWindFixesAsNavigableSet(trackedRace, inclusive?ceilingToResolution(fromElement):higherToResolution(fromElement), - /* to */ null); - } - - @Override - public SortedSet subSet(Wind fromElement, Wind toElement) { - return subSet(fromElement, true, toElement, false); - } - - @Override - public SortedSet headSet(Wind toElement) { - return headSet(toElement, false); - } - - @Override - public SortedSet tailSet(Wind fromElement) { - return tailSet(fromElement, true); - } - - @Override - public Comparator 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[] 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(); + } + } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedLegImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedLegImpl.java index 9b0e76be4f8..d577341f7c8 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedLegImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedLegImpl.java @@ -192,7 +192,7 @@ public class TrackedLegImpl implements TrackedLeg, RaceChangeListener implements WindTrack { */ protected Wind getEstimatedWindUnsynchronized(Position p, TimePoint at) { DummyWind atTimed = new DummyWind(at); - NavigableSet beforeSet = getInternalFixes().headSet(atTimed, /* inclusive */ true); + NavigableSet beforeSet = getInternalFixes().headSet(atTimed, /* inclusive */ false); NavigableSet afterSet = getInternalFixes().tailSet(atTimed, /* inclusive */ true); Iterator beforeIter = beforeSet.descendingIterator(); Iterator afterIter = afterSet.iterator(); @@ -188,8 +188,12 @@ public class WindTrackImpl extends TrackImpl 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"); } } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/util/impl/ArrayListNavigableSet.java b/java/com.sap.sailing.domain/src/com/sap/sailing/util/impl/ArrayListNavigableSet.java index 62c0a5d1639..772bfcfade4 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/util/impl/ArrayListNavigableSet.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/util/impl/ArrayListNavigableSet.java @@ -273,7 +273,7 @@ public class ArrayListNavigableSet implements NavigableSet { } @Override public void remove() { - list.remove(i); + list.remove(--i); } }; }