diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/RaceLogFinishPositioningConfirmedEvent.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/RaceLogFinishPositioningConfirmedEvent.java index 801779a9631..7f04d90f9f8 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/RaceLogFinishPositioningConfirmedEvent.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/RaceLogFinishPositioningConfirmedEvent.java @@ -2,10 +2,10 @@ package com.sap.sailing.domain.abstractlog.race; /** - * This RaceLog evet type confirms the entered positioning list. Since we do not guarantee the order of incoming events from the app (due to possible - * connectivity issues), this event type contains the the confirmed positioning list again. Although this racelog event type contains the same information - * such as {@link RaceLogFinishPositioningListChangedEvent} we keep it for backwards compatibility reasons. - * + * This RaceLog event type confirms the entered positioning list. Since we do not guarantee the order of incoming events + * from the app (due to possible connectivity issues), this event type contains the the confirmed positioning list + * again. Although this racelog event type contains the same information such as + * {@link RaceLogFinishPositioningListChangedEvent} we keep it for backwards compatibility reasons. */ public interface RaceLogFinishPositioningConfirmedEvent extends RaceLogFinishPositioningEvent { diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/RaceLogFinishPositioningListChangedEvent.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/RaceLogFinishPositioningListChangedEvent.java index 9fac2be5e73..072864893da 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/RaceLogFinishPositioningListChangedEvent.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/RaceLogFinishPositioningListChangedEvent.java @@ -1,6 +1,11 @@ package com.sap.sailing.domain.abstractlog.race; - +/** + * Captures a temporary race result description. The result is not final yet and needs to be confirmed by + * a {@link RaceLogFinishPositioningConfirmedEvent} before it is adopted by other parts of the model. This + * type of event can be used to store the interims state of a result editing process before the result + * if committed. + */ public interface RaceLogFinishPositioningListChangedEvent extends RaceLogFinishPositioningEvent { } \ No newline at end of file diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/MarkPassingUpdateFromRaceLogFinishPositioingListTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/MarkPassingUpdateFromRaceLogFinishPositioingListTest.java new file mode 100644 index 00000000000..cc14fb14f86 --- /dev/null +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/MarkPassingUpdateFromRaceLogFinishPositioingListTest.java @@ -0,0 +1,126 @@ +package com.sap.sailing.domain.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.text.ParseException; +import java.util.UUID; + +import org.junit.Before; +import org.junit.Test; + +import com.sap.sailing.domain.abstractlog.AbstractLogEventAuthor; +import com.sap.sailing.domain.abstractlog.impl.LogEventAuthorImpl; +import com.sap.sailing.domain.abstractlog.race.CompetitorResult; +import com.sap.sailing.domain.abstractlog.race.CompetitorResults; +import com.sap.sailing.domain.abstractlog.race.RaceLog; +import com.sap.sailing.domain.abstractlog.race.RaceLogFinishPositioningConfirmedEvent; +import com.sap.sailing.domain.abstractlog.race.impl.CompetitorResultImpl; +import com.sap.sailing.domain.abstractlog.race.impl.CompetitorResultsImpl; +import com.sap.sailing.domain.abstractlog.race.impl.RaceLogFinishPositioningConfirmedEventImpl; +import com.sap.sailing.domain.abstractlog.race.impl.RaceLogPassChangeEventImpl; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.base.impl.CourseAreaImpl; +import com.sap.sailing.domain.common.NoWindException; +import com.sap.sailing.domain.common.WindSourceType; +import com.sap.sailing.domain.common.impl.DegreeBearingImpl; +import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; +import com.sap.sailing.domain.common.impl.WindImpl; +import com.sap.sailing.domain.common.impl.WindSourceImpl; +import com.sap.sailing.domain.leaderboard.FlexibleLeaderboard; +import com.sap.sailing.domain.leaderboard.impl.FlexibleLeaderboardImpl; +import com.sap.sailing.domain.leaderboard.impl.LowPoint; +import com.sap.sailing.domain.leaderboard.impl.ThresholdBasedResultDiscardingRuleImpl; +import com.sap.sailing.domain.tracking.MarkPassing; +import com.sap.sailing.domain.tractracadapter.ReceiverType; +import com.sap.sse.common.Duration; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.Util; +import com.sap.sse.common.Util.Pair; +import com.sap.sse.common.impl.MillisecondsTimePoint; +import com.tractrac.model.lib.api.event.CreateModelException; +import com.tractrac.subscription.lib.api.SubscriberInitializationException; + +/** + * Tests the implementation for bug3420: when a {@link RaceLogFinishPositioningConfirmedEvent} is found in a race log + * that has a valid {@link CompetitorResult#getFinishingTime()} defined for a competitor, a wrapper mark passing is + * expected to be generated with that time point for the finish waypoint. When the event is invalidated, e.g., by + * detaching the race log or by starting a new pass or by sending a newer {@link RaceLogFinishPositioningConfirmedEvent} + * or one that has higher priority, the finish mark passings need to be updated accordingly. This can mean that entirely + * artificial mark passings need to be removed entirely; mark passings that only replaced an original mark passing by + * one with a modified finish time need to be reverted back to the original mark passing. + * + * @author Axel Uhl (D043530) + * + */ +public class MarkPassingUpdateFromRaceLogFinishPositioingListTest extends AbstractManeuverDetectionTestCase { + private FlexibleLeaderboard leaderboard; + private RaceLog raceLog; + private final AbstractLogEventAuthor author; + + public MarkPassingUpdateFromRaceLogFinishPositioingListTest() throws MalformedURLException, URISyntaxException { + super(); + author = new LogEventAuthorImpl("Me", 0); + } + + @Override + protected String getExpectedEventName() { + return "ESS Qingdao 2014"; + } + + + @Before + public void setUp() throws URISyntaxException, IOException, InterruptedException, ParseException, SubscriberInitializationException, CreateModelException { + super.setUp(); + URI storedUri = new URI("file:///"+new File("resources/event_20140429_ESSQingdao-Race_4.mtb").getCanonicalPath().replace('\\', '/')); + super.setUp( + new URL("file:///" + new File("resources/event_20140429_ESSQingdao-Race_4.txt").getCanonicalPath()), + /* liveUri */null, /* storedUri */storedUri, new ReceiverType[] { ReceiverType.MARKPASSINGS, + ReceiverType.MARKPOSITIONS, ReceiverType.RACECOURSE, ReceiverType.RAWPOSITIONS }); + getTrackedRace().recordWind( + new WindImpl(/* position */null, new MillisecondsTimePoint(dateFormat.parse("05/01/2014-09:02:00")), + new KnotSpeedWithBearingImpl(10, new DegreeBearingImpl(269))), + new WindSourceImpl(WindSourceType.WEB)); + leaderboard = new FlexibleLeaderboardImpl("Test", new ThresholdBasedResultDiscardingRuleImpl(new int[0]), new LowPoint(), new CourseAreaImpl("Here", UUID.randomUUID())); + final RaceColumn raceColumn = leaderboard.addRace(getTrackedRace(), "R", /* medalRace */ false); + raceLog = raceColumn.getRaceLog(raceColumn.getFleets().iterator().next()); + } + + /** + * Tests the Race 4 for competitor "Alinghi" at a time where the maneuver detection test is likely to compute + * a penalty although between the jibe and the tack there is a mark passing + */ + @Test + public void testOriginalFinishMarkPassingTime() throws ParseException, NoWindException { + Competitor alinghi = getCompetitorByName("Alinghi"); + final MarkPassing alinghiOriginalFinish = getTrackedRace().getMarkPassing(alinghi, getTrackedRace().getRace().getCourse().getLastWaypoint()); + final TimePoint alinghiOriginalFinishingTime = alinghiOriginalFinish.getTimePoint(); + assertNotNull(alinghiOriginalFinish.getTimePoint()); + final TimePoint now = MillisecondsTimePoint.now(); + final CompetitorResults alinghiFinishesNow = createCompetitorResults(new Util.Pair<>(alinghi, now)); + raceLog.add(new RaceLogFinishPositioningConfirmedEventImpl(now, author, /* pPassId */ 0, alinghiFinishesNow)); + final MarkPassing alinghiCorrectedFinishMarkPassing = getTrackedRace().getMarkPassing(alinghi, getTrackedRace().getRace().getCourse().getLastWaypoint()); + assertEquals(now, alinghiCorrectedFinishMarkPassing.getTimePoint()); + raceLog.add(new RaceLogPassChangeEventImpl(now.plus(Duration.ONE_SECOND), author, /* pass */ 1)); // this should invalidate the previously edited result + assertEquals(alinghiOriginalFinishingTime, getTrackedRace().getMarkPassing(alinghi, getTrackedRace().getRace().getCourse().getLastWaypoint()).getTimePoint()); + // TODO now eliminate the original finish mark passing, then set a finishing time in the race log to see how the synthetic mark passing is created and then again removed + } + + @SafeVarargs + private final CompetitorResults createCompetitorResults(Pair... finishingTimes) { + final CompetitorResults results = new CompetitorResultsImpl(); + int rank = 1; + for (Pair finishingTime : finishingTimes) { + results.add(new CompetitorResultImpl(finishingTime.getA().getId(), finishingTime.getA().getName(), rank++, /* maxPointsReason */ + null, /* score */null, finishingTime.getB(), /* comment */null)); + } + return results; + } +} diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/mock/MockedTrackedRace.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/mock/MockedTrackedRace.java index 50d85683056..f625d913af3 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/mock/MockedTrackedRace.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/mock/MockedTrackedRace.java @@ -7,6 +7,7 @@ import java.util.NavigableSet; import java.util.Set; import java.util.TreeSet; +import com.sap.sailing.domain.abstractlog.race.CompetitorResults; import com.sap.sailing.domain.abstractlog.race.RaceLog; import com.sap.sailing.domain.abstractlog.race.analyzing.impl.RaceLogResolver; import com.sap.sailing.domain.abstractlog.regatta.RegattaLog; @@ -1008,4 +1009,12 @@ public class MockedTrackedRace implements DynamicTrackedRace { public IsManagedByCache resolve(SharedDomainFactory domainFactory) { return this; } + + @Override + public void updateFinishingTimesFromRaceLog(CompetitorResults event) { + } + + @Override + public void updateMarkPassingsAfterRaceLogChanges() { + } } 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 85e2055ac8d..3b093a2eb58 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,5 +1,9 @@ package com.sap.sailing.domain.tracking; +import com.sap.sailing.domain.abstractlog.race.CompetitorResult; +import com.sap.sailing.domain.abstractlog.race.CompetitorResults; +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; @@ -34,13 +38,39 @@ public interface DynamicTrackedRace extends TrackedRace { DynamicGPSFixTrack getOrCreateTrack(Mark mark); /** - * 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. + * 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); + + /** + * The {@link CompetitorResults} from the race log may optionally set a finishing time for competitors. Also, + * depending on how the race log has been modified (e.g., a new pass could have been started or a + * {@link RaceLogFinishPositioningConfirmedEvent} could have been revoked) it may be possible that a finishing + * time previously set from the race log now has to be reset to its original state as coming from the tracking + * provider. This can either mean resetting the mark passing to its {@link MarkPassing#getOriginal() original} + * version or removing it in case the {@link MarkPassing#getOriginal()} method delivers {@code null}, meaning + * that this mark passing was solely based on the race log information which now would have disappeared. + */ + void updateFinishingTimesFromRaceLog(CompetitorResults result); + + /** + * 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 diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/MarkPassing.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/MarkPassing.java index c0e35282026..be1b5c8c789 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/MarkPassing.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/MarkPassing.java @@ -8,4 +8,15 @@ public interface MarkPassing extends Timed { Waypoint getWaypoint(); Competitor getCompetitor(); + + /** + * This mark passing may be a replacement of another mark passing that was created based on some other evidence, + * such as a race committee-provided finishing time. In this case, this method will return the original mark + * passing that was replaced. For regular mark passings, this method will return {@code this} object. It may + * return {@code null} if this mark passing has been synthesized, e.g., solely from a {@link RaceLog} event and needs + * to be removed again if that event is removed because it didn't replace any original mark passing. + * + * TODO what if we receive a real update for the finish mark passing? + */ + MarkPassing getOriginal(); } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DummyMarkPassingWithTimePointAndCompetitor.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DummyMarkPassingWithTimePointAndCompetitor.java index 8dfccfd3922..0ff9c034bac 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DummyMarkPassingWithTimePointAndCompetitor.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DummyMarkPassingWithTimePointAndCompetitor.java @@ -30,4 +30,9 @@ public class DummyMarkPassingWithTimePointAndCompetitor implements MarkPassing { public Competitor getCompetitor() { return competitor; } + + @Override + public MarkPassing getOriginal() { + return this; + } } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DummyMarkPassingWithTimePointOnly.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DummyMarkPassingWithTimePointOnly.java index 1edec662f70..8e94e07d7d1 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DummyMarkPassingWithTimePointOnly.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DummyMarkPassingWithTimePointOnly.java @@ -102,4 +102,9 @@ public class DummyMarkPassingWithTimePointOnly implements MarkPassing { } }; } + + @Override + public MarkPassing getOriginal() { + return this; + } } 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 6a9f5794f4d..f618e32cab7 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 @@ -3,11 +3,13 @@ package com.sap.sailing.domain.tracking.impl; import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable; +import java.util.ArrayList; import java.util.Collection; import java.util.ConcurrentModificationException; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.NavigableSet; import java.util.Set; @@ -15,7 +17,11 @@ import java.util.function.Consumer; import java.util.logging.Level; import java.util.logging.Logger; +import com.sap.sailing.domain.abstractlog.race.CompetitorResult; +import com.sap.sailing.domain.abstractlog.race.CompetitorResults; import com.sap.sailing.domain.abstractlog.race.RaceLog; +import com.sap.sailing.domain.abstractlog.race.RaceLogFinishPositioningConfirmedEvent; +import com.sap.sailing.domain.abstractlog.race.analyzing.impl.ConfirmedFinishPositioningListFinder; import com.sap.sailing.domain.abstractlog.race.analyzing.impl.RaceLogResolver; import com.sap.sailing.domain.base.BoatClass; import com.sap.sailing.domain.base.Competitor; @@ -468,8 +474,159 @@ DynamicTrackedRace, GPSTrackListener { notifyListeners(listener -> listener.markPassingReceived(competitor, oldMarkPassings, markPassings)); } + /** + * A mark passing produced from a {@link RaceLogFinishPositioningConfirmedEvent} where a + * {@link CompetitorResult#getFinishingTime() finishing time} has been provided for a competitor. The mark passing + * may have replaced an original mark passing, as provided by the {@link MarkPassingCalculator} or an external + * connector, or it may have been provided solely based on the presence of the finishing time in the + * {@link RaceLogFinishPositioningConfirmedEvent}. This leads to a different implementation of the + * {@link #getOriginal} implementation. + * + * @author Axel Uhl (D043530) + */ + private static class MarkPassingFromRaceLogProvidedFinishingTimeImpl extends MarkPassingImpl { + private static final long serialVersionUID = 2900812554717213461L; + private final MarkPassing original; + + public MarkPassingFromRaceLogProvidedFinishingTimeImpl(TimePoint timePoint, Waypoint waypoint, Competitor competitor, MarkPassing original) { + super(timePoint, waypoint, competitor); + this.original = original; + } + + /** + * May return {@code null} in case this mark passing was created based solely on the finishing + * time taken from a {@link RaceLogFinishPositioningConfirmedEvent}, or may contain the original + * {@link MarkPassing} that it replaced, otherwise. The original mark passing is expected not to + * implement this interface. + */ + @Override + public MarkPassing getOriginal() { + return original; + } + } + @Override - public void updateMarkPassings(Competitor competitor, Iterable markPassings) { + public void updateMarkPassings(Competitor competitor, final Iterable markPassings) { + final CompetitorResult resultFromRaceLog = getResultsFromRaceLog(competitor); + final Iterable markPassingsToUse = createOrUpdateFinishMarkPassingIfRequired(competitor, markPassings, resultFromRaceLog); + updateMarkPassingsNotConsideringFinishingTimesFromRaceLog(competitor, markPassingsToUse); + } + + @Override + public void updateMarkPassingsAfterRaceLogChanges() { + updateFinishingTimesFromRaceLog(getResultsFromRaceLogs()); + } + + /** + * Iterates over the {@code markPassings} and checks if the finish mark passing needs to be updated based on the + * {@link CompetitorResult}. In the following cases an updated mark passings {@link Iterable} is returned that + * contains an update regarding the finish mark passing: + * + *

    + *
  • An original finish mark passing exists, not wrapped by an updated mark passing or wrapped but not having the + * correct time, or no finish mark passing exists at all, and we have a finishing time in the + * {@code competitorResult}: then an updated wrapper mark passing is used; if an original finish mark passing + * existed, it will be used as the wrapper's {@link MarkPassing#getOriginal()} value; otherwise, {@code null} is + * used as the original.
  • + *
  • An original finish mark passing exists that is wrapped by an updated finishing time that came from the race + * log previously, and we have no finishing time in the {@code competitorResult}: then the wrapper is replaced by + * the original finish mark passing, thus reverting the finishing time to that of the original mark passing.
  • + *
  • A wrapper finish mark passing exists that has a {@code null} original, meaning it was created solely based + * on the race log finishing time, and we have no finish time in the {@code competitorResult}: then the synthetic + * finish mark passing is removed.
  • + *
+ * + * If no such modification was required, the unmodified {@code markPassings} object is returned; otherwise, a new + * {@link Iterable} that contains the modifications regarding the finish mark passing is returned.

+ * + * Note that if that is a result of {@link #getMarkPassings(Competitor)} or + * {@link #getMarkPassingsInOrder(Waypoint)} then the caller must hold the corresponding read lock while calling + * this method. + */ + private Iterable createOrUpdateFinishMarkPassingIfRequired(final Competitor competitor, Iterable markPassings, CompetitorResult competitorResult) { + assert competitorResult == null || competitorResult.getCompetitorId().equals(competitor.getId()); + final Waypoint finish = getRace().getCourse().getLastWaypoint(); + final List copyOfMarkPassings = new ArrayList<>(); + boolean neededToCreateOrUpdateFinishMarkPassing = false; + boolean foundFinishMarkPassing = false; + for (final MarkPassing originalMarkPassing : markPassings) { + if (originalMarkPassing.getWaypoint() != finish) { + copyOfMarkPassings.add(originalMarkPassing); + } else { + foundFinishMarkPassing = true; + final MarkPassing finishMarkPassingToUse; + if (competitorResult != null && competitorResult.getFinishingTime() != null + && (originalMarkPassing.getOriginal() == originalMarkPassing || !originalMarkPassing.getTimePoint().equals(competitorResult.getFinishingTime()))) { + // since we so far only have the original mark passing or a wrapper mark passing with an + // incorrect time point, we need to create a wrapper mark passing: + finishMarkPassingToUse = new MarkPassingFromRaceLogProvidedFinishingTimeImpl( + competitorResult.getFinishingTime(), finish, competitor, + originalMarkPassing.getOriginal()); + logger.info(getRace().getName()+": Updating finish mark passing "+originalMarkPassing.getOriginal()+" to "+finishMarkPassingToUse); + neededToCreateOrUpdateFinishMarkPassing = true; + } else { + finishMarkPassingToUse = originalMarkPassing.getOriginal(); + if (copyOfMarkPassings != originalMarkPassing) { + logger.info(getRace().getName()+": Reverting race log-based finish mark passing "+originalMarkPassing+" to "+finishMarkPassingToUse+ + " because no finishing time found anymore for that competitor in race log"); + neededToCreateOrUpdateFinishMarkPassing = true; + } + } + if (finishMarkPassingToUse != null) { + copyOfMarkPassings.add(finishMarkPassingToUse); + } + } + } + if (!foundFinishMarkPassing && competitorResult != null && competitorResult.getFinishingTime() != null) { + // need to create a synthetic finish mark passing based on the race log-provided finishing time + final MarkPassingFromRaceLogProvidedFinishingTimeImpl finishMarkPassingToUse = new MarkPassingFromRaceLogProvidedFinishingTimeImpl( + competitorResult.getFinishingTime(), finish, competitor, + /* no original finish mark passing exists; synthetic finish mark passing */ null); + copyOfMarkPassings.add(finishMarkPassingToUse); + logger.info(getRace().getName()+": Created "+finishMarkPassingToUse+" based on finishing time provided in race log"); + neededToCreateOrUpdateFinishMarkPassing = true; + } + return neededToCreateOrUpdateFinishMarkPassing ? copyOfMarkPassings : markPassings; + } + + /** + * Looks in all {@link #attachedRaceLogs attached race logs} for the last valid + * {@link RaceLogFinishPositioningConfirmedEvent} that has a {@link CompetitorResult} for {@code competitor}. The + * first one found will be returned; if none is found, null will be returned. + */ + private CompetitorResult getResultsFromRaceLog(Competitor competitor) { + CompetitorResult result = null; + final CompetitorResults results = getResultsFromRaceLogs(); + if (results != null) { + for (CompetitorResult competitorResult : results) { + if (getRace().getCompetitorById(competitorResult.getCompetitorId()) == competitor) { + result = competitorResult; + break; + } + } + } + return result; + } + + private CompetitorResults getResultsFromRaceLogs() { + CompetitorResults results = null; + for (final RaceLog raceLog : attachedRaceLogs.values()) { + results = new ConfirmedFinishPositioningListFinder(raceLog).analyze(); + if (results != null) { + break; + } + } + return results; + } + + /** + * Updates the {@code markPassings} into the {@link #getMarkPassing(Competitor, Waypoint) mark passing data + * structure for the waypoints passed} and the {@link #getMarkPassings(Competitor) mark passing data structure for + * the competitor}. The mark passings are use as-is, without considering any + * {@link CompetitorResult#getFinishingTime() finishing times} for competitors coming from any {@link RaceLog}. + * See also {@link #updateMarkPassings(Competitor, Iterable)} which does consider those. + */ + private void updateMarkPassingsNotConsideringFinishingTimesFromRaceLog(Competitor competitor, Iterable markPassings) { LockUtil.lockForRead(getSerializationLock()); // keep serializer from reading the mark passings collections try { Map oldMarkPassings = new HashMap(); @@ -574,6 +731,33 @@ DynamicTrackedRace, GPSTrackListener { } } + @Override + public void updateFinishingTimesFromRaceLog(CompetitorResults results) { + final Waypoint finish = getRace().getCourse().getLastWaypoint(); + if (finish != null) { // can't do anything for an empty course + final Map resultMap = new HashMap<>(); + if (results != null) { + for (final CompetitorResult result : results) { + resultMap.put(getRace().getCompetitorById(result.getCompetitorId()), result); + } + } + for (final Competitor competitor : getRace().getCompetitors()) { + final CompetitorResult competitorResult = resultMap.get(competitor); + final NavigableSet markPassings = getMarkPassings(competitor); + final Iterable markPassingsToUse; + lockForRead(markPassings); + try { + markPassingsToUse = createOrUpdateFinishMarkPassingIfRequired(competitor, markPassings, competitorResult); + } finally { + unlockAfterRead(markPassings); + } + if (markPassingsToUse != markPassings) { + updateMarkPassingsNotConsideringFinishingTimesFromRaceLog(competitor, markPassingsToUse); + } + } + } + } + @Override public void lockForRead(Iterable markPassings) { getRace().getCourse().lockForRead(); @@ -829,4 +1013,5 @@ DynamicTrackedRace, GPSTrackListener { public DynamicGPSFixTrack getTrack(Mark mark) { return (DynamicGPSFixTrack) super.getTrack(mark); } + } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DynamicTrackedRaceLogListener.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DynamicTrackedRaceLogListener.java index a88bd5419e5..5abcfdd225a 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DynamicTrackedRaceLogListener.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DynamicTrackedRaceLogListener.java @@ -9,6 +9,7 @@ import com.sap.sailing.domain.abstractlog.race.RaceLogCourseDesignChangedEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogDependentStartTimeEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogEndOfTrackingEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogFinishPositioningConfirmedEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogFixedMarkPassingEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogFlagEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogPassChangeEvent; @@ -122,6 +123,7 @@ public class DynamicTrackedRaceLogListener extends BaseRaceLogEventVisitor { if (markPassingUpdateListener != null) { removeMarkPassingEvents(); } + trackedRace.updateMarkPassingsAfterRaceLogChanges(); } } @@ -157,6 +159,7 @@ public class DynamicTrackedRaceLogListener extends BaseRaceLogEventVisitor { markPassingDataFinder = new MarkPassingDataFinder(raceLog); analyzeMarkPassings(); } + trackedRace.updateMarkPassingsAfterRaceLogChanges(); } private void analyseTrackingTimes(RaceLog raceLog) { @@ -237,6 +240,7 @@ public class DynamicTrackedRaceLogListener extends BaseRaceLogEventVisitor { // previous pass was aborted; notify TracTrac trackedRace.onAbortedByRaceCommittee(abortingFlag.getUpperFlag()); } + trackedRace.updateMarkPassingsAfterRaceLogChanges(); } @Override @@ -249,6 +253,16 @@ public class DynamicTrackedRaceLogListener extends BaseRaceLogEventVisitor { analyzeCourseDesign(event.getCourseDesign()); } + /** + * We don't know whether this event is the one that is now valid; it could have a priority too low, or it could have + * a time point too early and thus superseded by a later event of the same type. There may also be other race logs. + * Therefore, re-evaluate the current situation "from scratch." + */ + @Override + public void visit(RaceLogFinishPositioningConfirmedEvent event) { + trackedRace.updateMarkPassingsAfterRaceLogChanges(); + } + @Override public void visit(RaceLogWindFixEvent event) { Wind wind = new RaceLogWindFixDeclinationHelper().getOptionallyDeclinationCorrectedWind(event); diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/MarkPassingImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/MarkPassingImpl.java index 0c7ba574bab..8fbc636e24c 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/MarkPassingImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/MarkPassingImpl.java @@ -38,4 +38,9 @@ public class MarkPassingImpl implements MarkPassing { return ""+getTimePoint()+": "+getCompetitor()+" passed "+getWaypoint(); } + @Override + public MarkPassing getOriginal() { + return this; + } + } diff --git a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/MockedTrackedRace.java b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/MockedTrackedRace.java index 5d721c9a944..c63e64954e6 100755 --- a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/MockedTrackedRace.java +++ b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/MockedTrackedRace.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.NavigableSet; import java.util.Set; +import com.sap.sailing.domain.abstractlog.race.CompetitorResults; import com.sap.sailing.domain.abstractlog.race.RaceLog; import com.sap.sailing.domain.abstractlog.race.analyzing.impl.RaceLogResolver; import com.sap.sailing.domain.abstractlog.regatta.RegattaLog; @@ -790,4 +791,12 @@ public class MockedTrackedRace implements DynamicTrackedRace { public IsManagedByCache resolve(SharedDomainFactory domainFactory) { return this; } + + @Override + public void updateFinishingTimesFromRaceLog(CompetitorResults event) { + } + + @Override + public void updateMarkPassingsAfterRaceLogChanges() { + } } diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RaceLogScoringReplicator.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RaceLogScoringReplicator.java index e7e3f443243..a2b3ad0ec64 100644 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RaceLogScoringReplicator.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RaceLogScoringReplicator.java @@ -7,6 +7,7 @@ import com.sap.sailing.domain.abstractlog.race.CompetitorResults; import com.sap.sailing.domain.abstractlog.race.RaceLog; import com.sap.sailing.domain.abstractlog.race.RaceLogEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogFinishPositioningConfirmedEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogFinishPositioningListChangedEvent; import com.sap.sailing.domain.abstractlog.race.analyzing.impl.ConfirmedFinishPositioningListFinder; import com.sap.sailing.domain.abstractlog.race.analyzing.impl.FinishPositioningListFinder; import com.sap.sailing.domain.base.Competitor; @@ -89,11 +90,11 @@ public class RaceLogScoringReplicator implements RaceColumnListener { @Override public void raceLogEventAdded(RaceColumn raceColumn, RaceLogIdentifier raceLogIdentifier, RaceLogEvent event) { if (event instanceof RaceLogFinishPositioningConfirmedEvent) { - handleFinishPositioningList(raceColumn, raceLogIdentifier, event); + handleFinishPositioningList(raceColumn, raceLogIdentifier, (RaceLogFinishPositioningConfirmedEvent) event); } } - private void handleFinishPositioningList(RaceColumn raceColumn, RaceLogIdentifier raceLogIdentifier, RaceLogEvent event) { + private void handleFinishPositioningList(RaceColumn raceColumn, RaceLogIdentifier raceLogIdentifier, RaceLogFinishPositioningConfirmedEvent event) { Leaderboard leaderboard = service.getLeaderboardByName(raceLogIdentifier.getRegattaLikeParent().getName()); if (leaderboard != null) { Fleet fleet = raceColumn.getFleetByName(raceLogIdentifier.getFleetName()); @@ -108,11 +109,18 @@ public class RaceLogScoringReplicator implements RaceColumnListener { } /** - * Retrieves the last RaceLogFinishPositioningListChangedEvent from the racelog and compares the ranks and - * disqualifications entered by the race committee with the tracked ranks. When a tracked rank for a competitor is - * not the same as the rank of the race committee, a score correction is issued. The positioning list contains a - * list of competitors sorted by the positioning order when finishing. Additionally a MaxPointsReason might be - * entered by the Race Committee.

+ * Called when a {@link RaceLogFinishPositioningConfirmedEvent} was received by the {@link RaceLog}. Retrieves the + * last {@link RaceLogFinishPositioningConfirmedEvent} from the racelog and compares the ranks and disqualifications + * entered by the race committee with the tracked ranks. When a tracked rank for a competitor is not the same as the + * rank of the race committee, a score correction is issued. The positioning list contains a list of competitors + * sorted by the positioning order when finishing. Additionally a MaxPointsReason might be entered by the Race + * Committee. + *

+ * + * For backward compatibility (old releases didn't store the {@link CompetitorResults} in the event) if no + * {@link CompetitorResults} are found even though there was a {@link RaceLogFinishPositioningConfirmedEvent}, the + * last {@link RaceLogFinishPositioningListChangedEvent} event with {@link CompetitorResults} is looked up and its + * results are used. * * TODO bug 3420: also extract the optional score / finishing time / comment and make good use of it * @@ -130,7 +138,8 @@ public class RaceLogScoringReplicator implements RaceColumnListener { // we expect this case for old sailing events such as ESS Singapore, Quingdao, where the confirmation event did not contain the finish // positioning list FinishPositioningListFinder positioningListFinder = new FinishPositioningListFinder(raceLog); - positioningList = positioningListFinder.analyze(); + positioningList = positioningListFinder.analyze(); // this is OK because the precondition for calling this method is that a + // RaceLogFinishPositioningConfirmedEvent event was found in the race log } if (positioningList != null) { for (CompetitorResult positionedCompetitor : positioningList) {