diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/SensorFixStoreAndLoadTest.java b/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/SensorFixStoreAndLoadTest.java index 87b81f142f8..d29e2c061e7 100644 --- a/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/SensorFixStoreAndLoadTest.java +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/SensorFixStoreAndLoadTest.java @@ -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 > SensorFixMapper createCompetitorMapper( Class> 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 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> 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> recordedTransitionsIterator = transitions.iterator(); + for (TrackedRaceStatusEnum newStatusEnum : expectedStates) { + if (oldValue != null) { + Pair recordedTransition = recordedTransitionsIterator.next(); + assertEquals(oldValue, recordedTransition.getA()); + assertEquals(newStatusEnum, recordedTransition.getB()); + } + oldValue = newStatusEnum; + } + } + } } diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/TrackedRaceLoadsFixesTest.java b/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/TrackedRaceLoadsFixesTest.java index 5874f250eb5..fff10acdac6 100755 --- a/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/TrackedRaceLoadsFixesTest.java +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/TrackedRaceLoadsFixesTest.java @@ -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); } diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/fixtracker/FixLoaderAndTracker.java b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/fixtracker/FixLoaderAndTracker.java index dd1f7e64f8c..b16c3a206b4 100644 --- a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/fixtracker/FixLoaderAndTracker.java +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/fixtracker/FixLoaderAndTracker.java @@ -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 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.
+ * 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 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.
+ * 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.
+ * 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 { public FixLoaderDeviceMappings(Iterable 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, 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: + *
    + *
  • Initially loading fixes into tracks
  • + *
  • The tracking {@link TimeRange} changes
  • + *
+ */ + private class LoadFixesInTrackingTimeRangeJob extends AbstractLoadingJob { + + private final Map, MultiTimeRange> newlyCoveredTimeRanges; + private final TimeRange trackingTimeRange; + + public LoadFixesInTrackingTimeRangeJob(Map, 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, MultiTimeRange> newlyCoveredTimeRanges; + + public LoadFixesForNewlyCoveredTimeRangesJob(WithID item, + Map, MultiTimeRange> newlyCoveredTimeRanges) { + this.item = item; + this.newlyCoveredTimeRanges = newlyCoveredTimeRanges; + } + + @Override + protected void load() { loadFixesForNewlyCoveredTimeRanges(item, newlyCoveredTimeRanges); } } diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/fixtracker/RegattaLogDeviceMappings.java b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/fixtracker/RegattaLogDeviceMappings.java index 619828ed477..c7d23210455 100644 --- a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/fixtracker/RegattaLogDeviceMappings.java +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/fixtracker/RegattaLogDeviceMappings.java @@ -133,7 +133,7 @@ public abstract class RegattaLogDeviceMappings { }); } - protected void updateMappings() { + private void updateMappings() { try { updateMappingsInternal(); } catch (Exception e) { @@ -245,10 +245,10 @@ public abstract class RegattaLogDeviceMappings { } } newDeviceIds.addAll(mappingsByDevice.keySet()); + calculateDiff(oldMappings, newMappings, oldDeviceIds, newDeviceIds); } finally { LockUtil.unlockAfterWrite(mappingsLock); } - calculateDiff(oldMappings, newMappings, oldDeviceIds, newDeviceIds); } /** diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/TrackedRaceStatusUpdateTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/TrackedRaceStatusUpdateTest.java new file mode 100755 index 00000000000..1bb230d7ddb --- /dev/null +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/TrackedRaceStatusUpdateTest.java @@ -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()); + } +} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/DynamicTrackedRace.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/DynamicTrackedRace.java index 25f67f3f09e..b955bcfa34d 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/DynamicTrackedRace.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/DynamicTrackedRace.java @@ -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 wind fix into a {@link WindTrack} for the windSource 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 - * applyFilter to false. - */ - 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 getTrack(Competitor competitor); - - /** - * Yields the track describing mark's movement over time; never null because a - * new track will be created in case no track was present for mark so far. - */ - DynamicGPSFixTrack 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 - */ - > TrackT getOrCreateSensorTrack( - Competitor competitor, String trackName, TrackFactory 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 null if there is none. - */ - > TrackT getDynamicSensorTrack( - Competitor competitor, String trackName); - - void recordSensorFix(Competitor competitor, String trackName, SensorFix fix, boolean onlyWhenInTrackingTimeInterval); - - /** - * Updates all mark passings for competitor 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 competitor and ensures that the "leaderboard" and all other derived information - * are updated accordingly. - *

- * - * 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.

- */ - void updateMarkPassings(Competitor competitor, Iterable 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 null 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 raceIsKnownToStartUpwind is true, 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 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 wind fix into a {@link WindTrack} for the windSource 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 + * applyFilter to false. + */ + 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 getTrack(Competitor competitor); + + /** + * Yields the track describing mark's movement over time; never null because a + * new track will be created in case no track was present for mark so far. + */ + DynamicGPSFixTrack 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 + */ + > TrackT getOrCreateSensorTrack( + Competitor competitor, String trackName, TrackFactory 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 null if there is none. + */ + > TrackT getDynamicSensorTrack( + Competitor competitor, String trackName); + + void recordSensorFix(Competitor competitor, String trackName, SensorFix fix, boolean onlyWhenInTrackingTimeInterval); + + /** + * Updates all mark passings for competitor 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 competitor and ensures that the "leaderboard" and all other derived information + * are updated accordingly. + *

+ * + * 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.

+ */ + void updateMarkPassings(Competitor competitor, Iterable 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 null 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 raceIsKnownToStartUpwind is true, 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: + *

    + *
  • The {@link TrackedRace} is initially in the state PREPARED
  • + *
  • If any loader's state is ERROR, this will also be the case for the {@link TrackedRace}
  • + *
  • 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)
  • + *
  • Otherwise: If all laoders are in PREPARED state, this will also be the case for the {@link TrackedRace}
  • + *
  • Otherwise: The {@link TrackedRace} is in TRACKING state
  • + *
+ * + * @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 track); +} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java index 5038ee774b9..9b223f78af5 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java @@ -737,6 +737,17 @@ public interface TrackedRace extends Serializable, IsManagedByCache + * 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(); /** diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DynamicTrackedRaceImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DynamicTrackedRaceImpl.java index cc0ec94132d..b77bfe4e49a 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DynamicTrackedRaceImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DynamicTrackedRaceImpl.java @@ -223,27 +223,30 @@ DynamicTrackedRace, GPSTrackListener { 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) {