mirror of
https://github.com/eclipse-sailing-analytics/sailing-analytics.git
synced 2026-09-19 20:25:31 +00:00
Merge remote-tracking branch 'server/master'
This commit is contained in:
+139
-26
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -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);
|
||||
}
|
||||
|
||||
+166
-61
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Executable
+39
@@ -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());
|
||||
}
|
||||
}
|
||||
+233
-221
@@ -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();
|
||||
|
||||
/**
|
||||
|
||||
+13
-10
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user