diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/ArrayListNavigableSetTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/ArrayListNavigableSetTest.java new file mode 100755 index 00000000000..268eb1c8676 --- /dev/null +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/ArrayListNavigableSetTest.java @@ -0,0 +1,45 @@ +package com.sap.sailing.domain.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; + +import java.util.Comparator; +import java.util.HashSet; +import java.util.Set; + +import org.junit.Test; + +import com.sap.sailing.util.impl.ArrayListNavigableSet; + +public class ArrayListNavigableSetTest { + @Test + public void testReplacementInHashSet() { + Set hashSet = new HashSet(); + testSet(hashSet); + } + + @Test + public void testReplacementInArrayListNavigableSet() { + Set arrayListNavigableSet = new ArrayListNavigableSet(new Comparator() { + @Override + public int compare(Integer o1, Integer o2) { + return o1.compareTo(o2); + } + }); + testSet(arrayListNavigableSet); + } + + private void testSet(Set hashSet) { + Integer i1 = new Integer(1); + Integer i2 = new Integer(1); + assertNotSame(i1, i2); + assertEquals(i1, i2); + hashSet.add(i1); + boolean replaced = hashSet.add(i2); + assertFalse(replaced); // because s1 and s2 are equal + Integer integerFromSet = hashSet.iterator().next(); + assertSame(i1, integerFromSet); // supposedly not replaced + } +} diff --git a/java/com.sap.sailing.domain/META-INF/MANIFEST.MF b/java/com.sap.sailing.domain/META-INF/MANIFEST.MF index ef2fe81ff09..6df4517736c 100755 --- a/java/com.sap.sailing.domain/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.domain/META-INF/MANIFEST.MF @@ -15,7 +15,7 @@ Export-Package: com.sap.sailing.domain.base, com.sap.sailing.domain.tracking, com.sap.sailing.domain.tracking.impl, com.sap.sailing.util, - com.sap.sailing.util.impl;x-friends:="com.sap.sailing.server.replication" + com.sap.sailing.util.impl;x-friends:="com.sap.sailing.server.replication,com.sap.sailing.domain.test" Bundle-ActivationPolicy: lazy Require-Bundle: com.googlecode.java-diff-utils;bundle-version="1.3.0", com.sap.sailing.domain.common, 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 9f4e96cad12..97c10966697 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 @@ -4,10 +4,12 @@ import java.io.IOException; import java.io.ObjectOutputStream; import java.io.Serializable; import java.util.Arrays; +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.Timer; import java.util.TimerTask; @@ -128,10 +130,10 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl } /** - * {@link #scheduleCacheInvalidation(WindWithConfidence, TimePoint)} synchronizes on this object before changing it + * {@link #scheduleCacheRefresh(WindWithConfidence, TimePoint)} synchronizes on this object before changing it * and when actually invalidating the cache. */ - private final InvalidationInterval scheduledInvalidationInterval; + private final InvalidationInterval scheduledRefreshInterval; /** * @param delayForCacheInvalidationInMilliseconds @@ -139,14 +141,18 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl * time interval around the time point of the event. If the cache invalidation happens immediately, this * can cause significant load on the server. Delaying the cache refresh just a little will reduce server * load, sacrificing some accuracy of the wind estimation which can carefully be traded by this - * parameter. + * parameter. When the delay is set to 0, the cache contents for the affected time interval are + * immediately removed and will be re-computed upon the next request. If a positive delay is specified, + * the cache contents for the interval affected will be re-computed and will be replaced in the cache + * when the new results are available. Clients therefore won't have to wait for the valued to be + * re-computed but will be served from the old cache values until they will have been replaced. */ public TrackBasedEstimationWindTrackImpl(TrackedRace trackedRace, long millisecondsOverWhichToAverage, double baseConfidence, long delayForCacheInvalidationInMilliseconds) { super(trackedRace, millisecondsOverWhichToAverage, baseConfidence, WindSourceType.TRACK_BASED_ESTIMATION.useSpeed()); this.delayForCacheInvalidationInMilliseconds = delayForCacheInvalidationInMilliseconds; - this.scheduledInvalidationInterval = new InvalidationInterval(); + this.scheduledRefreshInterval = new InvalidationInterval(); cache = new ArrayListNavigableSet>( new SerializableComparator>() { private static final long serialVersionUID = 5760349397418542705L; @@ -208,74 +214,137 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl } protected synchronized void cache(TimePoint timePoint, WindWithConfidence fix) { - if (fix == null) { - getTimePointsWithCachedNullResult().add(timePoint); - timePointsWithCachedNullResultFastContains.add(timePoint); - } else { - getCachedFixes().add(fix); + synchronized (scheduledRefreshInterval) { + if (fix == null) { + getTimePointsWithCachedNullResult().add(timePoint); + timePointsWithCachedNullResultFastContains.add(timePoint); + } else { + getCachedFixes().add(fix); + } } } - protected synchronized void cacheNull(TimePoint timePoint) { - timePointsWithCachedNullResult.add(timePoint); - timePointsWithCachedNullResultFastContains.add(timePoint); - } - /** * Schedules a cache invalidation for the time interval specified. The scheduling delay is configured during construction of * this track. The longer the scheduling delay, the less load this track will cause for the server because invalidations will * be bundled, and during live mode the incoming requests for a time point close to the time for which new data is received * will not be massively delayed by having to re-calculate the estimation over and over again. */ - private synchronized void scheduleCacheInvalidation(WindWithConfidence startOfInvalidation, TimePoint endOfInvalidation) { - synchronized (scheduledInvalidationInterval) { - if (!scheduledInvalidationInterval.isSet()) { + private synchronized void scheduleCacheRefresh(WindWithConfidence startOfInvalidation, TimePoint endOfInvalidation) { + synchronized (scheduledRefreshInterval) { + if (!scheduledRefreshInterval.isSet()) { // according to the invariant this implies [1]==null - scheduledInvalidationInterval.set(startOfInvalidation, endOfInvalidation); + scheduledRefreshInterval.set(startOfInvalidation, endOfInvalidation); startSchedulerForCacheRefresh(); } else { // this means that an invalidation is already scheduled; as long as we're synchronized on scheduledInvalidationInterval // we can safely extend the interval; the invalidation won't start before we release the lock - scheduledInvalidationInterval.extend(startOfInvalidation, endOfInvalidation); + scheduledRefreshInterval.extend(startOfInvalidation, endOfInvalidation); } } } /** - * Invalidates the cache based on {@link #scheduledInvalidationInterval} and when done + * Invalidates the cache based on {@link #scheduledRefreshInterval} and when done * {@link InvalidationInterval#clear() clears} the invalidation interval, indicating that currently no scheduler is * running. */ private synchronized void invalidateCache() { - synchronized (scheduledInvalidationInterval) { - Iterator> iter = (scheduledInvalidationInterval.getStart() == null ? getCachedFixes() - : getCachedFixes().tailSet(scheduledInvalidationInterval.getStart(), /* inclusive */true)).iterator(); + synchronized (scheduledRefreshInterval) { + Iterator> iter = (scheduledRefreshInterval.getStart() == null ? getCachedFixes() + : getCachedFixes().tailSet(scheduledRefreshInterval.getStart(), /* inclusive */true)).iterator(); while (iter.hasNext()) { WindWithConfidence next = iter.next(); - if (scheduledInvalidationInterval.getEnd() == null || next.getObject().getTimePoint().compareTo(scheduledInvalidationInterval.getEnd()) < 0) { + if (scheduledRefreshInterval.getEnd() == null || next.getObject().getTimePoint().compareTo(scheduledRefreshInterval.getEnd()) < 0) { iter.remove(); } else { break; } } - Iterator nullIter = (scheduledInvalidationInterval.getStart() == null ? getTimePointsWithCachedNullResult() - : getTimePointsWithCachedNullResult().tailSet(scheduledInvalidationInterval.getStart().getObject().getTimePoint(), /* inclusive */ + Iterator nullIter = (scheduledRefreshInterval.getStart() == null ? getTimePointsWithCachedNullResult() + : getTimePointsWithCachedNullResult().tailSet(scheduledRefreshInterval.getStart().getObject().getTimePoint(), /* inclusive */ true)).iterator(); while (nullIter.hasNext()) { TimePoint next = nullIter.next(); - if (scheduledInvalidationInterval.getEnd() == null || next.compareTo(scheduledInvalidationInterval.getEnd()) < 0) { + if (scheduledRefreshInterval.getEnd() == null || next.compareTo(scheduledRefreshInterval.getEnd()) < 0) { nullIter.remove(); timePointsWithCachedNullResultFastContains.remove(next); } else { break; } } - scheduledInvalidationInterval.clear(); + scheduledRefreshInterval.clear(); } } + /** + * Incrementally replaces the cache elements based on {@link #scheduledRefreshInterval} using freshly computed values. + */ + private synchronized void refreshCacheIncrementally() { + Set> windFixesToRecalculate = new HashSet>(); + Set cachedNullResultsToRecalculate = new HashSet(); + synchronized (scheduledRefreshInterval) { + Iterator> iter = (scheduledRefreshInterval.getStart() == null ? getCachedFixes() + : getCachedFixes().tailSet(scheduledRefreshInterval.getStart(), /* inclusive */true)).iterator(); + Iterator nullIter = (scheduledRefreshInterval.getStart() == null ? getTimePointsWithCachedNullResult() + : getTimePointsWithCachedNullResult().tailSet(scheduledRefreshInterval.getStart().getObject().getTimePoint(), /* inclusive */ + true)).iterator(); + WindWithConfidence nextFixToRecalculate = null; + while (iter.hasNext() && + ((nextFixToRecalculate = iter.next()).getObject().getTimePoint().compareTo(scheduledRefreshInterval.getEnd()) < 0) || + scheduledRefreshInterval.getEnd() == null) { + windFixesToRecalculate.add(nextFixToRecalculate); + } + TimePoint nextNullResultToRecalculate = null; + while (nullIter.hasNext() && + ((nextNullResultToRecalculate = nullIter.next()).compareTo(scheduledRefreshInterval.getEnd()) < 0) || + scheduledRefreshInterval.getEnd() == null) { + cachedNullResultsToRecalculate.add(nextNullResultToRecalculate); + } + } + Set nullRemovals = new HashSet(); + Set nullInsertions = new HashSet(); + Map> cacheInsertions = new HashMap>(); + for (TimePoint cachedNullResultToRecalculate : cachedNullResultsToRecalculate) { + WindWithConfidence replacementFix = getTrackedRace() + .getEstimatedWindDirectionWithConfidence(/* position */ null, cachedNullResultToRecalculate); + if (replacementFix != null) { + nullRemovals.add(cachedNullResultToRecalculate); + cacheInsertions.put(cachedNullResultToRecalculate, replacementFix); + } // else no action required because the result is still null + } + for (WindWithConfidence windFixToRecalculate : windFixesToRecalculate) { + TimePoint timePoint = windFixToRecalculate.getObject().getTimePoint(); + Position position = windFixToRecalculate.getObject().getPosition(); + WindWithConfidence replacementFix = getTrackedRace() + .getEstimatedWindDirectionWithConfidence(position, timePoint); + if (replacementFix == null) { + nullInsertions.add(timePoint); + } else { + cacheInsertions.put(timePoint, replacementFix); + } + } + // apply the computed cache deltas + synchronized (scheduledRefreshInterval) { + for (TimePoint nullRemoval : nullRemovals) { + getTimePointsWithCachedNullResult().remove(nullRemoval); + timePointsWithCachedNullResultFastContains.remove(nullRemoval); + } + for (TimePoint nullInsertion : nullInsertions) { + cache(nullInsertion, null); + } + for (WindWithConfidence cacheRemoval : windFixesToRecalculate) { + getCachedFixes().remove(cacheRemoval); + } + for (Map.Entry> cacheInsertion : cacheInsertions.entrySet()) { + cache(cacheInsertion.getKey(), cacheInsertion.getValue()); + } + } + scheduledRefreshInterval.clear(); + } + private void startSchedulerForCacheRefresh() { - synchronized (scheduledInvalidationInterval) { + synchronized (scheduledRefreshInterval) { if (delayForCacheInvalidationInMilliseconds == 0) { invalidateCache(); } else { @@ -287,9 +356,9 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl // to avoid deadlock with another invalidateCache() and with scheduleCacheInvalidation we need // to obtain the TrackBasedEstimationWindTrackImpl.this monitor first (see bug 746). synchronized (TrackBasedEstimationWindTrackImpl.this) { - synchronized (scheduledInvalidationInterval) { + synchronized (scheduledRefreshInterval) { cacheInvalidationTimer.cancel(); // terminates the timer thread - invalidateCache(); + refreshCacheIncrementally(); } } } @@ -354,7 +423,7 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl WindWithConfidence startOfInvalidation = getDummyFixWithConfidence(new MillisecondsTimePoint(wind .getTimePoint().asMillis() - averagingInterval)); TimePoint endOfInvalidation = new MillisecondsTimePoint(wind.getTimePoint().asMillis() + averagingInterval); - scheduleCacheInvalidation(startOfInvalidation, endOfInvalidation); + scheduleCacheRefresh(startOfInvalidation, endOfInvalidation); } @Override @@ -378,7 +447,7 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl WindWithConfidence startOfInvalidation = getDummyFixWithConfidence(new MillisecondsTimePoint(fix .getTimePoint().asMillis() - averagingInterval)); TimePoint endOfInvalidation = new MillisecondsTimePoint(fix.getTimePoint().asMillis() + averagingInterval); - scheduleCacheInvalidation(startOfInvalidation, endOfInvalidation); + scheduleCacheRefresh(startOfInvalidation, endOfInvalidation); } @Override @@ -401,7 +470,7 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl - averagingInterval)); endOfInvalidation = new MillisecondsTimePoint(interval[1].asMillis() + averagingInterval); } - scheduleCacheInvalidation(startOfInvalidation, endOfInvalidation); + scheduleCacheRefresh(startOfInvalidation, endOfInvalidation); } } } @@ -419,7 +488,7 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl WindWithConfidence startOfInvalidation = interval.getA() == null ? null : getDummyFixWithConfidence(interval.getA()); TimePoint endOfInvalidation = interval.getB(); if (startOfInvalidation != null && endOfInvalidation != null) { - scheduleCacheInvalidation(startOfInvalidation, endOfInvalidation); + scheduleCacheRefresh(startOfInvalidation, endOfInvalidation); } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/racemap/RaceMap.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/racemap/RaceMap.java index f7f446c860e..0c0aaeb4512 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/racemap/RaceMap.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/racemap/RaceMap.java @@ -536,20 +536,22 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe protected void showMarksOnMap(CourseDTO courseDTO) { if (map != null && courseDTO != null) { Set toRemove = new HashSet(buoyMarkers.keySet()); - for (MarkDTO markDTO : courseDTO.buoys) { - Marker buoyMarker = buoyMarkers.get(markDTO); - if (buoyMarker == null) { - buoyMarker = createBuoyMarker(markDTO); - buoyMarkers.put(markDTO, buoyMarker); - map.addOverlay(buoyMarker); - } else { - buoyMarker.setLatLng(LatLng.newInstance(markDTO.position.latDeg, markDTO.position.lngDeg)); - toRemove.remove(markDTO); + if (courseDTO.buoys != null) { + for (MarkDTO markDTO : courseDTO.buoys) { + Marker buoyMarker = buoyMarkers.get(markDTO); + if (buoyMarker == null) { + buoyMarker = createBuoyMarker(markDTO); + buoyMarkers.put(markDTO, buoyMarker); + map.addOverlay(buoyMarker); + } else { + buoyMarker.setLatLng(LatLng.newInstance(markDTO.position.latDeg, markDTO.position.lngDeg)); + toRemove.remove(markDTO); + } + } + for (MarkDTO toRemoveMarkDTO : toRemove) { + Marker marker = buoyMarkers.remove(toRemoveMarkDTO); + map.removeOverlay(marker); } - } - for (MarkDTO toRemoveMarkDTO : toRemove) { - Marker marker = buoyMarkers.remove(toRemoveMarkDTO); - map.removeOverlay(marker); } } }