From adec8c11fd7634a7c87f79f8298ac14e69e049a0 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Thu, 4 May 2017 15:36:53 +0200 Subject: [PATCH 01/16] bug4125: Changed FixLoaderAndTracker to track the loading state for every loading process. In addition, fixes are now loaded in parallel for each item. This has multiple effects: * the race will switch to loading state every time tracks are loaded (e.g. tracking times change or mapping added) * loading progress isn't always 0%, 50% or 100% --- .../impl/fixtracker/FixLoaderAndTracker.java | 181 ++-- .../fixtracker/RegattaLogDeviceMappings.java | 792 +++++++++--------- 2 files changed, 516 insertions(+), 457 deletions(-) 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..1d3fa81d35d 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,7 @@ public class FixLoaderAndTracker implements TrackingDataLoader { protected final DynamicTrackedRace trackedRace; private final SensorFixStore sensorFixStore; private RegattaLogDeviceMappings deviceMappings; - private final AtomicInteger activeLoaders = new AtomicInteger(); + private final Set loadingJobs = ConcurrentHashMap.newKeySet(); private final SensorFixMapperFactory sensorFixMapperFactory; private AtomicBoolean preemptiveStopRequested = new AtomicBoolean(false); private AtomicBoolean stopRequested = new AtomicBoolean(false); @@ -450,8 +453,8 @@ public class FixLoaderAndTracker implements TrackingDataLoader { 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 +465,86 @@ 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)); } + private void updateStatusAndProgress() { + try { + synchronized (loadingJobs) { + updateStatusAndProgressWithExistingLock(); + } + } catch (Exception e) { + logger.log(Level.WARNING, "Error while updating status and progress for FixLoaderAndTracker", e); + } + } + + private void updateStatusAndProgressWithExistingLock() { + 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); + } + + private void addLoadingJob(AbstractLoadingJob job) { + synchronized (loadingJobs) { + addLoadingJobWithExistingLock(job); + updateStatusAndProgressWithExistingLock(); + } + ThreadPoolUtil.INSTANCE.getDefaultForegroundTaskThreadPoolExecutor().execute(job); + } + + private void addLoadingJobWithExistingLock(AbstractLoadingJob job) { + loadingJobs.add(job); + } + 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 +558,60 @@ public class FixLoaderAndTracker implements TrackingDataLoader { @Override protected void newTimeRangesCovered(WithID item, Map, MultiTimeRange> newlyCoveredTimeRanges) { + addLoadingJob(new LoadFixesForNewlyCoveredTimeRangesJob(item, newlyCoveredTimeRanges)); + } + } + + private abstract class AbstractLoadingJob implements Runnable { + double progress = 0; + boolean finished = false; + + @Override + public final void run() { + progress = 0.5; + updateStatusAndProgress(); + + try { + load(); + } finally { + progress = 1.0; + finished = true; + updateStatusAndProgress(); + } + } + + protected abstract void load(); + } + + 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); + } + } + + 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 966b31a1760..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 @@ -1,396 +1,396 @@ -package com.sap.sailing.domain.racelogtracking.impl.fixtracker; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.function.BiConsumer; -import java.util.function.Consumer; -import java.util.logging.Level; -import java.util.logging.Logger; - -import com.sap.sailing.domain.abstractlog.regatta.RegattaLog; -import com.sap.sailing.domain.abstractlog.regatta.RegattaLogEventVisitor; -import com.sap.sailing.domain.abstractlog.regatta.events.RegattaLogCloseOpenEndedDeviceMappingEvent; -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; -import com.sap.sailing.domain.abstractlog.regatta.events.RegattaLogDeviceMarkMappingEvent; -import com.sap.sailing.domain.abstractlog.regatta.events.RegattaLogRevokeEvent; -import com.sap.sailing.domain.abstractlog.regatta.impl.BaseRegattaLogEventVisitor; -import com.sap.sailing.domain.abstractlog.regatta.tracking.analyzing.impl.RegattaLogDeviceMappingFinder; -import com.sap.sailing.domain.common.racelog.tracking.DoesNotHaveRegattaLogException; -import com.sap.sailing.domain.racelogtracking.DeviceIdentifier; -import com.sap.sailing.domain.racelogtracking.DeviceMapping; -import com.sap.sailing.domain.racelogtracking.DeviceMappingWithRegattaLogEvent; -import com.sap.sailing.domain.tracking.DynamicTrack; -import com.sap.sse.common.MultiTimeRange; -import com.sap.sse.common.TimePoint; -import com.sap.sse.common.TimeRange; -import com.sap.sse.common.Timed; -import com.sap.sse.common.Util; -import com.sap.sse.common.Util.Pair; -import com.sap.sse.common.WithID; -import com.sap.sse.common.impl.MultiTimeRangeImpl; -import com.sap.sse.concurrent.LockUtil; -import com.sap.sse.concurrent.NamedReentrantReadWriteLock; - -/** - * Holds DeviceMappings to make it possible to track changes. This makes it possible to only process mappings that - * weren't processed before. - * - * @param The type of items the DeviceMappings are mapped to - */ -public abstract class RegattaLogDeviceMappings { - private static final Logger logger = Logger.getLogger(RegattaLogDeviceMappings.class.getName()); - /** - * We maintain our own collection that holds the RegattaLogs. The known RegattaLogs should by in sync with the ones that - * can be obtained from the TrackedRace. When stopping, there could be a concurrency issue that leads to a listener - * not being removed. This is prevented by remembering all RegattaLogs to which we attached a listener. So we can be - * sure to not produce a memory leak. - */ - private final Set knownRegattaLogs = new HashSet<>(); - - /** - * Lock object to be used when accessing {@link #knownRegattaLogs}. - */ - private final NamedReentrantReadWriteLock knownRegattaLogsLock; - - /** - * Lock object to be used when accessing {@link #mappings} or {@link #mappingsByDevice}. - */ - private final NamedReentrantReadWriteLock mappingsLock; - - private final Map>> mappings = new HashMap<>(); - private final Map>> mappingsByDevice = new HashMap<>(); - - private final RegattaLogEventVisitor regattaLogEventVisitor = new BaseRegattaLogEventVisitor() { - @Override - public void visit(RegattaLogDeviceCompetitorSensorDataMappingEvent event) { - logger.log(Level.FINE, "New mapping for: " + event.getMappedTo() + "; device: " + event.getDevice()); - updateMappings(); - } - - @Override - public void visit(RegattaLogDeviceMarkMappingEvent event) { - logger.log(Level.FINE, - "New DeviceMarkMapping for: " + event.getMappedTo() + "; device: " + event.getDevice()); - updateMappings(); - } - - @Override - public void visit(RegattaLogDeviceCompetitorMappingEvent event) { - logger.log(Level.FINE, - "New DeviceCompetitorMapping for: " + event.getMappedTo() + "; device: " + event.getDevice()); - updateMappings(); - } - - @Override - public void visit(RegattaLogCloseOpenEndedDeviceMappingEvent event) { - logger.log(Level.FINE, "CloseOpenEndedDeviceMapping closed: " + event.getDeviceMappingEventId()); - updateMappings(); - } - - @Override - public void visit(RegattaLogRevokeEvent event) { - logger.log(Level.FINE, "Mapping revoked for: " + event.getRevokedEventId()); - updateMappings(); - }; - }; - - public RegattaLogDeviceMappings(Iterable initialRegattaLogs, String raceNameForLock) { - mappingsLock = new NamedReentrantReadWriteLock("DeviceMapping lock for race " + raceNameForLock, false); - knownRegattaLogsLock = new NamedReentrantReadWriteLock("Lock for known RegattaLogs of race " + raceNameForLock, false); - final boolean hasRegattaLogs; - LockUtil.lockForWrite(knownRegattaLogsLock); - try { - initialRegattaLogs.forEach(this::addRegattaLogUnlocked); - hasRegattaLogs = !knownRegattaLogs.isEmpty(); - } finally { - LockUtil.unlockAfterWrite(knownRegattaLogsLock); - } - if (hasRegattaLogs) { - updateMappings(); - } - } - - public void addRegattaLog(RegattaLog regattaLog) { - LockUtil.executeWithWriteLock(knownRegattaLogsLock, () -> addRegattaLogUnlocked(regattaLog)); - updateMappings(); - } - - private void addRegattaLogUnlocked(RegattaLog log) { - log.addListener(regattaLogEventVisitor); - knownRegattaLogs.add(log); - } - - public void stop() { - LockUtil.executeWithWriteLock(knownRegattaLogsLock, () -> { - knownRegattaLogs.forEach((log) -> log.removeListener(regattaLogEventVisitor)); - }); - } - - protected void updateMappings() { - try { - updateMappingsInternal(); - } catch (Exception e) { - logger.log(Level.WARNING, "Could not update device mappings", e); - } - } - - /** - * Calls the given callback for every DeviceMapping that is known for the given {@link DeviceIdentifier} that - * includes the given {@link TimePoint}. - * - * @param device - * the device to get the mappings for - * @param timePoint - * the TimePoint to check DeviceMappings for - * @param callback - * the callback to call for every known mapping of the given {@link DeviceIdentifier} that includes the - * given {@link TimePoint}. - */ - public void forEachMappingOfDeviceIncludingTimePoint(DeviceIdentifier device, TimePoint timePoint, - Consumer> callback) { - LockUtil.executeWithReadLock(mappingsLock, () -> { - List> mappingsForDevice = mappingsByDevice.get(device); - if (mappingsForDevice != null) { - for (DeviceMappingWithRegattaLogEvent mapping : mappingsForDevice) { - if (mapping.getTimeRange().includes(timePoint)) { - callback.accept(mapping); - } - } - } - }); - } - - public void forEachItemAndCoveredTimeRanges(final BiConsumer, MultiTimeRange>> consumer) { - HashMap>> allMappings = LockUtil.executeWithReadLockAndResult(mappingsLock, () -> new HashMap<>(mappings)); - allMappings.forEach((item, mappings) -> { - final Map, MultiTimeRange> coveredTimeRanges = calculateCoveredTimeRanges(mappings); - consumer.accept(item, coveredTimeRanges); - }); - } - - /** - * Calculates an association of mapping events to the covered {@link MultiTimeRange}. - */ - private Map, MultiTimeRange> calculateCoveredTimeRanges( - final List> mappingsForItem) { - final Map, MultiTimeRange> coveredTimeRanges = new HashMap<>(); - if(mappingsForItem != null) { - Map>, Iterable>> groupedMappings = groupMappingsByDeviceIdAndMappingType(mappingsForItem); - groupedMappings.entrySet().forEach(entry -> { - Iterable> mappingsForDeviceIdAndMappingType = entry.getValue(); - final MultiTimeRange coveredTimeRange = getCoveredTimeRange(mappingsForDeviceIdAndMappingType); - if(!coveredTimeRange.isEmpty()) { - coveredTimeRanges.put(Util.get(mappingsForDeviceIdAndMappingType, 0).getRegattaLogEvent(), coveredTimeRange); - } - }); - } - return coveredTimeRanges; - } - - /** - * To be implemented by subclasses to calculate the current {@link DeviceMapping}s. - * - * @return All currently known DeviceMappings - */ - protected Map>> calculateMappings() { - Map>> result = new HashMap<>(); - forEachRegattaLog( - (log) -> result.putAll(new RegattaLogDeviceMappingFinder(log).analyze())); - return result; - } - - protected void forEachRegattaLog(Consumer regattaLogConsumer) { - LockUtil.executeWithReadLock(knownRegattaLogsLock, () -> knownRegattaLogs.forEach(regattaLogConsumer)); - } - - /** - * Adjusts the {@link #mappings} map according to the device mappings provided from the {@link #calculateMappings()} - * method. Afterwards, the start end end of tracking is {@link #updateStartAndEndOfTracking() updated} from the - * mapping intervals. - * - * @param loadIfNotCovered - * if true, the fixes for the mappings will be loaded based on a comparison of the previous - * mappings and the new mappings. - * - * @throws DoesNotHaveRegattaLogException - */ - private final > void updateMappingsInternal() { - final Map>> newMappings; - final Map>> oldMappings = new HashMap<>(); - final Set oldDeviceIds = new HashSet<>(); - final Set newDeviceIds = new HashSet<>(); - LockUtil.lockForWrite(mappingsLock); - try { - newMappings = calculateMappings(); - oldMappings.putAll(mappings); - oldDeviceIds.addAll(mappingsByDevice.keySet()); - mappings.clear(); - mappings.putAll(newMappings); - mappingsByDevice.clear(); - for (ItemT item : newMappings.keySet()) { - for (DeviceMappingWithRegattaLogEvent mapping : newMappings.get(item)) { - List> list = mappingsByDevice.get(mapping.getDevice()); - if (list == null) { - list = new ArrayList<>(); - mappingsByDevice.put(mapping.getDevice(), list); - } - list.add(mapping); - } - } - newDeviceIds.addAll(mappingsByDevice.keySet()); - } finally { - LockUtil.unlockAfterWrite(mappingsLock); - } - calculateDiff(oldMappings, newMappings, oldDeviceIds, newDeviceIds); - } - - /** - * Calculates and applies the mapping changes by removing listeners no longer needed for the mappings removed, - * and by loading and adding the fixes for extended or added mappings. - */ - private void calculateDiff(Map>> previousMappings, - Map>> newMappings, - Set oldDeviceIds, Set newDeviceIds) { - final Set removedDeviceIds = new HashSet<>(oldDeviceIds); - removedDeviceIds.removeAll(newDeviceIds); - removedDeviceIds.forEach(this::deviceIdRemovedInternal); - - final Set addedDeviceIds = new HashSet<>(newDeviceIds); - addedDeviceIds.removeAll(oldDeviceIds); - addedDeviceIds.forEach(this::deviceIdAddedInternal); - - // Only deviceIdentifier and mappingTypes are covered that are contained in newMappings. - // Those that are only found in oldMappings won't lead to new covered TimeRanges. - // DeviceIdentifiers, that aren't needed at all are already handled above. - newMappings.forEach((item, mappingsForItem) -> { - final Map, MultiTimeRange> newlyCoveredTimeRanges = new HashMap<>(); - - // The mappings are processes grouped by DeviceIdentifier and mapping type - // to build a consistent overall mapping update without the risk of fixes - // to get loaded multiple times - this.processNewAndChangedMappingsByDeviceIdAndEventType(previousMappings.get(item), mappingsForItem, - (deviceIdentifier, mappingType, oldMappingsForDeviceIdAndMappingType, - newMappingsForDeviceIdAndMappingType) -> { - assert (newMappingsForDeviceIdAndMappingType != null); - assert (!Util.isEmpty(newMappingsForDeviceIdAndMappingType)); - - final MultiTimeRange newCoveredTimeRanges = getCoveredTimeRange( - newMappingsForDeviceIdAndMappingType) - .subtract(getCoveredTimeRange(oldMappingsForDeviceIdAndMappingType)); - if (!newCoveredTimeRanges.isEmpty()) { - newlyCoveredTimeRanges.put( - Util.get(newMappingsForDeviceIdAndMappingType, 0).getRegattaLogEvent(), - newCoveredTimeRanges); - } - }); - if (!newlyCoveredTimeRanges.isEmpty()) { - // all updates for one item are handled at once - newTimeRangesCoveredInternal(item, newlyCoveredTimeRanges); - } - }); - } - - /** - * Processes the old and new mappings for mapping update by {@link DeviceIdentifier} and mapping type (class of the - * mapping event). the old and new mappings are first grouped by the already mentioned criteria. For each group in - * the new mappings, the associated group in the old mappings is identified. For these pairs, the given callback is - * called. Groups only found in the old mappings are explicitly not handled because these can't lead to new covered - * time ranges. - */ - private void processNewAndChangedMappingsByDeviceIdAndEventType( - Iterable> oldMappings, - Iterable> newMappings, - GroupedOldAndNewMappingsCallback callback) { - - final Map>, Iterable>> groupedOldMappings = groupMappingsByDeviceIdAndMappingType( - oldMappings != null ? oldMappings : Collections.emptySet()); - final Map>, Iterable>> groupedNewMappings = groupMappingsByDeviceIdAndMappingType( - newMappings); - - groupedNewMappings.forEach((key, newMappingsForDeviceIdAndMappingType) -> { - Iterable> oldMappingsForDeviceIdAndMappingType = groupedOldMappings - .get(key); - callback.process( - key.getA(), key.getB(), oldMappingsForDeviceIdAndMappingType != null - ? oldMappingsForDeviceIdAndMappingType : Collections.emptySet(), - newMappingsForDeviceIdAndMappingType); - }); - } - - /** - * Groups the given {@link DeviceMapping}s by pairs of {@link DeviceIdentifier} and mapping type (class of the - * mapping event). - */ - private Map>, Iterable>> groupMappingsByDeviceIdAndMappingType( - Iterable> mappings) { - return Util.group(mappings, value -> new Pair<>(value.getDevice(), value.getEventType()), HashSet::new); - } - - /** - * Calculates the {@link MultiTimeRange} as union of the {@link TimeRange}s of the given mappings. - */ - private MultiTimeRange getCoveredTimeRange(Iterable> mappings) { - MultiTimeRange result = new MultiTimeRangeImpl(); - for (DeviceMappingWithRegattaLogEvent mapping : mappings) { - result = result.union(mapping.getTimeRange()); - } - return result; - } - - /** - * Internal callback interface for {@link #processNewAndChangedMappingsByDeviceIdAndEventType}. - */ - private interface GroupedOldAndNewMappingsCallback { - void process(DeviceIdentifier deviceIdentifier, Class mappingType, - Iterable> oldMappings, - Iterable> newMappings); - } - - /** - * Called when at least one mapping for a previously not available {@link DeviceIdentifier} was added. - */ - protected abstract void deviceIdAdded(DeviceIdentifier deviceIdentifier); - - private void deviceIdAddedInternal(DeviceIdentifier deviceIdentifier) { - try { - deviceIdAdded(deviceIdentifier); - } catch (Exception e) { - logger.log(Level.SEVERE, "error while adding deviceIdentifier " + deviceIdentifier, e); - } - } - - /** - * Called when the last available mapping for a {@link DeviceIdentifier} was removed. - */ - protected abstract void deviceIdRemoved(DeviceIdentifier deviceIdentifier); - - private void deviceIdRemovedInternal(DeviceIdentifier deviceIdentifier) { - try { - deviceIdRemoved(deviceIdentifier); - } catch (Exception e) { - logger.log(Level.SEVERE, "error while removing deviceIdentifier " + deviceIdentifier, e); - } - } - - /** - * Called when a new MultiTimeRange could be identified that is now covered by the mappings for a item, - * DeviceIdentifier and mappingType. - */ - protected abstract void newTimeRangesCovered(ItemT item, Map, MultiTimeRange> newlyCoveredTimeRanges); - - private void newTimeRangesCoveredInternal(ItemT item, Map, MultiTimeRange> newlyCoveredTimeRanges) { - try { - newTimeRangesCovered(item, newlyCoveredTimeRanges); - } catch (Exception e) { - logger.log(Level.SEVERE, - "error while calling newTimeRangeCovered for item " + item, e); - } - } -} +package com.sap.sailing.domain.racelogtracking.impl.fixtracker; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.sap.sailing.domain.abstractlog.regatta.RegattaLog; +import com.sap.sailing.domain.abstractlog.regatta.RegattaLogEventVisitor; +import com.sap.sailing.domain.abstractlog.regatta.events.RegattaLogCloseOpenEndedDeviceMappingEvent; +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; +import com.sap.sailing.domain.abstractlog.regatta.events.RegattaLogDeviceMarkMappingEvent; +import com.sap.sailing.domain.abstractlog.regatta.events.RegattaLogRevokeEvent; +import com.sap.sailing.domain.abstractlog.regatta.impl.BaseRegattaLogEventVisitor; +import com.sap.sailing.domain.abstractlog.regatta.tracking.analyzing.impl.RegattaLogDeviceMappingFinder; +import com.sap.sailing.domain.common.racelog.tracking.DoesNotHaveRegattaLogException; +import com.sap.sailing.domain.racelogtracking.DeviceIdentifier; +import com.sap.sailing.domain.racelogtracking.DeviceMapping; +import com.sap.sailing.domain.racelogtracking.DeviceMappingWithRegattaLogEvent; +import com.sap.sailing.domain.tracking.DynamicTrack; +import com.sap.sse.common.MultiTimeRange; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.TimeRange; +import com.sap.sse.common.Timed; +import com.sap.sse.common.Util; +import com.sap.sse.common.Util.Pair; +import com.sap.sse.common.WithID; +import com.sap.sse.common.impl.MultiTimeRangeImpl; +import com.sap.sse.concurrent.LockUtil; +import com.sap.sse.concurrent.NamedReentrantReadWriteLock; + +/** + * Holds DeviceMappings to make it possible to track changes. This makes it possible to only process mappings that + * weren't processed before. + * + * @param The type of items the DeviceMappings are mapped to + */ +public abstract class RegattaLogDeviceMappings { + private static final Logger logger = Logger.getLogger(RegattaLogDeviceMappings.class.getName()); + /** + * We maintain our own collection that holds the RegattaLogs. The known RegattaLogs should by in sync with the ones that + * can be obtained from the TrackedRace. When stopping, there could be a concurrency issue that leads to a listener + * not being removed. This is prevented by remembering all RegattaLogs to which we attached a listener. So we can be + * sure to not produce a memory leak. + */ + private final Set knownRegattaLogs = new HashSet<>(); + + /** + * Lock object to be used when accessing {@link #knownRegattaLogs}. + */ + private final NamedReentrantReadWriteLock knownRegattaLogsLock; + + /** + * Lock object to be used when accessing {@link #mappings} or {@link #mappingsByDevice}. + */ + private final NamedReentrantReadWriteLock mappingsLock; + + private final Map>> mappings = new HashMap<>(); + private final Map>> mappingsByDevice = new HashMap<>(); + + private final RegattaLogEventVisitor regattaLogEventVisitor = new BaseRegattaLogEventVisitor() { + @Override + public void visit(RegattaLogDeviceCompetitorSensorDataMappingEvent event) { + logger.log(Level.FINE, "New mapping for: " + event.getMappedTo() + "; device: " + event.getDevice()); + updateMappings(); + } + + @Override + public void visit(RegattaLogDeviceMarkMappingEvent event) { + logger.log(Level.FINE, + "New DeviceMarkMapping for: " + event.getMappedTo() + "; device: " + event.getDevice()); + updateMappings(); + } + + @Override + public void visit(RegattaLogDeviceCompetitorMappingEvent event) { + logger.log(Level.FINE, + "New DeviceCompetitorMapping for: " + event.getMappedTo() + "; device: " + event.getDevice()); + updateMappings(); + } + + @Override + public void visit(RegattaLogCloseOpenEndedDeviceMappingEvent event) { + logger.log(Level.FINE, "CloseOpenEndedDeviceMapping closed: " + event.getDeviceMappingEventId()); + updateMappings(); + } + + @Override + public void visit(RegattaLogRevokeEvent event) { + logger.log(Level.FINE, "Mapping revoked for: " + event.getRevokedEventId()); + updateMappings(); + }; + }; + + public RegattaLogDeviceMappings(Iterable initialRegattaLogs, String raceNameForLock) { + mappingsLock = new NamedReentrantReadWriteLock("DeviceMapping lock for race " + raceNameForLock, false); + knownRegattaLogsLock = new NamedReentrantReadWriteLock("Lock for known RegattaLogs of race " + raceNameForLock, false); + final boolean hasRegattaLogs; + LockUtil.lockForWrite(knownRegattaLogsLock); + try { + initialRegattaLogs.forEach(this::addRegattaLogUnlocked); + hasRegattaLogs = !knownRegattaLogs.isEmpty(); + } finally { + LockUtil.unlockAfterWrite(knownRegattaLogsLock); + } + if (hasRegattaLogs) { + updateMappings(); + } + } + + public void addRegattaLog(RegattaLog regattaLog) { + LockUtil.executeWithWriteLock(knownRegattaLogsLock, () -> addRegattaLogUnlocked(regattaLog)); + updateMappings(); + } + + private void addRegattaLogUnlocked(RegattaLog log) { + log.addListener(regattaLogEventVisitor); + knownRegattaLogs.add(log); + } + + public void stop() { + LockUtil.executeWithWriteLock(knownRegattaLogsLock, () -> { + knownRegattaLogs.forEach((log) -> log.removeListener(regattaLogEventVisitor)); + }); + } + + private void updateMappings() { + try { + updateMappingsInternal(); + } catch (Exception e) { + logger.log(Level.WARNING, "Could not update device mappings", e); + } + } + + /** + * Calls the given callback for every DeviceMapping that is known for the given {@link DeviceIdentifier} that + * includes the given {@link TimePoint}. + * + * @param device + * the device to get the mappings for + * @param timePoint + * the TimePoint to check DeviceMappings for + * @param callback + * the callback to call for every known mapping of the given {@link DeviceIdentifier} that includes the + * given {@link TimePoint}. + */ + public void forEachMappingOfDeviceIncludingTimePoint(DeviceIdentifier device, TimePoint timePoint, + Consumer> callback) { + LockUtil.executeWithReadLock(mappingsLock, () -> { + List> mappingsForDevice = mappingsByDevice.get(device); + if (mappingsForDevice != null) { + for (DeviceMappingWithRegattaLogEvent mapping : mappingsForDevice) { + if (mapping.getTimeRange().includes(timePoint)) { + callback.accept(mapping); + } + } + } + }); + } + + public void forEachItemAndCoveredTimeRanges(final BiConsumer, MultiTimeRange>> consumer) { + HashMap>> allMappings = LockUtil.executeWithReadLockAndResult(mappingsLock, () -> new HashMap<>(mappings)); + allMappings.forEach((item, mappings) -> { + final Map, MultiTimeRange> coveredTimeRanges = calculateCoveredTimeRanges(mappings); + consumer.accept(item, coveredTimeRanges); + }); + } + + /** + * Calculates an association of mapping events to the covered {@link MultiTimeRange}. + */ + private Map, MultiTimeRange> calculateCoveredTimeRanges( + final List> mappingsForItem) { + final Map, MultiTimeRange> coveredTimeRanges = new HashMap<>(); + if(mappingsForItem != null) { + Map>, Iterable>> groupedMappings = groupMappingsByDeviceIdAndMappingType(mappingsForItem); + groupedMappings.entrySet().forEach(entry -> { + Iterable> mappingsForDeviceIdAndMappingType = entry.getValue(); + final MultiTimeRange coveredTimeRange = getCoveredTimeRange(mappingsForDeviceIdAndMappingType); + if(!coveredTimeRange.isEmpty()) { + coveredTimeRanges.put(Util.get(mappingsForDeviceIdAndMappingType, 0).getRegattaLogEvent(), coveredTimeRange); + } + }); + } + return coveredTimeRanges; + } + + /** + * To be implemented by subclasses to calculate the current {@link DeviceMapping}s. + * + * @return All currently known DeviceMappings + */ + protected Map>> calculateMappings() { + Map>> result = new HashMap<>(); + forEachRegattaLog( + (log) -> result.putAll(new RegattaLogDeviceMappingFinder(log).analyze())); + return result; + } + + protected void forEachRegattaLog(Consumer regattaLogConsumer) { + LockUtil.executeWithReadLock(knownRegattaLogsLock, () -> knownRegattaLogs.forEach(regattaLogConsumer)); + } + + /** + * Adjusts the {@link #mappings} map according to the device mappings provided from the {@link #calculateMappings()} + * method. Afterwards, the start end end of tracking is {@link #updateStartAndEndOfTracking() updated} from the + * mapping intervals. + * + * @param loadIfNotCovered + * if true, the fixes for the mappings will be loaded based on a comparison of the previous + * mappings and the new mappings. + * + * @throws DoesNotHaveRegattaLogException + */ + private final > void updateMappingsInternal() { + final Map>> newMappings; + final Map>> oldMappings = new HashMap<>(); + final Set oldDeviceIds = new HashSet<>(); + final Set newDeviceIds = new HashSet<>(); + LockUtil.lockForWrite(mappingsLock); + try { + newMappings = calculateMappings(); + oldMappings.putAll(mappings); + oldDeviceIds.addAll(mappingsByDevice.keySet()); + mappings.clear(); + mappings.putAll(newMappings); + mappingsByDevice.clear(); + for (ItemT item : newMappings.keySet()) { + for (DeviceMappingWithRegattaLogEvent mapping : newMappings.get(item)) { + List> list = mappingsByDevice.get(mapping.getDevice()); + if (list == null) { + list = new ArrayList<>(); + mappingsByDevice.put(mapping.getDevice(), list); + } + list.add(mapping); + } + } + newDeviceIds.addAll(mappingsByDevice.keySet()); + calculateDiff(oldMappings, newMappings, oldDeviceIds, newDeviceIds); + } finally { + LockUtil.unlockAfterWrite(mappingsLock); + } + } + + /** + * Calculates and applies the mapping changes by removing listeners no longer needed for the mappings removed, + * and by loading and adding the fixes for extended or added mappings. + */ + private void calculateDiff(Map>> previousMappings, + Map>> newMappings, + Set oldDeviceIds, Set newDeviceIds) { + final Set removedDeviceIds = new HashSet<>(oldDeviceIds); + removedDeviceIds.removeAll(newDeviceIds); + removedDeviceIds.forEach(this::deviceIdRemovedInternal); + + final Set addedDeviceIds = new HashSet<>(newDeviceIds); + addedDeviceIds.removeAll(oldDeviceIds); + addedDeviceIds.forEach(this::deviceIdAddedInternal); + + // Only deviceIdentifier and mappingTypes are covered that are contained in newMappings. + // Those that are only found in oldMappings won't lead to new covered TimeRanges. + // DeviceIdentifiers, that aren't needed at all are already handled above. + newMappings.forEach((item, mappingsForItem) -> { + final Map, MultiTimeRange> newlyCoveredTimeRanges = new HashMap<>(); + + // The mappings are processes grouped by DeviceIdentifier and mapping type + // to build a consistent overall mapping update without the risk of fixes + // to get loaded multiple times + this.processNewAndChangedMappingsByDeviceIdAndEventType(previousMappings.get(item), mappingsForItem, + (deviceIdentifier, mappingType, oldMappingsForDeviceIdAndMappingType, + newMappingsForDeviceIdAndMappingType) -> { + assert (newMappingsForDeviceIdAndMappingType != null); + assert (!Util.isEmpty(newMappingsForDeviceIdAndMappingType)); + + final MultiTimeRange newCoveredTimeRanges = getCoveredTimeRange( + newMappingsForDeviceIdAndMappingType) + .subtract(getCoveredTimeRange(oldMappingsForDeviceIdAndMappingType)); + if (!newCoveredTimeRanges.isEmpty()) { + newlyCoveredTimeRanges.put( + Util.get(newMappingsForDeviceIdAndMappingType, 0).getRegattaLogEvent(), + newCoveredTimeRanges); + } + }); + if (!newlyCoveredTimeRanges.isEmpty()) { + // all updates for one item are handled at once + newTimeRangesCoveredInternal(item, newlyCoveredTimeRanges); + } + }); + } + + /** + * Processes the old and new mappings for mapping update by {@link DeviceIdentifier} and mapping type (class of the + * mapping event). the old and new mappings are first grouped by the already mentioned criteria. For each group in + * the new mappings, the associated group in the old mappings is identified. For these pairs, the given callback is + * called. Groups only found in the old mappings are explicitly not handled because these can't lead to new covered + * time ranges. + */ + private void processNewAndChangedMappingsByDeviceIdAndEventType( + Iterable> oldMappings, + Iterable> newMappings, + GroupedOldAndNewMappingsCallback callback) { + + final Map>, Iterable>> groupedOldMappings = groupMappingsByDeviceIdAndMappingType( + oldMappings != null ? oldMappings : Collections.emptySet()); + final Map>, Iterable>> groupedNewMappings = groupMappingsByDeviceIdAndMappingType( + newMappings); + + groupedNewMappings.forEach((key, newMappingsForDeviceIdAndMappingType) -> { + Iterable> oldMappingsForDeviceIdAndMappingType = groupedOldMappings + .get(key); + callback.process( + key.getA(), key.getB(), oldMappingsForDeviceIdAndMappingType != null + ? oldMappingsForDeviceIdAndMappingType : Collections.emptySet(), + newMappingsForDeviceIdAndMappingType); + }); + } + + /** + * Groups the given {@link DeviceMapping}s by pairs of {@link DeviceIdentifier} and mapping type (class of the + * mapping event). + */ + private Map>, Iterable>> groupMappingsByDeviceIdAndMappingType( + Iterable> mappings) { + return Util.group(mappings, value -> new Pair<>(value.getDevice(), value.getEventType()), HashSet::new); + } + + /** + * Calculates the {@link MultiTimeRange} as union of the {@link TimeRange}s of the given mappings. + */ + private MultiTimeRange getCoveredTimeRange(Iterable> mappings) { + MultiTimeRange result = new MultiTimeRangeImpl(); + for (DeviceMappingWithRegattaLogEvent mapping : mappings) { + result = result.union(mapping.getTimeRange()); + } + return result; + } + + /** + * Internal callback interface for {@link #processNewAndChangedMappingsByDeviceIdAndEventType}. + */ + private interface GroupedOldAndNewMappingsCallback { + void process(DeviceIdentifier deviceIdentifier, Class mappingType, + Iterable> oldMappings, + Iterable> newMappings); + } + + /** + * Called when at least one mapping for a previously not available {@link DeviceIdentifier} was added. + */ + protected abstract void deviceIdAdded(DeviceIdentifier deviceIdentifier); + + private void deviceIdAddedInternal(DeviceIdentifier deviceIdentifier) { + try { + deviceIdAdded(deviceIdentifier); + } catch (Exception e) { + logger.log(Level.SEVERE, "error while adding deviceIdentifier " + deviceIdentifier, e); + } + } + + /** + * Called when the last available mapping for a {@link DeviceIdentifier} was removed. + */ + protected abstract void deviceIdRemoved(DeviceIdentifier deviceIdentifier); + + private void deviceIdRemovedInternal(DeviceIdentifier deviceIdentifier) { + try { + deviceIdRemoved(deviceIdentifier); + } catch (Exception e) { + logger.log(Level.SEVERE, "error while removing deviceIdentifier " + deviceIdentifier, e); + } + } + + /** + * Called when a new MultiTimeRange could be identified that is now covered by the mappings for a item, + * DeviceIdentifier and mappingType. + */ + protected abstract void newTimeRangesCovered(ItemT item, Map, MultiTimeRange> newlyCoveredTimeRanges); + + private void newTimeRangesCoveredInternal(ItemT item, Map, MultiTimeRange> newlyCoveredTimeRanges) { + try { + newTimeRangesCovered(item, newlyCoveredTimeRanges); + } catch (Exception e) { + logger.log(Level.SEVERE, + "error while calling newTimeRangeCovered for item " + item, e); + } + } +} From c2ecceb20f6c35a9fda2fa06267ab521913c24e5 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Fri, 5 May 2017 08:14:31 +0200 Subject: [PATCH 02/16] Fixed indeterministic test failures by adding waitForLoadingToFinish calls (loading of fixes on tracking times change used to be synchronous and is now done via executor) --- .../racelogtracking/test/impl/TrackedRaceLoadsFixesTest.java | 2 ++ 1 file changed, 2 insertions(+) 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 100644 --- 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); } From 8be33f9794c4ba0ef4875aaaa9d952dba5cfb0af Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Fri, 5 May 2017 10:39:47 +0200 Subject: [PATCH 03/16] Added failing test for newly introduced regression that makes a preemptive stop to hang infinitely --- .../test/impl/SensorFixStoreAndLoadTest.java | 45 +++++++++++++++++++ .../impl/fixtracker/FixLoaderAndTracker.java | 7 +++ 2 files changed, 52 insertions(+) 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..47c84cbae54 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 @@ -13,6 +13,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.UUID; +import java.util.concurrent.CyclicBarrier; import org.junit.After; import org.junit.Before; @@ -82,6 +83,7 @@ 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.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; @@ -788,5 +790,48 @@ 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 solved the + new Thread() { + public void run() { + try { + while(!fixLoaderAndTracker.isStopRequested()) { + sleep(100); + } + + cb.await(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }; + }.start(); + + fixLoaderAndTracker.stop(true); + } } 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 1d3fa81d35d..4078c08e45a 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 @@ -540,6 +540,13 @@ public class FixLoaderAndTracker implements TrackingDataLoader { loadingJobs.add(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); From 80caaac2605aa016adaf2cdcbd23fb6d57ec4225 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Fri, 5 May 2017 10:40:28 +0200 Subject: [PATCH 04/16] Bug 4125: Fixed hanging preemptive stop --- .../racelogtracking/impl/fixtracker/FixLoaderAndTracker.java | 1 + 1 file changed, 1 insertion(+) 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 4078c08e45a..6d2a2495870 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 @@ -526,6 +526,7 @@ public class FixLoaderAndTracker implements TrackingDataLoader { progress = 1.0; } setStatusAndProgress(status, progress); + loadingJobs.notifyAll(); } private void addLoadingJob(AbstractLoadingJob job) { From 77a41bafb788357a3e7de504e20dd873e46d7d0a Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Fri, 5 May 2017 11:31:20 +0200 Subject: [PATCH 05/16] Completed unfinished comment --- .../racelogtracking/test/impl/SensorFixStoreAndLoadTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 47c84cbae54..61fef49e681 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 @@ -816,7 +816,7 @@ public class SensorFixStoreAndLoadTest { trackedRace.attachRaceLog(raceLog); trackedRace.attachRegattaLog(regattaLog); - // This thread solved the + // This thread solves the loading state through the CyclicBarrier new Thread() { public void run() { try { From 42c067e8a1b6afbbdd1c2753e3253963b6810800 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Fri, 5 May 2017 11:57:13 +0200 Subject: [PATCH 06/16] Bug 4125: Added several test cases to verify that the loading state is now correctly triggered on changes after the initial loading process --- .../test/impl/SensorFixStoreAndLoadTest.java | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) 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 61fef49e681..2c29166849f 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,8 +9,10 @@ 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; @@ -53,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; @@ -83,6 +86,7 @@ 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; @@ -90,6 +94,7 @@ 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; @@ -831,7 +836,120 @@ public class SensorFixStoreAndLoadTest { }; }.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; + } + } + } } From d87abdc4a4d30d483b709aa33c4ee08a0dab9bf3 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Mon, 8 May 2017 11:28:52 +0200 Subject: [PATCH 07/16] Bug 4125: Added more JavaDocs --- .../impl/fixtracker/FixLoaderAndTracker.java | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) 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 6d2a2495870..e48ffe2c834 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 @@ -106,6 +106,14 @@ public class FixLoaderAndTracker implements TrackingDataLoader { protected final DynamicTrackedRace trackedRace; private final SensorFixStore sensorFixStore; private RegattaLogDeviceMappings deviceMappings; + /** + * 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); @@ -448,6 +456,14 @@ 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); @@ -492,6 +508,10 @@ public class FixLoaderAndTracker implements TrackingDataLoader { 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 updateStatusAndProgress() { try { synchronized (loadingJobs) { @@ -502,6 +522,10 @@ public class FixLoaderAndTracker implements TrackingDataLoader { } } + /** + * Updates the {@link FixLoaderAndTracker}'s overall state on the {@link TrackedRace} based on the progresses of + * {@link #loadingJobs}. Calls to this method need to be synchronized for {@link #loadingJobs}. + */ private void updateStatusAndProgressWithExistingLock() { final TrackedRaceStatusEnum status; final double progress; @@ -529,6 +553,10 @@ public class FixLoaderAndTracker implements TrackingDataLoader { 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) { addLoadingJobWithExistingLock(job); @@ -537,6 +565,10 @@ public class FixLoaderAndTracker implements TrackingDataLoader { ThreadPoolUtil.INSTANCE.getDefaultForegroundTaskThreadPoolExecutor().execute(job); } + /** + * Adds a {@link AbstractLoadingJob} to track its loading state. Calls to this method need to be synchronized for + * {@link #loadingJobs}. + */ private void addLoadingJobWithExistingLock(AbstractLoadingJob job) { loadingJobs.add(job); } @@ -570,6 +602,10 @@ public class FixLoaderAndTracker implements TrackingDataLoader { } } + /** + * 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; @@ -591,6 +627,13 @@ public class FixLoaderAndTracker implements TrackingDataLoader { 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; @@ -608,6 +651,9 @@ public class FixLoaderAndTracker implements TrackingDataLoader { } } + /** + * 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; From 1ab2b4e3a877d6a04cf6fa345555fb6e9ec78f66 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Tue, 9 May 2017 10:53:07 +0200 Subject: [PATCH 08/16] Added more JavaDocs --- .../sailing/domain/tracking/TrackedRace.java | 1976 +++++++++-------- 1 file changed, 993 insertions(+), 983 deletions(-) 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 314b4497209..248a7d2be03 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 @@ -1,983 +1,993 @@ -package com.sap.sailing.domain.tracking; - -import java.io.Serializable; -import java.util.List; -import java.util.NavigableSet; -import java.util.Set; -import java.util.SortedSet; - -import com.sap.sailing.domain.abstractlog.race.RaceLog; -import com.sap.sailing.domain.abstractlog.race.RaceLogRaceStatusEvent; -import com.sap.sailing.domain.abstractlog.race.analyzing.impl.RaceLogResolver; -import com.sap.sailing.domain.abstractlog.race.state.racingprocedure.RacingProcedure; -import com.sap.sailing.domain.abstractlog.regatta.RegattaLog; -import com.sap.sailing.domain.abstractlog.regatta.events.RegattaLogDefineMarkEvent; -import com.sap.sailing.domain.abstractlog.regatta.events.RegattaLogDeviceMappingEvent; -import com.sap.sailing.domain.base.Competitor; -import com.sap.sailing.domain.base.Course; -import com.sap.sailing.domain.base.Leg; -import com.sap.sailing.domain.base.Mark; -import com.sap.sailing.domain.base.RaceDefinition; -import com.sap.sailing.domain.base.Regatta; -import com.sap.sailing.domain.base.SharedDomainFactory; -import com.sap.sailing.domain.base.Sideline; -import com.sap.sailing.domain.base.SpeedWithConfidence; -import com.sap.sailing.domain.base.Waypoint; -import com.sap.sailing.domain.base.impl.DouglasPeucker; -import com.sap.sailing.domain.common.Distance; -import com.sap.sailing.domain.common.LegType; -import com.sap.sailing.domain.common.NoWindException; -import com.sap.sailing.domain.common.Position; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sailing.domain.common.Speed; -import com.sap.sailing.domain.common.SpeedWithBearing; -import com.sap.sailing.domain.common.Tack; -import com.sap.sailing.domain.common.TargetTimeInfo; -import com.sap.sailing.domain.common.TimingConstants; -import com.sap.sailing.domain.common.TrackedRaceStatusEnum; -import com.sap.sailing.domain.common.Wind; -import com.sap.sailing.domain.common.WindSource; -import com.sap.sailing.domain.common.WindSourceType; -import com.sap.sailing.domain.common.abstractlog.TimePointSpecificationFoundInLog; -import com.sap.sailing.domain.common.dto.TrackedRaceDTO; -import com.sap.sailing.domain.common.racelog.Flags; -import com.sap.sailing.domain.common.racelog.RaceLogRaceStatus; -import com.sap.sailing.domain.common.racelog.RacingProcedureType; -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.markpassingcalculation.MarkPassingCalculator; -import com.sap.sailing.domain.polars.NotEnoughDataHasBeenAddedException; -import com.sap.sailing.domain.polars.PolarDataService; -import com.sap.sailing.domain.racelog.tracking.GPSFixStore; -import com.sap.sailing.domain.ranking.RankingMetric; -import com.sap.sailing.domain.ranking.RankingMetric.RankingInfo; -import com.sap.sailing.domain.tracking.impl.NonCachingMarkPositionAtTimePointCache; -import com.sap.sailing.domain.tracking.impl.TrackedRaceImpl; -import com.sap.sse.common.Duration; -import com.sap.sse.common.IsManagedByCache; -import com.sap.sse.common.TimePoint; -import com.sap.sse.common.Util; -import com.sap.sse.common.Util.Pair; - -/** - * Live tracking data of a single race. The race follows a defined {@link Course} with a sequence of {@link Leg}s. The - * course may change over time as the race committee decides to change it. Therefore, a {@link TrackedRace} instance - * {@link Course#addCourseListener(com.sap.sailing.domain.base.CourseListener) observes} the race {@link Course} for - * such changes. The tracking information of a leg can be requested either for all competitors (see - * {@link #getTrackedLegs()} and {@link #getTrackedLeg(Leg)}) or for a single competitor (see - * {@link #getTrackedLeg(Competitor, Leg)}). - *

- * - * The overall race standings can be requested in terms of a competitor's ranking. More detailed information about what - * happens / happened within a leg is available from {@link TrackedLeg} and {@link TrackedLegOfCompetitor}. - * - * @author Axel Uhl (d043530) - * - */ -public interface TrackedRace extends Serializable, IsManagedByCache { - final Duration START_TRACKING_THIS_MUCH_BEFORE_RACE_START = Duration.ONE_MINUTE.times(5); - final Duration STOP_TRACKING_THIS_MUCH_AFTER_RACE_FINISH = Duration.ONE_MINUTE.times(2); - - final long MAX_TIME_BETWEEN_START_AND_FIRST_MARK_PASSING_IN_MILLISECONDS = 30000; - - final long DEFAULT_LIVE_DELAY_IN_MILLISECONDS = 5000; - - RaceDefinition getRace(); - - RegattaAndRaceIdentifier getRaceIdentifier(); - - /** - * Tells how ranks are to be assigned to the competitors at any time during the race. For one-design boat classes - * this will usually happen by projecting the competitors to the wind direction for upwind and downwind legs or to - * the leg's rhumb line for reaching legs, then comparing positions. For handicap races using a time-on-time, - * time-on-distance, combination thereof or a more complicated scheme such as ORC Performance Curve, the ranking - * process needs to take into account the competitor-specific correction factors defined in the measurement - * certificate. - */ - RankingMetric getRankingMetric(); - - /** - * Computes the estimated start time for this race (not to be confused with the {@link #getStartOfTracking()} time - * point which is expected to be before the race start time). The highest precedence take the - * {@link #attachedRaceLogs race logs} and their start time events, followed by the field {@link #startTimeReceived} - * which can explicitly be set using {@link #setStartTimeReceived(TimePoint)}. When there are no {@link MarkPassing}s - * for the first mark, null is returned. If there are mark passings for the first mark and the start - * time is less than {@link #MAX_TIME_BETWEEN_START_AND_FIRST_MARK_PASSING_IN_MILLISECONDS} before the first mark - * passing for the first mark. Otherwise, the first mark passing for the first mark minus - * {@link #MAX_TIME_BETWEEN_START_AND_FIRST_MARK_PASSING_IN_MILLISECONDS} is returned as the race start time. - *

- * - * If no start time can be determined this way, null is returned. - */ - TimePoint getStartOfRace(); - - /** - * Like {@link #getStartOfRace()}, but ignoring any inference from start mark passings in case {@code inferred} is - * {@code false}. In this case, if no official start time was set, e.g., in the {@link RaceLog} or explicitly using - * {@link #getStartTimeReceived()}, {@code null} will be returned by this method even if start mark passings are - * present. - */ - TimePoint getStartOfRace(boolean inferred); - - /** - * @return the time point taken from a valid, non-revoked {@link RaceLogRaceStatusEvent} that transfers the race - * into status {@link RaceLogRaceStatus#FINISHED} from any of the {@link RaceLog}s attached to this race, - * or {@code null} if no such event is found. - * - * @see {@link RaceChangeListener#finishedTimeChanged(TimePoint, TimePoint)} - */ - TimePoint getFinishedTime(); - - /** - * Determine the race end time is tricky. Boats may sink, stop, not finish, although they started the race. We - * therefore cannot wait for all boats to reach the finish line. The following rules are used to calculate the - * endOfRace: - *

    - *
  1. Returns null if no boat passed the finish line
  2. - *
  3. Returns time of the last mark passing recorded for the finish line
  4. - *
  5. TODO: Returns the time of the first passing of the finish line + the target window (defined in the - * competition rules) if a target window has been defined for the race
  6. - */ - TimePoint getEndOfRace(); - - /** - * Returns a list of the first and last mark passing times of all course waypoints. Callers wanting to iterate over - * the result must synchronize on the result. - */ - Iterable>> getMarkPassingsTimes(); - - /** - * Shorthand for {@link #getStart()}.{@link TimePoint#compareTo(TimePoint) compareTo(at)} <= 0 - */ - boolean hasStarted(TimePoint at); - - /** - * A race is considered "live" if it - * {@link TrackedRaceDTO#hasGPSData has GPS data} and {@link TrackedRaceDTO#hasWindData wind data} and if the - * at time point is between the start and the end of the race. - *

    - * - * The pre-start phase of a race is interesting also in live mode. Therefore, if a {@link TrackedRace#getStartOfRace start - * time} is available for the race, the {@link TimingConstants#PRE_START_PHASE_DURATION_IN_MILLIS} is subtracted - * from the actual start time so that the pre-start phase also counts as live. If no start time is known for the - * race, but a {@link TrackedRace#getStartOfTracking start of tracking time} is known, it is used as the start of - * the "live" interval. - *

    - * - * If an {@link TrackedRace#getEndOfRace end time} is already known for the race, - * {@link TimingConstants#IS_LIVE_GRACE_PERIOD_IN_MILLIS} is added to that and the result is taken to be the end of - * the "live" period. If no end time is known but a {@link TrackedRace#getTimePointOfNewestEvent} is set, again the - * {@link TimingConstants#IS_LIVE_GRACE_PERIOD_IN_MILLIS} is added to that to mark the end of the "live" interval. - *

    - * - * @param at - * the time point at which to determine whether the race for fleet - * is/was live. A null value will use the start time of the race. - * @return - */ - boolean isLive(TimePoint at); - - /** - * Clients can safely iterate over the iterable returned because it's a non-live copy of the tracked legs of this - * tracked race. This implies that should an update to the underlying list of waypoints in this race's - * {@link Course} take place after this method has returned, then this won't be reflected in the result returned. - * Callers should obtain the {@link Course#lockForRead() course's read lock} while using the result of this call if - * they want to ensure that no course update is applied concurrently. - */ - Iterable getTrackedLegs(); - - TrackedLeg getTrackedLeg(Leg leg); - - /** - * Tracking information about the leg competitor is on at timePoint, or null - * if the competitor hasn't started any leg yet at timePoint or has already finished the race. - */ - TrackedLegOfCompetitor getCurrentLeg(Competitor competitor, TimePoint timePoint); - - /** - * Tells which leg the leader at timePoint is on - */ - TrackedLeg getCurrentLeg(TimePoint timePoint); - - /** - * Tells the number of the last started leg at timePoint - * The leg number is 0 before the start, the number of the current leg during the race - * and the number of the last leg at the end of the race even if the race has finished. - */ - int getLastLegStarted(TimePoint timePoint); - - /** - * Precondition: waypoint must still be part of {@link #getRace()}.{@link RaceDefinition#getCourse() getCourse()}. - */ - TrackedLeg getTrackedLegFinishingAt(Waypoint endOfLeg); - - /** - * Precondition: waypoint must still be part of {@link #getRace()}.{@link RaceDefinition#getCourse() getCourse()}. - */ - TrackedLeg getTrackedLegStartingAt(Waypoint startOfLeg); - - /** - * The raw, updating feed of a single competitor participating in this race - */ - GPSFixTrack getTrack(Competitor competitor); - - /** - * {@link SensorFixTrack}s provide timed sensor data in addition to GPSFixes that are hold in {@link GPSFixTrack}s. - * In contrast to {@link GPSFixTrack}s there is a 1:n relation of competitors to tracks by introducing track names. - * So every type of track has an associated name. With this construct you can have track implementations that - * provide specific functionality based on the contained fix type. - * - * @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 getSensorTrack(Competitor competitor, String trackName); - - /** - * Tells the leg on which the competitor was at time at. If the competitor hasn't passed - * the start waypoint yet, null is returned because the competitor was not yet on any leg at that point - * in time. If the time point happens to be after the last fix received from that competitor, the last known leg for - * that competitor is returned. If the time point is after the competitor's mark passing for the finish line, - * null is returned. For all legs except the last, if the time point equals a mark passing time point - * of the leg's starting waypoint, that leg is returned. For the time point of the mark passing for the finish line, - * the last leg is returned. - */ - TrackedLegOfCompetitor getTrackedLeg(Competitor competitor, TimePoint at); - - TrackedLegOfCompetitor getTrackedLeg(Competitor competitor, Leg leg); - - /** - * @return a sequential number counting the updates that occurred to this tracked race. Callers may use this to ask - * for updates newer than such a sequence number. - */ - long getUpdateCount(); - - int getRankDifference(Competitor competitor, Leg leg, TimePoint timePoint); - - /** - * Computes the rank of the competitor in this race for the current time. - */ - int getRank(Competitor competitor) throws NoWindException; - - /** - * Computes the rank of competitor in this race. A competitor is ahead of all competitors that are one - * or more legs behind. Within the same leg, the rank is determined by the windward distance to go and therefore - * depends on the assumptions of the wind direction for the given timePoint. If the race hasn't - * {@link #hasStarted(TimePoint) started} yet, the result is undefined. - * - * @return 0 in case the competitor hasn't participated in the race; a rank starting with - * 1 where rank 1 identifies the leader otherwise - */ - int getRank(Competitor competitor, TimePoint timePoint); - - /** - * For the given waypoint lists the {@link MarkPassing} events that describe which competitor passed the waypoint at - * which point in time. This can, e.g., be used to sort those competitors who already finished a leg within the leg - * that ends with waypoint. The remaining competitors need to be ordered by the advantage line-related - * distance to the waypoint. - * - * @return the iterable sequence of {@link MarkPassing}s as described above. If the caller wants to iterate on the - * resulting collection, the caller needs to invoke {@link #lockForRead(Iterable)} with the collection - * returned as parameter because insertions into the competitor's mark passing collection will obtain the - * corresponding write lock. - */ - Iterable getMarkPassingsInOrder(Waypoint waypoint); - - /** - * Obtains the {@link MarkPassing} for competitor passing waypoint. If no such mark - * passing has been reported (yet), null is returned. - */ - MarkPassing getMarkPassing(Competitor competitor, Waypoint waypoint); - - /** - * 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. - * - * @see #getTrack(Mark) - */ - GPSFixTrack getOrCreateTrack(Mark mark); - - /** - * Yields the track describing mark's movement over time; null if no track exists for - * mark so far. - * - * @see #getOrCreateTrack(Mark) - */ - GPSFixTrack getTrack(Mark mark); - - /** - * Retrieves all marks assigned to the race. They are not necessarily part of the race course. - */ - Iterable getMarks(); - - /** - * Retrieves all course side lines assigned to the race. - */ - Iterable getCourseSidelines(); - - /** - * If the waypoint only has one {@link #getMarks() mark}, its position at time timePoint - * is returned. Otherwise, the center of gravity between the mark positions is computed and returned. - */ - default Position getApproximatePosition(Waypoint waypoint, TimePoint timePoint) { - return getApproximatePosition(waypoint, timePoint, new NonCachingMarkPositionAtTimePointCache(this, timePoint)); - } - - /** - * Same as {@link #getApproximatePosition(Waypoint, TimePoint)}, but giving the caller the possibility to pass a - * cache of mark positions and related information that can help speed up compound operations requiring frequent - * access to the same marks in the same race for the same time point. - * - * @param markPositionCache - * a cache for this {@link MarkPositionAtTimePointCache#getTrackedRace() race} and the - * {@link MarkPositionAtTimePointCache#getTimePoint() timePoint} passed - */ - Position getApproximatePosition(Waypoint waypoint, TimePoint timePoint, - MarkPositionAtTimePointCache markPositionCache); - - /** - * Checks whether the {@link Wind#getTimePoint()} is in range of start and end {@link TimePoint}s plus extra time - * for wind recording. If, based on a {@link RaceExecutionOrderProvider}, there is no previous race that takes the - * wind fix, an extended time range lead (see - * {@link TrackedRaceImpl#EXTRA_LONG_TIME_BEFORE_START_TO_TRACK_WIND_MILLIS}) is used to record wind even a long - * time before the race start. - *

    - * - * A race does not record wind when both, {@link #getStartOfTracking()} and {@link #getStartOfRace()} are - * null. Wind is not recorded when it is after the later of {@link #getEndOfRace()} and - * {@link #getEndOfTracking()} and one of the two is not null. - *

    - * - * This default implementation returns true which may be useful for tests and mocked implementations; however, real - * implementations shall override this and provide a meaningful implementation according to the specification given - * above. - */ - default boolean takesWindFixWithTimePoint(TimePoint timePoint) { - return true; - } - - default boolean takesWindFixWithTimePointRecursively(TimePoint windFixTimePoint, Set visited) { - return true; - } - - /** - * Same as {@link #getWind(Position, TimePoint, Set) getWind(p, at, Collections.emptyList())} - */ - Wind getWind(Position p, TimePoint at); - - /** - * Obtains estimated interpolated wind information for a given position and time point. The information is taken - * from all wind sources available except for those listed in windSourcesToExclude, using the - * confidences of the wind values provided by the various sources during averaging. - */ - Wind getWind(Position p, TimePoint at, Set windSourcesToExclude); - - /** - * Retrieves the wind sources used so far by this race that have the specified type as their - * {@link WindSource#getType() type}. Always returns a non-null iterable which may be empty in case the - * race does not use any wind source of the specified type (yet).

    - * - * It is possible to ask for the {@link WindSourceType#COMBINED} and {@link WindSourceType#LEG_MIDDLE} types and - * get a non-empty result although those sources are never returned by {@link #getWindSources()}. - */ - Set getWindSources(WindSourceType type); - - /** - * Retrieves all wind sources known to this race, including those {@link #getWindSourcesToExclude() to exclude}. - * Callers can freely iterate because a copied collection is returned. The {@link WindSourceType#COMBINED} wind source - * as well as the {@link WindSourceType#LEG_MIDDLE} sources are never part of the result. - */ - Set getWindSources(); - - /** - * Same as {@link #getOrCreateWindTrack(WindSource, long) getOrCreateWindTrack(windSource, - * getMillisecondsOverWhichToAverageWind())}. - */ - WindTrack getOrCreateWindTrack(WindSource windSource); - - WindTrack getOrCreateWindTrack(WindSource windSource, long delayForWindEstimationCacheInvalidation); - - /** - * Waits until {@link #getUpdateCount()} is after sinceUpdate. - */ - void waitForNextUpdate(int sinceUpdate) throws InterruptedException; - - /** - * Time stamp of the start of the actual tracking. The value can be null (e.g. if we have not received any signal - * from the tracking infrastructure) - */ - TimePoint getStartOfTracking(); - - /** - * Time stamp of the end of the actual tracking. The value can be null (e.g. if we have not received any signal from - * the tracking infrastructure) - */ - TimePoint getEndOfTracking(); - - /** - * Regardless of the order in which events were received, this method returns the latest time point contained by any - * of the events received and processed. - */ - TimePoint getTimePointOfNewestEvent(); - - /** - * Regardless of the order in which events were received, this method returns the oldest time point contained by any - * of the events received and processed. - */ - TimePoint getTimePointOfOldestEvent(); - - /** - * @return the mark passings for competitor in this race received so far; the mark passing objects are - * returned such that their {@link MarkPassing#getWaypoint() waypoints} are ordered in the same way they are - * ordered in the race's {@link Course}. Note, that this doesn't necessarily guarantee ascending time - * points, particularly if premature mark passings have been detected accidentally as can be the case with - * some tracking providers such as TracTrac. If the caller wants to iterate on the resulting collection or - * construct a {@link SortedSet#headSet(Object)} or {@link SortedSet#tailSet(Object)} and then iterate over - * that, the caller needs to invoke {@link #lockForRead(Iterable)} with the collection returned as parameter - * because insertions into the competitor's mark passing collection will obtain the corresponding write - * lock. - */ - NavigableSet getMarkPassings(Competitor competitor); - - /** - * Returns competitor's mark passings. - * - * @param waitForLatestUpdates - * if any mark passing updates are pending because some calculations are currently going on and updates - * haven't all been processed yet then the call will block until these updates have been processed in - * case this parameter is set to {@code true}. For this the method uses a lock on the - * {@link MarkPassingCalculator }to block the thread until all calculations will be finished. - */ - NavigableSet getMarkPassings(Competitor competitor, boolean waitForLatestUpdates); - - /** - * This obtains the course's read lock before asking for the read lock for the markPassings structure. - * See also bug 1370 (http://bugzilla.sapsailing.com/bugzilla/show_bug.cgi?id=1370). This is necessary because the - * code that executes a course update will first ask the course's write lock and then relay execution to the - * course change listeners among which there is a {@link TrackedRace} which will then update the mark passings - * for all competitors and therefore will need to ask the write lock for those. If the thread calling this method - * first obtains the mark passings read lock and later, while holding on to that lock, asks for the course's read - * lock, a deadlock may result.

    - * - * Furthermore, when trying to acquire both, a lock for the {@link #getMarkPassings(Competitor) mark passings for a competitor} - * and a lock for the {@link #getMarkPassingsInOrder(Waypoint) mark passings for a waypoint, this needs to happen in exactly - * this order, or a deadlock may result.

    - * - * The {@link #unlockAfterRead(Iterable)} method will symmetrically unlock the course's read lock after releasing the - * read lock for the mark passings. - */ - void lockForRead(Iterable markPassings); - - /** - * Releases the read lock for the mark passings and then the read lock for the course. - * - * @see #lockForRead(Iterable) - */ - void unlockAfterRead(Iterable markPassings); - - /** - * Time stamp that the event received last from the underlying push service carried on it. Note that these times may - * not increase monotonically. - */ - TimePoint getTimePointOfLastEvent(); - - long getMillisecondsOverWhichToAverageSpeed(); - - long getMillisecondsOverWhichToAverageWind(); - - /** - * Gets the current delay of incoming events to the real time of the events in milliseconds - */ - long getDelayToLiveInMillis(); - - /** - * Estimates the wind direction based on the observed boat courses at the time given for the position provided. The - * estimate is based on the assumption that the boats which are on an upwind or a downwind leg sail with very - * similar angles on the starboard and the port side. There should be clusters of courses which are close to each - * other (within a threshold of, say, +/- 5 degrees), whereas for the upwind group there should be two clusters with - * angles about 90 degrees apart; similarly, for the downwind leg there should be two clusters, only that the - * general jibing angle may vary more, based on the wind speed and the boat class. - *

    - * - * Boats {@link GPSFixTrack#hasDirectionChange(TimePoint, double) currently maneuvering} are not considered for this - * analysis. - *

    - * - * This wind direction should not be used directly to compute the leg's wind direction and hence the {@link LegType - * leg type} because an endless recursion may result: an implementation of this method signature will need to know - * whether a leg is an upwind or downwind leg for which it has to know where the wind is coming from. - * - * @return null if no sufficient boat track information is available or leg type identification (upwind - * vs. downwind) is not possible; a valid {@link Wind} fix otherwise whose bearing is inferred from the boat - * courses and whose speed in knots is currently a rough indication of how many boats' courses contributed - * to determining the bearing. If in the future we have data about polar diagrams specific to boat classes, - * we may be able to also infer the wind speed from the boat tracks. - */ - Wind getEstimatedWindDirection(TimePoint timePoint); - - /** - * Determines whether the competitor is sailing on port or starboard tack at the timePoint - * requested. Note that this will have to retrieve information about the wind. This, in turn, can lead to the - * current thread obtaining the monitor of the various wind tracks, and, if the - * {@link WindSource#TRACK_BASED_ESTIMATION} source is used, also the monitors of the competitors' GPS tracks. - */ - Tack getTack(Competitor competitor, TimePoint timePoint) throws NoWindException; - - /** - * Determines whether the competitor is sailing on port or starboard tack at the timePoint - * requested. - *

    - * This method outperforms {@link #getTack(Competitor, TimePoint)}, based on being passed an already calculated wind - * for the given time and competitor position as well as the competitors speed and course over ground. - *

    - * This method will acquire the read lock for the competitor's track. - */ - Tack getTack(SpeedWithBearing speedWithBearing, Wind wind, TimePoint timePoint); - - TrackedRegatta getTrackedRegatta(); - - /** - * Computes a default wind direction based on the direction of the first leg at time at, with a default - * speed of zero knots. Note that this wind direction can only be used if {@link #raceIsKnownToStartUpwind()} returns - * true. - * - * @param at - * usually the {@link #getStart() start time} should be used; if no valid start time is provided, the - * current time point may serve as a default - * @return null in case the first leg's direction cannot be determined, e.g., because the necessary - * mark positions are not known (yet) - */ - Wind getDirectionFromStartToNextMark(TimePoint at); - - /** - * Uses a {@link DouglasPeucker Douglas-Peucker} algorithm to approximate this track's fixes starting at time - * from until time point to such that the maximum distance between the track's fixes and - * the approximation is at most maxDistance. The approximation's fixes are original fixes from - * the competitor's {@link GPSFixTrack track}. - */ - Iterable approximate(Competitor competitor, Distance maxDistance, TimePoint from, TimePoint to); - - /** - * @return a non-null but perhaps empty list of the maneuvers that competitor performed in - * this race between from and to. Depending on waitForLatest the - * result is taken from the cache straight away (waitForLatest==false) or, if a re-calculation - * for the key is still ongoing, the result of that ongoing re-calculation is returned. - */ - Iterable getManeuvers(Competitor competitor, TimePoint from, TimePoint to, boolean waitForLatest); - - /** - * @return true if this race is known to start with an {@link LegType#UPWIND upwind} leg. If this is - * the case, the wind estimation may default to using the first leg's direction at race start time as the - * direction the wind comes from. - */ - boolean raceIsKnownToStartUpwind(); - - /** - * Many calculations require valid wind data. In order to prevent NoWindException's to be handled by those - * calculation this method can be used to check whether the tracked race has sufficient wind information available. - * - * @return true if {@link #getWind(Position, TimePoint)} delivers a (not null) wind fix. - */ - boolean hasWindData(); - - /** - * - * @return true if at least one GPS fix for one of the competitors is available for this race. - */ - boolean hasGPSData(); - - /** - * Adds a race change listener to the set of listeners that will be notified about changes to this race. The - * listener won't be serialized together with this object. - */ - void addListener(RaceChangeListener listener); - - /** - * Like {@link #addListener(RaceChangeListener)}, but notifies the listener about the wind fixes known so far by the - * tracked race. This runs synchronized with the otherwise asynchronous loading of wind tracks, triggered by the - * constructor of the {@link TrackedRace} implementation classes. This procedure guarantees that eventually the - * listener will have received a notification for all wind fixes, regardless of whether they were already loaded at - * the time the listener is registered or they are loaded after the registration has completed.

    - * - * The same is true for the GPS fixes for marks and competitors. - */ - void addListener(RaceChangeListener listener, boolean notifyAboutWindFixesAlreadyLoaded, - boolean notifyAboutGPSFixesAlreadyLoaded); - - void removeListener(RaceChangeListener listener); - - /** - * @return null if there are no mark passings for the competitor in this race - * or if the competitor has not finished one of the legs in the race. - */ - Distance getDistanceTraveled(Competitor competitor, TimePoint timePoint); - - /** - * See {@link TrackedLegOfCompetitor#getDistanceTraveledConsideringGateStart(TimePoint)} - */ - Distance getDistanceTraveledIncludingGateStart(Competitor competitor, TimePoint timePoint); - - /** - * See {@link TrackedLegOfCompetitor#getWindwardDistanceToCompetitorFarthestAhead(TimePoint, WindPositionMode, RankingInfo)} - */ - Distance getWindwardDistanceToCompetitorFarthestAhead(Competitor competitor, TimePoint timePoint, WindPositionMode windPositionMode); - - /** - * Same as {@link #getWindwardDistanceToCompetitorFarthestAhead(Competitor, TimePoint, WindPositionMode)}, only with an - * additional cache to speed up wind and leg type and leg bearing calculations in case of multiple similar look-ups - * for the same time point. - * - * @param rankingInfo - * materialized ranking information that is expensive to calculate, avoiding redundant calculations - */ - Distance getWindwardDistanceToCompetitorFarthestAhead(Competitor competitor, TimePoint timePoint, - WindPositionMode windPositionMode, RankingInfo rankingInfo, WindLegTypeAndLegBearingCache cache); - - /** - * Calls {@link #getWindWithConfidence(Position, TimePoint, Iterable)} and excludes those wind sources listed in - * {@link #getWindSourcesToExclude}. - */ - WindWithConfidence> getWindWithConfidence(Position p, TimePoint at); - - /** - * Lists those wind sources which by default are not considered in {@link #getWind(Position, TimePoint)} and - * {@link #getWindWithConfidence(Position, TimePoint)}. - */ - Set getWindSourcesToExclude(); - - /** - * Loops over this tracked race's wind sources and from each asks its averaged wind for the position p - * and time point at, using the particular wind source's averaging interval. The confidences delivered - * by each wind source are used during computing the averaged result across the wind sources. The result has the - * averaged confidence attached. - */ - WindWithConfidence> getWindWithConfidence(Position p, TimePoint at, - Set windSourcesToExclude); - - /** - * Same as {@link #getEstimatedWindDirection(TimePoint)}, but propagates the confidence of the wind - * estimation, relative to the timePoint for which the request is made, in the result. The - * {@link Wind#getPosition() position} of all {@link Wind} fixes returned is null. - */ - WindWithConfidence getEstimatedWindDirectionWithConfidence(TimePoint timePoint); - - /** - * After the call returns, {@link #getWindSourcesToExclude()} returns an iterable that equals - * windSourcesToExclude - */ - void setWindSourcesToExclude(Iterable windSourcesToExclude); - - /** - * Computes the average cross-track error for the legs with type {@link LegType#UPWIND}. - * - * @param waitForLatestAnalysis - * if true and any cache update is currently going on, wait for the update to complete and - * then fetch the updated value; otherwise, serve this requests from whatever is currently in the cache - */ - Distance getAverageAbsoluteCrossTrackError(Competitor competitor, TimePoint timePoint, boolean waitForLatestAnalysis) - throws NoWindException; - - /** - * Same as {@link #getAverageAbsoluteCrossTrackError(Competitor, TimePoint, boolean)}, only that a cache for leg type, - * wind on leg and leg bearing is provided. - */ - Distance getAverageAbsoluteCrossTrackError(Competitor competitor, TimePoint timePoint, boolean waitForLatestAnalyses, - WindLegTypeAndLegBearingCache cache) throws NoWindException; - - Distance getAverageAbsoluteCrossTrackError(Competitor competitor, TimePoint from, TimePoint to, boolean upwindOnly, - boolean waitForLatestAnalyses) throws NoWindException; - - Distance getAverageSignedCrossTrackError(Competitor competitor, TimePoint timePoint, boolean waitForLatestAnalysis) - throws NoWindException; - - /** - * Same as {@link #getAverageSignedCrossTrackError(Competitor, TimePoint, boolean)}, only that a cache for leg type, - * wind direction and leg bearing is provided. - */ - Distance getAverageSignedCrossTrackError(Competitor competitor, TimePoint timePoint, boolean waitForLatestAnalyses, - WindLegTypeAndLegBearingCache cache) throws NoWindException; - - Distance getAverageSignedCrossTrackError(Competitor competitor, TimePoint from, TimePoint to, boolean upwindOnly, - boolean waitForLatestAnalysis) throws NoWindException; - - public Distance getAverageRideHeight(Competitor competitor, TimePoint timePoint); - - WindStore getWindStore(); - - Competitor getOverallLeader(TimePoint timePoint); - - Competitor getOverallLeader(TimePoint timePoint, WindLegTypeAndLegBearingCache cache); - - /** - * Returns the competitors of this tracked race, according to their ranking. Competitors whose - * {@link #getRank(Competitor)} is 0 will be sorted "worst". - */ - List getCompetitorsFromBestToWorst(TimePoint timePoint); - - /** - * Same as {@link #getCompetitorsFromBestToWorst(TimePoint)}, using a cache for wind, leg type and leg - * bearing values. - */ - List getCompetitorsFromBestToWorst(TimePoint timePoint, WindLegTypeAndLegBearingCache cache); - - /** - * When provided with a {@link WindStore} during construction, the tracked race will - * asynchronously load the wind data for this tracked race from the wind store and the GPS store in a - * background thread and update this tracked race with the results. Clients that want to wait for the wind - * loading process to complete can do so by calling this method which will block until the wind loading has - * completed. - */ - void waitUntilLoadingFromWindStoreComplete() throws InterruptedException; - - /** - * Whenever a {@link RegattaLog} is attached, fixes are loaded from the {@link GPSFixStore} for all mappings - * found in the {@code RegattaLog} in a separate thread. This method blocks if there is such a thread loading - * fixes, until that thread is finished. - */ - void waitForLoadingToFinish() throws InterruptedException; - - TrackedRaceStatus getStatus(); - - /** - * If the {@link #getStatus() status} is currently {@link TrackedRaceStatusEnum#LOADING}, blocks until the status changes to any - * other status. - */ - void waitUntilNotLoading(); - - /** - * Detaches the race log associated with this {@link TrackedRace}. - * - * @return the race log detached or {@code null} if no race log can be found by the {@code identifier} - */ - RaceLog detachRaceLog(Serializable identifier); - - /** - * Detaches the link {@link RaceExecutionOrderProvider} - */ - void detachRaceExecutionOrderProvider(RaceExecutionOrderProvider raceExecutionOrderProvider); - - /** - * Attaches the passed race log with this {@link TrackedRace}. - * This causes fixes from the {@link GPSFixStore} to be loaded for such {@link RegattaLogDeviceMappingEvent}s - * that are present in the raceLog. This loading is offloaded into a separate thread, that blocks - * serialization until it is finished. If multiple race logs are attached, the loading process is - * forced to be serialized. - * To garuantee that a the fixes for a race log have been fully loaded before continuing, - * {@link #waitForLoadingFromGPSFixStoreToFinishRunning} can be used. - * @param raceLog to be attached. - */ - void attachRaceLog(RaceLog raceLog); - - /** - * Attaches the passed regatta log with this {@link TrackedRace}. - * This also causes fixes from the {@link GPSFixStore} to be loaded (see {@link #attachRaceLog} for details). - */ - void attachRegattaLog(RegattaLog regattaLog); - - /** - * @return all currently attached {@link RegattaLog}s or an empty Iterable if there aren't any - */ - Iterable getAttachedRegattaLogs(); - - /** - * Attaches a {@link RaceExecutionOrderProvider} to make a {@link TrackedRace} aware - * which races are scheduled around it in the execution order of a {@link Regatta}. - * */ - void attachRaceExecutionProvider(RaceExecutionOrderProvider raceExecutionOrderProvider); - - /** - * Returns the attached race log event track for this race if any. - * Otherwise null. - */ - RaceLog getRaceLog(Serializable identifier); - - /** - * A setter for the listener on course design changes suggested by one of the {@link RaceLog}s attached to this - * race. The listener is mostly part of the tracking provider adapter. - * - * @param listener - * the listener to operate with. - */ - void addCourseDesignChangedListener(CourseDesignChangedListener listener); - - void addStartTimeChangedListener(StartTimeChangedListener listener); - - void removeStartTimeChangedListener(StartTimeChangedListener listener); - - void addRaceAbortedListener(RaceAbortedListener listener); - - /** - * Tells how far the given competitor was from the start line at the time point of the given seconds before the start. - *

    - * - * The distance to the line is calculated by projecting the competitor's position onto the line orthogonally and - * computing the distance of the projected position and the competitor's position. - *

    - * - * Should the course be empty, null is returned. If the course's first waypoint is not a line or gate, - * the geometric distance between the first waypoint and the competitor's position at timePoint is - * returned. If the competitor's position cannot be determined, null is returned. - */ - Distance getDistanceToStartLine(Competitor competitor, long millisecondsBeforeRaceStart); - - /** - * Tells how far the given competitor was from the start line at the given timePoint. - * Using the {@link #getStartOfRace() race start time} for timePoint, this tells the competitor's - * distance to the line when the race was started. - *

    - * - * The distance to the line is calculated by projecting the competitor's position onto the line orthogonally and - * computing the distance of the projected position and the competitor's position. - *

    - * - * Should the course be empty, null is returned. If the course's first waypoint is not a line or gate, - * the geometric distance between the first waypoint and the competitor's position at timePoint is - * returned. If the competitor's position cannot be determined, null is returned. - */ - Distance getDistanceToStartLine(Competitor competitor, TimePoint timePoint); - - /** - * When the competitor has started, this method returns the distance to the starboard end of the start line - * or---if the start waypoint was a single mark---the distance to the single start mark at the time the competitor started. - * If the competitor hasn't started yet, null is returned. - */ - Distance getDistanceFromStarboardSideOfStartLineWhenPassingStart(Competitor competitor); - - /** - * At the given timepoint and for the competitor, this method returns the distance to the starboard end of the start line - * or---if the start waypoint was a single mark---the distance to the single start mark at the timepoint. - * If the competitor hasn't started yet, null is returned. - * - */ - Distance getDistanceFromStarboardSideOfStartLine(Competitor competitor, TimePoint timePoint); - - /** - * The estimated speed of the competitor at the time point of the given seconds before the start of race. - */ - Speed getSpeed(Competitor competitor, long millisecondsBeforeRaceStart); - - /** - * The speed of the competitor when crossing the start line. It will return null if there are no recorded - * mark passings for this competitor (competitor did not yet or never pass the start line). - */ - Speed getSpeedWhenCrossingStartLine(Competitor competitor); - - /** - * Start time received by the tracking infrastructure. To determine real start time use {@link #getStartOfRace()}. - */ - TimePoint getStartTimeReceived(); - - /** - * @return null if the start waypoint does not have two marks or the course - * is empty or the start waypoint is the only waypoint - */ - LineDetails getStartLine(TimePoint at); - - /** - * @return null if the finish waypoint does not have two marks or the course - * is empty or the finish waypoint is the only waypoint - */ - LineDetails getFinishLine(TimePoint at); - - /** - * Length of course if there are mark passings for competitors. - */ - Distance getCourseLength(); - - /** - * The average wind speed with confidence for this race. It uses the timepoint of the race end as - * a reference point. - */ - SpeedWithConfidence getAverageWindSpeedWithConfidence(long resolutionInMillis); - - /** - * Computes the center point of the course's marks at the given time point. - */ - Position getCenterOfCourse(TimePoint at); - - /** - * If the {@link RacingProcedure} defined by any of the {@link #attachedRaceLogs attached} {@link RaceLog}s - * has type {@link RacingProcedureType#GateStart}, this method returns true, false for - * any other type found. If no type is found, e.g., because no race log is currently attached to this tracked race, - * null is returned, meaning that the type is not known. - */ - Boolean isGateStart(); - - /** - * Returns the time in milliseconds when the line was closed with lowering flag {@link Flags#GOLF} if {@link #isGateStart()} is true. - * If flag was not raised or {@link #isGateStart()} is false it returns null. - */ - long getGateStartGolfDownTime(); - - /** - * If the race was started with a gate start (see {@link #isGateStart()}, this method returns the distance between - * the competitor's starting position and the port side of the start line (pin end); otherwise, returns a zero - * distance. - */ - Distance getAdditionalGateStartDistance(Competitor competitor, TimePoint timePoint); - - boolean isUsingMarkPassingCalculator(); - - /** - * Calculates the estimated time it takes a competitor to sail the race, from start to finish. - * - * @param timepoint - * Used for positions of marks and wind information; note that sometimes the marks are not in place yet - * when the race starts and that a windward mark may be collected already before the race finishes. - * - * @return estimated time it takes to complete the race, plus more useful information about how this result came about - * - * @throws NotEnoughDataHasBeenAddedException - * thrown if not enough polar data has been added or polar data service is not available - * @throws NoWindException - */ - TargetTimeInfo getEstimatedTimeToComplete(TimePoint timepoint) throws NotEnoughDataHasBeenAddedException, NoWindException; - - void setPolarDataService(PolarDataService polarDataService); - - default RaceLogResolver getRaceLogResolver() { - return null; - } - - default Pair getTrackingTimesFromRaceLogs() { - return null; - } - - default Pair getStartAndFinishedTimeFromRaceLogs() { - return null; - } - - /** - * Returns all marks found in the {@link #markTracks} map and the mark device mappings and mark - * definition events in all attached race and regatta logs. Note that usually a device mapping should - * exist for a mark only if that mark is also defined by a {@link RegattaLogDefineMarkEvent}, so for - * this standard case, adding the marks from the device mark mappings would be redundant. - */ - default Iterable getMarksFromRegattaLogs() { - return getMarks(); - } - - /** - * Updates the start and end of tracking in the following precedence order: - * - *

      - *
    1. start/end of tracking in Racelog
    2. - *
    3. manually set start/end of tracking via {@link #setStartOfTrackingReceived(TimePoint, boolean)} and {@link #setEndOfTrackingReceived(TimePoint, boolean)}
    4. - *
    5. start/end of race in Racelog -/+ {@link #START_TRACKING_THIS_MUCH_BEFORE_RACE_START}/{@link #STOP_TRACKING_THIS_MUCH_AFTER_RACE_FINISH}
    6. - *
    - */ - public void updateStartAndEndOfTracking(boolean waitForGPSFixesToLoad); - - default void lockForSerializationRead() { - } - - default void unlockAfterSerializationRead() { - } - - /** - * @return all currently attached {@link RaceLog}s or an empty Iterable if there aren't any - */ - Iterable getAttachedRaceLogs(); - -} +package com.sap.sailing.domain.tracking; + +import java.io.Serializable; +import java.util.List; +import java.util.NavigableSet; +import java.util.Set; +import java.util.SortedSet; + +import com.sap.sailing.domain.abstractlog.race.RaceLog; +import com.sap.sailing.domain.abstractlog.race.RaceLogRaceStatusEvent; +import com.sap.sailing.domain.abstractlog.race.analyzing.impl.RaceLogResolver; +import com.sap.sailing.domain.abstractlog.race.state.racingprocedure.RacingProcedure; +import com.sap.sailing.domain.abstractlog.regatta.RegattaLog; +import com.sap.sailing.domain.abstractlog.regatta.events.RegattaLogDefineMarkEvent; +import com.sap.sailing.domain.abstractlog.regatta.events.RegattaLogDeviceMappingEvent; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.Course; +import com.sap.sailing.domain.base.Leg; +import com.sap.sailing.domain.base.Mark; +import com.sap.sailing.domain.base.RaceDefinition; +import com.sap.sailing.domain.base.Regatta; +import com.sap.sailing.domain.base.SharedDomainFactory; +import com.sap.sailing.domain.base.Sideline; +import com.sap.sailing.domain.base.SpeedWithConfidence; +import com.sap.sailing.domain.base.Waypoint; +import com.sap.sailing.domain.base.impl.DouglasPeucker; +import com.sap.sailing.domain.common.Distance; +import com.sap.sailing.domain.common.LegType; +import com.sap.sailing.domain.common.NoWindException; +import com.sap.sailing.domain.common.Position; +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.domain.common.Speed; +import com.sap.sailing.domain.common.SpeedWithBearing; +import com.sap.sailing.domain.common.Tack; +import com.sap.sailing.domain.common.TargetTimeInfo; +import com.sap.sailing.domain.common.TimingConstants; +import com.sap.sailing.domain.common.TrackedRaceStatusEnum; +import com.sap.sailing.domain.common.Wind; +import com.sap.sailing.domain.common.WindSource; +import com.sap.sailing.domain.common.WindSourceType; +import com.sap.sailing.domain.common.abstractlog.TimePointSpecificationFoundInLog; +import com.sap.sailing.domain.common.dto.TrackedRaceDTO; +import com.sap.sailing.domain.common.racelog.Flags; +import com.sap.sailing.domain.common.racelog.RaceLogRaceStatus; +import com.sap.sailing.domain.common.racelog.RacingProcedureType; +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.markpassingcalculation.MarkPassingCalculator; +import com.sap.sailing.domain.polars.NotEnoughDataHasBeenAddedException; +import com.sap.sailing.domain.polars.PolarDataService; +import com.sap.sailing.domain.racelog.tracking.GPSFixStore; +import com.sap.sailing.domain.ranking.RankingMetric; +import com.sap.sailing.domain.ranking.RankingMetric.RankingInfo; +import com.sap.sailing.domain.tracking.impl.NonCachingMarkPositionAtTimePointCache; +import com.sap.sailing.domain.tracking.impl.TrackedRaceImpl; +import com.sap.sse.common.Duration; +import com.sap.sse.common.IsManagedByCache; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.Util; +import com.sap.sse.common.Util.Pair; + +/** + * Live tracking data of a single race. The race follows a defined {@link Course} with a sequence of {@link Leg}s. The + * course may change over time as the race committee decides to change it. Therefore, a {@link TrackedRace} instance + * {@link Course#addCourseListener(com.sap.sailing.domain.base.CourseListener) observes} the race {@link Course} for + * such changes. The tracking information of a leg can be requested either for all competitors (see + * {@link #getTrackedLegs()} and {@link #getTrackedLeg(Leg)}) or for a single competitor (see + * {@link #getTrackedLeg(Competitor, Leg)}). + *

    + * + * The overall race standings can be requested in terms of a competitor's ranking. More detailed information about what + * happens / happened within a leg is available from {@link TrackedLeg} and {@link TrackedLegOfCompetitor}. + * + * @author Axel Uhl (d043530) + * + */ +public interface TrackedRace extends Serializable, IsManagedByCache { + final Duration START_TRACKING_THIS_MUCH_BEFORE_RACE_START = Duration.ONE_MINUTE.times(5); + final Duration STOP_TRACKING_THIS_MUCH_AFTER_RACE_FINISH = Duration.ONE_MINUTE.times(2); + + final long MAX_TIME_BETWEEN_START_AND_FIRST_MARK_PASSING_IN_MILLISECONDS = 30000; + + final long DEFAULT_LIVE_DELAY_IN_MILLISECONDS = 5000; + + RaceDefinition getRace(); + + RegattaAndRaceIdentifier getRaceIdentifier(); + + /** + * Tells how ranks are to be assigned to the competitors at any time during the race. For one-design boat classes + * this will usually happen by projecting the competitors to the wind direction for upwind and downwind legs or to + * the leg's rhumb line for reaching legs, then comparing positions. For handicap races using a time-on-time, + * time-on-distance, combination thereof or a more complicated scheme such as ORC Performance Curve, the ranking + * process needs to take into account the competitor-specific correction factors defined in the measurement + * certificate. + */ + RankingMetric getRankingMetric(); + + /** + * Computes the estimated start time for this race (not to be confused with the {@link #getStartOfTracking()} time + * point which is expected to be before the race start time). The highest precedence take the + * {@link #attachedRaceLogs race logs} and their start time events, followed by the field {@link #startTimeReceived} + * which can explicitly be set using {@link #setStartTimeReceived(TimePoint)}. When there are no {@link MarkPassing}s + * for the first mark, null is returned. If there are mark passings for the first mark and the start + * time is less than {@link #MAX_TIME_BETWEEN_START_AND_FIRST_MARK_PASSING_IN_MILLISECONDS} before the first mark + * passing for the first mark. Otherwise, the first mark passing for the first mark minus + * {@link #MAX_TIME_BETWEEN_START_AND_FIRST_MARK_PASSING_IN_MILLISECONDS} is returned as the race start time. + *

    + * + * If no start time can be determined this way, null is returned. + */ + TimePoint getStartOfRace(); + + /** + * Like {@link #getStartOfRace()}, but ignoring any inference from start mark passings in case {@code inferred} is + * {@code false}. In this case, if no official start time was set, e.g., in the {@link RaceLog} or explicitly using + * {@link #getStartTimeReceived()}, {@code null} will be returned by this method even if start mark passings are + * present. + */ + TimePoint getStartOfRace(boolean inferred); + + /** + * @return the time point taken from a valid, non-revoked {@link RaceLogRaceStatusEvent} that transfers the race + * into status {@link RaceLogRaceStatus#FINISHED} from any of the {@link RaceLog}s attached to this race, + * or {@code null} if no such event is found. + * + * @see {@link RaceChangeListener#finishedTimeChanged(TimePoint, TimePoint)} + */ + TimePoint getFinishedTime(); + + /** + * Determine the race end time is tricky. Boats may sink, stop, not finish, although they started the race. We + * therefore cannot wait for all boats to reach the finish line. The following rules are used to calculate the + * endOfRace: + *

      + *
    1. Returns null if no boat passed the finish line
    2. + *
    3. Returns time of the last mark passing recorded for the finish line
    4. + *
    5. TODO: Returns the time of the first passing of the finish line + the target window (defined in the + * competition rules) if a target window has been defined for the race
    6. + */ + TimePoint getEndOfRace(); + + /** + * Returns a list of the first and last mark passing times of all course waypoints. Callers wanting to iterate over + * the result must synchronize on the result. + */ + Iterable>> getMarkPassingsTimes(); + + /** + * Shorthand for {@link #getStart()}.{@link TimePoint#compareTo(TimePoint) compareTo(at)} <= 0 + */ + boolean hasStarted(TimePoint at); + + /** + * A race is considered "live" if it + * {@link TrackedRaceDTO#hasGPSData has GPS data} and {@link TrackedRaceDTO#hasWindData wind data} and if the + * at time point is between the start and the end of the race. + *

      + * + * The pre-start phase of a race is interesting also in live mode. Therefore, if a {@link TrackedRace#getStartOfRace start + * time} is available for the race, the {@link TimingConstants#PRE_START_PHASE_DURATION_IN_MILLIS} is subtracted + * from the actual start time so that the pre-start phase also counts as live. If no start time is known for the + * race, but a {@link TrackedRace#getStartOfTracking start of tracking time} is known, it is used as the start of + * the "live" interval. + *

      + * + * If an {@link TrackedRace#getEndOfRace end time} is already known for the race, + * {@link TimingConstants#IS_LIVE_GRACE_PERIOD_IN_MILLIS} is added to that and the result is taken to be the end of + * the "live" period. If no end time is known but a {@link TrackedRace#getTimePointOfNewestEvent} is set, again the + * {@link TimingConstants#IS_LIVE_GRACE_PERIOD_IN_MILLIS} is added to that to mark the end of the "live" interval. + *

      + * + * @param at + * the time point at which to determine whether the race for fleet + * is/was live. A null value will use the start time of the race. + * @return + */ + boolean isLive(TimePoint at); + + /** + * Clients can safely iterate over the iterable returned because it's a non-live copy of the tracked legs of this + * tracked race. This implies that should an update to the underlying list of waypoints in this race's + * {@link Course} take place after this method has returned, then this won't be reflected in the result returned. + * Callers should obtain the {@link Course#lockForRead() course's read lock} while using the result of this call if + * they want to ensure that no course update is applied concurrently. + */ + Iterable getTrackedLegs(); + + TrackedLeg getTrackedLeg(Leg leg); + + /** + * Tracking information about the leg competitor is on at timePoint, or null + * if the competitor hasn't started any leg yet at timePoint or has already finished the race. + */ + TrackedLegOfCompetitor getCurrentLeg(Competitor competitor, TimePoint timePoint); + + /** + * Tells which leg the leader at timePoint is on + */ + TrackedLeg getCurrentLeg(TimePoint timePoint); + + /** + * Tells the number of the last started leg at timePoint + * The leg number is 0 before the start, the number of the current leg during the race + * and the number of the last leg at the end of the race even if the race has finished. + */ + int getLastLegStarted(TimePoint timePoint); + + /** + * Precondition: waypoint must still be part of {@link #getRace()}.{@link RaceDefinition#getCourse() getCourse()}. + */ + TrackedLeg getTrackedLegFinishingAt(Waypoint endOfLeg); + + /** + * Precondition: waypoint must still be part of {@link #getRace()}.{@link RaceDefinition#getCourse() getCourse()}. + */ + TrackedLeg getTrackedLegStartingAt(Waypoint startOfLeg); + + /** + * The raw, updating feed of a single competitor participating in this race + */ + GPSFixTrack getTrack(Competitor competitor); + + /** + * {@link SensorFixTrack}s provide timed sensor data in addition to GPSFixes that are hold in {@link GPSFixTrack}s. + * In contrast to {@link GPSFixTrack}s there is a 1:n relation of competitors to tracks by introducing track names. + * So every type of track has an associated name. With this construct you can have track implementations that + * provide specific functionality based on the contained fix type. + * + * @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 getSensorTrack(Competitor competitor, String trackName); + + /** + * Tells the leg on which the competitor was at time at. If the competitor hasn't passed + * the start waypoint yet, null is returned because the competitor was not yet on any leg at that point + * in time. If the time point happens to be after the last fix received from that competitor, the last known leg for + * that competitor is returned. If the time point is after the competitor's mark passing for the finish line, + * null is returned. For all legs except the last, if the time point equals a mark passing time point + * of the leg's starting waypoint, that leg is returned. For the time point of the mark passing for the finish line, + * the last leg is returned. + */ + TrackedLegOfCompetitor getTrackedLeg(Competitor competitor, TimePoint at); + + TrackedLegOfCompetitor getTrackedLeg(Competitor competitor, Leg leg); + + /** + * @return a sequential number counting the updates that occurred to this tracked race. Callers may use this to ask + * for updates newer than such a sequence number. + */ + long getUpdateCount(); + + int getRankDifference(Competitor competitor, Leg leg, TimePoint timePoint); + + /** + * Computes the rank of the competitor in this race for the current time. + */ + int getRank(Competitor competitor) throws NoWindException; + + /** + * Computes the rank of competitor in this race. A competitor is ahead of all competitors that are one + * or more legs behind. Within the same leg, the rank is determined by the windward distance to go and therefore + * depends on the assumptions of the wind direction for the given timePoint. If the race hasn't + * {@link #hasStarted(TimePoint) started} yet, the result is undefined. + * + * @return 0 in case the competitor hasn't participated in the race; a rank starting with + * 1 where rank 1 identifies the leader otherwise + */ + int getRank(Competitor competitor, TimePoint timePoint); + + /** + * For the given waypoint lists the {@link MarkPassing} events that describe which competitor passed the waypoint at + * which point in time. This can, e.g., be used to sort those competitors who already finished a leg within the leg + * that ends with waypoint. The remaining competitors need to be ordered by the advantage line-related + * distance to the waypoint. + * + * @return the iterable sequence of {@link MarkPassing}s as described above. If the caller wants to iterate on the + * resulting collection, the caller needs to invoke {@link #lockForRead(Iterable)} with the collection + * returned as parameter because insertions into the competitor's mark passing collection will obtain the + * corresponding write lock. + */ + Iterable getMarkPassingsInOrder(Waypoint waypoint); + + /** + * Obtains the {@link MarkPassing} for competitor passing waypoint. If no such mark + * passing has been reported (yet), null is returned. + */ + MarkPassing getMarkPassing(Competitor competitor, Waypoint waypoint); + + /** + * 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. + * + * @see #getTrack(Mark) + */ + GPSFixTrack getOrCreateTrack(Mark mark); + + /** + * Yields the track describing mark's movement over time; null if no track exists for + * mark so far. + * + * @see #getOrCreateTrack(Mark) + */ + GPSFixTrack getTrack(Mark mark); + + /** + * Retrieves all marks assigned to the race. They are not necessarily part of the race course. + */ + Iterable getMarks(); + + /** + * Retrieves all course side lines assigned to the race. + */ + Iterable getCourseSidelines(); + + /** + * If the waypoint only has one {@link #getMarks() mark}, its position at time timePoint + * is returned. Otherwise, the center of gravity between the mark positions is computed and returned. + */ + default Position getApproximatePosition(Waypoint waypoint, TimePoint timePoint) { + return getApproximatePosition(waypoint, timePoint, new NonCachingMarkPositionAtTimePointCache(this, timePoint)); + } + + /** + * Same as {@link #getApproximatePosition(Waypoint, TimePoint)}, but giving the caller the possibility to pass a + * cache of mark positions and related information that can help speed up compound operations requiring frequent + * access to the same marks in the same race for the same time point. + * + * @param markPositionCache + * a cache for this {@link MarkPositionAtTimePointCache#getTrackedRace() race} and the + * {@link MarkPositionAtTimePointCache#getTimePoint() timePoint} passed + */ + Position getApproximatePosition(Waypoint waypoint, TimePoint timePoint, + MarkPositionAtTimePointCache markPositionCache); + + /** + * Checks whether the {@link Wind#getTimePoint()} is in range of start and end {@link TimePoint}s plus extra time + * for wind recording. If, based on a {@link RaceExecutionOrderProvider}, there is no previous race that takes the + * wind fix, an extended time range lead (see + * {@link TrackedRaceImpl#EXTRA_LONG_TIME_BEFORE_START_TO_TRACK_WIND_MILLIS}) is used to record wind even a long + * time before the race start. + *

      + * + * A race does not record wind when both, {@link #getStartOfTracking()} and {@link #getStartOfRace()} are + * null. Wind is not recorded when it is after the later of {@link #getEndOfRace()} and + * {@link #getEndOfTracking()} and one of the two is not null. + *

      + * + * This default implementation returns true which may be useful for tests and mocked implementations; however, real + * implementations shall override this and provide a meaningful implementation according to the specification given + * above. + */ + default boolean takesWindFixWithTimePoint(TimePoint timePoint) { + return true; + } + + default boolean takesWindFixWithTimePointRecursively(TimePoint windFixTimePoint, Set visited) { + return true; + } + + /** + * Same as {@link #getWind(Position, TimePoint, Set) getWind(p, at, Collections.emptyList())} + */ + Wind getWind(Position p, TimePoint at); + + /** + * Obtains estimated interpolated wind information for a given position and time point. The information is taken + * from all wind sources available except for those listed in windSourcesToExclude, using the + * confidences of the wind values provided by the various sources during averaging. + */ + Wind getWind(Position p, TimePoint at, Set windSourcesToExclude); + + /** + * Retrieves the wind sources used so far by this race that have the specified type as their + * {@link WindSource#getType() type}. Always returns a non-null iterable which may be empty in case the + * race does not use any wind source of the specified type (yet).

      + * + * It is possible to ask for the {@link WindSourceType#COMBINED} and {@link WindSourceType#LEG_MIDDLE} types and + * get a non-empty result although those sources are never returned by {@link #getWindSources()}. + */ + Set getWindSources(WindSourceType type); + + /** + * Retrieves all wind sources known to this race, including those {@link #getWindSourcesToExclude() to exclude}. + * Callers can freely iterate because a copied collection is returned. The {@link WindSourceType#COMBINED} wind source + * as well as the {@link WindSourceType#LEG_MIDDLE} sources are never part of the result. + */ + Set getWindSources(); + + /** + * Same as {@link #getOrCreateWindTrack(WindSource, long) getOrCreateWindTrack(windSource, + * getMillisecondsOverWhichToAverageWind())}. + */ + WindTrack getOrCreateWindTrack(WindSource windSource); + + WindTrack getOrCreateWindTrack(WindSource windSource, long delayForWindEstimationCacheInvalidation); + + /** + * Waits until {@link #getUpdateCount()} is after sinceUpdate. + */ + void waitForNextUpdate(int sinceUpdate) throws InterruptedException; + + /** + * Time stamp of the start of the actual tracking. The value can be null (e.g. if we have not received any signal + * from the tracking infrastructure) + */ + TimePoint getStartOfTracking(); + + /** + * Time stamp of the end of the actual tracking. The value can be null (e.g. if we have not received any signal from + * the tracking infrastructure) + */ + TimePoint getEndOfTracking(); + + /** + * Regardless of the order in which events were received, this method returns the latest time point contained by any + * of the events received and processed. + */ + TimePoint getTimePointOfNewestEvent(); + + /** + * Regardless of the order in which events were received, this method returns the oldest time point contained by any + * of the events received and processed. + */ + TimePoint getTimePointOfOldestEvent(); + + /** + * @return the mark passings for competitor in this race received so far; the mark passing objects are + * returned such that their {@link MarkPassing#getWaypoint() waypoints} are ordered in the same way they are + * ordered in the race's {@link Course}. Note, that this doesn't necessarily guarantee ascending time + * points, particularly if premature mark passings have been detected accidentally as can be the case with + * some tracking providers such as TracTrac. If the caller wants to iterate on the resulting collection or + * construct a {@link SortedSet#headSet(Object)} or {@link SortedSet#tailSet(Object)} and then iterate over + * that, the caller needs to invoke {@link #lockForRead(Iterable)} with the collection returned as parameter + * because insertions into the competitor's mark passing collection will obtain the corresponding write + * lock. + */ + NavigableSet getMarkPassings(Competitor competitor); + + /** + * Returns competitor's mark passings. + * + * @param waitForLatestUpdates + * if any mark passing updates are pending because some calculations are currently going on and updates + * haven't all been processed yet then the call will block until these updates have been processed in + * case this parameter is set to {@code true}. For this the method uses a lock on the + * {@link MarkPassingCalculator }to block the thread until all calculations will be finished. + */ + NavigableSet getMarkPassings(Competitor competitor, boolean waitForLatestUpdates); + + /** + * This obtains the course's read lock before asking for the read lock for the markPassings structure. + * See also bug 1370 (http://bugzilla.sapsailing.com/bugzilla/show_bug.cgi?id=1370). This is necessary because the + * code that executes a course update will first ask the course's write lock and then relay execution to the + * course change listeners among which there is a {@link TrackedRace} which will then update the mark passings + * for all competitors and therefore will need to ask the write lock for those. If the thread calling this method + * first obtains the mark passings read lock and later, while holding on to that lock, asks for the course's read + * lock, a deadlock may result.

      + * + * Furthermore, when trying to acquire both, a lock for the {@link #getMarkPassings(Competitor) mark passings for a competitor} + * and a lock for the {@link #getMarkPassingsInOrder(Waypoint) mark passings for a waypoint, this needs to happen in exactly + * this order, or a deadlock may result.

      + * + * The {@link #unlockAfterRead(Iterable)} method will symmetrically unlock the course's read lock after releasing the + * read lock for the mark passings. + */ + void lockForRead(Iterable markPassings); + + /** + * Releases the read lock for the mark passings and then the read lock for the course. + * + * @see #lockForRead(Iterable) + */ + void unlockAfterRead(Iterable markPassings); + + /** + * Time stamp that the event received last from the underlying push service carried on it. Note that these times may + * not increase monotonically. + */ + TimePoint getTimePointOfLastEvent(); + + long getMillisecondsOverWhichToAverageSpeed(); + + long getMillisecondsOverWhichToAverageWind(); + + /** + * Gets the current delay of incoming events to the real time of the events in milliseconds + */ + long getDelayToLiveInMillis(); + + /** + * Estimates the wind direction based on the observed boat courses at the time given for the position provided. The + * estimate is based on the assumption that the boats which are on an upwind or a downwind leg sail with very + * similar angles on the starboard and the port side. There should be clusters of courses which are close to each + * other (within a threshold of, say, +/- 5 degrees), whereas for the upwind group there should be two clusters with + * angles about 90 degrees apart; similarly, for the downwind leg there should be two clusters, only that the + * general jibing angle may vary more, based on the wind speed and the boat class. + *

      + * + * Boats {@link GPSFixTrack#hasDirectionChange(TimePoint, double) currently maneuvering} are not considered for this + * analysis. + *

      + * + * This wind direction should not be used directly to compute the leg's wind direction and hence the {@link LegType + * leg type} because an endless recursion may result: an implementation of this method signature will need to know + * whether a leg is an upwind or downwind leg for which it has to know where the wind is coming from. + * + * @return null if no sufficient boat track information is available or leg type identification (upwind + * vs. downwind) is not possible; a valid {@link Wind} fix otherwise whose bearing is inferred from the boat + * courses and whose speed in knots is currently a rough indication of how many boats' courses contributed + * to determining the bearing. If in the future we have data about polar diagrams specific to boat classes, + * we may be able to also infer the wind speed from the boat tracks. + */ + Wind getEstimatedWindDirection(TimePoint timePoint); + + /** + * Determines whether the competitor is sailing on port or starboard tack at the timePoint + * requested. Note that this will have to retrieve information about the wind. This, in turn, can lead to the + * current thread obtaining the monitor of the various wind tracks, and, if the + * {@link WindSource#TRACK_BASED_ESTIMATION} source is used, also the monitors of the competitors' GPS tracks. + */ + Tack getTack(Competitor competitor, TimePoint timePoint) throws NoWindException; + + /** + * Determines whether the competitor is sailing on port or starboard tack at the timePoint + * requested. + *

      + * This method outperforms {@link #getTack(Competitor, TimePoint)}, based on being passed an already calculated wind + * for the given time and competitor position as well as the competitors speed and course over ground. + *

      + * This method will acquire the read lock for the competitor's track. + */ + Tack getTack(SpeedWithBearing speedWithBearing, Wind wind, TimePoint timePoint); + + TrackedRegatta getTrackedRegatta(); + + /** + * Computes a default wind direction based on the direction of the first leg at time at, with a default + * speed of zero knots. Note that this wind direction can only be used if {@link #raceIsKnownToStartUpwind()} returns + * true. + * + * @param at + * usually the {@link #getStart() start time} should be used; if no valid start time is provided, the + * current time point may serve as a default + * @return null in case the first leg's direction cannot be determined, e.g., because the necessary + * mark positions are not known (yet) + */ + Wind getDirectionFromStartToNextMark(TimePoint at); + + /** + * Uses a {@link DouglasPeucker Douglas-Peucker} algorithm to approximate this track's fixes starting at time + * from until time point to such that the maximum distance between the track's fixes and + * the approximation is at most maxDistance. The approximation's fixes are original fixes from + * the competitor's {@link GPSFixTrack track}. + */ + Iterable approximate(Competitor competitor, Distance maxDistance, TimePoint from, TimePoint to); + + /** + * @return a non-null but perhaps empty list of the maneuvers that competitor performed in + * this race between from and to. Depending on waitForLatest the + * result is taken from the cache straight away (waitForLatest==false) or, if a re-calculation + * for the key is still ongoing, the result of that ongoing re-calculation is returned. + */ + Iterable getManeuvers(Competitor competitor, TimePoint from, TimePoint to, boolean waitForLatest); + + /** + * @return true if this race is known to start with an {@link LegType#UPWIND upwind} leg. If this is + * the case, the wind estimation may default to using the first leg's direction at race start time as the + * direction the wind comes from. + */ + boolean raceIsKnownToStartUpwind(); + + /** + * Many calculations require valid wind data. In order to prevent NoWindException's to be handled by those + * calculation this method can be used to check whether the tracked race has sufficient wind information available. + * + * @return true if {@link #getWind(Position, TimePoint)} delivers a (not null) wind fix. + */ + boolean hasWindData(); + + /** + * + * @return true if at least one GPS fix for one of the competitors is available for this race. + */ + boolean hasGPSData(); + + /** + * Adds a race change listener to the set of listeners that will be notified about changes to this race. The + * listener won't be serialized together with this object. + */ + void addListener(RaceChangeListener listener); + + /** + * Like {@link #addListener(RaceChangeListener)}, but notifies the listener about the wind fixes known so far by the + * tracked race. This runs synchronized with the otherwise asynchronous loading of wind tracks, triggered by the + * constructor of the {@link TrackedRace} implementation classes. This procedure guarantees that eventually the + * listener will have received a notification for all wind fixes, regardless of whether they were already loaded at + * the time the listener is registered or they are loaded after the registration has completed.

      + * + * The same is true for the GPS fixes for marks and competitors. + */ + void addListener(RaceChangeListener listener, boolean notifyAboutWindFixesAlreadyLoaded, + boolean notifyAboutGPSFixesAlreadyLoaded); + + void removeListener(RaceChangeListener listener); + + /** + * @return null if there are no mark passings for the competitor in this race + * or if the competitor has not finished one of the legs in the race. + */ + Distance getDistanceTraveled(Competitor competitor, TimePoint timePoint); + + /** + * See {@link TrackedLegOfCompetitor#getDistanceTraveledConsideringGateStart(TimePoint)} + */ + Distance getDistanceTraveledIncludingGateStart(Competitor competitor, TimePoint timePoint); + + /** + * See {@link TrackedLegOfCompetitor#getWindwardDistanceToCompetitorFarthestAhead(TimePoint, WindPositionMode, RankingInfo)} + */ + Distance getWindwardDistanceToCompetitorFarthestAhead(Competitor competitor, TimePoint timePoint, WindPositionMode windPositionMode); + + /** + * Same as {@link #getWindwardDistanceToCompetitorFarthestAhead(Competitor, TimePoint, WindPositionMode)}, only with an + * additional cache to speed up wind and leg type and leg bearing calculations in case of multiple similar look-ups + * for the same time point. + * + * @param rankingInfo + * materialized ranking information that is expensive to calculate, avoiding redundant calculations + */ + Distance getWindwardDistanceToCompetitorFarthestAhead(Competitor competitor, TimePoint timePoint, + WindPositionMode windPositionMode, RankingInfo rankingInfo, WindLegTypeAndLegBearingCache cache); + + /** + * Calls {@link #getWindWithConfidence(Position, TimePoint, Iterable)} and excludes those wind sources listed in + * {@link #getWindSourcesToExclude}. + */ + WindWithConfidence> getWindWithConfidence(Position p, TimePoint at); + + /** + * Lists those wind sources which by default are not considered in {@link #getWind(Position, TimePoint)} and + * {@link #getWindWithConfidence(Position, TimePoint)}. + */ + Set getWindSourcesToExclude(); + + /** + * Loops over this tracked race's wind sources and from each asks its averaged wind for the position p + * and time point at, using the particular wind source's averaging interval. The confidences delivered + * by each wind source are used during computing the averaged result across the wind sources. The result has the + * averaged confidence attached. + */ + WindWithConfidence> getWindWithConfidence(Position p, TimePoint at, + Set windSourcesToExclude); + + /** + * Same as {@link #getEstimatedWindDirection(TimePoint)}, but propagates the confidence of the wind + * estimation, relative to the timePoint for which the request is made, in the result. The + * {@link Wind#getPosition() position} of all {@link Wind} fixes returned is null. + */ + WindWithConfidence getEstimatedWindDirectionWithConfidence(TimePoint timePoint); + + /** + * After the call returns, {@link #getWindSourcesToExclude()} returns an iterable that equals + * windSourcesToExclude + */ + void setWindSourcesToExclude(Iterable windSourcesToExclude); + + /** + * Computes the average cross-track error for the legs with type {@link LegType#UPWIND}. + * + * @param waitForLatestAnalysis + * if true and any cache update is currently going on, wait for the update to complete and + * then fetch the updated value; otherwise, serve this requests from whatever is currently in the cache + */ + Distance getAverageAbsoluteCrossTrackError(Competitor competitor, TimePoint timePoint, boolean waitForLatestAnalysis) + throws NoWindException; + + /** + * Same as {@link #getAverageAbsoluteCrossTrackError(Competitor, TimePoint, boolean)}, only that a cache for leg type, + * wind on leg and leg bearing is provided. + */ + Distance getAverageAbsoluteCrossTrackError(Competitor competitor, TimePoint timePoint, boolean waitForLatestAnalyses, + WindLegTypeAndLegBearingCache cache) throws NoWindException; + + Distance getAverageAbsoluteCrossTrackError(Competitor competitor, TimePoint from, TimePoint to, boolean upwindOnly, + boolean waitForLatestAnalyses) throws NoWindException; + + Distance getAverageSignedCrossTrackError(Competitor competitor, TimePoint timePoint, boolean waitForLatestAnalysis) + throws NoWindException; + + /** + * Same as {@link #getAverageSignedCrossTrackError(Competitor, TimePoint, boolean)}, only that a cache for leg type, + * wind direction and leg bearing is provided. + */ + Distance getAverageSignedCrossTrackError(Competitor competitor, TimePoint timePoint, boolean waitForLatestAnalyses, + WindLegTypeAndLegBearingCache cache) throws NoWindException; + + Distance getAverageSignedCrossTrackError(Competitor competitor, TimePoint from, TimePoint to, boolean upwindOnly, + boolean waitForLatestAnalysis) throws NoWindException; + + public Distance getAverageRideHeight(Competitor competitor, TimePoint timePoint); + + WindStore getWindStore(); + + Competitor getOverallLeader(TimePoint timePoint); + + Competitor getOverallLeader(TimePoint timePoint, WindLegTypeAndLegBearingCache cache); + + /** + * Returns the competitors of this tracked race, according to their ranking. Competitors whose + * {@link #getRank(Competitor)} is 0 will be sorted "worst". + */ + List getCompetitorsFromBestToWorst(TimePoint timePoint); + + /** + * Same as {@link #getCompetitorsFromBestToWorst(TimePoint)}, using a cache for wind, leg type and leg + * bearing values. + */ + List getCompetitorsFromBestToWorst(TimePoint timePoint, WindLegTypeAndLegBearingCache cache); + + /** + * When provided with a {@link WindStore} during construction, the tracked race will + * asynchronously load the wind data for this tracked race from the wind store and the GPS store in a + * background thread and update this tracked race with the results. Clients that want to wait for the wind + * loading process to complete can do so by calling this method which will block until the wind loading has + * completed. + */ + void waitUntilLoadingFromWindStoreComplete() throws InterruptedException; + + /** + * Whenever a {@link RegattaLog} is attached, fixes are loaded from the {@link GPSFixStore} for all mappings + * found in the {@code RegattaLog} in a separate thread. This method blocks if there is such a thread loading + * fixes, until that thread is finished. + */ + 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.
      + * 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 monotonous (progress jump to a lower percentage when one loader + * that had a progress of 100% is finished and thus removed). + * + * @see TrackedRaceStatus + */ + TrackedRaceStatus getStatus(); + + /** + * If the {@link #getStatus() status} is currently {@link TrackedRaceStatusEnum#LOADING}, blocks until the status changes to any + * other status. + */ + void waitUntilNotLoading(); + + /** + * Detaches the race log associated with this {@link TrackedRace}. + * + * @return the race log detached or {@code null} if no race log can be found by the {@code identifier} + */ + RaceLog detachRaceLog(Serializable identifier); + + /** + * Detaches the link {@link RaceExecutionOrderProvider} + */ + void detachRaceExecutionOrderProvider(RaceExecutionOrderProvider raceExecutionOrderProvider); + + /** + * Attaches the passed race log with this {@link TrackedRace}. + * This causes fixes from the {@link GPSFixStore} to be loaded for such {@link RegattaLogDeviceMappingEvent}s + * that are present in the raceLog. This loading is offloaded into a separate thread, that blocks + * serialization until it is finished. If multiple race logs are attached, the loading process is + * forced to be serialized. + * To garuantee that a the fixes for a race log have been fully loaded before continuing, + * {@link #waitForLoadingFromGPSFixStoreToFinishRunning} can be used. + * @param raceLog to be attached. + */ + void attachRaceLog(RaceLog raceLog); + + /** + * Attaches the passed regatta log with this {@link TrackedRace}. + * This also causes fixes from the {@link GPSFixStore} to be loaded (see {@link #attachRaceLog} for details). + */ + void attachRegattaLog(RegattaLog regattaLog); + + /** + * @return all currently attached {@link RegattaLog}s or an empty Iterable if there aren't any + */ + Iterable getAttachedRegattaLogs(); + + /** + * Attaches a {@link RaceExecutionOrderProvider} to make a {@link TrackedRace} aware + * which races are scheduled around it in the execution order of a {@link Regatta}. + * */ + void attachRaceExecutionProvider(RaceExecutionOrderProvider raceExecutionOrderProvider); + + /** + * Returns the attached race log event track for this race if any. + * Otherwise null. + */ + RaceLog getRaceLog(Serializable identifier); + + /** + * A setter for the listener on course design changes suggested by one of the {@link RaceLog}s attached to this + * race. The listener is mostly part of the tracking provider adapter. + * + * @param listener + * the listener to operate with. + */ + void addCourseDesignChangedListener(CourseDesignChangedListener listener); + + void addStartTimeChangedListener(StartTimeChangedListener listener); + + void removeStartTimeChangedListener(StartTimeChangedListener listener); + + void addRaceAbortedListener(RaceAbortedListener listener); + + /** + * Tells how far the given competitor was from the start line at the time point of the given seconds before the start. + *

      + * + * The distance to the line is calculated by projecting the competitor's position onto the line orthogonally and + * computing the distance of the projected position and the competitor's position. + *

      + * + * Should the course be empty, null is returned. If the course's first waypoint is not a line or gate, + * the geometric distance between the first waypoint and the competitor's position at timePoint is + * returned. If the competitor's position cannot be determined, null is returned. + */ + Distance getDistanceToStartLine(Competitor competitor, long millisecondsBeforeRaceStart); + + /** + * Tells how far the given competitor was from the start line at the given timePoint. + * Using the {@link #getStartOfRace() race start time} for timePoint, this tells the competitor's + * distance to the line when the race was started. + *

      + * + * The distance to the line is calculated by projecting the competitor's position onto the line orthogonally and + * computing the distance of the projected position and the competitor's position. + *

      + * + * Should the course be empty, null is returned. If the course's first waypoint is not a line or gate, + * the geometric distance between the first waypoint and the competitor's position at timePoint is + * returned. If the competitor's position cannot be determined, null is returned. + */ + Distance getDistanceToStartLine(Competitor competitor, TimePoint timePoint); + + /** + * When the competitor has started, this method returns the distance to the starboard end of the start line + * or---if the start waypoint was a single mark---the distance to the single start mark at the time the competitor started. + * If the competitor hasn't started yet, null is returned. + */ + Distance getDistanceFromStarboardSideOfStartLineWhenPassingStart(Competitor competitor); + + /** + * At the given timepoint and for the competitor, this method returns the distance to the starboard end of the start line + * or---if the start waypoint was a single mark---the distance to the single start mark at the timepoint. + * If the competitor hasn't started yet, null is returned. + * + */ + Distance getDistanceFromStarboardSideOfStartLine(Competitor competitor, TimePoint timePoint); + + /** + * The estimated speed of the competitor at the time point of the given seconds before the start of race. + */ + Speed getSpeed(Competitor competitor, long millisecondsBeforeRaceStart); + + /** + * The speed of the competitor when crossing the start line. It will return null if there are no recorded + * mark passings for this competitor (competitor did not yet or never pass the start line). + */ + Speed getSpeedWhenCrossingStartLine(Competitor competitor); + + /** + * Start time received by the tracking infrastructure. To determine real start time use {@link #getStartOfRace()}. + */ + TimePoint getStartTimeReceived(); + + /** + * @return null if the start waypoint does not have two marks or the course + * is empty or the start waypoint is the only waypoint + */ + LineDetails getStartLine(TimePoint at); + + /** + * @return null if the finish waypoint does not have two marks or the course + * is empty or the finish waypoint is the only waypoint + */ + LineDetails getFinishLine(TimePoint at); + + /** + * Length of course if there are mark passings for competitors. + */ + Distance getCourseLength(); + + /** + * The average wind speed with confidence for this race. It uses the timepoint of the race end as + * a reference point. + */ + SpeedWithConfidence getAverageWindSpeedWithConfidence(long resolutionInMillis); + + /** + * Computes the center point of the course's marks at the given time point. + */ + Position getCenterOfCourse(TimePoint at); + + /** + * If the {@link RacingProcedure} defined by any of the {@link #attachedRaceLogs attached} {@link RaceLog}s + * has type {@link RacingProcedureType#GateStart}, this method returns true, false for + * any other type found. If no type is found, e.g., because no race log is currently attached to this tracked race, + * null is returned, meaning that the type is not known. + */ + Boolean isGateStart(); + + /** + * Returns the time in milliseconds when the line was closed with lowering flag {@link Flags#GOLF} if {@link #isGateStart()} is true. + * If flag was not raised or {@link #isGateStart()} is false it returns null. + */ + long getGateStartGolfDownTime(); + + /** + * If the race was started with a gate start (see {@link #isGateStart()}, this method returns the distance between + * the competitor's starting position and the port side of the start line (pin end); otherwise, returns a zero + * distance. + */ + Distance getAdditionalGateStartDistance(Competitor competitor, TimePoint timePoint); + + boolean isUsingMarkPassingCalculator(); + + /** + * Calculates the estimated time it takes a competitor to sail the race, from start to finish. + * + * @param timepoint + * Used for positions of marks and wind information; note that sometimes the marks are not in place yet + * when the race starts and that a windward mark may be collected already before the race finishes. + * + * @return estimated time it takes to complete the race, plus more useful information about how this result came about + * + * @throws NotEnoughDataHasBeenAddedException + * thrown if not enough polar data has been added or polar data service is not available + * @throws NoWindException + */ + TargetTimeInfo getEstimatedTimeToComplete(TimePoint timepoint) throws NotEnoughDataHasBeenAddedException, NoWindException; + + void setPolarDataService(PolarDataService polarDataService); + + default RaceLogResolver getRaceLogResolver() { + return null; + } + + default Pair getTrackingTimesFromRaceLogs() { + return null; + } + + default Pair getStartAndFinishedTimeFromRaceLogs() { + return null; + } + + /** + * Returns all marks found in the {@link #markTracks} map and the mark device mappings and mark + * definition events in all attached race and regatta logs. Note that usually a device mapping should + * exist for a mark only if that mark is also defined by a {@link RegattaLogDefineMarkEvent}, so for + * this standard case, adding the marks from the device mark mappings would be redundant. + */ + default Iterable getMarksFromRegattaLogs() { + return getMarks(); + } + + /** + * Updates the start and end of tracking in the following precedence order: + * + *

        + *
      1. start/end of tracking in Racelog
      2. + *
      3. manually set start/end of tracking via {@link #setStartOfTrackingReceived(TimePoint, boolean)} and {@link #setEndOfTrackingReceived(TimePoint, boolean)}
      4. + *
      5. start/end of race in Racelog -/+ {@link #START_TRACKING_THIS_MUCH_BEFORE_RACE_START}/{@link #STOP_TRACKING_THIS_MUCH_AFTER_RACE_FINISH}
      6. + *
      + */ + public void updateStartAndEndOfTracking(boolean waitForGPSFixesToLoad); + + default void lockForSerializationRead() { + } + + default void unlockAfterSerializationRead() { + } + + /** + * @return all currently attached {@link RaceLog}s or an empty Iterable if there aren't any + */ + Iterable getAttachedRaceLogs(); + +} From 24afab2a942d5fc42cdbda60105be36fc16b6264 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Tue, 9 May 2017 14:13:33 +0200 Subject: [PATCH 09/16] avoid using "break" Change-Id: Iae9db92cf91311beaa04d35f74feac1f0c82f2a6 --- .../domain/tracking/impl/DynamicTrackedRaceImpl.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) 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..4c18f6c903d 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 @@ -230,12 +230,13 @@ DynamicTrackedRace, GPSTrackListener { 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; + raceStatus = TrackedRaceStatusEnum.ERROR; + } else { + if (status.getStatus() == TrackedRaceStatusEnum.LOADING) { + raceStatus = TrackedRaceStatusEnum.LOADING; + } + sumOfLoaderProgresses += status.getLoadingProgress(); } - sumOfLoaderProgresses += status.getLoadingProgress(); } if (raceStatus == TrackedRaceStatusEnum.LOADING) { totalProgress = sumOfLoaderProgresses / loaderStatus.size(); From 37bf56b7001ef8fde5d104b11653f87f21c2ad5e Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Tue, 9 May 2017 14:20:51 +0200 Subject: [PATCH 10/16] added failing test for PREPAREAD status; I think the onStatusChanged implementation is faulty and should pass this test Change-Id: Iabdb4839455e736504ab589ac7da59c337e5b64e --- .../test/TrackedRaceStatusUpdateTest.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100755 java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/TrackedRaceStatusUpdateTest.java 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()); + } +} From 09f349c9c95083313b41d9e3919dfde3eb44e813 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Tue, 9 May 2017 14:24:57 +0200 Subject: [PATCH 11/16] fixed formatting Change-Id: Id935fe7ce105c01e172ce7b14df6683517114dd5 --- .../racelogtracking/impl/fixtracker/FixLoaderAndTracker.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 6d2a2495870..9688bbdd23a 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 @@ -505,14 +505,14 @@ public class FixLoaderAndTracker implements TrackingDataLoader { private void updateStatusAndProgressWithExistingLock() { final TrackedRaceStatusEnum status; final double progress; - if(!loadingJobs.isEmpty()) { + if (!loadingJobs.isEmpty()) { double progressSum = 0.0; boolean allFinished = true; for (AbstractLoadingJob loadingJob : loadingJobs) { allFinished &= loadingJob.finished; progressSum += loadingJob.progress; } - if(allFinished) { + if (allFinished) { loadingJobs.clear(); status = stopRequested.get() ? TrackedRaceStatusEnum.FINISHED : TrackedRaceStatusEnum.TRACKING; progress = 1.0; From 0fd7e4ab1911460c58047a0a9d958d61d42a5408 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Tue, 9 May 2017 14:41:02 +0200 Subject: [PATCH 12/16] whitespace changes, formatting Change-Id: Ifb80522136cea6e6479f07afbfb14a577397c7d8 --- .../test/impl/SensorFixStoreAndLoadTest.java | 60 ++----------------- .../sailing/domain/tracking/TrackedRace.java | 2 +- 2 files changed, 6 insertions(+), 56 deletions(-) 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 2c29166849f..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 @@ -510,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"); @@ -640,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) { @@ -688,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); } @@ -706,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); } @@ -731,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); } @@ -749,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); } @@ -802,9 +778,7 @@ public class SensorFixStoreAndLoadTest { 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 @@ -816,26 +790,22 @@ public class SensorFixStoreAndLoadTest { } } }); - 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()) { + 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); } @@ -846,18 +816,13 @@ public class SensorFixStoreAndLoadTest { 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); } @@ -865,20 +830,15 @@ public class SensorFixStoreAndLoadTest { /** 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); } @@ -888,19 +848,14 @@ public class SensorFixStoreAndLoadTest { 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); } @@ -910,20 +865,15 @@ public class SensorFixStoreAndLoadTest { 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); } @@ -934,7 +884,7 @@ public class SensorFixStoreAndLoadTest { public void statusChanged(TrackedRaceStatus newStatus, TrackedRaceStatus oldStatus) { TrackedRaceStatusEnum oldStatusEnum = oldStatus.getStatus(); TrackedRaceStatusEnum newStatusEnum = newStatus.getStatus(); - if(oldStatusEnum != newStatusEnum) { + if (oldStatusEnum != newStatusEnum) { transitions.add(new Pair<>(oldStatusEnum, newStatusEnum)); } } @@ -943,7 +893,7 @@ public class SensorFixStoreAndLoadTest { TrackedRaceStatusEnum oldValue = null; Iterator> recordedTransitionsIterator = transitions.iterator(); for (TrackedRaceStatusEnum newStatusEnum : expectedStates) { - if(oldValue != null) { + if (oldValue != null) { Pair recordedTransition = recordedTransitionsIterator.next(); assertEquals(oldValue, recordedTransition.getA()); assertEquals(newStatusEnum, recordedTransition.getB()); 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 248a7d2be03..875b3df78f0 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 @@ -742,7 +742,7 @@ 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 monotonous (progress jump to a lower percentage when one loader + * 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 From b4f9aeb16c879e1d2eec565d254d525dd41dd53d Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Tue, 9 May 2017 14:54:43 +0200 Subject: [PATCH 13/16] Removed unused log statements --- .../sailing/domain/tracking/impl/DynamicTrackedRaceImpl.java | 2 -- 1 file changed, 2 deletions(-) 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 4c18f6c903d..a7c85b5b702 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,7 +223,6 @@ 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()) { @@ -244,7 +243,6 @@ DynamicTrackedRace, GPSTrackListener { } } this.setStatus(new TrackedRaceStatusImpl(raceStatus, totalProgress)); - // logger.info("Global status: " + raceStatus + " | Progress: " + totalProgress); } private void updateLoaderStatus(TrackingDataLoader loader, TrackedRaceStatus status) { From ac3290ef33ac1f07e9d049a518fe01fe102bb5e2 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Tue, 9 May 2017 15:40:06 +0200 Subject: [PATCH 14/16] Fixed condition that broke on refactoring --- .../sailing/domain/tracking/impl/DynamicTrackedRaceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a7c85b5b702..6f6892f7f94 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 @@ -230,7 +230,7 @@ DynamicTrackedRace, GPSTrackListener { for (TrackedRaceStatus status : loaderStatus.values()) { if (status.getStatus() == TrackedRaceStatusEnum.ERROR) { raceStatus = TrackedRaceStatusEnum.ERROR; - } else { + } else if (raceStatus != TrackedRaceStatusEnum.ERROR) { if (status.getStatus() == TrackedRaceStatusEnum.LOADING) { raceStatus = TrackedRaceStatusEnum.LOADING; } From cec08f79a123e2b81abb48bca707351d17209389 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Wed, 10 May 2017 16:41:38 +0200 Subject: [PATCH 15/16] Bug 4125: Fixed PREPARED state for loaders and added JavaDocs for DynamicTrackedRace.onStatusChanged to explain what it means in detail --- .../domain/tracking/DynamicTrackedRace.java | 454 +++++++++--------- .../sailing/domain/tracking/TrackedRace.java | 1 + .../tracking/impl/DynamicTrackedRaceImpl.java | 24 +- 3 files changed, 248 insertions(+), 231 deletions(-) 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 875b3df78f0..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 @@ -746,6 +746,7 @@ public interface TrackedRace extends Serializable, IsManagedByCache { double totalProgress = 1.0; synchronized (loaderStatus) { 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; - } else if (raceStatus != TrackedRaceStatusEnum.ERROR) { - if (status.getStatus() == TrackedRaceStatusEnum.LOADING) { - raceStatus = TrackedRaceStatusEnum.LOADING; - } - sumOfLoaderProgresses += status.getLoadingProgress(); - } + 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; } } } From a010bd5ef0759c7d89cab5c60c55629dfe8975d5 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Wed, 10 May 2017 16:49:15 +0200 Subject: [PATCH 16/16] Bug 4125: Cleanup regarding synchronization --- .../impl/fixtracker/FixLoaderAndTracker.java | 68 ++++++++----------- 1 file changed, 30 insertions(+), 38 deletions(-) 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 728d6d5f31a..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 @@ -512,11 +512,9 @@ public class FixLoaderAndTracker implements TrackingDataLoader { * Updates the {@link FixLoaderAndTracker}'s overall state on the {@link TrackedRace} based on the progresses of * {@link #loadingJobs}. */ - private void updateStatusAndProgress() { + private void updateStatusAndProgressWithErrorHandling() { try { - synchronized (loadingJobs) { - updateStatusAndProgressWithExistingLock(); - } + updateStatusAndProgress(); } catch (Exception e) { logger.log(Level.WARNING, "Error while updating status and progress for FixLoaderAndTracker", e); } @@ -524,33 +522,35 @@ public class FixLoaderAndTracker implements TrackingDataLoader { /** * Updates the {@link FixLoaderAndTracker}'s overall state on the {@link TrackedRace} based on the progresses of - * {@link #loadingJobs}. Calls to this method need to be synchronized for {@link #loadingJobs}. + * {@link #loadingJobs}. */ - private void updateStatusAndProgressWithExistingLock() { - 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(); + 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; - } else { - progress = progressSum / loadingJobs.size(); - status = TrackedRaceStatusEnum.LOADING; } - - } else { - status = stopRequested.get() ? TrackedRaceStatusEnum.FINISHED : TrackedRaceStatusEnum.TRACKING; - progress = 1.0; + setStatusAndProgress(status, progress); + loadingJobs.notifyAll(); } - setStatusAndProgress(status, progress); - loadingJobs.notifyAll(); } /** @@ -559,20 +559,12 @@ public class FixLoaderAndTracker implements TrackingDataLoader { */ private void addLoadingJob(AbstractLoadingJob job) { synchronized (loadingJobs) { - addLoadingJobWithExistingLock(job); - updateStatusAndProgressWithExistingLock(); + loadingJobs.add(job); + updateStatusAndProgress(); } ThreadPoolUtil.INSTANCE.getDefaultForegroundTaskThreadPoolExecutor().execute(job); } - /** - * Adds a {@link AbstractLoadingJob} to track its loading state. Calls to this method need to be synchronized for - * {@link #loadingJobs}. - */ - private void addLoadingJobWithExistingLock(AbstractLoadingJob job) { - loadingJobs.add(job); - } - /** * Used for testing purposes only. */ @@ -613,14 +605,14 @@ public class FixLoaderAndTracker implements TrackingDataLoader { @Override public final void run() { progress = 0.5; - updateStatusAndProgress(); + updateStatusAndProgressWithErrorHandling(); try { load(); } finally { progress = 1.0; finished = true; - updateStatusAndProgress(); + updateStatusAndProgressWithErrorHandling(); } }