Merge remote-tracking branch 'server/master'

This commit is contained in:
Axel Uhl
2017-05-11 16:03:46 +02:00
11 changed files with 640 additions and 355 deletions
@@ -9,10 +9,13 @@ import static org.mockito.Mockito.mock;
import java.io.Serializable;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CyclicBarrier;
import org.junit.After;
import org.junit.Before;
@@ -52,6 +55,7 @@ import com.sap.sailing.domain.base.impl.RaceDefinitionImpl;
import com.sap.sailing.domain.base.impl.RegattaImpl;
import com.sap.sailing.domain.base.impl.WaypointImpl;
import com.sap.sailing.domain.common.Distance;
import com.sap.sailing.domain.common.TrackedRaceStatusEnum;
import com.sap.sailing.domain.common.impl.DegreeBearingImpl;
import com.sap.sailing.domain.common.impl.DegreePosition;
import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl;
@@ -82,12 +86,15 @@ import com.sap.sailing.domain.tracking.DynamicTrack;
import com.sap.sailing.domain.tracking.DynamicTrackedRace;
import com.sap.sailing.domain.tracking.DynamicTrackedRegatta;
import com.sap.sailing.domain.tracking.Track;
import com.sap.sailing.domain.tracking.TrackedRaceStatus;
import com.sap.sailing.domain.tracking.impl.AbstractRaceChangeListener;
import com.sap.sailing.domain.tracking.impl.DynamicTrackedRaceImpl;
import com.sap.sailing.domain.tracking.impl.DynamicTrackedRegattaImpl;
import com.sap.sailing.domain.tracking.impl.EmptyWindStore;
import com.sap.sailing.domain.tracking.impl.SensorFixTrackImpl;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.Timed;
import com.sap.sse.common.Util.Pair;
import com.sap.sse.common.WithID;
import com.sap.sse.common.impl.MillisecondsTimePoint;
@@ -503,10 +510,10 @@ public class SensorFixStoreAndLoadTest {
@Override
public <FixT extends Timed, TrackT extends DynamicTrack<FixT>> SensorFixMapper<FixT, TrackT, Competitor> createCompetitorMapper(
Class<? extends RegattaLogDeviceMappingEvent<?>> eventType) {
if(bravoDataFixMapper.isResponsibleFor(eventType)) {
if (bravoDataFixMapper.isResponsibleFor(eventType)) {
return (SensorFixMapper) bravoDataFixMapper;
}
if(testDataFixMapper.isResponsibleFor(eventType)) {
if (testDataFixMapper.isResponsibleFor(eventType)) {
return (SensorFixMapper) testDataFixMapper;
}
throw new IllegalArgumentException("Unknown event type");
@@ -633,7 +640,6 @@ public class SensorFixStoreAndLoadTest {
implements RegattaLogDeviceCompetitorSensorDataMappingEvent {
private static final long serialVersionUID = -14940305448048753L;
public RegattaLogDeviceCompetitorTestMappingEventImpl(TimePoint createdAt, TimePoint logicalTimePoint,
AbstractLogEventAuthor author, Serializable pId, Competitor mappedTo, DeviceIdentifier device,
TimePoint from, TimePoint to) {
@@ -681,15 +687,11 @@ public class SensorFixStoreAndLoadTest {
public void testThatNoSensorFixesAreAddedToTrackOfCompetitorWhoIsntPartOfTheRace() throws InterruptedException {
regattaLog.add(new RegattaLogDeviceCompetitorBravoMappingEventImpl(new MillisecondsTimePoint(3), author, compNotPartOfRace,
device, new MillisecondsTimePoint(START_OF_TRACKING), new MillisecondsTimePoint(END_OF_TRACKING)));
addBravoFixes();
FixLoaderAndTracker fixLoaderAndTracker = createFixLoaderAndTracker();
trackedRace.attachRaceLog(raceLog);
trackedRace.attachRegattaLog(regattaLog);
trackedRace.waitForLoadingToFinish();
assertNull(trackedRace.getSensorTrack(compNotPartOfRace, BravoFixTrack.TRACK_NAME));
fixLoaderAndTracker.stop(true);
}
@@ -699,23 +701,17 @@ public class SensorFixStoreAndLoadTest {
public void testThatNoSensorFixesAreLoadedAsLongAsStartOfTrackingIsNull() throws InterruptedException {
regattaLog.add(new RegattaLogDeviceCompetitorBravoMappingEventImpl(new MillisecondsTimePoint(3), author, comp,
device, new MillisecondsTimePoint(START_OF_TRACKING), new MillisecondsTimePoint(END_OF_TRACKING)));
addBravoFixes();
FixLoaderAndTracker fixLoaderAndTracker = createFixLoaderAndTracker();
// raceLog is intentionally not attached
trackedRace.attachRegattaLog(regattaLog);
trackedRace.waitForLoadingToFinish();
// No fixes are loaded because startOfTracking isn't set through the raceLog yet
assertNull(trackedRace.getSensorTrack(comp, BravoFixTrack.TRACK_NAME));
// Loading of fixes is triggered by setting startOfTracking
trackedRace.setStartOfTrackingReceived(new MillisecondsTimePoint(START_OF_TRACKING));
trackedRace.waitForLoadingToFinish();
testNumberOfRawFixes(trackedRace.getSensorTrack(comp, BravoFixTrack.TRACK_NAME), 3);
fixLoaderAndTracker.stop(true);
}
@@ -724,16 +720,11 @@ public class SensorFixStoreAndLoadTest {
public void testThatNoSensorFixesAreRecordedAsWhenStartOfTrackingIsNull() throws InterruptedException {
regattaLog.add(new RegattaLogDeviceCompetitorBravoMappingEventImpl(new MillisecondsTimePoint(3), author, comp,
device, new MillisecondsTimePoint(START_OF_TRACKING), new MillisecondsTimePoint(END_OF_TRACKING)));
FixLoaderAndTracker fixLoaderAndTracker = createFixLoaderAndTracker();
// raceLog is intentionally not attached
trackedRace.attachRegattaLog(regattaLog);
addBravoFixes();
assertNull(trackedRace.getSensorTrack(comp, BravoFixTrack.TRACK_NAME));
fixLoaderAndTracker.stop(true);
}
@@ -742,27 +733,19 @@ public class SensorFixStoreAndLoadTest {
public void testThatNoMoreSensorFixesAreLoadedWhenStartOfTrackingChangesToNull() throws InterruptedException {
regattaLog.add(new RegattaLogDeviceCompetitorBravoMappingEventImpl(new MillisecondsTimePoint(3), author, comp,
device, new MillisecondsTimePoint(START_OF_TRACKING), new MillisecondsTimePoint(END_OF_TRACKING)));
addBravoFixes();
FixLoaderAndTracker fixLoaderAndTracker = createFixLoaderAndTracker();
trackedRace.attachRaceLog(raceLog);
trackedRace.attachRegattaLog(regattaLog);
trackedRace.setStartOfTrackingReceived(new MillisecondsTimePoint(START_OF_TRACKING));
trackedRace.waitForLoadingToFinish();
testNumberOfRawFixes(trackedRace.getSensorTrack(comp, BravoFixTrack.TRACK_NAME), 3);
raceLog.add(new RaceLogStartOfTrackingEventImpl(null, author, 0));
assertNull(trackedRace.getStartOfTracking());
addMoreBravoFixes();
trackedRace.waitForLoadingToFinish();
// only the initial 3 fixes are available
testNumberOfRawFixes(trackedRace.getSensorTrack(comp, BravoFixTrack.TRACK_NAME), 3);
fixLoaderAndTracker.stop(true);
}
@@ -788,5 +771,135 @@ public class SensorFixStoreAndLoadTest {
testNumberOfRawFixes(trackedRace.getSensorTrack(comp, BravoFixTrack.TRACK_NAME), 3);
fixLoaderAndTracker.stop(true);
}
@Test(timeout=10_000)
/** Test for regression introduced while working on bug 4125 - https://bugzilla.sapsailing.com/bugzilla/show_bug.cgi?id=4125 */
public void testPreemptiveStopDoesNotBlockThread() throws InterruptedException {
regattaLog.add(new RegattaLogDeviceCompetitorBravoMappingEventImpl(new MillisecondsTimePoint(3), author, comp,
device, new MillisecondsTimePoint(START_OF_TRACKING), new MillisecondsTimePoint(END_OF_TRACKING)));
addBravoFixes();
CyclicBarrier cb = new CyclicBarrier(2);
// This listener enforces the race to stay in loading state until a preemptive stop is triggered below
trackedRace.addListener(new AbstractRaceChangeListener() {
@Override
public void competitorSensorTrackAdded(DynamicSensorFixTrack<Competitor, ?> track) {
try {
cb.await();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
});
final FixLoaderAndTracker fixLoaderAndTracker = createFixLoaderAndTracker();
trackedRace.attachRaceLog(raceLog);
trackedRace.attachRegattaLog(regattaLog);
// This thread solves the loading state through the CyclicBarrier
new Thread() {
public void run() {
try {
while (!fixLoaderAndTracker.isStopRequested()) {
sleep(100);
}
cb.await();
} catch (Exception e) {
throw new RuntimeException(e);
}
};
}.start();
// When the bug is triggered, this call would hang until the test timeout is reached
fixLoaderAndTracker.stop(true);
}
@Test
/** Test for bug 4125 - https://bugzilla.sapsailing.com/bugzilla/show_bug.cgi?id=4125 */
public void testThatLoadingStateIsTriggeredOnInitialLoad() throws InterruptedException {
regattaLog.add(new RegattaLogDeviceCompetitorBravoMappingEventImpl(new MillisecondsTimePoint(3), author, comp,
device, new MillisecondsTimePoint(START_OF_TRACKING), new MillisecondsTimePoint(END_OF_TRACKING)));
addBravoFixes();
StatusTransitionListener statusTransitionListener = new StatusTransitionListener();
trackedRace.addListener(statusTransitionListener);
final FixLoaderAndTracker fixLoaderAndTracker = createFixLoaderAndTracker();
trackedRace.attachRaceLog(raceLog);
trackedRace.attachRegattaLog(regattaLog);
trackedRace.waitForLoadingToFinish();
fixLoaderAndTracker.stop(true);
statusTransitionListener.assertTransitions(TrackedRaceStatusEnum.PREPARED, TrackedRaceStatusEnum.TRACKING, TrackedRaceStatusEnum.LOADING, TrackedRaceStatusEnum.TRACKING, TrackedRaceStatusEnum.FINISHED);
}
@Test
/** Test for bug 4125 - https://bugzilla.sapsailing.com/bugzilla/show_bug.cgi?id=4125 */
public void testThatLoadingStateIsTriggeredWhenAddingMapping() throws InterruptedException {
addBravoFixes();
StatusTransitionListener statusTransitionListener = new StatusTransitionListener();
trackedRace.addListener(statusTransitionListener);
final FixLoaderAndTracker fixLoaderAndTracker = createFixLoaderAndTracker();
trackedRace.attachRaceLog(raceLog);
trackedRace.attachRegattaLog(regattaLog);
regattaLog.add(new RegattaLogDeviceCompetitorBravoMappingEventImpl(new MillisecondsTimePoint(3), author, comp,
device, new MillisecondsTimePoint(START_OF_TRACKING), new MillisecondsTimePoint(END_OF_TRACKING)));
trackedRace.waitForLoadingToFinish();
fixLoaderAndTracker.stop(true);
statusTransitionListener.assertTransitions(TrackedRaceStatusEnum.PREPARED, TrackedRaceStatusEnum.TRACKING, TrackedRaceStatusEnum.LOADING, TrackedRaceStatusEnum.TRACKING, TrackedRaceStatusEnum.FINISHED);
}
@Test
/** Test for bug 4125 - https://bugzilla.sapsailing.com/bugzilla/show_bug.cgi?id=4125 */
public void testThatLoadingStateIsTriggeredWhenStartOfTrackingIsSet() throws InterruptedException {
regattaLog.add(new RegattaLogDeviceCompetitorBravoMappingEventImpl(new MillisecondsTimePoint(3), author, comp,
device, new MillisecondsTimePoint(START_OF_TRACKING), new MillisecondsTimePoint(END_OF_TRACKING)));
addBravoFixes();
StatusTransitionListener statusTransitionListener = new StatusTransitionListener();
trackedRace.addListener(statusTransitionListener);
final FixLoaderAndTracker fixLoaderAndTracker = createFixLoaderAndTracker();
// raceLog is intentionally not attached
trackedRace.attachRegattaLog(regattaLog);
trackedRace.setStartOfTrackingReceived(new MillisecondsTimePoint(START_OF_TRACKING));
trackedRace.waitForLoadingToFinish();
fixLoaderAndTracker.stop(true);
statusTransitionListener.assertTransitions(TrackedRaceStatusEnum.PREPARED, TrackedRaceStatusEnum.TRACKING, TrackedRaceStatusEnum.LOADING, TrackedRaceStatusEnum.TRACKING, TrackedRaceStatusEnum.FINISHED);
}
@Test
/** Test for bug 4125 - https://bugzilla.sapsailing.com/bugzilla/show_bug.cgi?id=4125 */
public void testThatLoadingStateIsTriggeredWhenStartOfTrackingChanges() throws InterruptedException {
regattaLog.add(new RegattaLogDeviceCompetitorBravoMappingEventImpl(new MillisecondsTimePoint(3), author, comp,
device, new MillisecondsTimePoint(START_OF_TRACKING), new MillisecondsTimePoint(END_OF_TRACKING)));
addBravoFixes();
StatusTransitionListener statusTransitionListener = new StatusTransitionListener();
trackedRace.addListener(statusTransitionListener);
trackedRace.setStartOfTrackingReceived(new MillisecondsTimePoint(END_OF_TRACKING));
final FixLoaderAndTracker fixLoaderAndTracker = createFixLoaderAndTracker();
// raceLog is intentionally not attached
trackedRace.attachRegattaLog(regattaLog);
trackedRace.setStartOfTrackingReceived(new MillisecondsTimePoint(START_OF_TRACKING));
trackedRace.waitForLoadingToFinish();
fixLoaderAndTracker.stop(true);
statusTransitionListener.assertTransitions(TrackedRaceStatusEnum.PREPARED, TrackedRaceStatusEnum.TRACKING, TrackedRaceStatusEnum.LOADING, TrackedRaceStatusEnum.TRACKING, TrackedRaceStatusEnum.FINISHED);
}
private class StatusTransitionListener extends AbstractRaceChangeListener {
List<Pair<TrackedRaceStatusEnum, TrackedRaceStatusEnum>> transitions = new ArrayList<>();
@Override
public void statusChanged(TrackedRaceStatus newStatus, TrackedRaceStatus oldStatus) {
TrackedRaceStatusEnum oldStatusEnum = oldStatus.getStatus();
TrackedRaceStatusEnum newStatusEnum = newStatus.getStatus();
if (oldStatusEnum != newStatusEnum) {
transitions.add(new Pair<>(oldStatusEnum, newStatusEnum));
}
}
public void assertTransitions(TrackedRaceStatusEnum... expectedStates) {
TrackedRaceStatusEnum oldValue = null;
Iterator<Pair<TrackedRaceStatusEnum, TrackedRaceStatusEnum>> recordedTransitionsIterator = transitions.iterator();
for (TrackedRaceStatusEnum newStatusEnum : expectedStates) {
if (oldValue != null) {
Pair<TrackedRaceStatusEnum, TrackedRaceStatusEnum> recordedTransition = recordedTransitionsIterator.next();
assertEquals(oldValue, recordedTransition.getA());
assertEquals(newStatusEnum, recordedTransition.getB());
}
oldValue = newStatusEnum;
}
}
}
}
@@ -80,9 +80,11 @@ public class TrackedRaceLoadsFixesTest extends AbstractGPSFixStoreTest {
testNumberOfRawFixes(trackedRace.getOrCreateTrack(mark), 1);
// now extend the tracking interval of the tracked race and assert that the additional fixes are loaded
trackedRace.setEndOfTrackingReceived(new MillisecondsTimePoint(2500), /* wait for fixes to load */ true);
trackedRace.waitForLoadingToFinish();
testNumberOfRawFixes(trackedRace.getTrack(comp), 2);
testNumberOfRawFixes(trackedRace.getOrCreateTrack(mark), 2);
trackedRace.setStartOfTrackingReceived(new MillisecondsTimePoint(0), /* wait for fixes to load */ true);
trackedRace.waitForLoadingToFinish();
testNumberOfRawFixes(trackedRace.getTrack(comp), 3);
testNumberOfRawFixes(trackedRace.getOrCreateTrack(mark), 3);
}
@@ -2,14 +2,16 @@ package com.sap.sailing.domain.racelogtracking.impl.fixtracker;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.sap.sailing.domain.abstractlog.regatta.MappingEventVisitor;
import com.sap.sailing.domain.abstractlog.regatta.RegattaLog;
import com.sap.sailing.domain.abstractlog.regatta.RegattaLogEventVisitor;
import com.sap.sailing.domain.abstractlog.regatta.events.RegattaLogDeviceCompetitorMappingEvent;
import com.sap.sailing.domain.abstractlog.regatta.events.RegattaLogDeviceCompetitorSensorDataMappingEvent;
import com.sap.sailing.domain.abstractlog.regatta.events.RegattaLogDeviceMappingEvent;
@@ -33,6 +35,7 @@ import com.sap.sailing.domain.tracking.DynamicGPSFixTrack;
import com.sap.sailing.domain.tracking.DynamicSensorFixTrack;
import com.sap.sailing.domain.tracking.DynamicTrack;
import com.sap.sailing.domain.tracking.DynamicTrackedRace;
import com.sap.sailing.domain.tracking.RaceChangeListener;
import com.sap.sailing.domain.tracking.Track;
import com.sap.sailing.domain.tracking.TrackedRace;
import com.sap.sailing.domain.tracking.TrackingDataLoader;
@@ -103,7 +106,15 @@ public class FixLoaderAndTracker implements TrackingDataLoader {
protected final DynamicTrackedRace trackedRace;
private final SensorFixStore sensorFixStore;
private RegattaLogDeviceMappings<WithID> deviceMappings;
private final AtomicInteger activeLoaders = new AtomicInteger();
/**
* Loading fixes into tracks is done one a per item base using jobs that are being run on an executor. These jobs
* are recognized to be able to calculate an overall progress. To ensure a consistent progress, no job is removed
* when finished. The set is cleared instead, when all jobs are finished.<br>
* The alternative would be to use a {@link TrackingDataLoader} per loading job. This would make things more
* complicated to ensure a consistent progress and not leak loader instances. In addition we would need to implement
* one more {@link TrackingDataLoader} that ensures the loading state of the associated {@link TrackedRace}.
*/
private final Set<AbstractLoadingJob> loadingJobs = ConcurrentHashMap.newKeySet();
private final SensorFixMapperFactory sensorFixMapperFactory;
private AtomicBoolean preemptiveStopRequested = new AtomicBoolean(false);
private AtomicBoolean stopRequested = new AtomicBoolean(false);
@@ -445,13 +456,21 @@ public class FixLoaderAndTracker implements TrackingDataLoader {
endOfTracking == null ? TimePoint.EndOfTime : endOfTracking);
}
/**
* Stops this {@link FixLoaderAndTracker}. No more fixes are loaded on model changes and no new fixes are being
* tracked.<br>
* If stopping non-preemtively, all already started loading jobs are finished. When GPS fixes are being loaded from
* TracTrac for archived races, loading is automatically stopped. Finishing already started loading jobs ensures,
* that e.g. bravo fixes are completely loaded even if loading from TracTrac is faster.<br>
* If stopping preemptively, the call will block until all already started loading jobs are aborted or finished.
*/
public void stop(boolean preemptive) {
preemptiveStopRequested.set(preemptive);
stopRequested.set(true);
trackedRace.removeListener(raceChangeListener);
deviceMappings.stop();
synchronized (this) {
if (activeLoaders.get() == 0) {
synchronized (loadingJobs) {
if (loadingJobs.isEmpty()) {
setStatusAndProgress(TrackedRaceStatusEnum.FINISHED, 1.0);
}
}
@@ -462,84 +481,102 @@ public class FixLoaderAndTracker implements TrackingDataLoader {
}
private void startTracking() {
setStatusAndProgress(TrackedRaceStatusEnum.TRACKING, 0.0);
trackedRace.addListener(raceChangeListener);
this.deviceMappings = new FixLoaderDeviceMappings(trackedRace.getAttachedRegattaLogs(),
trackedRace.getRace().getName());
}
private synchronized void waitForLoadingToFinishRunning() {
try {
while (activeLoaders.get() > 0) {
wait();
private void waitForLoadingToFinishRunning() {
synchronized (loadingJobs) {
try {
while (!loadingJobs.isEmpty()) {
loadingJobs.wait();
}
} catch (InterruptedException e) {
logger.log(Level.WARNING, "Interrupted while waiting for Fixes to be loaded", e);
}
} catch (InterruptedException e) {
logger.log(Level.WARNING, "Interrupted while waiting for Fixes to be loaded", e);
}
}
private void loadFixesForExtendedTimeRange(final TimeRange extendedTimeRange) {
deviceMappings.forEachItemAndCoveredTimeRanges((item, mappingsAndCoveredTimeRanges) -> loadFixesInTrackingTimeRange(mappingsAndCoveredTimeRanges, extendedTimeRange));
}
/**
* This method runs the given update callback in a separate {@link Thread} by handling technical concurrency aspects
* and potential {@link #preemptiveStopRequested preemptive stop requests} internally. Thus, it separates the
* functional updating process from technical aspects.
*
* @param updateCallback
* the {@link Runnable} callback used to run the update
*/
private void updateAsyncInternal(final Runnable updateCallback) {
synchronized (FixLoaderAndTracker.this) {
activeLoaders.incrementAndGet();
setStatusAndProgress(TrackedRaceStatusEnum.LOADING, 0.5);
}
ThreadPoolUtil.INSTANCE.getDefaultForegroundTaskThreadPoolExecutor().execute(new Runnable() {
@Override
public void run() {
trackedRace.lockForSerializationRead();
try {
if (!preemptiveStopRequested.get()) {
setStatusAndProgress(TrackedRaceStatusEnum.LOADING, 0.5);
synchronized (FixLoaderAndTracker.this) {
FixLoaderAndTracker.this.notifyAll();
}
updateCallback.run();
}
} catch(Throwable t) {
logger.log(Level.SEVERE, "Error while updating device mappings and loading fixes for race: " + trackedRace.getRaceIdentifier(), t);
} finally {
try {
synchronized (FixLoaderAndTracker.this) {
int currentActiveLoaders = activeLoaders.decrementAndGet();
FixLoaderAndTracker.this.notifyAll();
if (currentActiveLoaders == 0) {
setStatusAndProgress(stopRequested.get() ? TrackedRaceStatusEnum.FINISHED
: TrackedRaceStatusEnum.TRACKING, 1.0);
}
}
} finally {
trackedRace.unlockAfterSerializationRead();
}
}
}
});
deviceMappings.forEachItemAndCoveredTimeRanges((item, mappingsAndCoveredTimeRanges) -> addLoadingJob(
new LoadFixesInTrackingTimeRangeJob(mappingsAndCoveredTimeRanges, extendedTimeRange)));
}
private void setStatusAndProgress(TrackedRaceStatusEnum status, double progress) {
trackedRace.onStatusChanged(this, new TrackedRaceStatusImpl(status, progress));
}
/**
* Updates the {@link FixLoaderAndTracker}'s overall state on the {@link TrackedRace} based on the progresses of
* {@link #loadingJobs}.
*/
private void updateStatusAndProgressWithErrorHandling() {
try {
updateStatusAndProgress();
} catch (Exception e) {
logger.log(Level.WARNING, "Error while updating status and progress for FixLoaderAndTracker", e);
}
}
/**
* Updates the {@link FixLoaderAndTracker}'s overall state on the {@link TrackedRace} based on the progresses of
* {@link #loadingJobs}.
*/
private void updateStatusAndProgress() {
synchronized (loadingJobs) {
final TrackedRaceStatusEnum status;
final double progress;
if (!loadingJobs.isEmpty()) {
double progressSum = 0.0;
boolean allFinished = true;
for (AbstractLoadingJob loadingJob : loadingJobs) {
allFinished &= loadingJob.finished;
progressSum += loadingJob.progress;
}
if (allFinished) {
loadingJobs.clear();
status = stopRequested.get() ? TrackedRaceStatusEnum.FINISHED : TrackedRaceStatusEnum.TRACKING;
progress = 1.0;
} else {
progress = progressSum / loadingJobs.size();
status = TrackedRaceStatusEnum.LOADING;
}
} else {
status = stopRequested.get() ? TrackedRaceStatusEnum.FINISHED : TrackedRaceStatusEnum.TRACKING;
progress = 1.0;
}
setStatusAndProgress(status, progress);
loadingJobs.notifyAll();
}
}
/**
* Adds a {@link AbstractLoadingJob} to track its loading state and updates the {@link FixLoaderAndTracker}'s
* overall state on the {@link TrackedRace}.
*/
private void addLoadingJob(AbstractLoadingJob job) {
synchronized (loadingJobs) {
loadingJobs.add(job);
updateStatusAndProgress();
}
ThreadPoolUtil.INSTANCE.getDefaultForegroundTaskThreadPoolExecutor().execute(job);
}
/**
* Used for testing purposes only.
*/
public boolean isStopRequested() {
return stopRequested.get();
}
private class FixLoaderDeviceMappings extends RegattaLogDeviceMappings<WithID> {
public FixLoaderDeviceMappings(Iterable<RegattaLog> initialRegattaLogs, String raceNameForLock) {
super(initialRegattaLogs, raceNameForLock);
}
@Override
protected void updateMappings() {
updateAsyncInternal(FixLoaderDeviceMappings.super::updateMappings);
}
@Override
protected void deviceIdAdded(DeviceIdentifier deviceIdentifier) {
sensorFixStore.addListener(listener, deviceIdentifier);
@@ -553,6 +590,74 @@ public class FixLoaderAndTracker implements TrackingDataLoader {
@Override
protected void newTimeRangesCovered(WithID item,
Map<RegattaLogDeviceMappingEvent<WithID>, MultiTimeRange> newlyCoveredTimeRanges) {
addLoadingJob(new LoadFixesForNewlyCoveredTimeRangesJob(item, newlyCoveredTimeRanges));
}
}
/**
* Abstract implementation of a job to load fixes into tracks that supports tracking the loading progress.
* Subclasses are intended to be run using an executor.
*/
private abstract class AbstractLoadingJob implements Runnable {
double progress = 0;
boolean finished = false;
@Override
public final void run() {
progress = 0.5;
updateStatusAndProgressWithErrorHandling();
try {
load();
} finally {
progress = 1.0;
finished = true;
updateStatusAndProgressWithErrorHandling();
}
}
protected abstract void load();
}
/**
* Loads fixes for an item's mappings in a defined tracking {@link TimeRange}. This is used when:
* <ul>
* <li>Initially loading fixes into tracks</li>
* <li>The tracking {@link TimeRange} changes</li>
* </ul>
*/
private class LoadFixesInTrackingTimeRangeJob extends AbstractLoadingJob {
private final Map<RegattaLogDeviceMappingEvent<WithID>, MultiTimeRange> newlyCoveredTimeRanges;
private final TimeRange trackingTimeRange;
public LoadFixesInTrackingTimeRangeJob(Map<RegattaLogDeviceMappingEvent<WithID>, MultiTimeRange> newlyCoveredTimeRanges,
TimeRange trackingTimeRange) {
this.newlyCoveredTimeRanges = newlyCoveredTimeRanges;
this.trackingTimeRange = trackingTimeRange;
}
@Override
protected void load() {
loadFixesInTrackingTimeRange(newlyCoveredTimeRanges, trackingTimeRange);
}
}
/**
* This is used when device mappings for an item changed so that fixes in a new {@link TimeRange} are covered.
*/
private class LoadFixesForNewlyCoveredTimeRangesJob extends AbstractLoadingJob {
private final WithID item;
private final Map<RegattaLogDeviceMappingEvent<WithID>, MultiTimeRange> newlyCoveredTimeRanges;
public LoadFixesForNewlyCoveredTimeRangesJob(WithID item,
Map<RegattaLogDeviceMappingEvent<WithID>, MultiTimeRange> newlyCoveredTimeRanges) {
this.item = item;
this.newlyCoveredTimeRanges = newlyCoveredTimeRanges;
}
@Override
protected void load() {
loadFixesForNewlyCoveredTimeRanges(item, newlyCoveredTimeRanges);
}
}
@@ -133,7 +133,7 @@ public abstract class RegattaLogDeviceMappings<ItemT extends WithID> {
});
}
protected void updateMappings() {
private void updateMappings() {
try {
updateMappingsInternal();
} catch (Exception e) {
@@ -245,10 +245,10 @@ public abstract class RegattaLogDeviceMappings<ItemT extends WithID> {
}
}
newDeviceIds.addAll(mappingsByDevice.keySet());
calculateDiff(oldMappings, newMappings, oldDeviceIds, newDeviceIds);
} finally {
LockUtil.unlockAfterWrite(mappingsLock);
}
calculateDiff(oldMappings, newMappings, oldDeviceIds, newDeviceIds);
}
/**
@@ -0,0 +1,39 @@
package com.sap.sailing.domain.test;
import static org.junit.Assert.assertSame;
import java.util.Collections;
import org.junit.Before;
import org.junit.Test;
import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.common.TrackedRaceStatusEnum;
import com.sap.sailing.domain.tracking.DynamicTrackedRace;
import com.sap.sailing.domain.tracking.TrackingDataLoader;
import com.sap.sailing.domain.tracking.impl.TrackedRaceStatusImpl;
import com.sap.sse.common.impl.MillisecondsTimePoint;
/**
* See bug 4125 which handles race status updates more sophisticatedly.
*
* @author Axel Uhl (d043530)
*
*/
public class TrackedRaceStatusUpdateTest extends TrackBasedTest {
private Competitor competitor;
private DynamicTrackedRace trackedRace;
@Before
public void setUp() {
competitor = createCompetitor("Test Competitor");
trackedRace = createTestTrackedRace("Test Regatta", "Test Race", "505", Collections.singleton(competitor), MillisecondsTimePoint.now(), /* useMarkPassingCalculator */ false);
}
@Test
public void testSetPreparedStatus() {
assertSame(TrackedRaceStatusEnum.PREPARED, trackedRace.getStatus().getStatus());
trackedRace.onStatusChanged(new TrackingDataLoader() {}, new TrackedRaceStatusImpl(TrackedRaceStatusEnum.PREPARED, 0.0));
assertSame(TrackedRaceStatusEnum.PREPARED, trackedRace.getStatus().getStatus());
}
}
@@ -1,221 +1,233 @@
package com.sap.sailing.domain.tracking;
import com.sap.sailing.domain.abstractlog.race.CompetitorResult;
import com.sap.sailing.domain.abstractlog.race.RaceLog;
import com.sap.sailing.domain.abstractlog.race.RaceLogFinishPositioningConfirmedEvent;
import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.base.CourseBase;
import com.sap.sailing.domain.base.Mark;
import com.sap.sailing.domain.common.Wind;
import com.sap.sailing.domain.common.WindSource;
import com.sap.sailing.domain.common.racelog.Flags;
import com.sap.sailing.domain.common.tracking.GPSFix;
import com.sap.sailing.domain.common.tracking.GPSFixMoving;
import com.sap.sailing.domain.common.tracking.SensorFix;
import com.sap.sailing.domain.tracking.impl.DynamicTrackedRaceLogListener;
import com.sap.sse.common.TimePoint;
public interface DynamicTrackedRace extends TrackedRace {
/**
* Records a position and speed and course over ground fix for a competitor, but only if the fix's {@link GPSFixMoving#getTimePoint()}
* is within this race's {@link #getStartOfTracking() start} and {@link #getEndOfTracking() end} of tracking time interval.
*/
default void recordFix(Competitor competitor, GPSFixMoving fix) {
recordFix(competitor, fix, /* onlyWhenInTrackingTimeInterval */ true);
}
/**
* Records a position and speed and course over ground fix for a competitor. If
* {@code onlyWhenInTrackingTimeInterval} is {@code true}, the fix is recorded only if the fix's
* {@link GPSFixMoving#getTimePoint()} is within this race's {@link #getStartOfTracking() start} and
* {@link #getEndOfTracking() end} of tracking time interval. If {@code onlyWhenInTrackingTimeInterval} is
* {@code false}, the fix is recorded regardless of this race's tracking times interval.
*/
void recordFix(Competitor competitor, GPSFixMoving fix, boolean onlyWhenInTrackingTimeInterval);
/**
* Records a position fix for a mark, but only if the fix's {@link GPSFixMoving#getTimePoint()} is within this
* race's {@link #getStartOfTracking() start} and {@link #getEndOfTracking() end} of tracking time interval.
*/
default void recordFix(Mark mark, GPSFix fix) {
recordFix(mark, fix, /* onlyWhenInTrackingTimeInterval */ true);
}
/**
* Records a position fix for a mark. If {@code onlyWhenInTrackingTimeInterval} is {@code true}, the fix is recorded
* only if the fix's {@link GPSFixMoving#getTimePoint()} is within this race's {@link #getStartOfTracking() start}
* and {@link #getEndOfTracking() end} of tracking time interval. If {@code onlyWhenInTrackingTimeInterval} is
* {@code false}, the fix is recorded regardless of this race's tracking times interval.
*/
void recordFix(Mark mark, GPSFix fix, boolean onlyWhenInTrackingTimeInterval);
/**
* Inserts a <code>wind</code> fix into a {@link WindTrack} for the <code>windSource</code> if the current filtering
* rules accept the wind fix. Filtering applies based upon timing considerations, assuming that wind fixes are not
* relevant if they are outside of the tracking interval. There may be exceptions for races acting as default wind
* acceptors in case no other race in the regatta would accept the wind fix.
*
* @return True if the specified wind has been accepted and added to this race's wind track and database, else false.
*/
default boolean recordWind(Wind wind, WindSource windSource) {
return recordWind(wind, windSource, /* applyFilter */ true);
}
/**
* Like {@link #recordWind(Wind, WindSource)}, only that filtering may be disabled by setting
* <code>applyFilter</code> to <code>false</code>.
*/
boolean recordWind(Wind wind, WindSource windSource, boolean applyFilter);
void removeWind(Wind wind, WindSource windSource);
/**
* The raw, updating feed of a single competitor participating in this race
*/
DynamicGPSFixTrack<Competitor, GPSFixMoving> getTrack(Competitor competitor);
/**
* Yields the track describing <code>mark</code>'s movement over time; never <code>null</code> because a
* new track will be created in case no track was present for <code>mark</code> so far.
*/
DynamicGPSFixTrack<Mark, GPSFix> getOrCreateTrack(Mark mark);
/**
* Gets an existing {@link DynamicSensorFixTrack} or creates and returns a new one if there is none yet. If a
* Competitor is given who is not part of this race, no track is created.
*
* @see #getDynamicSensorTrack(Competitor, String)
*
* @param competitor
* the competitor to get the track for
* @param trackName
* the name of the track to get
* @param newTrackFactory
* factory to create a new track instance if there isn't one yet for the competitor and the given track
* name.
* @return the track for the competitor and track name of null if the given {@link Competitor} isn't part of this
* race
*/
<FixT extends SensorFix, TrackT extends DynamicSensorFixTrack<Competitor, FixT>> TrackT getOrCreateSensorTrack(
Competitor competitor, String trackName, TrackFactory<TrackT> newTrackFactory);
/**
* @see TrackedRace#getSensorTrack(Competitor, String)
*
* @param competitor the competitor to get the track for
* @param trackName the name of the track to get
* @return the track associated to the given Competitor and name or <code>null</code> if there is none.
*/
<FixT extends SensorFix, TrackT extends DynamicSensorFixTrack<Competitor, FixT>> TrackT getDynamicSensorTrack(
Competitor competitor, String trackName);
void recordSensorFix(Competitor competitor, String trackName, SensorFix fix, boolean onlyWhenInTrackingTimeInterval);
/**
* Updates all mark passings for <code>competitor</code> for this race. The mark passings must be provided in the
* order of the race's course and in increasing time stamps. Calling this method replaces all previous mark passings
* for this race for <code>competitor</code> and ensures that the "leaderboard" and all other derived information
* are updated accordingly.
* <p>
*
* When an attached {@link RaceLog} has a {@link RaceLogFinishPositioningConfirmedEvent} that sets a
* {@link CompetitorResult#getFinishingTime() finishing time} for a competitor, it will be used to override the
* {@link MarkPassing#getTimePoint() time point} of the finishing waypoint's mark passing or, if no mark passing for
* the finishing waypoint exists yet for that competitor, create one. This can, in particular, be helpful when
* determining the time sailed for the {@code competitor} in order to determine the calculated time after applying
* any handicap rules and metrics.<p>
*/
void updateMarkPassings(Competitor competitor, Iterable<MarkPassing> markPassings);
/**
* When there is a significant change in the race logs attached to this race, such as adding another race log or
* removing a race log or switching to another pass in one of the race logs attached, the effects on the
* valid {@link RaceLogFinishPositioningConfirmedEvent} are analyzed, and if relevant, the mark passings
* for the finish line that are affected will be {@link #updateMarkPassings(Competitor, Iterable) updated}.
*/
void updateMarkPassingsAfterRaceLogChanges();
/**
* Sets the start time as received from the tracking infrastructure. This isn't necessarily
* what {@link #getStart()} will deliver which assumes that the time announced here may be
* significantly off.
*/
void setStartTimeReceived(TimePoint start);
/**
* A new finished time has been received by the {@link DynamicTrackedRaceLogListener} and is announced to this race
* by calling this method. The {@link RaceChangeListener}s will be
* {@link RaceChangeListener#finishedTimeChanged(TimePoint, TimePoint) notified} about this change, and the result
* of {@link #getFinishedTime()} will return the {@code newFinishedTime} after this call returns.
*/
void setFinishedTime(final TimePoint newFinishedTime);
/** Sets the start of tracking as received from the tracking infrastructure.
* This isn't necessarily what {@link #getStartOfTracking()} will deliver because we might consider other values to
* calculate the start of tracking.
*/
void setStartOfTrackingReceived(TimePoint startOfTrackingReceived);
/** Sets the end of tracking as received from the tracking infrastructure.
* This isn't necessarily what {@link #getEndOfTracking()} will deliver because we might consider other values to
* calculate the end of tracking.
*/
void setEndOfTrackingReceived(TimePoint endOfTrackingReceived);
/**
* A time point is considered "in" if it is (inclusively) between {@link #getStartOfTracking()} and {@link #getEndOfTracking()}.
* A <code>null</code> value for one of the two interval demarcations means an open-ended interval.
*/
boolean isWithinStartAndEndOfTracking(TimePoint timePoint);
void setMillisecondsOverWhichToAverageSpeed(long millisecondsOverWhichToAverageSpeed);
void setMillisecondsOverWhichToAverageWind(long millisecondsOverWhichToAverageWind);
/**
* Same as {@link #setDelayToLiveInMillis(long)}, except that afterwards, a {@link #setDelayToLiveInMillis(long)} will no longer
* take effect.
*/
void setAndFixDelayToLiveInMillis(long delayToLiveInMillis);
/**
* Updates the value returned by {@link #getDelayToLiveInMillis()}, except that {@link #setAndFixDelayToLiveInMillis(long)} was called
* on this object before, in which case this call takes no effect.
*/
void setDelayToLiveInMillis(long delayToLiveInMillis);
DynamicTrackedRegatta getTrackedRegatta();
/**
* If and only if <code>raceIsKnownToStartUpwind</code> is <code>true</code>, this tracked race is allowed to use
* the start leg's direction as a fallback for estimating the wind direction.
*/
void setRaceIsKnownToStartUpwind(boolean raceIsKnownToStartUpwind);
void setStatus(TrackedRaceStatus newStatus);
void onStatusChanged(TrackingDataLoader source, TrackedRaceStatus status);
/**
* whenever a new course design is published by the race committee and the appropriate event occurs in the race log,
* this method is called to propagate the course design to the tracking provider.
*
* @param courseDesign
* the new course design to be published
*/
void onCourseDesignChangedByRaceCommittee(CourseBase courseDesign);
void onStartTimeChangedByRaceCommittee(TimePoint newStartTime);
void onAbortedByRaceCommittee(Flags flag);
void invalidateStartTime();
void invalidateEndTime();
/**
* Adds a {@link DynamicSensorFixTrack} for the given Competitor and track name.
* @see #getDynamicSensorTrack(Competitor, String)
*/
void addSensorTrack(Competitor trackedItem, String trackName, DynamicSensorFixTrack<Competitor, ?> track);
}
package com.sap.sailing.domain.tracking;
import com.sap.sailing.domain.abstractlog.race.CompetitorResult;
import com.sap.sailing.domain.abstractlog.race.RaceLog;
import com.sap.sailing.domain.abstractlog.race.RaceLogFinishPositioningConfirmedEvent;
import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.base.CourseBase;
import com.sap.sailing.domain.base.Mark;
import com.sap.sailing.domain.common.Wind;
import com.sap.sailing.domain.common.WindSource;
import com.sap.sailing.domain.common.racelog.Flags;
import com.sap.sailing.domain.common.tracking.GPSFix;
import com.sap.sailing.domain.common.tracking.GPSFixMoving;
import com.sap.sailing.domain.common.tracking.SensorFix;
import com.sap.sailing.domain.tracking.impl.DynamicTrackedRaceLogListener;
import com.sap.sse.common.TimePoint;
public interface DynamicTrackedRace extends TrackedRace {
/**
* Records a position and speed and course over ground fix for a competitor, but only if the fix's {@link GPSFixMoving#getTimePoint()}
* is within this race's {@link #getStartOfTracking() start} and {@link #getEndOfTracking() end} of tracking time interval.
*/
default void recordFix(Competitor competitor, GPSFixMoving fix) {
recordFix(competitor, fix, /* onlyWhenInTrackingTimeInterval */ true);
}
/**
* Records a position and speed and course over ground fix for a competitor. If
* {@code onlyWhenInTrackingTimeInterval} is {@code true}, the fix is recorded only if the fix's
* {@link GPSFixMoving#getTimePoint()} is within this race's {@link #getStartOfTracking() start} and
* {@link #getEndOfTracking() end} of tracking time interval. If {@code onlyWhenInTrackingTimeInterval} is
* {@code false}, the fix is recorded regardless of this race's tracking times interval.
*/
void recordFix(Competitor competitor, GPSFixMoving fix, boolean onlyWhenInTrackingTimeInterval);
/**
* Records a position fix for a mark, but only if the fix's {@link GPSFixMoving#getTimePoint()} is within this
* race's {@link #getStartOfTracking() start} and {@link #getEndOfTracking() end} of tracking time interval.
*/
default void recordFix(Mark mark, GPSFix fix) {
recordFix(mark, fix, /* onlyWhenInTrackingTimeInterval */ true);
}
/**
* Records a position fix for a mark. If {@code onlyWhenInTrackingTimeInterval} is {@code true}, the fix is recorded
* only if the fix's {@link GPSFixMoving#getTimePoint()} is within this race's {@link #getStartOfTracking() start}
* and {@link #getEndOfTracking() end} of tracking time interval. If {@code onlyWhenInTrackingTimeInterval} is
* {@code false}, the fix is recorded regardless of this race's tracking times interval.
*/
void recordFix(Mark mark, GPSFix fix, boolean onlyWhenInTrackingTimeInterval);
/**
* Inserts a <code>wind</code> fix into a {@link WindTrack} for the <code>windSource</code> if the current filtering
* rules accept the wind fix. Filtering applies based upon timing considerations, assuming that wind fixes are not
* relevant if they are outside of the tracking interval. There may be exceptions for races acting as default wind
* acceptors in case no other race in the regatta would accept the wind fix.
*
* @return True if the specified wind has been accepted and added to this race's wind track and database, else false.
*/
default boolean recordWind(Wind wind, WindSource windSource) {
return recordWind(wind, windSource, /* applyFilter */ true);
}
/**
* Like {@link #recordWind(Wind, WindSource)}, only that filtering may be disabled by setting
* <code>applyFilter</code> to <code>false</code>.
*/
boolean recordWind(Wind wind, WindSource windSource, boolean applyFilter);
void removeWind(Wind wind, WindSource windSource);
/**
* The raw, updating feed of a single competitor participating in this race
*/
DynamicGPSFixTrack<Competitor, GPSFixMoving> getTrack(Competitor competitor);
/**
* Yields the track describing <code>mark</code>'s movement over time; never <code>null</code> because a
* new track will be created in case no track was present for <code>mark</code> so far.
*/
DynamicGPSFixTrack<Mark, GPSFix> getOrCreateTrack(Mark mark);
/**
* Gets an existing {@link DynamicSensorFixTrack} or creates and returns a new one if there is none yet. If a
* Competitor is given who is not part of this race, no track is created.
*
* @see #getDynamicSensorTrack(Competitor, String)
*
* @param competitor
* the competitor to get the track for
* @param trackName
* the name of the track to get
* @param newTrackFactory
* factory to create a new track instance if there isn't one yet for the competitor and the given track
* name.
* @return the track for the competitor and track name of null if the given {@link Competitor} isn't part of this
* race
*/
<FixT extends SensorFix, TrackT extends DynamicSensorFixTrack<Competitor, FixT>> TrackT getOrCreateSensorTrack(
Competitor competitor, String trackName, TrackFactory<TrackT> newTrackFactory);
/**
* @see TrackedRace#getSensorTrack(Competitor, String)
*
* @param competitor the competitor to get the track for
* @param trackName the name of the track to get
* @return the track associated to the given Competitor and name or <code>null</code> if there is none.
*/
<FixT extends SensorFix, TrackT extends DynamicSensorFixTrack<Competitor, FixT>> TrackT getDynamicSensorTrack(
Competitor competitor, String trackName);
void recordSensorFix(Competitor competitor, String trackName, SensorFix fix, boolean onlyWhenInTrackingTimeInterval);
/**
* Updates all mark passings for <code>competitor</code> for this race. The mark passings must be provided in the
* order of the race's course and in increasing time stamps. Calling this method replaces all previous mark passings
* for this race for <code>competitor</code> and ensures that the "leaderboard" and all other derived information
* are updated accordingly.
* <p>
*
* When an attached {@link RaceLog} has a {@link RaceLogFinishPositioningConfirmedEvent} that sets a
* {@link CompetitorResult#getFinishingTime() finishing time} for a competitor, it will be used to override the
* {@link MarkPassing#getTimePoint() time point} of the finishing waypoint's mark passing or, if no mark passing for
* the finishing waypoint exists yet for that competitor, create one. This can, in particular, be helpful when
* determining the time sailed for the {@code competitor} in order to determine the calculated time after applying
* any handicap rules and metrics.<p>
*/
void updateMarkPassings(Competitor competitor, Iterable<MarkPassing> markPassings);
/**
* When there is a significant change in the race logs attached to this race, such as adding another race log or
* removing a race log or switching to another pass in one of the race logs attached, the effects on the
* valid {@link RaceLogFinishPositioningConfirmedEvent} are analyzed, and if relevant, the mark passings
* for the finish line that are affected will be {@link #updateMarkPassings(Competitor, Iterable) updated}.
*/
void updateMarkPassingsAfterRaceLogChanges();
/**
* Sets the start time as received from the tracking infrastructure. This isn't necessarily
* what {@link #getStart()} will deliver which assumes that the time announced here may be
* significantly off.
*/
void setStartTimeReceived(TimePoint start);
/**
* A new finished time has been received by the {@link DynamicTrackedRaceLogListener} and is announced to this race
* by calling this method. The {@link RaceChangeListener}s will be
* {@link RaceChangeListener#finishedTimeChanged(TimePoint, TimePoint) notified} about this change, and the result
* of {@link #getFinishedTime()} will return the {@code newFinishedTime} after this call returns.
*/
void setFinishedTime(final TimePoint newFinishedTime);
/** Sets the start of tracking as received from the tracking infrastructure.
* This isn't necessarily what {@link #getStartOfTracking()} will deliver because we might consider other values to
* calculate the start of tracking.
*/
void setStartOfTrackingReceived(TimePoint startOfTrackingReceived);
/** Sets the end of tracking as received from the tracking infrastructure.
* This isn't necessarily what {@link #getEndOfTracking()} will deliver because we might consider other values to
* calculate the end of tracking.
*/
void setEndOfTrackingReceived(TimePoint endOfTrackingReceived);
/**
* A time point is considered "in" if it is (inclusively) between {@link #getStartOfTracking()} and {@link #getEndOfTracking()}.
* A <code>null</code> value for one of the two interval demarcations means an open-ended interval.
*/
boolean isWithinStartAndEndOfTracking(TimePoint timePoint);
void setMillisecondsOverWhichToAverageSpeed(long millisecondsOverWhichToAverageSpeed);
void setMillisecondsOverWhichToAverageWind(long millisecondsOverWhichToAverageWind);
/**
* Same as {@link #setDelayToLiveInMillis(long)}, except that afterwards, a {@link #setDelayToLiveInMillis(long)} will no longer
* take effect.
*/
void setAndFixDelayToLiveInMillis(long delayToLiveInMillis);
/**
* Updates the value returned by {@link #getDelayToLiveInMillis()}, except that {@link #setAndFixDelayToLiveInMillis(long)} was called
* on this object before, in which case this call takes no effect.
*/
void setDelayToLiveInMillis(long delayToLiveInMillis);
DynamicTrackedRegatta getTrackedRegatta();
/**
* If and only if <code>raceIsKnownToStartUpwind</code> is <code>true</code>, this tracked race is allowed to use
* the start leg's direction as a fallback for estimating the wind direction.
*/
void setRaceIsKnownToStartUpwind(boolean raceIsKnownToStartUpwind);
void setStatus(TrackedRaceStatus newStatus);
/**
* Updates the status of one {@link TrackingDataLoader}. This influences the overall status of the TrackedRace with the following rules:
* <ul>
* <li>The {@link TrackedRace} is initially in the state PREPARED</li>
* <li>If any loader's state is ERROR, this will also be the case for the {@link TrackedRace}</li>
* <li>Otherwise: If any loader's state is LOADING, the {@link TrackedRace} will also be in loading state with the progress being the average progress of all loaders (including those not being in loading state)</li>
* <li>Otherwise: If all laoders are in PREPARED state, this will also be the case for the {@link TrackedRace}</li>
* <li>Otherwise: The {@link TrackedRace} is in TRACKING state</li>
* </ul>
*
* @see TrackedRace#getStatus()
*/
void onStatusChanged(TrackingDataLoader source, TrackedRaceStatus status);
/**
* whenever a new course design is published by the race committee and the appropriate event occurs in the race log,
* this method is called to propagate the course design to the tracking provider.
*
* @param courseDesign
* the new course design to be published
*/
void onCourseDesignChangedByRaceCommittee(CourseBase courseDesign);
void onStartTimeChangedByRaceCommittee(TimePoint newStartTime);
void onAbortedByRaceCommittee(Flags flag);
void invalidateStartTime();
void invalidateEndTime();
/**
* Adds a {@link DynamicSensorFixTrack} for the given Competitor and track name.
* @see #getDynamicSensorTrack(Competitor, String)
*/
void addSensorTrack(Competitor trackedItem, String trackName, DynamicSensorFixTrack<Competitor, ?> track);
}
@@ -737,6 +737,17 @@ public interface TrackedRace extends Serializable, IsManagedByCache<SharedDomain
*/
void waitForLoadingToFinish() throws InterruptedException;
/**
* Returns the current status of the {@link TrackedRace}. This consists of one of the {@link TrackedRaceStatusEnum}
* values plus a progress for LOADING state.<br>
* Due to the fact that multiple loaders can exist that load data into the {@link TrackedRace}, the returned status
* is a composite of those loader statuses. When a loader is finished, its status isn't tracked anymore. This causes
* the overall progress to not be guaranteed to be monotonic (progress may jump to a lower percentage when one loader
* that had a progress of 100% is finished and thus removed).
*
* @see TrackedRaceStatus
* @see DynamicTrackedRace#onStatusChanged(TrackingDataLoader, TrackedRaceStatus)
*/
TrackedRaceStatus getStatus();
/**
@@ -223,27 +223,30 @@ DynamicTrackedRace, GPSTrackListener<Competitor, GPSFixMoving> {
TrackedRaceStatusEnum raceStatus = TrackedRaceStatusEnum.FINISHED;
double totalProgress = 1.0;
synchronized (loaderStatus) {
// logger.info("Status changed: " + newStatus.getStatus() + " | " + source);
this.updateLoaderStatus(source, newStatus);
double sumOfLoaderProgresses = 0.0;
if (!loaderStatus.isEmpty()) {
double sumOfLoaderProgresses = 0.0;
boolean anyError = false;
boolean anyLoading = false;
boolean allPrepared = true;
raceStatus = TrackedRaceStatusEnum.TRACKING;
for (TrackedRaceStatus status : loaderStatus.values()) {
if (status.getStatus() == TrackedRaceStatusEnum.ERROR) {
raceStatus = TrackedRaceStatusEnum.ERROR; break;
}
if (status.getStatus() == TrackedRaceStatusEnum.LOADING) {
raceStatus = TrackedRaceStatusEnum.LOADING;
}
anyError |= (status.getStatus() == TrackedRaceStatusEnum.ERROR);
anyLoading |= (status.getStatus() == TrackedRaceStatusEnum.LOADING);
allPrepared &= (status.getStatus() == TrackedRaceStatusEnum.PREPARED);
sumOfLoaderProgresses += status.getLoadingProgress();
}
if (raceStatus == TrackedRaceStatusEnum.LOADING) {
if (anyError) {
raceStatus = TrackedRaceStatusEnum.ERROR;
} else if (anyLoading) {
raceStatus = TrackedRaceStatusEnum.LOADING;
totalProgress = sumOfLoaderProgresses / loaderStatus.size();
} else {
raceStatus = allPrepared ? TrackedRaceStatusEnum.PREPARED : TrackedRaceStatusEnum.TRACKING;
}
}
}
this.setStatus(new TrackedRaceStatusImpl(raceStatus, totalProgress));
// logger.info("Global status: " + raceStatus + " | Progress: " + totalProgress);
}
private void updateLoaderStatus(TrackingDataLoader loader, TrackedRaceStatus status) {