Merge remote-tracking branch 'server/master'

Change-Id: I58698543eeb8ce5f350e5a2f73f0024da893cf32
This commit is contained in:
Axel Uhl committed 2019-05-09 10:05:24 +02:00
commit c745c909fd
11 files changed
+478 -152

No files matched your search

@@ -0,0 +1,124 @@
package com.sap.sailing.domain.markpassingcalculation.impl;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.util.Comparator;
import java.util.NavigableSet;
import java.util.TreeSet;
import org.junit.Before;
import org.junit.Test;
import com.sap.sailing.domain.markpassingcalculation.Candidate;
import com.sap.sailing.domain.tracking.impl.TimedComparator;
import com.sap.sse.common.Duration;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.impl.MillisecondsTimePoint;
public class MostProbableCandidatesInSmallTimeRangeFilterTest {
private Candidate c1, c2, c3, c4, c5, c6, c7, c8, c9, c10;
private NavigableSet<Candidate> competitorCandidates;
private MostProbableCandidatesInSmallTimeRangeFilter filter;
@Before
public void setUp() {
final TimePoint now = MillisecondsTimePoint.now();
final Candidate start = new CandidateImpl(0, now.minus(Duration.ONE_HOUR), 1.0, null);
final Candidate end = new CandidateImpl(6, now.plus(Duration.ONE_HOUR), 1.0, null);
final Comparator<Candidate> candidateComparator = (c1, c2)->TimedComparator.INSTANCE.compare(c1, c2);
filter = new MostProbableCandidatesInSmallTimeRangeFilter(
candidateComparator, start, end);
competitorCandidates = new TreeSet<>(candidateComparator);
final Duration gapless = MostProbableCandidatesInSmallTimeRangeFilter.CANDIDATE_FILTER_TIME_WINDOW.divide(2);
c1 = candidate(now, "c1");
c2 = candidate(c1.getTimePoint().plus(gapless), "c2");
c3 = candidate(c2.getTimePoint().plus(gapless), "c3");
c4 = candidate(c3.getTimePoint().plus(gapless), "c4");
c5 = candidate(c4.getTimePoint().plus(gapless), "c5");
// add a cap here that is greater than the threshold but not twice as long;
// this way, adding one in the middle of this gap is the interesting case
c6 = candidate(c5.getTimePoint().plus(MostProbableCandidatesInSmallTimeRangeFilter.CANDIDATE_FILTER_TIME_WINDOW.times(1.5)), "c6");
c7 = candidate(c6.getTimePoint().plus(gapless), "c7");
c8 = candidate(c7.getTimePoint().plus(gapless), "c8");
c9 = candidate(c8.getTimePoint().plus(gapless), "c9");
c10= candidate(c9.getTimePoint().plus(gapless), "c10");
competitorCandidates.add(c1);
competitorCandidates.add(c2);
competitorCandidates.add(c3);
competitorCandidates.add(c4);
competitorCandidates.add(c5);
competitorCandidates.add(c6);
competitorCandidates.add(c7);
competitorCandidates.add(c8);
competitorCandidates.add(c9);
competitorCandidates.add(c10);
}
@Test
public void testGetTimeWiseContiguousDistanceCandidates() {
final NavigableSet<Candidate> result = filter.getTimeWiseContiguousDistanceCandidates(competitorCandidates, c3, /* includeStartFrom */ true);
assertContains(result, c1, c2, c3, c4, c5);
}
@Test
public void testGetTimeWiseContiguousDistanceCandidatesOnRightBorder() {
final NavigableSet<Candidate> result = filter.getTimeWiseContiguousDistanceCandidates(competitorCandidates, c5, /* includeStartFrom */ true);
assertContains(result, c1, c2, c3, c4, c5);
}
@Test
public void testGetTimeWiseContiguousDistanceCandidatesOnLeftBorder() {
final NavigableSet<Candidate> result = filter.getTimeWiseContiguousDistanceCandidates(competitorCandidates, c6, /* includeStartFrom */ true);
assertContains(result, c6, c7, c8, c9, c10);
}
@Test
public void testGetTimeWiseContiguousDistanceCandidatesInMiddleOfGapNotInserted() {
final Candidate middleOfGap = candidate(c5.getTimePoint().plus(c5.getTimePoint().until(c6.getTimePoint()).divide(2)), "gap");
final NavigableSet<Candidate> result = filter.getTimeWiseContiguousDistanceCandidates(competitorCandidates, middleOfGap,
/* includeStartFrom */ true);
assertContains(result, c1, c2, c3, c4, c5, middleOfGap, c6, c7, c8, c9, c10);
}
@Test
public void testGetTimeWiseContiguousDistanceCandidatesInMiddleOfGapNotInsertedDontIncludeStartFrom() {
final Candidate middleOfGap = candidate(c5.getTimePoint().plus(c5.getTimePoint().until(c6.getTimePoint()).divide(2)), "gap");
final NavigableSet<Candidate> result = filter.getTimeWiseContiguousDistanceCandidates(competitorCandidates, middleOfGap,
/* includeStartFrom */ false);
assertContains(result, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10);
}
@Test
public void testGetTimeWiseContiguousDistanceCandidatesInMiddleOfGapInserted() {
final Candidate middleOfGap = candidate(c5.getTimePoint().plus(c5.getTimePoint().until(c6.getTimePoint()).divide(2)), "gap");
competitorCandidates.add(middleOfGap);
final NavigableSet<Candidate> result = filter.getTimeWiseContiguousDistanceCandidates(competitorCandidates, middleOfGap,
/* includeStartFrom */ true);
assertContains(result, c1, c2, c3, c4, c5, middleOfGap, c6, c7, c8, c9, c10);
}
@Test
public void testGetTimeWiseContiguousDistanceCandidatesInMiddleOfWidenedGap() {
competitorCandidates.remove(c5);
competitorCandidates.remove(c6);
final Candidate middleOfGap = candidate(c5.getTimePoint().plus(c5.getTimePoint().until(c6.getTimePoint()).divide(2)), "gap");
final NavigableSet<Candidate> result = filter.getTimeWiseContiguousDistanceCandidates(competitorCandidates, middleOfGap,
/* includeStartFrom */ true);
assertContains(result, middleOfGap);
}
private void assertContains(NavigableSet<Candidate> candidates, Candidate... candidatesExpected) {
assertEquals(candidatesExpected.length, candidates.size());
for (final Candidate candidateExpected : candidatesExpected) {
assertTrue(candidates.contains(candidateExpected));
}
}
private Candidate candidate(TimePoint timePoint, String name) {
return new CandidateImpl(1, timePoint, 1.0, null) {
private static final long serialVersionUID = 8547646939054562751L;
@Override public String toString() { return name; }
};
}
}
@@ -120,11 +120,8 @@ public class MarkPassingCalculator {
return null;
});
}
try {
executor.invokeAll(tasks);
} catch (Exception e) {
logger.log(Level.SEVERE, "Error trying to compute initial set of mark passings for race "+race.getRace().getName(), e);
}
ThreadPoolUtil.INSTANCE.invokeAllAndLogExceptions(executor, Level.SEVERE,
"Error trying to compute initial set of mark passings for race "+race.getRace().getName()+": %s", tasks);
if (listener != null) {
synchronized (MarkPassingCalculator.this) {
if (listenerThread == null) {
@@ -290,9 +287,14 @@ public class MarkPassingCalculator {
// haven't processed them yet with the CandidateFinder or CandidateChooser.
if (!finished && !suspended) {
if (smallestChangedWaypointIndex.value != -1) {
logger.finer("Handling course change; smallesChangedWaypointIndex=="+smallestChangedWaypointIndex.value+
"; waypoints added: "+addedWaypoints+"; waypoints removed: "+removedWaypoints);
// there was a course change; finder and chooser need to be updated in this regard;
// do this before the other changes such as new / replaced position fixes are considered:
final Course course = race.getRace().getCourse();
logger.finer(()->"Handling course change; smallestChangedWaypointIndex=="+smallestChangedWaypointIndex.value+
"; waypoints added: "+addedWaypoints+"; waypoints removed: "+removedWaypoints+
"; current course: "+course);
final Map<Competitor, Util.Pair<List<Candidate>, List<Candidate>>> candidateDeltas;
// obtain the course's read lock so that the finder creates the candidate deltas under the
// premise of the same course as the chooser updates its end proxy node's index; they have
@@ -311,11 +313,19 @@ public class MarkPassingCalculator {
for (Entry<Competitor, Util.Pair<List<Candidate>, List<Candidate>>> entry : candidateDeltas.entrySet()) {
tasks.add(()->{
Util.Pair<List<Candidate>, List<Candidate>> pair = entry.getValue();
chooser.calculateMarkPassDeltas(entry.getKey(), pair.getA(), pair.getB());
return null;
try {
chooser.calculateMarkPassDeltas(entry.getKey(), pair.getA(), pair.getB());
return null;
} catch (Exception e) {
logger.log(Level.SEVERE, "Exception trying to calculate mark passing deltas for competitor "+entry.getKey(), e);
throw e;
}
});
}
executor.invokeAll(tasks);
logger.finer(()->"Calculating mark passing deltas after course change in executor");
ThreadPoolUtil.INSTANCE.invokeAllAndLogExceptions(executor, Level.SEVERE,
"Error calculating mark passing deltas after course change in executor: %s", tasks);
logger.finer(()->"Done calculating mark passing deltas after course change");
}
updateManuallySetMarkPassings(fixedMarkPassings, removedFixedMarkPassings, suppressedMarkPassings, unsuppressedMarkPassings);
computeMarkPasses(competitorFixes, competitorFixesThatReplacedExistingOnes, markFixes);
@@ -412,11 +422,8 @@ public class MarkPassingCalculator {
}
});
}
try {
executor.invokeAll(tasks);
} catch (InterruptedException e) {
logger.log(Level.INFO, "mark passing calculation interrupted", e);
}
ThreadPoolUtil.INSTANCE.invokeAllAndLogExceptions(executor, Level.INFO,
"Error during mark passing calculation: %s", tasks);
}
private class ComputeMarkPassings implements Runnable {
@@ -370,9 +370,11 @@ public class CandidateFinderImpl implements CandidateFinder {
}
/**
* determines the candidates that are new and those that have been removed, keyed by the competitors; the
* {@link Pair#getA() first} element of each value pair has the candidates that are new, the {@link Pair#getB()
* second} element has those that have been removed.
* For all waypoints starting at {@code zeroBasedIndexOfWaypointChanged-1} in the current course removes all
* candidates for all competitors and starts from scratch to determine the candidates for the waypoints starting at
* {@code zeroBasedIndexOfWaypointChanged-1}. The current and the removed candidates are returned, keyed by the
* competitors; the {@link Pair#getA() first} element of each value pair has the candidates that are new, the
* {@link Pair#getB() second} element has those that have been removed.
*/
private Map<Competitor, Util.Pair<List<Candidate>, List<Candidate>>> invalidateAfterCourseChange(int zeroBasedIndexOfWaypointChanged) {
ConcurrentMap<Competitor, Util.Pair<List<Candidate>, List<Candidate>>> result = new ConcurrentHashMap<>();
@@ -393,6 +395,7 @@ public class CandidateFinderImpl implements CandidateFinder {
changedWaypoints.add(w);
}
}
logger.finer(()->"Changed waypoints: "+changedWaypoints);
final Set<Callable<Void>> tasks = new HashSet<>();
final Thread executingThread = Thread.currentThread(); // most likely the MarkPassingCalculator.Listen thread
for (Competitor c : race.getRace().getCompetitors()) {
@@ -419,10 +422,9 @@ public class CandidateFinderImpl implements CandidateFinder {
}
});
}
executor.invokeAll(tasks);
} catch (InterruptedException e) {
logger.log(Level.SEVERE, "Problem trying to update competitor candidate sets after waypoints starting at zero-based index "+
zeroBasedIndexOfWaypointChanged+" have changed", e);
ThreadPoolUtil.INSTANCE.invokeAllAndLogExceptions(executor, Level.SEVERE,
"Problem trying to update competitor candidate sets after waypoints starting at zero-based index "+
zeroBasedIndexOfWaypointChanged+" have changed: %s", tasks);
} finally {
course.unlockAfterRead();
}
@@ -431,9 +433,10 @@ public class CandidateFinderImpl implements CandidateFinder {
}
/**
* determines the candidates that are new and those that have been removed, keyed by the competitors; the
* {@link Pair#getA() first} element of each value pair has the candidates that are new, the {@link Pair#getB()
* second} element has those that have been removed.
* After a course change, described by the waypoints added ({@code addedWaypoints}) and the waypoints removed
* ({@code removedWaypoints}), determines the candidates that are new and those that have been removed, keyed by the
* competitors; the {@link Pair#getA() first} element of each value pair has the candidates that are new, the
* {@link Pair#getB() second} element has those that have been removed.
*/
@Override
public Map<Competitor, Util.Pair<List<Candidate>, List<Candidate>>> updateWaypoints(
@@ -449,6 +452,11 @@ public class CandidateFinderImpl implements CandidateFinder {
return newAndUpdatedCandidates;
}
/**
* For all {@code waypoints} removed, removes the {@link #passingInstructions} entry and clears all
* {@link #xteCandidates} and {@link #distanceCandidates} for all competitors. The candidates cleared this
* way are returned, keyed by the {@link Competitor} to which they belonged.
*/
private Map<Competitor, List<Candidate>> removeWaypoints(Iterable<Waypoint> waypoints) {
Map<Competitor, List<Candidate>> result = new HashMap<>();
for (Competitor c : race.getRace().getCompetitors()) {
@@ -726,7 +734,7 @@ public class CandidateFinderImpl implements CandidateFinder {
t, probability, startProbabilityBasedOnOtherCompetitors, w, onCorrectSideOfWaypoint, distance);
getDistanceCandidates(c, w).put(fix, newCan);
result.getA().add(newCan);
logger.finest("Added distance candidate " + newCan.toString() + "for " + c);
logger.finest("Added distance candidate " + newCan.toString() + " for " + c);
} else if (wasCan && !isCan) {
getDistanceCandidates(c, w).remove(fix);
result.getB().add(oldCan);
@@ -735,7 +743,7 @@ public class CandidateFinderImpl implements CandidateFinder {
t, probability, startProbabilityBasedOnOtherCompetitors, w, onCorrectSideOfWaypoint, distance);
getDistanceCandidates(c, w).put(fix, newCan);
result.getA().add(newCan);
logger.finest("Added distance candidate " + newCan.toString() + "for " + c);
logger.finest("Added distance candidate " + newCan.toString() + " for " + c);
result.getB().add(oldCan);
}
}
@@ -894,14 +902,14 @@ public class CandidateFinderImpl implements CandidateFinder {
waypointCandidates.remove(canFixes);
if (newCan.getProbability() > penaltyForSkipping) {
result.getA().add(newCan);
logger.finest("Added XTE " + newCan.toString() + "for " + c);
logger.finest("Added XTE " + newCan.toString() + " for " + c);
waypointCandidates.put(canFixes, newCan);
}
}
} else {
if (newCan.getProbability() > penaltyForSkipping) {
result.getA().add(newCan);
logger.finest("Added XTE " + newCan.toString() + "for " + c);
logger.finest("Added XTE " + newCan.toString() + " for " + c);
waypointCandidates.put(canFixes, newCan);
}
}
@@ -41,7 +41,7 @@ public class MostProbableCandidatesInSmallTimeRangeFilter {
* path's ends such that each path step spans a duration less than or equal to the duration specified by this field.
* In other words, each cluster is the transitive hull of fixes not further than this duration apart.
*/
private static final Duration CANDIDATE_FILTER_TIME_WINDOW = Duration.ONE_SECOND.times(5);
public static final Duration CANDIDATE_FILTER_TIME_WINDOW = Duration.ONE_SECOND.times(5);
/**
* When looking for the most probable candidates within time windows of length {@link #CANDIDATE_FILTER_TIME_WINDOW},
@@ -55,7 +55,7 @@ public class MostProbableCandidatesInSmallTimeRangeFilter {
private final Candidate startProxyCandidate;
private final Candidate endProxyCandidate;
MostProbableCandidatesInSmallTimeRangeFilter(Comparator<Candidate> candidateComparator, Candidate startProxyCandidate,
public MostProbableCandidatesInSmallTimeRangeFilter(Comparator<Candidate> candidateComparator, Candidate startProxyCandidate,
Candidate endProxyCandidate) {
this.candidateComparator = candidateComparator;
filteredCandidates = new TreeSet<>(candidateComparator);
@@ -66,18 +66,18 @@ public class MostProbableCandidatesInSmallTimeRangeFilter {
/**
* Finds the contiguous (by definition of {@link #CANDIDATE_FILTER_TIME_WINDOW}) sequences of candidates adjacent to
* those candidates added / removed. The {@code competitorCandidates} set is expected to already contain the
* {@code newCandidates} and to no longer contain the {@code removedCandidates}. {@link #filteredCandidatesStage1} is
* {@code newCandidates} and to no longer contain the {@code removedCandidates}. {@link #filteredCandidates} is
* expected to not yet reflect the changes described by {@code newCandidates} and {@code removedCandidates}.
* <p>
*
* The following steps are performed:
* <ul>
* <li>All candidates from {@code removedCandidates} are removed from {@link #filteredCandidatesStage1}.</li>
* <li>All candidates from {@code removedCandidates} are removed from {@link #filteredCandidates}.</li>
* <li>The disjoint sequences containing all new candidates and adjacent to all removed candidates are computed.</li>
* <li>The most probable candidate(s) from each contiguous sequence is/are determined. Those pass the filter and are
* added to {@link #filteredCandidatesStage1}</li>
* added to {@link #filteredCandidates}</li>
* <li>All other candidates from those sequences do not pass the filter. All of those that are still in
* {@link #filteredCandidatesStage1} are removed from {@link #filteredCandidatesStage1}.</li>
* {@link #filteredCandidates} are removed from {@link #filteredCandidates}.</li>
* </ul>
* @return a pair whose first element holds the set of candidates that now pass the filter and didn't before, and
* whose second element holds the set of candidates that did pass the filter before but don't anymore
@@ -86,13 +86,13 @@ public class MostProbableCandidatesInSmallTimeRangeFilter {
Iterable<Candidate> newCandidates, Iterable<Candidate> removedCandidates) {
assert Util.containsAll(competitorCandidates, newCandidates); // all new candidates must be in competitorCandidates
assert !competitorCandidates.stream().filter(c->Util.contains(removedCandidates, c)).findAny().isPresent(); // no removedCandidate must be in competitorCandidates
final Set<Candidate> candidatesAddedToFiltered = new HashSet<>();
final Set<Candidate> candidatesRemovedFromFiltered = new HashSet<>();
final NavigableSet<Candidate> candidatesAddedToFiltered = new TreeSet<>(candidateComparator);
final NavigableSet<Candidate> candidatesRemovedFromFiltered = new TreeSet<>(candidateComparator);
// create a copy of newCandidates so we can remove objects from the copy as we add candidates to sequences;
// this way we'll obtain disjoint sequences and don't need to consider candidates again which have already
// been added to a sequence:
final Set<Candidate> newCandidatesModifiableCopy = new HashSet<>();
final Set<Candidate> removedCandidatesModifiableCopy = new HashSet<>();
final NavigableSet<Candidate> newCandidatesModifiableCopy = new TreeSet<>(candidateComparator);
final NavigableSet<Candidate> removedCandidatesModifiableCopy = new TreeSet<>(candidateComparator);
Util.addAll(newCandidates, newCandidatesModifiableCopy);
Util.addAll(removedCandidates, removedCandidatesModifiableCopy);
// If the start/end candidates are part of newCandidates and/or removedCandidates, pass them on as is;
@@ -107,11 +107,16 @@ public class MostProbableCandidatesInSmallTimeRangeFilter {
// remove the candidate grouped in the sequence from the remaining new candidates to consider
// because they have been "scooped up" by this sequence already and need no further consideration:
Util.removeAll(contiguousSequenceForNextNewCandidate, newCandidatesModifiableCopy);
// Candidates that were removed but fall into the time range of the contiguous sequence just constructed
// can be considered processed: the probabilities for the sequence will be recalculated anyway, and
// the removal cannot have caused a split.
// The candidates removed are added to candidatesRemovedFromFiltered a few lines below.
removeAllRemovedCandidatesInOrNearSequence(contiguousSequenceForNextNewCandidate, removedCandidatesModifiableCopy);
}
// Those candidates that were not yet removed from removedCandidatesModifiableCopy were not within or near
// any of the sequences produced by new candidates. Checking their CANDIDATE_FILTER_TIME_WINDOW-neighborhood
// and constructing the sequences affected by their removal.
// and constructing the sequences affected by their removal. Those sequences' max probabilities need to be
// re-assessed, and the removal may cause a split.
Util.addAll(removedCandidates, candidatesRemovedFromFiltered);
candidatesRemovedFromFiltered.retainAll(filteredCandidates); // those explicitly removed and previously accepted by the filter go away
while (!removedCandidatesModifiableCopy.isEmpty()) {
@@ -157,7 +162,7 @@ public class MostProbableCandidatesInSmallTimeRangeFilter {
ArrayList<Candidate> sortedByProbabilityFromLowToHigh = new ArrayList<>(contiguousCandidateSequence);
Collections.sort(sortedByProbabilityFromLowToHigh, (c1, c2)->Double.compare(c1.getProbability(), c2.getProbability()));
double maxProbability = sortedByProbabilityFromLowToHigh.get(sortedByProbabilityFromLowToHigh.size()-1).getProbability();
Set<Candidate> candidatesAcceptedFromSequence = new HashSet<>();
Set<Candidate> candidatesAcceptedFromSequence = new TreeSet<>(candidateComparator);
Candidate currentCandidate;
for (int i=sortedByProbabilityFromLowToHigh.size()-1; i>=0 &&
(currentCandidate=sortedByProbabilityFromLowToHigh.get(i)).getProbability()+MAX_PROBABILITY_DELTA >= maxProbability; i--) {
@@ -222,34 +227,32 @@ public class MostProbableCandidatesInSmallTimeRangeFilter {
* @param includeStartFrom
* whether or not to include {@code startFrom} in the resulting set
*/
private NavigableSet<Candidate> getTimeWiseContiguousDistanceCandidates(NavigableSet<Candidate> competitorCandidates, Candidate startFrom, boolean includeStartFrom) {
public NavigableSet<Candidate> getTimeWiseContiguousDistanceCandidates(NavigableSet<Candidate> competitorCandidates, Candidate startFrom, boolean includeStartFrom) {
final NavigableSet<Candidate> result = new TreeSet<>(candidateComparator);
if (includeStartFrom) {
result.add(startFrom);
}
addContiguousCandidates(competitorCandidates.descendingSet().tailSet(startFrom), result);
addContiguousCandidates(competitorCandidates.tailSet(startFrom), result);
addContiguousCandidates(competitorCandidates.descendingSet().tailSet(startFrom, /* inclusive */ false), startFrom, result);
addContiguousCandidates(competitorCandidates.tailSet(startFrom, /* inclusive */ false), startFrom, result);
return result;
}
/**
* The first element in the iteration order of {@code candidates} will not be added to {@code addTo}
* but serves as the first element to compute distance to. The {@link #start} and {@link #end} proxy
* candidates are ignored by this method and will never be added to {@code addTo}.
* The first element in the iteration order of {@code candidates} will also be added to {@code addTo}. The
* {@code startFrom} candidate serves as the first element to compute distance to. The {@link #start} and {@link #end}
* proxy candidates are ignored by this method and will never be added to {@code addTo}.
*/
private void addContiguousCandidates(Iterable<Candidate> candidates, Collection<Candidate> addTo) {
private void addContiguousCandidates(Iterable<Candidate> candidates, Candidate startFrom, Collection<Candidate> addTo) {
Candidate current = startFrom;
final Iterator<Candidate> iter = candidates.iterator();
if (iter.hasNext()) {
Candidate current = iter.next();
while (iter.hasNext()) {
final Candidate next = iter.next();
if (next != startProxyCandidate && next != endProxyCandidate) {
if (next.getTimePoint().until(current.getTimePoint()).abs().compareTo(CANDIDATE_FILTER_TIME_WINDOW) <= 0) {
addTo.add(next);
current = next;
} else {
break; // too large a gap; end of contiguous sequence in this direction found
}
while (iter.hasNext()) {
final Candidate next = iter.next();
if (next != startProxyCandidate && next != endProxyCandidate) {
if (next.getTimePoint().until(current.getTimePoint()).abs().compareTo(CANDIDATE_FILTER_TIME_WINDOW) <= 0) {
addTo.add(next);
current = next;
} else {
break; // too large a gap; end of contiguous sequence in this direction found
}
}
}
@@ -1,8 +1,8 @@
package com.sap.sailing.domain.markpassingcalculation.impl;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.NavigableSet;
import java.util.Set;
@@ -213,8 +213,8 @@ public class StationarySequence {
private SortedSet<Candidate> getCandidatesInTimeRange(final TimeRange timeRange) {
final SortedSet<Candidate> candidatesNoLongerPassingFilter = candidates.subSet(
createDummyCandidate(timeRange.from()),
createDummyCandidate(timeRange.to()));
getCandidateMatchingTimePointOrCreateDummy(timeRange.from()),
getCandidateMatchingTimePointOrCreateDummy(timeRange.to()));
return candidatesNoLongerPassingFilter;
}
@@ -227,8 +227,12 @@ public class StationarySequence {
candidate.getTimePoint().until(getLast().getTimePoint()).abs().compareTo(CANDIDATE_FILTER_TIME_WINDOW) < 0;
}
Iterable<Candidate> getAllCandidates() {
return candidates;
int size() {
return candidates.size();
}
boolean isEmpty() {
return candidates.isEmpty();
}
/**
@@ -258,7 +262,9 @@ public class StationarySequence {
* @param stationarySequenceSetToUpdate
* when this method causes a change in what {@link #getFirst()} returns before and after the call, this
* method maintains the set referenced by this parameter accordingly, assuming that the position in the
* set may change, or, if this sequence runs empty, it has to be removed from the set altogether.
* set may change, or, if this sequence runs empty, it has to be removed from the set altogether. Furthermore,
* should removing the {@code candidate} reduce this sequence's size to {@code 1}, this sequence is
* removed from the {@code stationarySequenceSetToUpdate}.
*/
void remove(Candidate candidate, Set<Candidate> candidatesEffectivelyAdded, Set<Candidate> candidatesEffectivelyRemoved,
NavigableSet<StationarySequence> stationarySequenceSetToUpdate) {
@@ -266,11 +272,11 @@ public class StationarySequence {
final boolean wasValidCandidate = isCloseEnoughToSequenceBorder(candidate);
final boolean wasFirst = candidate == getFirst();
final boolean wasLast = candidate == getLast();
if (wasFirst) {
if (wasFirst || size() == 2) { // if size() == 2 then it will shrink to 1 and this sequence shall be removed
stationarySequenceSetToUpdate.remove(this);
}
candidates.remove(candidate);
if (wasFirst && !candidates.isEmpty()) {
if (wasFirst && candidates.size() > 1) {
stationarySequenceSetToUpdate.add(this);
}
if (wasValidCandidate) {
@@ -302,9 +308,13 @@ public class StationarySequence {
}
private void refreshBoundingBox() {
boundingBoxOfTrackSpanningCandidates = createNewBounds(getFirst());
boundingBoxOfTrackSpanningCandidates = computeExtendedBoundsForFixesBetweenCandidates(getFirst(), getLast());
assert boundingBoxOfTrackSpanningCandidates != null;
if (isEmpty()) {
boundingBoxOfTrackSpanningCandidates = null;
} else {
boundingBoxOfTrackSpanningCandidates = createNewBounds(getFirst());
boundingBoxOfTrackSpanningCandidates = computeExtendedBoundsForFixesBetweenCandidates(getFirst(), getLast());
assert boundingBoxOfTrackSpanningCandidates != null;
}
}
/**
@@ -366,45 +376,55 @@ public class StationarySequence {
tailSequence = null;
} else {
// split:
final Set<Candidate> oldValidCandidates = new HashSet<>();
final Set<Candidate> oldValidCandidates = new TreeSet<>(candidateComparator);
Util.addAll(getValidCandidates(), oldValidCandidates);
final Candidate dummyCandidateForFix = createDummyCandidate(newFix.getTimePoint());
SortedSet<Candidate> tailSet = candidates.tailSet(dummyCandidateForFix);
final Candidate candidateForFixTimePoint = getCandidateMatchingTimePointOrCreateDummy(newFix.getTimePoint());
SortedSet<Candidate> tailSet = candidates.tailSet(candidateForFixTimePoint); // will contain a candidate for newFix.getTimePoint() if such a candidate exists
boolean tryToAddCandidateAtFixLater = false;
if (!tailSet.isEmpty() && tailSet.first().getTimePoint().equals(dummyCandidateForFix.getTimePoint())) {
// new fix is exactly on a candidate in this stationary sequence; construct the tailing stationary sequence
if (!tailSet.isEmpty() && tailSet.first().getTimePoint().equals(candidateForFixTimePoint.getTimePoint())) {
// new fix TimePoint matches exactly with that of a candidate in this stationary sequence;
// construct the tailing stationary sequence
// without this candidate to start with, then try whether it can be extended to the left:
tryToAddCandidateAtFixLater = true;
tailSet = candidates.tailSet(dummyCandidateForFix, /* inclusive */ false);
tailSet = candidates.tailSet(candidateForFixTimePoint, /* inclusive */ false);
assert !tailSet.contains(candidateForFixTimePoint);
}
tailSequence = tailSet.isEmpty() ? null : createStationarySequence(tailSet);
if (tailSequence != null && tryToAddCandidateAtFixLater) {
tailSequence.tryToExtendBeforeFirst(candidates.floor(dummyCandidateForFix), new HashSet<>(), new HashSet<>(), stationarySequenceSetToUpdate);
tailSequence.tryToExtendBeforeFirst(candidates.floor(candidateForFixTimePoint),
new TreeSet<>(candidateComparator), new TreeSet<>(candidateComparator), stationarySequenceSetToUpdate);
}
// now remove the tail set candidates from this stationary sequence:
final ArrayList<Candidate> fullTailSet = new ArrayList<>(candidates.tailSet(dummyCandidateForFix));
final ArrayList<Candidate> fullTailSet = new ArrayList<>(candidates.tailSet(candidateForFixTimePoint));
if (!fullTailSet.isEmpty() && fullTailSet.get(0) == getFirst()) {
// all candidates will be removed from this sequence; the sequence must be removed from its containing set before removing the first candidate...
stationarySequenceSetToUpdate.remove(this);
}
candidates.removeAll(fullTailSet);
if (size() == 1) {
stationarySequenceSetToUpdate.remove(this);
}
// ...and it doesn't need adding because if it was removed, it's empty now.
refreshBoundingBox();
final Set<Candidate> newValidCandidates = new HashSet<>();
final Set<Candidate> newValidCandidates = new TreeSet<>(candidateComparator);
Util.addAll(getValidCandidates(), newValidCandidates);
if (tailSequence != null) { // this includes the possibility of a single candidate being added to the tail set
Util.addAll(tailSequence.getValidCandidates(), newValidCandidates);
}
final Set<Candidate> candidatesAdded = new HashSet<>(newValidCandidates);
final Set<Candidate> candidatesAdded = new TreeSet<>(candidateComparator);
candidatesAdded.addAll(newValidCandidates);
candidatesAdded.removeAll(newValidCandidates);
final Set<Candidate> candidatesRemoved = new HashSet<>(oldValidCandidates);
final Set<Candidate> candidatesRemoved = new TreeSet<>(candidateComparator);
candidatesRemoved.addAll(oldValidCandidates);
candidatesRemoved.removeAll(newValidCandidates);
candidatesEffectivelyAdded.addAll(candidatesAdded);
candidatesEffectivelyRemoved.removeAll(candidatesAdded);
candidatesEffectivelyRemoved.addAll(candidatesRemoved);
candidatesEffectivelyAdded.removeAll(candidatesRemoved);
}
return tailSequence != null && Util.size(tailSequence.getAllCandidates()) >= 2 ? tailSequence : null;
final StationarySequence result = tailSequence != null && tailSequence.size() >= 2 ? tailSequence : null;
assert result == null || !this.getLast().getTimePoint().equals(result.getFirst().getTimePoint());
return result;
}
/**
@@ -416,8 +436,8 @@ public class StationarySequence {
* @return a sequence with the candidate(s)
*/
private StationarySequence createStationarySequence(SortedSet<Candidate> candidates) {
final Set<Candidate> candidatesEffectivelyAdded = new HashSet<>();
final Set<Candidate> candidatesEffectivelyRemoved = new HashSet<>();
final Set<Candidate> candidatesEffectivelyAdded = new TreeSet<>(candidateComparator);
final Set<Candidate> candidatesEffectivelyRemoved = new TreeSet<>(candidateComparator);
final Iterator<Candidate> candidateIterator = candidates.iterator();
assert candidateIterator.hasNext();
final StationarySequence result = new StationarySequence(candidateIterator.next(), candidateComparator, track);
@@ -428,6 +448,33 @@ public class StationarySequence {
return result;
}
/**
* If this sequence contains a {@link Candidate} that matches the {@code timePoint} exactly, that candidate is
* returned. Otherwise, a dummy candidate is created that has the {@code timePoint} provided.<p>
*
* Note that depending on the {@link #candidateComparator} used, two candidates are not necessarily considered
* equal only because they have equal time points.
*/
private Candidate getCandidateMatchingTimePointOrCreateDummy(TimePoint timePoint) {
final Candidate dummy = new CandidateImpl(/* one-based index of waypoint */ 1, timePoint, /* probability */ 0, /* waypoint */ null);
final Candidate floorCandidate = candidates.floor(dummy);
final Candidate ceilingCandidate = candidates.ceiling(dummy);
final Candidate result;
if (floorCandidate.getTimePoint().equals(timePoint)) {
result = floorCandidate;
} else if (ceilingCandidate.getTimePoint().equals(timePoint)) {
result = ceilingCandidate;
} else {
result = dummy;
}
return result;
}
@Override
public String toString() {
return "Stationary Sequence "+candidates+", bounds diameter "+boundingBoxOfTrackSpanningCandidates.getDiameter();
}
/**
* Creates a dummy time point that can be used for searching in {@link #candidates} by time point. It
* uses {@code 1} for the one-based waypoint index, {@code null} for the waypoint and {@code 0.0} for its
@@ -436,5 +483,17 @@ public class StationarySequence {
static Candidate createDummyCandidate(TimePoint timePoint) {
return new CandidateImpl(/* one-based index of waypoint */ 1, timePoint, /* probability */ 0, /* waypoint */ null);
}
Iterable<Candidate> getAllCandidates() {
return Collections.unmodifiableCollection(candidates);
}
/**
* Tells whether the {@link #candidates} set contains {@code candidate}. Containment is decided based on
* the {@link #candidateComparator}'s notion of equality.
*/
public boolean contains(Candidate candidate) {
return candidates.contains(candidate);
}
}
@@ -5,6 +5,7 @@ import java.util.HashSet;
import java.util.NavigableSet;
import java.util.Set;
import java.util.TreeSet;
import java.util.logging.Logger;
import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.common.Bounds;
@@ -35,10 +36,13 @@ import com.sap.sse.common.Util.Pair;
*
* The filter is created empty. It keeps track of all stationary sequences found so far and sorts them by time. The
* algorithm guarantees that after a call to {@link #updateCandidates(Iterable, Iterable)} as well as after a call to
* {@link #addFix} each stationary sequence has at least two candidates in them which passed the first filter state, and
* no stationary sequence can be extended to the next candidate following it or any previous candidate preceding it
* because the track leading there would extend the stationary sequence's bounding box beyond limits, and no two
* adjacent candidates that are not part of the same {@link StationarySequence} can be joined into a valid such sequence.
* {@link #addFix} each stationary sequence has at least two candidates in them which passed the first filter state. No
* stationary sequence can be extended to the next candidate following it or any previous candidate preceding it because
* the track leading there would extend the stationary sequence's bounding box beyond limits, or the candidate outside
* the sequence already belongs to an adjacent sequence. No two adjacent candidates that are not part of any
* {@link StationarySequence} can be joined into a valid sequence.
* <p>
*
* Furthermore, all {@link StationarySequence}s maintain their invariant, in particular that the track leading from
* their {@link StationarySequence#getFirst() first} to their {@link StationarySequence#getLast() last} candidate
* remains within a bounding box whose {@link Bounds#getDiameter() diameter} remains below the threshold. Calling
@@ -47,6 +51,11 @@ import com.sap.sse.common.Util.Pair;
* {@link #updateCandidates(Iterable, Iterable)} as having been removed and that currently pass this filter.
* <p>
*
* Two adjacent {@link StationarySequence}s must not overlap. The {@link StationarySequence#getLast() last candidate} of
* a sequence must be truly less---according to the {@link #candidateComparator}---than the
* {@link StationarySequence#getFirst() first candidate} of the next sequence.
* <p>
*
* The sequences are non-overlapping but may touch each other, meaning that a preceding sequence's
* {@link StationarySequence#getLast() last} candidate is the same as the following sequence's
* {@link StationarySequence#getFirst() first} candidate.
@@ -73,6 +82,8 @@ import com.sap.sse.common.Util.Pair;
*
*/
public class StationarySequenceBasedFilter {
private static final Logger logger = Logger.getLogger(StationarySequenceBasedFilter.class.getName());
/**
* Set of sequences managed here; ordered by their {@link StationarySequence#getFirst() first} candidate,
* based on the candidate comparator passed to the constructor. This implies that {@link StationarySequence} objects
@@ -195,6 +206,7 @@ public class StationarySequenceBasedFilter {
removeCandidate(removedCandidate, candidatesEffectivelyAdded, candidatesEffectivelyRemoved);
}
updateFilteredCandidates(candidatesEffectivelyAdded, candidatesEffectivelyRemoved);
assert isCandidatesConsistent();
return new Pair<>(candidatesEffectivelyAdded, candidatesEffectivelyRemoved);
}
@@ -217,7 +229,7 @@ public class StationarySequenceBasedFilter {
* be extended up to the new candidate, try to construct a new {@link StationarySequence} from the new candidate to
* the neighbor (new candidate and neighbor will still pass the filter if such a sequence can be validly constructed
* as they are the only two candidates in the new sequence for now). (Note: It is not possible that a single
* {@link StationarySequence} can be constructed spanning between the two neighbors because it it were possible then
* {@link StationarySequence} can be constructed spanning between the two neighbors because if it were possible then
* this would have had to have happened before as it does not depend on the appearance of the new candidate.)</li>
* <li>The candidate falls into an existing stationary sequence (at or after first and at or before last candidate
* in sequence). In this case the set of fixes on the track considered within the sequence hasn't changed. The
@@ -227,7 +239,7 @@ public class StationarySequenceBasedFilter {
*/
private void addCandidate(Candidate newCandidate,
Set<Candidate> candidatesEffectivelyAdded, Set<Candidate> candidatesEffectivelyRemoved) {
assert !candidates.contains(newCandidate);
assert !candidates.contains(newCandidate) && isCandidatesConsistent();
if (newCandidate == startProxyCandidate) {
candidatesEffectivelyAdded.add(newCandidate);
candidatesEffectivelyRemoved.remove(newCandidate);
@@ -236,18 +248,68 @@ public class StationarySequenceBasedFilter {
candidatesEffectivelyRemoved.remove(newCandidate);
} else {
candidates.add(newCandidate);
lookLeft(newCandidate, candidatesEffectivelyAdded, candidatesEffectivelyRemoved);
lookRight(newCandidate, candidatesEffectivelyAdded, candidatesEffectivelyRemoved);
// First try adding the new candidate to a sequence starting at or before newCandidate.
// This could either succeed by adding it to an existing sequence, or by creating a new
// sequence with newCandidate and its immediate predecessor candidate that so far was
// not part of a sequence. If this was successful, don't even try to extend any later sequence
// towards newCandidate. But it newCandidate could not be added to a sequence looking into the past ("left"),
// try our luck by looking into the future ("right").
if (!lookLeft(newCandidate, candidatesEffectivelyAdded, candidatesEffectivelyRemoved)) {
lookRight(newCandidate, candidatesEffectivelyAdded, candidatesEffectivelyRemoved);
}
}
assert isCandidatesConsistent();
}
private void lookLeft(Candidate newCandidate, Set<Candidate> candidatesEffectivelyAdded,
/**
* Checks whether the union of all candidates of all stationary sequences from {@link #stationarySequences} is
* contained in {@link #candidates}.
*/
private boolean isCandidatesConsistent() {
final TreeSet<Candidate> union = new TreeSet<>(candidateComparator);
Candidate lastCandidateInPreviousSequence = null;
boolean overlappingTimePointFound = false;
for (final StationarySequence stationarySequence : stationarySequences) {
if (lastCandidateInPreviousSequence != null &&
candidateComparator.compare(lastCandidateInPreviousSequence, stationarySequence.getFirst()) >= 0) {
overlappingTimePointFound = true;
logger.severe("Last candidate "+lastCandidateInPreviousSequence+
" in sequence overlaps with first element in next sequence "+stationarySequence);
}
lastCandidateInPreviousSequence = stationarySequence.getLast();
Util.addAll(stationarySequence.getAllCandidates(), union);
}
final boolean allSequenceCandidatesInCandidates = candidates.containsAll(union);
if (!allSequenceCandidatesInCandidates) {
union.removeAll(candidates);
logger.severe("Candidates "+union+" from sequences missing from full candidates collection");
}
return allSequenceCandidatesInCandidates && !overlappingTimePointFound;
}
/**
* Searches the latest {@link StationarySequence} in {@link #stationarySequences} that starts at or before
* {@code newCandidate}. If the {@code newCandidate} is within that sequence's time range, it's simply
* {@link StationarySequence#addWithin(Candidate, Set, Set) added}. Otherwise, it has to be later than the current
* end of that sequence, so {@link StationarySequence#tryToExtendAfterLast(Candidate, Set, Set) extending} the
* sequence is tried.
* <p>
*
* If extending the sequence before the {@code newCandidate} does not work because it would exceed the sequence's
* bounding box diameter threshold, an attempt is made to join the {@code newCandidate} with its predecessor
* candidate from {@link #candidates} into a new {@link StationarySequence}.
*
* @return {@code true} in case the {@code newCandidate} was added to a {@link StationarySequence}, either a new one
* because the preceding sequence could not be extended, or as an extension of the preceding sequence.
*/
private boolean lookLeft(Candidate newCandidate, Set<Candidate> candidatesEffectivelyAdded,
Set<Candidate> candidatesEffectivelyRemoved) {
final boolean addedToSequence;
final StationarySequence latestStationarySequenceStartingAtOrBeforeNewCandidate =
stationarySequences.floor(createStationarySequence(newCandidate));
final boolean createNewSequenceFromLowerToNew;
if (latestStationarySequenceStartingAtOrBeforeNewCandidate != null) {
if (latestStationarySequenceStartingAtOrBeforeNewCandidate.getLast().getTimePoint().before(newCandidate.getTimePoint())) {
if (candidateComparator.compare(latestStationarySequenceStartingAtOrBeforeNewCandidate.getLast(), newCandidate) < 0) {
// earlier sequence ends before newCandidate; try to extend, and if extending doesn't work,
// request a new sequence to be constructed:
createNewSequenceFromLowerToNew = !latestStationarySequenceStartingAtOrBeforeNewCandidate.tryToExtendAfterLast(
@@ -266,15 +328,27 @@ public class StationarySequenceBasedFilter {
candidatesEffectivelyRemoved.remove(newCandidate);
// neighbour's stationary sequence can't be extended to newCandidate or there was no earlier stationary sequence,
// look for an earlier candidate and try to create a new stationary sequence; no candidate from the new
// sequence will be eliminated by this filter for now as they form the sequence's boundaries.
// sequence will be eliminated by this filter for now as they form the sequence's boundaries. Construct such
// a sequence only if the "lower" candidate is not already part of the previous sequence, because sequences
// must not share candidates.
final Candidate lower = candidates.lower(newCandidate);
if (lower != null) {
if (lower != null && (latestStationarySequenceStartingAtOrBeforeNewCandidate == null ||
candidateComparator.compare(latestStationarySequenceStartingAtOrBeforeNewCandidate.getLast(), lower) < 0)) {
final StationarySequence newSequence = tryToConstructStationarySequence(lower, newCandidate);
if (newSequence != null) {
addedToSequence = true;
stationarySequences.add(newSequence);
} else {
addedToSequence = false;
}
} else {
addedToSequence = false;
}
} else {
addedToSequence = true;
}
assert containsNoEmptyOrSingleCandidateStationarySequence() && isCandidatesConsistent();
return addedToSequence;
}
/**
@@ -294,13 +368,17 @@ public class StationarySequenceBasedFilter {
candidatesEffectivelyAdded.add(newCandidate);
candidatesEffectivelyRemoved.remove(newCandidate);
final Candidate higher = candidates.higher(newCandidate);
if (higher != null) {
// Construct such a sequence only if the "higher" candidate is not already part of the next sequence, because sequences
// must not share candidates.
if (higher != null && (earliestStationarySequenceStartingAfterNewCandidate == null ||
candidateComparator.compare(higher, earliestStationarySequenceStartingAfterNewCandidate.getFirst()) < 0)) {
final StationarySequence newSequence = tryToConstructStationarySequence(newCandidate, higher);
if (newSequence != null) {
stationarySequences.add(newSequence);
}
}
}
assert containsNoEmptyOrSingleCandidateStationarySequence() && isCandidatesConsistent();
}
private StationarySequence tryToConstructStationarySequence(Candidate start, Candidate end) {
@@ -343,20 +421,35 @@ public class StationarySequenceBasedFilter {
candidatesEffectivelyRemoved.add(removedCandidate);
candidatesEffectivelyAdded.remove(removedCandidate);
} else {
assert candidates.contains(removedCandidate);
assert candidates.contains(removedCandidate) && isCandidatesConsistent();
candidates.remove(removedCandidate);
final StationarySequence searchDummySequence = createStationarySequence(removedCandidate);
final StationarySequence latestStationarySequenceStartingAtOrBeforeRemovedCandidate =
stationarySequences.floor(createStationarySequence(removedCandidate));
stationarySequences.floor(searchDummySequence);
final boolean addToEffectivelyRemoved;
if (latestStationarySequenceStartingAtOrBeforeRemovedCandidate != null) {
if (!latestStationarySequenceStartingAtOrBeforeRemovedCandidate.getLast().getTimePoint()
.before(removedCandidate.getTimePoint())) {
// If removedCandidate is at the beginning of the sequence (having a time point equal to that of the sequence's
// first element) then it may also be at the end of the previous sequence (having a time point equal to the previous
// sequence's last element).
// Being at the beginning of latestStationarySequenceStartingAtOrBeforeRemovedCandidate will make is be considered
// effectively removed. This is consistent in case it is also at the end of the previous sequence where it
// will then also be removed and be considered effectively removed.
final StationarySequence previousSequence;
if (!removedCandidate.getTimePoint().after(latestStationarySequenceStartingAtOrBeforeRemovedCandidate.getFirst().getTimePoint())
&& (previousSequence=stationarySequences.lower(searchDummySequence)) != null
&& previousSequence.contains(removedCandidate)) {
assert removedCandidate.getTimePoint().equals(previousSequence.getLast().getTimePoint());
previousSequence.remove(removedCandidate, candidatesEffectivelyAdded, candidatesEffectivelyRemoved,
/* StationarySequence set to update */ stationarySequences);
}
if (candidateComparator.compare(latestStationarySequenceStartingAtOrBeforeRemovedCandidate.getLast(), removedCandidate) >= 0) {
// within the sequence; remove:
latestStationarySequenceStartingAtOrBeforeRemovedCandidate.remove(removedCandidate,
candidatesEffectivelyAdded, candidatesEffectivelyRemoved,
/* StationarySequence set to update */ stationarySequences);
if (Util.size(latestStationarySequenceStartingAtOrBeforeRemovedCandidate.getAllCandidates()) <= 1) {
stationarySequences.remove(latestStationarySequenceStartingAtOrBeforeRemovedCandidate);
}
assert !stationarySequences.contains(latestStationarySequenceStartingAtOrBeforeRemovedCandidate) ||
latestStationarySequenceStartingAtOrBeforeRemovedCandidate.size() > 1;
assert isCandidatesConsistent();
addToEffectivelyRemoved = false; // already taken care of by the remove call above
} else {
// candidate not in any sequence
@@ -371,8 +464,18 @@ public class StationarySequenceBasedFilter {
candidatesEffectivelyAdded.remove(removedCandidate);
}
}
assert containsNoEmptyOrSingleCandidateStationarySequence() && isCandidatesConsistent();
}
private boolean containsNoEmptyOrSingleCandidateStationarySequence() {
for (final StationarySequence ss : stationarySequences) {
if (ss.size() < 2) {
return false;
}
}
return true;
}
/**
* Informs this filter that for its competitor a new position fix was added or replaced. All necessary updates to
* the stationary sequences managed by this filter are carried out, and the changes that this has to the result of
@@ -412,6 +515,7 @@ public class StationarySequenceBasedFilter {
*/
Pair<Iterable<Candidate>, Iterable<Candidate>> updateFixes(Iterable<GPSFixMoving> newFixes,
Iterable<GPSFixMoving> fixesReplacingExistingOnes) {
assert isCandidatesConsistent();
final Set<Candidate> candidatesEffectivelyAdded = new HashSet<>();
final Set<Candidate> candidatesEffectivelyRemoved = new HashSet<>();
if (newFixes != null) {
@@ -421,10 +525,9 @@ public class StationarySequenceBasedFilter {
if (lastSequenceStartingAtOrBeforeFix != null && !lastSequenceStartingAtOrBeforeFix.getLast().getTimePoint().before(newFix.getTimePoint())) {
// fix falls into the existing StationarySequence; update its bounding box:
final StationarySequence splitResult = lastSequenceStartingAtOrBeforeFix.tryToAddFix(newFix, candidatesEffectivelyAdded, candidatesEffectivelyRemoved, /* StationarySequence set to update */ stationarySequences);
if (Util.size(lastSequenceStartingAtOrBeforeFix.getAllCandidates()) <= 1) {
stationarySequences.remove(lastSequenceStartingAtOrBeforeFix);
}
assert !stationarySequences.contains(lastSequenceStartingAtOrBeforeFix) || lastSequenceStartingAtOrBeforeFix.size() > 1;
if (splitResult != null) {
assert splitResult.size() > 1;
stationarySequences.add(splitResult);
}
}
@@ -470,6 +573,7 @@ public class StationarySequenceBasedFilter {
}
}
updateFilteredCandidates(candidatesEffectivelyAdded, candidatesEffectivelyRemoved);
assert containsNoEmptyOrSingleCandidateStationarySequence() && isCandidatesConsistent();
return new Pair<>(candidatesEffectivelyAdded, candidatesEffectivelyRemoved);
}
@@ -161,7 +161,6 @@ public class SettingsTest extends AbstractSeleniumTest {
leaderboardSettingsPanelPO.selectDetailsAndDeselectOther(detailsToSelect);
leaderboardSettingsPanelPO.setRefreshInterval(1);
leaderboardSettingsDialog.pressOk(false, false);
mapSettings = raceboard.openMapSettings();
// verify default mode settings override custom user settings
Assert.assertTrue(mapSettings.isWindUp());
@@ -171,12 +170,9 @@ public class SettingsTest extends AbstractSeleniumTest {
Assert.assertFalse(mapSettings.isTransparentHoverlines());
mapSettings.setWindUp(false);
mapSettings.setTransparentHoverlines(false);
mapSettings.pressOk(false, false);
raceboard = RaceBoardPage.goToRaceboardUrl(getWebDriver(), getContextRoot(), BMW_CUP_REGATTA, BMW_CUP_REGATTA,
String.format(BMW_RACE, 1), "WINNING_LANES");
leaderboardSettingsDialog = raceboard.openLeaderboardSettingsDialog();
leaderboardSettingsPanelPO = leaderboardSettingsDialog.getLeaderboardSettingsPanelPO();
selectedDetails = leaderboardSettingsPanelPO.getSelectedDetails();
@@ -185,39 +181,31 @@ public class SettingsTest extends AbstractSeleniumTest {
Assert.assertArrayEquals(detailsToSelect, selectedDetails);
// verify that document settings are able to override custom user settings by a system default value
Assert.assertEquals(1, leaderboardSettingsPanelPO.getRefreshInterval());
leaderboardSettingsDialog.pressCancel();
mapSettings = raceboard.openMapSettings();
// Verify that mode settings are overridden by document settings
Assert.assertFalse(mapSettings.isWindUp());
// Verify custom user settings are independent for modes
Assert.assertFalse(mapSettings.isTransparentHoverlines());
//FIXME uncomment when START_ANALYSIS mode can be handled by remote CI server
// verify that custom document settings override mode settings of other modes
// verify that custom document settings override mode settings of other modes
raceboard = RaceBoardPage.goToRaceboardUrl(getWebDriver(), getContextRoot(), BMW_CUP_REGATTA, BMW_CUP_REGATTA,
String.format(BMW_RACE, 1), "START_ANALYSIS");
leaderboardSettingsDialog = raceboard.openLeaderboardSettingsDialog();
leaderboardSettingsPanelPO = leaderboardSettingsDialog.getLeaderboardSettingsPanelPO();
selectedDetails = leaderboardSettingsPanelPO.getSelectedDetails();
detailsToSelect = new DetailCheckboxInfo[] { DetailCheckboxInfo.RACE_GAP_TO_LEADER, // start analysis mode
DetailCheckboxInfo.RACE_DISTANCE_TO_START_FIVE_SECONDS_BEFORE_START, // start analysis mode
DetailCheckboxInfo.RACE_SPEED_OVER_GROUND_FIVE_SECONDS_BEFORE_START, // start analysis mode
DetailCheckboxInfo.DISTANCE_TO_START_AT_RACE_START, // start analysis mode
DetailCheckboxInfo.TIME_BETWEEN_RACE_START_AND_COMPETITOR_START, // start analysis mode
DetailCheckboxInfo.SPEED_OVER_GROUND_AT_RACE_START, // start analysis mode
DetailCheckboxInfo.SPEED_OVER_GROUND_WHEN_STARTING, // start analysis mode
DetailCheckboxInfo.DISTANCE_TO_STARBOARD_END_OF_STARTLINE_WHEN_STARTING, // start analysis mode
DetailCheckboxInfo.TIME_BETWEEN_RACE_START_AND_COMPETITOR_START,
DetailCheckboxInfo.START_TACK, // start analysis mode
DetailCheckboxInfo.AVERAGE_SPEED_OVER_GROUND, DetailCheckboxInfo.DISTANCE, DetailCheckboxInfo.RANK_GAIN,
DetailCheckboxInfo.TACK, DetailCheckboxInfo.JIBE, DetailCheckboxInfo.PENALTY_CIRCLE };
Assert.assertArrayEquals(detailsToSelect, selectedDetails);
leaderboardSettingsDialog.pressCancel();
mapSettings = raceboard.openMapSettings();
Assert.assertTrue(mapSettings.isWindUp());
}
@@ -2,11 +2,8 @@ package com.sap.sailing.server.trackfiles.impl;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
@@ -44,7 +41,6 @@ public class TrackFileExporterImpl implements TrackFileExporter {
@Override
public void writeAllData(List<TrackFilesDataSource> data, TrackFilesFormat format, List<TrackedRace> races,
boolean dataBeforeAfter, boolean rawFixes, final ZipOutputStream out) throws IOException {
WriteZipCallback callback = new WriteZipCallback() {
@Override
public synchronized void write(ZipEntry entry, byte[] data) throws IOException {
@@ -53,7 +49,6 @@ public class TrackFileExporterImpl implements TrackFileExporter {
out.closeEntry();
}
};
List<WriteRaceDataCallable> callables = new ArrayList<>();
ExecutorService executor = ThreadPoolUtil.INSTANCE.getDefaultForegroundTaskThreadPoolExecutor();
List<String> errors = new ArrayList<>();
@@ -64,32 +59,7 @@ public class TrackFileExporterImpl implements TrackFileExporter {
callables.add(callable);
}
}
List<Future<Void>> results = Collections.<Future<Void>> emptyList();
try {
results = executor.invokeAll(callables);
} catch (InterruptedException e) {
errors.add(e.getMessage());
log.log(Level.WARNING, "Error exporting race: " + e.getMessage());
e.printStackTrace();
}
for (Future<Void> result : results) {
try {
result.get();
} catch (ExecutionException e) {
Throwable t = e.getCause();
errors.add(t.getMessage());
log.log(Level.WARNING, "Error exporting race: " + t.getMessage());
t.printStackTrace();
} catch (InterruptedException e) {
errors.add(e.getMessage());
log.log(Level.WARNING, "Error exporting race: " + e.getMessage());
e.printStackTrace();
}
}
ThreadPoolUtil.INSTANCE.invokeAllAndLogExceptions(executor, Level.WARNING, "Error exporting race: %s", callables);
if (errors.size() > 0) {
StringBuilder sb = new StringBuilder();
for (String error : errors) {
@@ -100,7 +70,6 @@ public class TrackFileExporterImpl implements TrackFileExporter {
callback.write(new ZipEntry("ERRORS"), sb.toString().getBytes());
} catch (Exception e) {
log.log(Level.WARNING, "Error exporting race: " + e.getMessage());
e.printStackTrace();
}
}
@@ -1,6 +1,11 @@
package com.sap.sse.util;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.logging.Level;
import com.sap.sse.util.impl.ThreadPoolUtilImpl;
@@ -79,4 +84,24 @@ public interface ThreadPoolUtil {
ScheduledExecutorService createForegroundTaskThreadPoolExecutor(String name);
ScheduledExecutorService createForegroundTaskThreadPoolExecutor(int size, String name);
/**
* Logs all exceptions that occurred during the execution of the {@code futures} provided.
*
* @param messageTemplate
* a message string that must contain a single {@code %s} parameter placeholder that will be substituted
* by the exception's message.
*/
void logExceptionsFromFutures(Level logLevel, String messageTemplate, Iterable<? extends Future<?>> futures);
/**
* Uses the {@code executor}'s {@link ExecutorService#invokeAll(java.util.Collection) invokeAll} method to execute
* all {@code tasks}. All exceptions that occur are logged, using the {@code logLevel} provided, using as the log
* message the message template, parameterized with the exception message.
*
* @param messageTemplate
* a message string that must contain a single {@code %s} parameter placeholder that will be substituted
* by the exception's message.
*/
<T> List<Future<T>> invokeAllAndLogExceptions(ExecutorService executor, Level logLevel, String messageTemplate, Iterable<? extends Callable<T>> tasks);
}
@@ -1,10 +1,20 @@
package com.sap.sse.util.impl;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.sap.sse.common.Util;
import com.sap.sse.util.ThreadPoolUtil;
public class ThreadPoolUtilImpl implements ThreadPoolUtil {
private static final Logger logger = Logger.getLogger(ThreadPoolUtilImpl.class.getName());
private static final int REASONABLE_THREAD_POOL_SIZE = Math.max(Runtime.getRuntime().availableProcessors()-1, 3);
private final ScheduledExecutorService defaultBackgroundTaskThreadPoolExecutor;
@@ -58,4 +68,33 @@ public class ThreadPoolUtilImpl implements ThreadPoolUtil {
public int getReasonableThreadPoolSize() {
return REASONABLE_THREAD_POOL_SIZE;
}
@Override
public void logExceptionsFromFutures(Level logLevel, String messageTemplate, Iterable<? extends Future<?>> futures) {
for (final Future<?> result : futures) {
try {
result.get();
} catch (ExecutionException e) {
Throwable t = e.getCause();
logger.log(logLevel, String.format(messageTemplate, t.getMessage()), t);
} catch (InterruptedException e) {
logger.log(logLevel, String.format(messageTemplate, e.getMessage()), e);
}
}
}
@Override
public <T> List<Future<T>> invokeAllAndLogExceptions(ExecutorService executor, Level logLevel, String messageTemplate,
Iterable<? extends Callable<T>> tasks) {
final List<Callable<T>> tasksAsList = new ArrayList<>();
Util.addAll(tasks, tasksAsList);
List<Future<T>> result = null;
try {
result = executor.invokeAll(tasksAsList);
logExceptionsFromFutures(logLevel, messageTemplate, result);
} catch (InterruptedException e) {
logger.log(logLevel, String.format(messageTemplate, e.getMessage()), e);
}
return result;
}
}
@@ -18,7 +18,7 @@ java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
# Overwrite the log leves for specific loggers here
com.sap.sailing.level = FINE
com.sap.sse.level = FINE
com.sap.sailing.domain.markpassingcalculation.level = FINE
com.sap.sailing.domain.markpassingcalculation.level = FINEST
# log at level FINEST for TransientCompetitorStoreIMpl
#com.sap.sailing.domain.base.impl.TransientCompetitorStoreImpl.level = FINEST
#com.sap.sailing.domain.base.impl.SharedDomainFactoryImpl.level = FINEST