diff --git a/ios/SAPTracker/src/xcode/SAPTracker/SignUp.strings b/ios/SAPTracker/src/xcode/SAPTracker/SignUp.strings deleted file mode 100644 index 837e90ea66f..00000000000 --- a/ios/SAPTracker/src/xcode/SAPTracker/SignUp.strings +++ /dev/null @@ -1,47 +0,0 @@ -/* - SignUp.strings - SAPTracker - - Created by Raimund Wege on 10.08.17. - Copyright © 2017 com.sap.sailing. All rights reserved. -*/ - -/* MARK: - Common */ - -"Common.Email" = "E-Mail"; -"Common.Password" = "Passwort"; -"Common.Server" = "Server"; -"Common.UserName" = "Benutzername"; - -/* MARK: - ForgotPasswordView */ - -"ForgotPasswordView.Title" = "Passwort vergessen"; -"ForgotPasswordView.InfoLabel.Text" = "Benutzernamen oder E-Mail Adresse angeben"; -"ForgotPasswordView.ResetPasswordButton.Title" = "Passwort zurücksetzen"; - -/* MARK: - LoginView */ - -"LoginView.Title" = "Anmelden"; -"LoginView.ForgotPasswordButton.Title" = "Passwort vergessen"; -"LoginView.LoginButton.Title" = "Anmelden"; - -/* MARK: - SignUpControllerError */ - -"SignUpControllerError.LoginFailed" = "Anmeldung fehlgeschlagen. Zugangsdaten überprüfen."; - -/* MARK: - SignUpView */ - -"SignUpView.Title" = "Registrieren"; -"SignUpView.InfoLabel.Text" = "Herzlich Willkommen."; -"SignUpView.FullNameLabel.Text" = "Vollständiger Name"; -"SignUpView.CompanyLabel.Text" = "Unternehmen"; -"SignUpView.PasswordTextField.Placeholder" = "Geben Sie ein neues Passwort ein"; -"SignUpView.RepeatPasswordLabel.Text" = "Passwort Wiederholung"; -"SignUpView.RepeatPasswordTextField.Placeholder" = "Wiederholen Sie das Passwort"; -"SignUpView.SignUpButton.Title" = "Registrieren"; - -/* MARK: - SignUpViewError */ - -"SignUpViewError.UserNameIsToShort" = "Bitte einen Benutzername mit mindestens 3 Zeichen eingeben."; -"SignUpViewError.PasswordIsToShort" = "Bitte ein Passwort mit mindestens 5 Zeichen eingeben."; -"SignUpViewError.PasswordsNotEqual" = "Passwort und Passwortwiederholung müssen übereinstimmen."; diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/SpeedWithBearing.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/SpeedWithBearing.java index 922e93cf28b..4cbd2514d53 100755 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/SpeedWithBearing.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/SpeedWithBearing.java @@ -1,6 +1,7 @@ package com.sap.sailing.domain.common; import com.sap.sse.common.Bearing; +import com.sap.sse.common.Duration; import com.sap.sse.common.Speed; import com.sap.sse.common.TimePoint; import com.sap.sse.datamining.annotations.Statistic; @@ -17,6 +18,10 @@ public interface SpeedWithBearing extends Speed { * applied in reverse. */ Position travelTo(Position pos, TimePoint from, TimePoint to); + + default Position travelTo(Position from, Duration duration) { + return from.translateGreatCircle(getBearing(), travel(duration)); + } /** * Computes the minimal (in terms of bearing change) course and speed change required to reach the diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/LeaderboardEntryDTO.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/LeaderboardEntryDTO.java index 96a2282e80c..c3b55b0444b 100755 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/LeaderboardEntryDTO.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/LeaderboardEntryDTO.java @@ -108,6 +108,12 @@ public class LeaderboardEntryDTO implements Serializable { * race up to the leg's end, regardless of where the leading and fastest boat are. */ public Duration gapToLeaderInOwnTime; + + /** + * The time sailed since the race start time. When the competitor represented by this record has finished the + * race, this will be the duration between race start time and the time when the competitor finished the race. + */ + public Duration timeSailedSinceRaceStart; /** * The corrected time spent in the race; usually based on the current time and distance, calculated by the {@link RankingMetric}. @@ -594,6 +600,7 @@ public class LeaderboardEntryDTO implements Serializable { result = prime * result + ((averageSignedCrossTrackErrorInMeters == null) ? 0 : averageSignedCrossTrackErrorInMeters.hashCode()); result = prime * result + ((boat == null) ? 0 : boat.hashCode()); + result = prime * result + ((timeSailedSinceRaceStart == null) ? 0 : timeSailedSinceRaceStart.hashCode()); result = prime * result + ((calculatedTime == null) ? 0 : calculatedTime.hashCode()); result = prime * result + ((calculatedTimeAtEstimatedArrivalAtCompetitorFarthestAhead == null) ? 0 : calculatedTimeAtEstimatedArrivalAtCompetitorFarthestAhead.hashCode()); @@ -666,6 +673,11 @@ public class LeaderboardEntryDTO implements Serializable { return false; } else if (!boat.equals(other.boat)) return false; + if (timeSailedSinceRaceStart == null) { + if (other.timeSailedSinceRaceStart != null) + return false; + } else if (!timeSailedSinceRaceStart.equals(other.timeSailedSinceRaceStart)) + return false; if (calculatedTime == null) { if (other.calculatedTime != null) return false; diff --git a/java/com.sap.sailing.domain.test/SailingDomainTest (Proxy).launch b/java/com.sap.sailing.domain.test/SailingDomainTest (Proxy).launch deleted file mode 100644 index 0af5786014b..00000000000 --- a/java/com.sap.sailing.domain.test/SailingDomainTest (Proxy).launch +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/java/com.sap.sailing.domain.test/SailingDomainTest (Proxy, No Tunnel).launch b/java/com.sap.sailing.domain.test/SailingDomainTest (Proxy, No Tunnel).launch deleted file mode 100644 index 9860c5f4d43..00000000000 --- a/java/com.sap.sailing.domain.test/SailingDomainTest (Proxy, No Tunnel).launch +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/java/com.sap.sailing.domain.test/SailingDomainTest (Proxy, Remote Profiling).launch b/java/com.sap.sailing.domain.test/SailingDomainTest (Proxy, Remote Profiling).launch deleted file mode 100644 index 11fec4bc9a7..00000000000 --- a/java/com.sap.sailing.domain.test/SailingDomainTest (Proxy, Remote Profiling).launch +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/markpassingcalculation/impl/AbstractCandidateFilterTestSupport.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/markpassingcalculation/impl/AbstractCandidateFilterTestSupport.java new file mode 100755 index 00000000000..0ff8f07fde5 --- /dev/null +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/markpassingcalculation/impl/AbstractCandidateFilterTestSupport.java @@ -0,0 +1,78 @@ +package com.sap.sailing.domain.markpassingcalculation.impl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.NavigableSet; +import java.util.TreeSet; + +import org.junit.Before; + +import com.sap.sailing.domain.markpassingcalculation.Candidate; +import com.sap.sailing.domain.tracking.impl.TimedComparator; +import com.sap.sse.common.Duration; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.Util; +import com.sap.sse.common.impl.MillisecondsTimePoint; + +public class AbstractCandidateFilterTestSupport { + protected Candidate c1, c2, c3, c4, c5, c6, c7, c8, c9, c10; + protected NavigableSet competitorCandidates; + protected Comparator candidateComparator; + protected Candidate start; + protected Candidate end; + + @Before + public void setUp() { + final TimePoint now = MillisecondsTimePoint.now(); + start = new CandidateImpl(0, now.minus(Duration.ONE_HOUR), 1.0, null); + end = new CandidateImpl(6, now.plus(Duration.ONE_HOUR), 1.0, null); + candidateComparator = (c1, c2)->TimedComparator.INSTANCE.compare(c1, c2); + competitorCandidates = new TreeSet<>(candidateComparator); + final Duration gapless = MostProbableCandidatesInSmallTimeRangeFilter.CANDIDATE_FILTER_TIME_WINDOW.divide(2); + c1 = candidate(now, "c1"); + c2 = candidate(c1.getTimePoint().plus(gapless), "c2"); + c3 = candidate(c2.getTimePoint().plus(gapless), "c3"); + c4 = candidate(c3.getTimePoint().plus(gapless), "c4"); + c5 = candidate(c4.getTimePoint().plus(gapless), "c5"); + // add a cap here that is greater than the threshold but not twice as long; + // this way, adding one in the middle of this gap is the interesting case + c6 = candidate(c5.getTimePoint().plus(MostProbableCandidatesInSmallTimeRangeFilter.CANDIDATE_FILTER_TIME_WINDOW.times(1.5)), "c6"); + c7 = candidate(c6.getTimePoint().plus(gapless), "c7"); + c8 = candidate(c7.getTimePoint().plus(gapless), "c8"); + c9 = candidate(c8.getTimePoint().plus(gapless), "c9"); + c10= candidate(c9.getTimePoint().plus(gapless), "c10"); + competitorCandidates.add(c1); + competitorCandidates.add(c2); + competitorCandidates.add(c3); + competitorCandidates.add(c4); + competitorCandidates.add(c5); + competitorCandidates.add(c6); + competitorCandidates.add(c7); + competitorCandidates.add(c8); + competitorCandidates.add(c9); + competitorCandidates.add(c10); + } + + protected void assertContainsExactly(Iterable candidates, Candidate... candidatesExpected) { + assertEquals(""+candidates+"'s size does not match that of "+Arrays.toString(candidatesExpected)+"; ", + candidatesExpected.length, Util.size(candidates)); + assertContains(candidates, candidatesExpected); + } + + protected void assertContains(Iterable candidates, Candidate...candidatesExpected) { + for (final Candidate candidateExpected : candidatesExpected) { + assertTrue("Expected "+candidateExpected+" to be in "+candidates+" but it wasn't; ", + Util.contains(candidates, candidateExpected)); + } + } + + protected Candidate candidate(TimePoint timePoint, String name) { + return new CandidateImpl(1, timePoint, 1.0, null) { + private static final long serialVersionUID = 8547646939054562751L; + @Override public String toString() { return name; } + }; + } +} diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/markpassingcalculation/impl/MostProbableCandidatesInSmallTimeRangeFilterTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/markpassingcalculation/impl/MostProbableCandidatesInSmallTimeRangeFilterTest.java index f9cc9978e56..9753bb6abdd 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/markpassingcalculation/impl/MostProbableCandidatesInSmallTimeRangeFilterTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/markpassingcalculation/impl/MostProbableCandidatesInSmallTimeRangeFilterTest.java @@ -1,76 +1,38 @@ package com.sap.sailing.domain.markpassingcalculation.impl; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -import java.util.Comparator; import java.util.NavigableSet; -import java.util.TreeSet; import org.junit.Before; import org.junit.Test; import com.sap.sailing.domain.markpassingcalculation.Candidate; -import com.sap.sailing.domain.tracking.impl.TimedComparator; -import com.sap.sse.common.Duration; -import com.sap.sse.common.TimePoint; -import com.sap.sse.common.impl.MillisecondsTimePoint; -public class MostProbableCandidatesInSmallTimeRangeFilterTest { - private Candidate c1, c2, c3, c4, c5, c6, c7, c8, c9, c10; - private NavigableSet competitorCandidates; +public class MostProbableCandidatesInSmallTimeRangeFilterTest extends AbstractCandidateFilterTestSupport { private MostProbableCandidatesInSmallTimeRangeFilter filter; @Before public void setUp() { - final TimePoint now = MillisecondsTimePoint.now(); - final Candidate start = new CandidateImpl(0, now.minus(Duration.ONE_HOUR), 1.0, null); - final Candidate end = new CandidateImpl(6, now.plus(Duration.ONE_HOUR), 1.0, null); - final Comparator candidateComparator = (c1, c2)->TimedComparator.INSTANCE.compare(c1, c2); + super.setUp(); filter = new MostProbableCandidatesInSmallTimeRangeFilter( candidateComparator, start, end); - competitorCandidates = new TreeSet<>(candidateComparator); - final Duration gapless = MostProbableCandidatesInSmallTimeRangeFilter.CANDIDATE_FILTER_TIME_WINDOW.divide(2); - c1 = candidate(now, "c1"); - c2 = candidate(c1.getTimePoint().plus(gapless), "c2"); - c3 = candidate(c2.getTimePoint().plus(gapless), "c3"); - c4 = candidate(c3.getTimePoint().plus(gapless), "c4"); - c5 = candidate(c4.getTimePoint().plus(gapless), "c5"); - // add a cap here that is greater than the threshold but not twice as long; - // this way, adding one in the middle of this gap is the interesting case - c6 = candidate(c5.getTimePoint().plus(MostProbableCandidatesInSmallTimeRangeFilter.CANDIDATE_FILTER_TIME_WINDOW.times(1.5)), "c6"); - c7 = candidate(c6.getTimePoint().plus(gapless), "c7"); - c8 = candidate(c7.getTimePoint().plus(gapless), "c8"); - c9 = candidate(c8.getTimePoint().plus(gapless), "c9"); - c10= candidate(c9.getTimePoint().plus(gapless), "c10"); - competitorCandidates.add(c1); - competitorCandidates.add(c2); - competitorCandidates.add(c3); - competitorCandidates.add(c4); - competitorCandidates.add(c5); - competitorCandidates.add(c6); - competitorCandidates.add(c7); - competitorCandidates.add(c8); - competitorCandidates.add(c9); - competitorCandidates.add(c10); } @Test public void testGetTimeWiseContiguousDistanceCandidates() { final NavigableSet result = filter.getTimeWiseContiguousDistanceCandidates(competitorCandidates, c3, /* includeStartFrom */ true); - assertContains(result, c1, c2, c3, c4, c5); + assertContainsExactly(result, c1, c2, c3, c4, c5); } @Test public void testGetTimeWiseContiguousDistanceCandidatesOnRightBorder() { final NavigableSet result = filter.getTimeWiseContiguousDistanceCandidates(competitorCandidates, c5, /* includeStartFrom */ true); - assertContains(result, c1, c2, c3, c4, c5); + assertContainsExactly(result, c1, c2, c3, c4, c5); } @Test public void testGetTimeWiseContiguousDistanceCandidatesOnLeftBorder() { final NavigableSet result = filter.getTimeWiseContiguousDistanceCandidates(competitorCandidates, c6, /* includeStartFrom */ true); - assertContains(result, c6, c7, c8, c9, c10); + assertContainsExactly(result, c6, c7, c8, c9, c10); } @Test @@ -78,7 +40,7 @@ public class MostProbableCandidatesInSmallTimeRangeFilterTest { final Candidate middleOfGap = candidate(c5.getTimePoint().plus(c5.getTimePoint().until(c6.getTimePoint()).divide(2)), "gap"); final NavigableSet result = filter.getTimeWiseContiguousDistanceCandidates(competitorCandidates, middleOfGap, /* includeStartFrom */ true); - assertContains(result, c1, c2, c3, c4, c5, middleOfGap, c6, c7, c8, c9, c10); + assertContainsExactly(result, c1, c2, c3, c4, c5, middleOfGap, c6, c7, c8, c9, c10); } @Test @@ -86,7 +48,7 @@ public class MostProbableCandidatesInSmallTimeRangeFilterTest { final Candidate middleOfGap = candidate(c5.getTimePoint().plus(c5.getTimePoint().until(c6.getTimePoint()).divide(2)), "gap"); final NavigableSet result = filter.getTimeWiseContiguousDistanceCandidates(competitorCandidates, middleOfGap, /* includeStartFrom */ false); - assertContains(result, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10); + assertContainsExactly(result, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10); } @Test @@ -95,7 +57,7 @@ public class MostProbableCandidatesInSmallTimeRangeFilterTest { competitorCandidates.add(middleOfGap); final NavigableSet result = filter.getTimeWiseContiguousDistanceCandidates(competitorCandidates, middleOfGap, /* includeStartFrom */ true); - assertContains(result, c1, c2, c3, c4, c5, middleOfGap, c6, c7, c8, c9, c10); + assertContainsExactly(result, c1, c2, c3, c4, c5, middleOfGap, c6, c7, c8, c9, c10); } @Test @@ -105,20 +67,6 @@ public class MostProbableCandidatesInSmallTimeRangeFilterTest { final Candidate middleOfGap = candidate(c5.getTimePoint().plus(c5.getTimePoint().until(c6.getTimePoint()).divide(2)), "gap"); final NavigableSet result = filter.getTimeWiseContiguousDistanceCandidates(competitorCandidates, middleOfGap, /* includeStartFrom */ true); - assertContains(result, middleOfGap); - } - - private void assertContains(NavigableSet candidates, Candidate... candidatesExpected) { - assertEquals(candidatesExpected.length, candidates.size()); - for (final Candidate candidateExpected : candidatesExpected) { - assertTrue(candidates.contains(candidateExpected)); - } - } - - private Candidate candidate(TimePoint timePoint, String name) { - return new CandidateImpl(1, timePoint, 1.0, null) { - private static final long serialVersionUID = 8547646939054562751L; - @Override public String toString() { return name; } - }; + assertContainsExactly(result, middleOfGap); } } diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/markpassingcalculation/impl/StationarySequenceBasedFilterTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/markpassingcalculation/impl/StationarySequenceBasedFilterTest.java new file mode 100755 index 00000000000..082c3959d80 --- /dev/null +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/markpassingcalculation/impl/StationarySequenceBasedFilterTest.java @@ -0,0 +1,122 @@ +package com.sap.sailing.domain.markpassingcalculation.impl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + +import java.util.NavigableSet; +import java.util.TreeSet; + +import org.junit.Before; +import org.junit.Test; + +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.common.Position; +import com.sap.sailing.domain.common.SpeedWithBearing; +import com.sap.sailing.domain.common.impl.DegreePosition; +import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; +import com.sap.sailing.domain.common.tracking.GPSFixMoving; +import com.sap.sailing.domain.common.tracking.impl.GPSFixMovingImpl; +import com.sap.sailing.domain.markpassingcalculation.Candidate; +import com.sap.sailing.domain.test.TrackBasedTest; +import com.sap.sailing.domain.tracking.DynamicGPSFixTrack; +import com.sap.sailing.domain.tracking.impl.DynamicGPSFixMovingTrackImpl; +import com.sap.sse.common.Bearing; +import com.sap.sse.common.Duration; +import com.sap.sse.common.Speed; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.Util; +import com.sap.sse.common.impl.DegreeBearingImpl; + +public class StationarySequenceBasedFilterTest extends AbstractCandidateFilterTestSupport { + private StationarySequence stationarySequence; + private DynamicGPSFixTrack track; + private final static int FIXES_BETWEEN_CANDIDATES = 3; + + @Before + public void setUp() { + super.setUp(); + track = new DynamicGPSFixMovingTrackImpl(TrackBasedTest.createCompetitorWithBoat("Someone"), /* millisecondsOverWhichToAverage */ 15000); + // construct a track such that all ten candidates at first end up within a single stationary sequence: + Position position = new DegreePosition(0, 0); + final SpeedWithBearing verySmallSpeed = new KnotSpeedWithBearingImpl(StationarySequence.CANDIDATE_FILTER_DISTANCE.scale(0.5/ /* number of candidates */ 10. / (double) FIXES_BETWEEN_CANDIDATES). + inTime(c1.getTimePoint().until(c10.getTimePoint())).getKnots(), new DegreeBearingImpl(0)); + TimePoint previousTimePoint = c1.getTimePoint(); + for (final Candidate candidate : new Candidate[] { c2, c3, c4, c5, c6, c7, c8, c9, c10 }) { + final Duration durationBetweenFixes = previousTimePoint.until(candidate.getTimePoint()).divide(FIXES_BETWEEN_CANDIDATES); + TimePoint timePoint = previousTimePoint; + for (int i=0; i candidatesEffectivelyAdded = new TreeSet<>(candidateComparator); + TreeSet candidatesEffectivelyRemoved = new TreeSet<>(candidateComparator); + for (final Candidate candidate : new Candidate[] { c2, c3, c4, c5, c6, c7, c8, c9, c10 }) { + assertTrue(stationarySequence.tryToExtendAfterLast(candidate, candidatesEffectivelyAdded, candidatesEffectivelyRemoved)); + } + } + + @Test + public void basicStationarySequenceTest() { + assertEquals(10, stationarySequence.size()); + assertContainsExactly(stationarySequence.getAllCandidates(), c1, c2, c3, c4, c5, c6, c7, c8, c9, c10); + final Iterable validCandidates = stationarySequence.getValidCandidates(); + assertContains(validCandidates, c1, c10); // at least the first and the last are expected to be contained + // but those in the middle, particularly with the gap between c5 and c6, are not expected to be contained + assertFalse(Util.contains(validCandidates, c5)); + assertFalse(Util.contains(validCandidates, c6)); + } + + @Test + public void testAddingOutlierFix() { + // additional set-up: + NavigableSet originalValidCandidates = new TreeSet<>(candidateComparator); + Util.addAll(stationarySequence.getValidCandidates(), originalValidCandidates); + final GPSFixMoving fixAtC2 = track.getFirstFixAtOrAfter(c2.getTimePoint()); + final GPSFixMoving fixAfterFixAtC2 = track.getFirstFixAfter(fixAtC2.getTimePoint()); + assertNotSame(fixAtC2, fixAfterFixAtC2); + final Bearing outlierCourse = fixAtC2.getSpeed().getBearing().add(new DegreeBearingImpl(90)); + // add fix in between, traveling more than StationarySequence.CANDIDATE_FILTER_DISTANCE + final Position outlierPosition = fixAtC2.getPosition().translateGreatCircle( + outlierCourse, StationarySequence.CANDIDATE_FILTER_DISTANCE.scale(2)); + final TimePoint inBetweenTimePoint = fixAtC2.getTimePoint().plus(fixAtC2.getTimePoint().until(fixAfterFixAtC2.getTimePoint()).divide(2)); + final Speed outlierSpeed = fixAtC2.getPosition().getDistance(outlierPosition).inTime(fixAtC2.getTimePoint().until(inBetweenTimePoint)); + final GPSFixMoving fixInBetween = new GPSFixMovingImpl(outlierPosition, inBetweenTimePoint, new KnotSpeedWithBearingImpl(outlierSpeed.getKnots(), outlierCourse)); + track.add(fixInBetween); + TreeSet candidatesEffectivelyAdded = new TreeSet<>(candidateComparator); + TreeSet candidatesEffectivelyRemoved = new TreeSet<>(candidateComparator); + final StationarySequence splitResult = stationarySequence.tryToAddFix(fixInBetween, candidatesEffectivelyAdded, candidatesEffectivelyRemoved, + /* stationarySequenceSetToUpdate */ null, /* isReplacement */ false); + // assertions: + assertEquals(2, stationarySequence.size()); + assertContainsExactly(stationarySequence.getAllCandidates(), c1, c2); + assertContainsExactly(stationarySequence.getValidCandidates(), c1, c2); + assertEquals(8, splitResult.size()); + assertContainsExactly(splitResult.getAllCandidates(), c3, c4, c5, c6, c7, c8, c9, c10); + assertContains(splitResult.getValidCandidates(), c3, c10); + NavigableSet validCandidates = new TreeSet<>(candidateComparator); + Util.addAll(stationarySequence.getValidCandidates(), validCandidates); + Util.addAll(splitResult.getValidCandidates(), validCandidates); + assertCorrectDiff(originalValidCandidates, validCandidates, candidatesEffectivelyAdded, candidatesEffectivelyRemoved); + } + + private void assertCorrectDiff(NavigableSet originalValidCandidates, + NavigableSet validCandidates, TreeSet candidatesEffectivelyAdded, + TreeSet candidatesEffectivelyRemoved) { + NavigableSet newValidCandidates = new TreeSet<>(candidateComparator); + newValidCandidates.addAll(validCandidates); + newValidCandidates.removeAll(originalValidCandidates); + assertContainsExactly(candidatesEffectivelyAdded, newValidCandidates.toArray(new Candidate[0])); + NavigableSet removedValidCandidates = new TreeSet<>(candidateComparator); + removedValidCandidates.addAll(originalValidCandidates); + removedValidCandidates.removeAll(validCandidates); + assertContainsExactly(candidatesEffectivelyRemoved, removedValidCandidates.toArray(new Candidate[0])); + } +} diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/LeaderboardScoringAndRankingTestBase.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/LeaderboardScoringAndRankingTestBase.java index d650dec691f..169ef1b0971 100644 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/LeaderboardScoringAndRankingTestBase.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/LeaderboardScoringAndRankingTestBase.java @@ -38,6 +38,7 @@ import com.sap.sailing.domain.tracking.MarkPassing; import com.sap.sailing.domain.tracking.TrackedRace; import com.sap.sailing.domain.tracking.impl.MarkPassingImpl; import com.sap.sailing.domain.tracking.impl.TimedComparator; +import com.sap.sse.common.Duration; import com.sap.sse.common.TimePoint; import com.sap.sse.common.impl.MillisecondsTimePoint; import com.sap.sse.util.impl.ArrayListNavigableSet; @@ -98,6 +99,12 @@ public class LeaderboardScoringAndRankingTestBase extends AbstractLeaderboardTes result.add(new MarkPassingImpl(lastMarkPassingTimes.get(competitor), finish, competitor)); return result; } + @Override + public Duration getTimeSailedSinceRaceStart(Competitor competitor, TimePoint timePoint) { + final TimePoint timePointOfFinishMarkPassing = getMarkPassings(competitor).last().getTimePoint(); + final TimePoint to = timePointOfFinishMarkPassing.before(timePoint) ? timePointOfFinishMarkPassing : timePoint; + return to.before(getStartOfRace()) ? null : getStartOfRace().until(to); + } }; trackedRace.getRace().getCourse().addWaypoint(0, start); trackedRace.getRace().getCourse().addWaypoint(1, finish); diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractLeaderboardWithCache.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractLeaderboardWithCache.java index 6eb406ee3c7..3f9d47db486 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractLeaderboardWithCache.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractLeaderboardWithCache.java @@ -118,13 +118,14 @@ public abstract class AbstractLeaderboardWithCache implements Leaderboard { private final Distance averageAbsoluteCrossTrackError; private final Distance averageSignedCrossTrackError; private final Duration gapToLeaderInOwnTime; + private final Duration timeSailedSinceRaceStart; private final Duration correctedTime; private final Duration correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead; public RaceDetails(List legDetails, Distance windwardDistanceToCompetitorFarthestAhead, Distance averageAbsoluteCrossTrackError, Distance averageSignedCrossTrackError, - Duration gapToLeaderInOwnTime, Duration correctedTime, - Duration correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead) { + Duration gapToLeaderInOwnTime, Duration timeSailedSinceRaceStart, + Duration correctedTime, Duration correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead) { super(); this.legDetails = legDetails; this.windwardDistanceToCompetitorFarthestAhead = windwardDistanceToCompetitorFarthestAhead; @@ -132,6 +133,7 @@ public abstract class AbstractLeaderboardWithCache implements Leaderboard { this.averageSignedCrossTrackError = averageSignedCrossTrackError; this.gapToLeaderInOwnTime = gapToLeaderInOwnTime; this.correctedTime = correctedTime; + this.timeSailedSinceRaceStart = timeSailedSinceRaceStart; this.correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead = correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead; } public List getLegDetails() { @@ -149,6 +151,9 @@ public abstract class AbstractLeaderboardWithCache implements Leaderboard { public Duration getGapToLeaderInOwnTime() { return gapToLeaderInOwnTime; } + public Duration getTimeSailedSinceRaceStart() { + return timeSailedSinceRaceStart; + } public Duration getCorrectedTime() { return correctedTime; } @@ -618,6 +623,7 @@ public abstract class AbstractLeaderboardWithCache implements Leaderboard { : raceDetails.getAverageAbsoluteCrossTrackError().getMeters(); entryDTO.averageSignedCrossTrackErrorInMeters = raceDetails.getAverageSignedCrossTrackError() == null ? null : raceDetails.getAverageSignedCrossTrackError().getMeters(); + entryDTO.timeSailedSinceRaceStart = raceDetails.getTimeSailedSinceRaceStart(); entryDTO.calculatedTime = raceDetails.getCorrectedTime(); entryDTO.calculatedTimeAtEstimatedArrivalAtCompetitorFarthestAhead = raceDetails.getCorrectedTimeAtEstimatedArrivalAtCompetitorFarthestAhead(); entryDTO.gapToLeaderInOwnTime = raceDetails.getGapToLeaderInOwnTime(); @@ -874,6 +880,7 @@ public abstract class AbstractLeaderboardWithCache implements Leaderboard { final CompetitorRankingInfo competitorRankingInfo = rankingInfo.getCompetitorRankingInfo().apply(competitor); return new RaceDetails(legDetails, windwardDistanceToCompetitorFarthestAhead, averageAbsoluteCrossTrackError, averageSignedCrossTrackError, trackedRace.getRankingMetric().getGapToLeaderInOwnTime(rankingInfo, competitor, cache), + trackedRace.getTimeSailedSinceRaceStart(competitor, timePoint), trackedRace.getRankingMetric().getCorrectedTime(competitor, timePoint), competitorRankingInfo == null ? null : competitorRankingInfo.getCorrectedTimeAtEstimatedArrivalAtCompetitorFarthestAhead()); } finally { @@ -1202,32 +1209,12 @@ public abstract class AbstractLeaderboardWithCache implements Leaderboard { Duration result = null; for (TrackedRace trackedRace : getTrackedRaces()) { if (Util.contains(trackedRace.getRace().getCompetitors(), competitor)) { - NavigableSet markPassings = trackedRace.getMarkPassings(competitor); - if (!markPassings.isEmpty()) { - TimePoint from = trackedRace.getStartOfRace(); // start counting at race start, not when the competitor passed the line - if (from != null && !timePoint.before(from)) { // but only if the race started after timePoint - TimePoint to; - if (timePoint.after(markPassings.last().getTimePoint()) - && markPassings.last().getWaypoint() == trackedRace.getRace().getCourse() - .getLastWaypoint()) { - // stop counting when competitor finished the race - to = markPassings.last().getTimePoint(); - } else { - if (trackedRace.getEndOfTracking() != null - && timePoint.after(trackedRace.getEndOfTracking())) { - result = null; // race not finished until end of tracking; no reasonable value can be - // computed for competitor - break; - } else { - to = timePoint; - } - } - Duration timeSpent = from.until(to); - if (result == null) { - result = timeSpent; - } else { - result=result.plus(timeSpent); - } + final Duration timeSpent = trackedRace.getTimeSailedSinceRaceStart(competitor, timePoint); + if (timeSpent != null) { + if (result == null) { + result = timeSpent; + } else { + result=result.plus(timeSpent); } } } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateFinderImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateFinderImpl.java index 797468bfd04..25140af58e7 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateFinderImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateFinderImpl.java @@ -799,10 +799,11 @@ public class CandidateFinderImpl implements CandidateFinder { // case we can quickly obtain the next "firstFixAfter" by simply calling next() on the iterator. firstFixAfterIterator = track.getFixesIterator(t, /* inclusive */ false); } - if (firstFixAfterIterator.hasNext()) { - fixAfter = firstFixAfterIterator.next(); - } else { - fixAfter = null; + try { + fixAfter = Util.nextOrNull(firstFixAfterIterator); + } catch (ConcurrentModificationException e) { + firstFixAfterIterator = track.getFixesIterator(t, /* inclusive */ false); + fixAfter = Util.nextOrNull(firstFixAfterIterator); } lastIterationFix = fix; lastIterationAfterFix = fixAfter; diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/StationarySequence.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/StationarySequence.java index 7d7267e6aaf..cc1284d6283 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/StationarySequence.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/StationarySequence.java @@ -38,9 +38,9 @@ public class StationarySequence { * If we identify several consecutive candidates that all lie in a bounding box with a {@link Bounds#getDiameter() * diameter} less than or equal to this distance, only the first and the last of those candidates pass the filter. */ - private static final Distance CANDIDATE_FILTER_DISTANCE = new MeterDistance(30); + public static final Distance CANDIDATE_FILTER_DISTANCE = new MeterDistance(30); - private static final Duration CANDIDATE_FILTER_TIME_WINDOW = Duration.ONE_SECOND.times(10); + public static final Duration CANDIDATE_FILTER_TIME_WINDOW = Duration.ONE_SECOND.times(10); private final NavigableSet candidates; @@ -55,7 +55,7 @@ public class StationarySequence { * first} and {@link #getLast() last} element at the same time. Therefore, the track segment spanned by the * resulting sequence is empty which trivially fulfills the bounding box criterion. */ - StationarySequence(Candidate seed, Comparator candidateComparator, GPSFixTrack track) { + public StationarySequence(Candidate seed, Comparator candidateComparator, GPSFixTrack track) { this.candidates = new TreeSet<>(candidateComparator); this.candidateComparator = candidateComparator; this.track = track; @@ -75,7 +75,7 @@ public class StationarySequence { * {@code candidatesEffectivelyRemoved}; {@code true} is returned in this case, indicating that the sequence * has been extended successfully. */ - boolean tryToExtendAfterLast(Candidate candidateAfterSequence, + public boolean tryToExtendAfterLast(Candidate candidateAfterSequence, Set candidatesEffectivelyAdded, Set candidatesEffectivelyRemoved) { Bounds bounds = computeExtendedBoundsForFixesBetweenCandidates(getLast(), candidateAfterSequence); if (bounds != null) { @@ -141,7 +141,7 @@ public class StationarySequence { * method maintains the set referenced by this parameter accordingly, assuming that the position in the * set may change, or, if this sequence runs empty, it has to be removed from the set altogether. */ - boolean tryToExtendBeforeFirst(Candidate candidateBeforeSequence, + public boolean tryToExtendBeforeFirst(Candidate candidateBeforeSequence, Set candidatesEffectivelyAdded, Set candidatesEffectivelyRemoved, NavigableSet stationarySequenceSetToUpdate) { Bounds bounds = computeExtendedBoundsForFixesBetweenCandidates(candidateBeforeSequence, getFirst()); @@ -218,7 +218,7 @@ public class StationarySequence { return candidatesNoLongerPassingFilter; } - Iterable getValidCandidates() { + public Iterable getValidCandidates() { return ()->candidates.stream().filter(c->isCloseEnoughToSequenceBorder(c)).iterator(); } @@ -227,11 +227,11 @@ public class StationarySequence { candidate.getTimePoint().until(getLast().getTimePoint()).abs().compareTo(CANDIDATE_FILTER_TIME_WINDOW) < 0; } - int size() { + public int size() { return candidates.size(); } - boolean isEmpty() { + public boolean isEmpty() { return candidates.isEmpty(); } @@ -365,8 +365,9 @@ public class StationarySequence { * no split took place, or the split didn't leave more than one candidate for a second sequence}; the new * sequence created by a split otherwise. */ - StationarySequence tryToAddFix(GPSFixMoving newFix, Set candidatesEffectivelyAdded, - Set candidatesEffectivelyRemoved, NavigableSet stationarySequenceSetToUpdate) { + public StationarySequence tryToAddFix(GPSFixMoving newFix, Set candidatesEffectivelyAdded, + Set candidatesEffectivelyRemoved, NavigableSet stationarySequenceSetToUpdate, + boolean isReplacement) { assert !newFix.getTimePoint().before(getFirst().getTimePoint()); assert !newFix.getTimePoint().after(getLast().getTimePoint()); final Bounds newBounds = boundingBoxOfTrackSpanningCandidates.extend(newFix.getPosition()); @@ -374,6 +375,12 @@ public class StationarySequence { if (newBounds.getDiameter().compareTo(CANDIDATE_FILTER_DISTANCE) < 0) { boundingBoxOfTrackSpanningCandidates = newBounds; // ...and we're done tailSequence = null; + if (isReplacement) { + // the fix could have replaced another one, and the bounding box may shrink now; + // later additions may unnecessarily split, so we should be kind and refresh the bounding + // box in this case: + refreshBoundingBox(); + } } else { // split: final Set oldValidCandidates = new TreeSet<>(candidateComparator); @@ -413,7 +420,7 @@ public class StationarySequence { } final Set candidatesAdded = new TreeSet<>(candidateComparator); candidatesAdded.addAll(newValidCandidates); - candidatesAdded.removeAll(newValidCandidates); + candidatesAdded.removeAll(oldValidCandidates); final Set candidatesRemoved = new TreeSet<>(candidateComparator); candidatesRemoved.addAll(oldValidCandidates); candidatesRemoved.removeAll(newValidCandidates); @@ -450,19 +457,20 @@ public class StationarySequence { /** * If this sequence contains a {@link Candidate} that matches the {@code timePoint} exactly, that candidate is - * returned. Otherwise, a dummy candidate is created that has the {@code timePoint} provided.

+ * returned. Otherwise, a dummy candidate is created that has the {@code timePoint} provided. + *

* - * Note that depending on the {@link #candidateComparator} used, two candidates are not necessarily considered - * equal only because they have equal time points. + * Note that depending on the {@link #candidateComparator} used, two candidates are not necessarily considered equal + * only because they have equal time points. */ private Candidate getCandidateMatchingTimePointOrCreateDummy(TimePoint timePoint) { final Candidate dummy = new CandidateImpl(/* one-based index of waypoint */ 1, timePoint, /* probability */ 0, /* waypoint */ null); final Candidate floorCandidate = candidates.floor(dummy); final Candidate ceilingCandidate = candidates.ceiling(dummy); final Candidate result; - if (floorCandidate.getTimePoint().equals(timePoint)) { + if (floorCandidate != null && floorCandidate.getTimePoint().equals(timePoint)) { result = floorCandidate; - } else if (ceilingCandidate.getTimePoint().equals(timePoint)) { + } else if (ceilingCandidate != null && ceilingCandidate.getTimePoint().equals(timePoint)) { result = ceilingCandidate; } else { result = dummy; @@ -484,7 +492,7 @@ public class StationarySequence { return new CandidateImpl(/* one-based index of waypoint */ 1, timePoint, /* probability */ 0, /* waypoint */ null); } - Iterable getAllCandidates() { + public Iterable getAllCandidates() { return Collections.unmodifiableCollection(candidates); } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/StationarySequenceBasedFilter.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/StationarySequenceBasedFilter.java index 830ce564e10..b51a8a00627 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/StationarySequenceBasedFilter.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/StationarySequenceBasedFilter.java @@ -526,7 +526,10 @@ public class StationarySequenceBasedFilter { StationarySequence.createDummyCandidate(newFix.getTimePoint()))); if (lastSequenceStartingAtOrBeforeFix != null && !lastSequenceStartingAtOrBeforeFix.getLast().getTimePoint().before(newFix.getTimePoint())) { // fix falls into the existing StationarySequence; update its bounding box: - final StationarySequence splitResult = lastSequenceStartingAtOrBeforeFix.tryToAddFix(newFix, candidatesEffectivelyAdded, candidatesEffectivelyRemoved, /* StationarySequence set to update */ stationarySequences); + final StationarySequence splitResult = lastSequenceStartingAtOrBeforeFix.tryToAddFix( + newFix, candidatesEffectivelyAdded, candidatesEffectivelyRemoved, + /* StationarySequence set to update */ stationarySequences, + /* was replacement */ fixesReplacingExistingOnes != null && Util.contains(fixesReplacingExistingOnes, newFix)); assert !stationarySequences.contains(lastSequenceStartingAtOrBeforeFix) || lastSequenceStartingAtOrBeforeFix.size() > 1; if (splitResult != null) { assert splitResult.size() > 1; @@ -549,29 +552,34 @@ public class StationarySequenceBasedFilter { final boolean fixIsInStationarySequence = lastSequenceStartingAtOrBeforeFix != null && !lastSequenceStartingAtOrBeforeFix.getLast().getTimePoint().before(fixReplacingExistingOne.getTimePoint()); if (!fixIsInStationarySequence) { - final StationarySequence lastSequenceEndingBeforeFix = // null in case fix is within a stationary sequence - lastSequenceStartingAtOrBeforeFix != null && fixIsInStationarySequence ? null : lastSequenceStartingAtOrBeforeFix; + final StationarySequence lastSequenceEndingBeforeFix = lastSequenceStartingAtOrBeforeFix; // null in case there is no sequence coming before the fix final Candidate lastCandidateBeforeReplacementFix = candidates.lower(dummyCandidateForReplacementFix); - if (lastCandidateBeforeReplacementFix != null) { + if (lastCandidateBeforeReplacementFix != null) { // no need to try to extend a later sequence to "the left" if there is nothing final Candidate firstCandidateAfterReplacementFix = candidates.higher(dummyCandidateForReplacementFix); - if (firstCandidateAfterReplacementFix != null) { - // the fix is between two candidates, so we may try to extend or create a stationary sequence: - if (lastCandidateBeforeReplacementFix == lastSequenceEndingBeforeFix.getLast()) { - // previous candidate is end of a sequence; try to extend - lastSequenceEndingBeforeFix.tryToExtendAfterLast(firstCandidateAfterReplacementFix, candidatesEffectivelyAdded, candidatesEffectivelyRemoved); + if (firstCandidateAfterReplacementFix != null) { // no need to try to extend an earlier sequence to "the right" if there is nothing + // the fix is between two candidates, so we may try to extend or create a stationary sequence if not the next + // candidate is already part of a sequence. We don't merge sequences as of now, and sequences must be disjoint regarding + // their set of candidates. + final StationarySequence firstSequenceStartingAfterFix = stationarySequences.higher(dummyStationarySequenceForFix); + final boolean lookRight; + final boolean previousCandidateIsLastInSequence = lastSequenceEndingBeforeFix != null && lastCandidateBeforeReplacementFix == lastSequenceEndingBeforeFix.getLast(); + final boolean nextCandidateIsFirstInSequence = firstSequenceStartingAfterFix != null && firstCandidateAfterReplacementFix == firstSequenceStartingAfterFix.getFirst(); + if (previousCandidateIsLastInSequence && !nextCandidateIsFirstInSequence) { + // previous candidate is end of a sequence and next one is not part of a sequence; try to extend previous sequence to "the right": + lookRight = !lastSequenceEndingBeforeFix.tryToExtendAfterLast(firstCandidateAfterReplacementFix, candidatesEffectivelyAdded, candidatesEffectivelyRemoved); } else { - final StationarySequence firstSequenceStartingAfterFix = stationarySequences.higher(dummyStationarySequenceForFix); - if (firstCandidateAfterReplacementFix == firstSequenceStartingAfterFix.getFirst()) { - // next candidate is start of a sequence; try to extend - firstSequenceStartingAfterFix.tryToExtendBeforeFirst(lastCandidateBeforeReplacementFix, - candidatesEffectivelyAdded, candidatesEffectivelyRemoved, - /* StationarySequence set to update */ stationarySequences); - } else { - // none of the adjacent candidates is part of a sequence; try to create a new one: - final StationarySequence newSequence = createStationarySequence(lastCandidateBeforeReplacementFix); - if (newSequence.tryToExtendAfterLast(firstCandidateAfterReplacementFix, candidatesEffectivelyAdded, candidatesEffectivelyRemoved)) { - stationarySequences.add(newSequence); - } + lookRight = true; + } + if (lookRight && nextCandidateIsFirstInSequence && !previousCandidateIsLastInSequence) { + // next candidate is start of a sequence and previous candidate is not part of a sequence; try to extend next sequence to "the left": + firstSequenceStartingAfterFix.tryToExtendBeforeFirst(lastCandidateBeforeReplacementFix, + candidatesEffectivelyAdded, candidatesEffectivelyRemoved, + /* StationarySequence set to update */ stationarySequences); + } else if (!previousCandidateIsLastInSequence && !nextCandidateIsFirstInSequence) { + // none of the adjacent candidates is part of a sequence; try to create a new one: + final StationarySequence newSequence = createStationarySequence(lastCandidateBeforeReplacementFix); + if (newSequence.tryToExtendAfterLast(firstCandidateAfterReplacementFix, candidatesEffectivelyAdded, candidatesEffectivelyRemoved)) { + stationarySequences.add(newSequence); } } } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/ranking/AbstractRankingMetric.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/ranking/AbstractRankingMetric.java index a5a26187e77..b8b460e45c0 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/ranking/AbstractRankingMetric.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/ranking/AbstractRankingMetric.java @@ -261,12 +261,15 @@ public abstract class AbstractRankingMetric implements RankingMetric { /** * The time from the {@link TrackedRace#getStartOfRace() race start} until timePoint or until - * the point in time when competitor passed the finish mark, whichever comes first. + * the point in time when competitor passed the finish mark, whichever comes first. If there is + * no mark passing for {@code competitor} for the last waypoint or no {@link TrackedRace#getStartOfRace()} is + * known, {@code null} is returned. */ - protected Duration getActualTimeSinceStartOfRace(Competitor competitor, TimePoint timePoint) { + @Override + public Duration getActualTimeSinceStartOfRace(Competitor competitor, TimePoint timePoint) { final Duration result; final TimePoint startOfRace = getTrackedRace().getStartOfRace(); - if (startOfRace == null) { + if (startOfRace == null || timePoint.before(startOfRace)) { result = null; } else { final Waypoint finish = getTrackedRace().getRace().getCourse().getLastWaypoint(); @@ -277,7 +280,11 @@ public abstract class AbstractRankingMetric implements RankingMetric { if (finishingMarkPassing != null && finishingMarkPassing.getTimePoint().before(timePoint)) { result = startOfRace.until(finishingMarkPassing.getTimePoint()); } else { - result = startOfRace.until(timePoint); + if (trackedRace.getEndOfTracking() != null && timePoint.after(trackedRace.getEndOfTracking())) { + result = null; // race not finished until end of tracking; no reasonable value can be computed for competitor + } else { + result = startOfRace.until(timePoint); + } } } } @@ -435,7 +442,9 @@ public abstract class AbstractRankingMetric implements RankingMetric { getTrackedRace().getRace().getCourse().getIndexOfWaypoint(legWho.getTrackedLeg().getLeg().getFrom()) <= getTrackedRace().getRace().getCourse().getIndexOfWaypoint(legTo.getTrackedLeg().getLeg().getFrom()); final Duration result; - if (legWho == null || legTo == null || !legWho.hasStartedLeg(timePoint) || !legTo.hasStartedLeg(timePoint)) { + if (legWho == null || legTo == null || + !(legWho.hasStartedLeg(timePoint) || legWho.hasFinishedLeg(timePoint)) || + !(legTo.hasStartedLeg(timePoint) || legTo.hasFinishedLeg(timePoint))) { result = null; } else { final Competitor who = legWho.getCompetitor(); @@ -494,18 +503,17 @@ public abstract class AbstractRankingMetric implements RankingMetric { * For the situation at timePoint, determines how long in real, uncorrected time who lags * behind to in the leg identified by legWho. If both are still sailing in the leg at * timePoint, this is the time who needs with constant average VMG to reach - * to's position at timePoint. If only to has already finished the leg and no - * mark passing time is known yet for who for the end of the leg then who is projected to - * the end of the leg using her average VMG on the leg, and the difference between who's projected and - * to's actual mark passing times is returned. - *

- * - * If leg finish mark passings are available for both, who and to, the difference between - * them is returned. + * to's position at timePoint. If only to has already finished the leg at + * {@code timePoint} then who is projected to the end of the leg using her average VMG on the leg, and + * the difference between who's projected and to's actual mark passing times is returned. + * Note that in this latter case it doesn't matter whether {@code who} already has a mark passing for the end of the + * leg or not; the idea is to not "rewrite history" by letting the mark passing have an impact on the rankings prior + * to the mark passing. *

* * The result may be a negative duration in case who reached the position in question before - * timePoint.

+ * timePoint. + *

* * Precondition: who and to have both started sailing the leg at timePoint * and to has sailed a greater or equal windward distance compared to who. If not, the @@ -513,8 +521,8 @@ public abstract class AbstractRankingMetric implements RankingMetric { */ protected Duration getPredictedDurationToEndOfLegOrTo(TimePoint timePoint, final TrackedLegOfCompetitor legWho, final TrackedLegOfCompetitor legTo, WindLegTypeAndLegBearingCache cache) { - assert legWho.hasStartedLeg(timePoint); - assert legTo.hasStartedLeg(timePoint); + assert legWho.hasStartedLeg(timePoint) || legWho.hasFinishedLeg(timePoint); + assert legTo.hasStartedLeg(timePoint) || legTo.hasFinishedLeg(timePoint); final Duration toEndOfLegOrTo; if (legTo.hasFinishedLeg(timePoint)) { // calculate actual time it takes who to reach the end of the leg starting at timePoint: @@ -527,9 +535,8 @@ public abstract class AbstractRankingMetric implements RankingMetric { getWindwardDistanceTraveled(legWho.getCompetitor(), legWho.hasFinishedLeg(timePoint)?legWho.getFinishTime():timePoint, cache)) >= 0; // estimate who's leg finishing time by extrapolating with the average VMG (if available) or the current VMG // (if no average VMG can currently be computed, e.g., because the time point is exactly at the leg start) - final Position windwardPositionToReachInWhosCurrentLeg = - getTrackedRace().getApproximatePosition(legWho.getLeg().getTo(), timePoint); - toEndOfLegOrTo = getDurationToReach(windwardPositionToReachInWhosCurrentLeg, timePoint, legWho, cache); + final Position positionOfEndOfLeg = getTrackedRace().getApproximatePosition(legWho.getLeg().getTo(), timePoint); + toEndOfLegOrTo = getDurationToReach(positionOfEndOfLeg, timePoint, legWho, cache); } } else { // competitor "to" is still in same leg; project "who" to "to"'s position using VMG diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/ranking/OneDesignRankingMetric.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/ranking/OneDesignRankingMetric.java index 2f38163ebef..dbfdb71615b 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/ranking/OneDesignRankingMetric.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/ranking/OneDesignRankingMetric.java @@ -197,7 +197,6 @@ public class OneDesignRankingMetric extends AbstractRankingMetric { public Duration getCorrectedTimeAtEstimatedArrivalAtCompetitorFarthestAhead() { return getEstimatedActualDurationFromRaceStartToCompetitorFarthestAhead(); } - }; } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/ranking/RankingMetric.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/ranking/RankingMetric.java index 0dd2269675b..9c9d8208e3a 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/ranking/RankingMetric.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/ranking/RankingMetric.java @@ -154,6 +154,14 @@ public interface RankingMetric extends Serializable { Comparator getLegRankingComparator(TrackedLeg trackedLeg, TimePoint timePoint, WindLegTypeAndLegBearingCache cache); + /** + * Determine the time sailed for the {@code competitor} at {@code timePoint} in this race. This ignores whether or not + * the race has recorded a start mark passing for the {@code competitor}. If no finish mark passing is found either, the + * duration between the {@link #getStartOfRace() race start time} and {@code timePoint} is returned; otherwise the duration + * between the {@link #getStartOfRace() race start time} and the time when the {@code competitor} finished the race. + */ + Duration getActualTimeSinceStartOfRace(Competitor competitor, TimePoint timePoint); + /** * How much time did the competitor spend in the {@link TrackedRace#getRace() race} described by * trackedRace at timePoint, corrected by the handicapping rules defined by this ranking 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 bde6658fda8..120e12fa6e2 100755 --- 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 @@ -999,6 +999,18 @@ public interface TrackedRace */ TargetTimeInfo getEstimatedTimeToComplete(TimePoint timepoint) throws NotEnoughDataHasBeenAddedException, NoWindException; + /** + * Determine the time sailed for the {@code competitor} at {@code timePoint} in this race. This ignores whether or + * not the race has recorded a start mark passing for the {@code competitor}. If no finish mark passing is found + * either, the duration between the {@link #getStartOfRace() race start time} and {@code timePoint} is returned; + * otherwise the duration between the {@link #getStartOfRace() race start time} and the time when the + * {@code competitor} finished the race. If there is no mark passing for {@code competitor} for the last waypoint or + * no {@link TrackedRace#getStartOfRace()} is known, {@code null} is returned. + */ + default Duration getTimeSailedSinceRaceStart(Competitor competitor, TimePoint timePoint) { + return null; + } + /** * Calculates the estimated distance it takes a competitor to sail the race, from start to finish. * 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 25686b0339d..bfce78a01cf 100644 --- 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 @@ -1097,8 +1097,9 @@ DynamicTrackedRace, GPSTrackListener { @Override public void attachRaceLog(RaceLog raceLog) { - logListener.addTo(raceLog); + logListener.beforeAttaching(raceLog); super.attachRaceLog(raceLog); + logListener.afterAttaching(raceLog); getRaceState(raceLog).addChangedListener(raceStateBasedStartTimeChangedListener); } @@ -1106,9 +1107,12 @@ DynamicTrackedRace, GPSTrackListener { public RaceLog detachRaceLog(Serializable identifier) { final RaceLog attachedRaceLog = attachedRaceLogs.get(identifier); if (attachedRaceLog != null) { - logListener.removeFrom(attachedRaceLog); + logListener.beforeDetaching(attachedRaceLog); } final RaceLog raceLogDetached = super.detachRaceLog(identifier); + if (attachedRaceLog != null) { + logListener.afterDetaching(attachedRaceLog); + } assert raceLogDetached == attachedRaceLog; synchronized (raceStates) { final ReadonlyRaceState raceState = raceStates.remove(attachedRaceLog); 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 bcc66c88fa7..f9ced9a8709 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 @@ -47,7 +47,7 @@ import com.sap.sse.common.Util.Triple; * Listens for changes on a {@link RaceLog} and forwards the relevant ones to a {@link TrackedRace}. Examples: start time changes; * fixed mark passings changes; course design changes; wind fixes entered by the race committee.

* - * For each {@link RaceLog} {@link #addTo(RaceLog) attached}, a {@link ReadonlyRaceState} is constructed that is used to observe + * For each {@link RaceLog} {@link #beforeAttaching(RaceLog) attached}, a {@link ReadonlyRaceState} is constructed that is used to observe * some properties in an easier to use form than observing the {@link RaceLog}s directly. For example, the {@link ReadonlyRaceState} * can tell the {@link ReadonlyRaceState#getFinishedTime() finished time} and it is possible to receive change notifications for * this value. @@ -74,7 +74,7 @@ public class DynamicTrackedRaceLogListener extends BaseRaceLogEventVisitor { raceCommitteeWindSource = new WindSourceImpl(WindSourceType.RACECOMMITTEE); } - public void addTo(RaceLog raceLog) { + public void beforeAttaching(RaceLog raceLog) { if (raceLog == null) { logger.severe("Trying to add " + this + " as listener to a null race log for tracked race " + trackedRace.getRace()); } else { @@ -87,12 +87,15 @@ public class DynamicTrackedRaceLogListener extends BaseRaceLogEventVisitor { trackedRace.setFinishedTime(state.getFinishedTime()); } }); - trackedRace.invalidateStartTime(); - trackedRace.invalidateEndTime(); - analyze(raceLog); } } + public void afterAttaching(RaceLog raceLog) { + trackedRace.invalidateStartTime(); + trackedRace.invalidateEndTime(); + analyze(raceLog); + } + /** * @return the first non-{@code null} {@link ReadonlyRaceState#getFinishedTime() finished time} that is found in any * of the {@link RaceState}s constructed for all the {@link RaceLog}s observed by this listener, or {@code null} @@ -139,7 +142,7 @@ public class DynamicTrackedRaceLogListener extends BaseRaceLogEventVisitor { } } - public void removeFrom(RaceLog raceLog) { + public void beforeDetaching(RaceLog raceLog) { // Gets called whenever a RaceColumn was already linked to a TrackedRace on linkage. // Maybe we need to reset the status of the old race somehow? There might be no new // TrackedRace to be linked... @@ -147,13 +150,19 @@ public class DynamicTrackedRaceLogListener extends BaseRaceLogEventVisitor { // TODO: // ??? trackedRace.setStatus(new TrackedRaceStatusImpl(TrackedRaceStatusEnum.PREPARED, 0.0)); if (raceLog != null) { - trackedRace.invalidateStartTime(); - removeAllWindFixesFromWindTrack(raceLog); raceLog.removeListener(this); raceLogs.remove(raceLog); if (markPassingUpdateListener != null) { removeMarkPassingEvents(); } + } + } + + + public void afterDetaching(RaceLog raceLog) { + if (raceLog != null) { + trackedRace.invalidateStartTime(); + removeAllWindFixesFromWindTrack(raceLog); trackedRace.updateMarkPassingsAfterRaceLogChanges(); } } @@ -356,5 +365,4 @@ public class DynamicTrackedRaceLogListener extends BaseRaceLogEventVisitor { trackedRace.invalidateEndTime(); } } - } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java index 012947267cc..d8297647876 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java @@ -3548,6 +3548,11 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl return new TargetTimeInfoImpl(legTargetTimes); } + @Override + public Duration getTimeSailedSinceRaceStart(Competitor competitor, TimePoint timePoint) { + return getRankingMetric().getActualTimeSinceStartOfRace(competitor, timePoint); + } + @Override public Distance getEstimatedDistanceToComplete(final TimePoint timepoint) throws NotEnoughDataHasBeenAddedException, NoWindException { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/whatsnew/resources/SailingAnalyticsNotes.html b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/whatsnew/resources/SailingAnalyticsNotes.html index 7935c90bbea..b046b1848c8 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/whatsnew/resources/SailingAnalyticsNotes.html +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/whatsnew/resources/SailingAnalyticsNotes.html @@ -4,6 +4,13 @@

What's New - SAP Sailing Analytics

+
May 2019
+
    +
  • Improvements for handicap regattas: when finishing times have been entered, leading to finish line + mark passings, those times will now be considered during calculating the corrected times. This way + it has become possible to fix results even if tracking was incomplete.
  • +
+
April 2019
  • Competitor tails on the map can now be colored according to a metric. This enables quick and intuitive diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/DetailTypeFormatter.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/DetailTypeFormatter.java index 1cfb170489e..b9f3053564c 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/DetailTypeFormatter.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/DetailTypeFormatter.java @@ -461,9 +461,7 @@ public class DetailTypeFormatter { case RACE_GAP_TO_LEADER_IN_SECONDS: case LEG_ESTIMATED_TIME_TO_NEXT_WAYPOINT_IN_SECONDS: case LEG_CORRECTED_TIME_TRAVELED: - case RACE_CALCULATED_TIME_TRAVELED: case TIME_BETWEEN_RACE_START_AND_COMPETITOR_START: - case RACE_CALCULATED_TIME_AT_ESTIMATED_ARRIVAL_AT_COMPETITOR_FARTHEST_AHEAD: case EXPEDITION_RACE_TIME_TO_BURN_TO_LINE: case EXPEDITION_LEG_TIME_TO_BURN_TO_LINE: case EXPEDITION_RACE_TIME_TO_GUN: @@ -484,6 +482,8 @@ public class DetailTypeFormatter { case RACE_TIME_TRAVELED_DOWNWIND: case RACE_TIME_TRAVELED_REACHING: case RACE_TIME_TRAVELED_UPWIND: + case RACE_CALCULATED_TIME_TRAVELED: + case RACE_CALCULATED_TIME_AT_ESTIMATED_ARRIVAL_AT_COMPETITOR_FARTHEST_AHEAD: case LEG_TIME_TRAVELED: case BRAVOEXTENDED_LEG_CURRENT_DURATION_FOILED_IN_SECONDS: case RACE_CURRENT_DURATION_FOILED_IN_SECONDS: diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardPanel.java index 547e5880dbf..e841fe0cdfa 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardPanel.java @@ -1265,17 +1265,17 @@ public abstract class LeaderboardPanel extends A result.put(DetailType.RACE_CURRENT_LEG, new FormattedDoubleLeaderboardRowDTODetailTypeColumn(DetailType.RACE_CURRENT_LEG, new CurrentLeg(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); result.put(DetailType.RACE_TIME_TRAVELED, - new TimeTraveledRaceColumn(getLeaderboardPanel(), this, stringMessages, LEG_COLUMN_HEADER_STYLE, + new TimeTraveledRaceColumnInSeconds(getLeaderboardPanel(), this, stringMessages, LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LEG_DETAIL_COLUMN_HEADER_STYLE, LEG_DETAIL_COLUMN_STYLE)); result.put(DetailType.RACE_CALCULATED_TIME_TRAVELED, - new FormattedDoubleLeaderboardRowDTODetailTypeColumn(DetailType.RACE_CALCULATED_TIME_TRAVELED, + new TotalTimeColumn(DetailType.RACE_CALCULATED_TIME_TRAVELED, new RaceCalculatedTimeTraveledInSeconds(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); result.put(DetailType.RACE_CALCULATED_TIME_AT_ESTIMATED_ARRIVAL_AT_COMPETITOR_FARTHEST_AHEAD, - new FormattedDoubleLeaderboardRowDTODetailTypeColumn( - DetailType.RACE_CALCULATED_TIME_AT_ESTIMATED_ARRIVAL_AT_COMPETITOR_FARTHEST_AHEAD, + new TotalTimeColumn(DetailType.RACE_CALCULATED_TIME_AT_ESTIMATED_ARRIVAL_AT_COMPETITOR_FARTHEST_AHEAD, new RaceCalculatedTimeAtEstimatedArrivalAtCompetitorFarthestAheadInSeconds(), - LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); + LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, + LeaderboardPanel.this)); result.put(DetailType.EXPEDITION_RACE_AWA, new FormattedDoubleLeaderboardRowDTODetailTypeColumn(DetailType.EXPEDITION_RACE_AWA, new DoubleTextRaceDetailTypeExtractor(LeaderboardEntryDTO::getExpeditionAWA), @@ -3030,12 +3030,13 @@ public abstract class LeaderboardPanel extends A } /** - * To be expandable, a race column needs to have one or more tracked races associated and needs to have at least - * some GPS data available. Wind data is not required for expandability because several metrics can reasonably be - * determined even without reliable wind information. + * To be expandable, a race column needs to have one or more tracked races associated. No GPS data is required + * (anymore) because there are some interesting columns that may be fed solely based on the Race Log contents, such + * as finishing times and start times, resulting in time sailed and calculated times. Wind data is not required for + * expandability either because several metrics can reasonably be determined even without reliable wind information. */ private boolean shallExpandRaceColumn(RaceColumnDTO raceColumnDTO) { - return showRaceDetails && raceColumnDTO.hasTrackedRaces() && raceColumnDTO.hasGPSData(); + return showRaceDetails && raceColumnDTO.hasTrackedRaces(); } private void removeUnusedRaceColumns(LeaderboardDTO leaderboard) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/OverallTimeTraveledColumn.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/OverallTimeTraveledColumn.java index 955061d4576..32be47e2b1a 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/OverallTimeTraveledColumn.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/OverallTimeTraveledColumn.java @@ -50,7 +50,6 @@ public class OverallTimeTraveledColumn extends ExpandableSortableColumn LeaderboardPanel leaderboardPanel, StringMessages stringConstants, String detailHeaderStyle, String detailColumnStyle) { Map> result = new HashMap<>(); - result.put(DetailType.TOTAL_TIME_SAILED_UPWIND_IN_SECONDS, new TotalTimeColumn(DetailType.TOTAL_TIME_SAILED_UPWIND_IN_SECONDS, new TotalTimeSailedUpwindInSeconds(), detailHeaderStyle, detailColumnStyle, leaderboardPanel)); @@ -60,7 +59,6 @@ public class OverallTimeTraveledColumn extends ExpandableSortableColumn result.put(DetailType.TOTAL_TIME_SAILED_REACHING_IN_SECONDS, new TotalTimeColumn(DetailType.TOTAL_TIME_SAILED_REACHING_IN_SECONDS, new TotalTimeSailedReachingInSeconds(), detailHeaderStyle, detailColumnStyle, leaderboardPanel)); - return result; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/TimeTraveledRaceColumn.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/TimeTraveledRaceColumnInSeconds.java similarity index 85% rename from java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/TimeTraveledRaceColumn.java rename to java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/TimeTraveledRaceColumnInSeconds.java index 8802fa01d27..9442c5f6bc5 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/TimeTraveledRaceColumn.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/TimeTraveledRaceColumnInSeconds.java @@ -20,7 +20,7 @@ import com.sap.sse.common.InvertibleComparator; import com.sap.sse.common.impl.InvertibleComparatorAdapter; import com.sap.sse.gwt.client.celltable.AbstractSortableColumnWithMinMax; -public class TimeTraveledRaceColumn extends ExpandableSortableColumn implements HasStringAndDoubleValue { +public class TimeTraveledRaceColumnInSeconds extends ExpandableSortableColumn implements HasStringAndDoubleValue { private static final DetailType DETAIL_TYPE = DetailType.RACE_TIME_TRAVELED; @@ -31,7 +31,7 @@ public class TimeTraveledRaceColumn extends ExpandableSortableColumn imp private String headerStyle; private MinMaxRenderer minmaxRenderer; - public TimeTraveledRaceColumn(LeaderboardPanel leaderboardPanel, RaceNameProvider raceNameProvider, StringMessages stringMessages, String headerStyle, String columnStyle, + public TimeTraveledRaceColumnInSeconds(LeaderboardPanel leaderboardPanel, RaceNameProvider raceNameProvider, StringMessages stringMessages, String headerStyle, String columnStyle, String detailHeaderStyle, String detailColumnStyle) { super(leaderboardPanel, /* expandable */true, new TextCell(), DETAIL_TYPE.getDefaultSortingOrder(), stringMessages, detailHeaderStyle, detailColumnStyle, @@ -46,24 +46,17 @@ public class TimeTraveledRaceColumn extends ExpandableSortableColumn imp @Override public Double getDoubleValue(LeaderboardRowDTO row) { - Long result = null; + final Double result; LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null && fieldsForRace.legDetails != null) { - for (LegEntryDTO legDetail : fieldsForRace.legDetails) { - if (legDetail != null) { - if (legDetail.timeInMilliseconds != null) { - if (result == null) { - result = 0l; - } - result += legDetail.timeInMilliseconds; - } - } - } + if (fieldsForRace != null && fieldsForRace.timeSailedSinceRaceStart != null) { + result = fieldsForRace.timeSailedSinceRaceStart.asSeconds(); + } else { + result = null; } - return result == null ? null : new Long(result / 1000).doubleValue(); + return result; } - private Double getTimeTraveledFor(LeaderboardRowDTO row, LegType legType) { + private Double getTimeTraveledForLegTypeInSeconds(LeaderboardRowDTO row, LegType legType) { Long result = null; LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); if (fieldsForRace != null && fieldsForRace.legDetails != null) { @@ -101,21 +94,21 @@ public class TimeTraveledRaceColumn extends ExpandableSortableColumn imp private class RaceTimeTraveledUpwindInSeconds implements DataExtractor { @Override public Double get(LeaderboardRowDTO row) { - return getTimeTraveledFor(row, LegType.UPWIND); + return getTimeTraveledForLegTypeInSeconds(row, LegType.UPWIND); } } private class RaceTimeTraveledDownwindInSeconds implements DataExtractor { @Override public Double get(LeaderboardRowDTO row) { - return getTimeTraveledFor(row, LegType.DOWNWIND); + return getTimeTraveledForLegTypeInSeconds(row, LegType.DOWNWIND); } } private class RaceTimeTraveledReachingInSeconds implements DataExtractor { @Override public Double get(LeaderboardRowDTO row) { - return getTimeTraveledFor(row, LegType.REACHING); + return getTimeTraveledForLegTypeInSeconds(row, LegType.REACHING); } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/TotalTimeColumn.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/TotalTimeColumn.java index c80a6b93e6d..dc1fcc04d3b 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/TotalTimeColumn.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/TotalTimeColumn.java @@ -12,9 +12,9 @@ import com.sap.sailing.gwt.ui.client.DurationAsHoursMinutesSecondsFormatter; */ public class TotalTimeColumn extends FormattedDoubleLeaderboardRowDTODetailTypeColumn { - protected TotalTimeColumn(DetailType detailType, DataExtractor field, String headerStyle, + protected TotalTimeColumn(DetailType detailType, DataExtractor fieldWithTimeInSeconds, String headerStyle, String columnStyle, DisplayedLeaderboardRowsProvider displayedLeaderboardRowsProvider) { - super(detailType, field, headerStyle, columnStyle, displayedLeaderboardRowsProvider); + super(detailType, fieldWithTimeInSeconds, headerStyle, columnStyle, displayedLeaderboardRowsProvider); } @Override diff --git a/java/com.sap.sailing.selenium.test/com.sap.sailing.selenium.test (Proxy).launch b/java/com.sap.sailing.selenium.test/com.sap.sailing.selenium.test (Proxy).launch deleted file mode 100644 index 3cd0d9e57aa..00000000000 --- a/java/com.sap.sailing.selenium.test/com.sap.sailing.selenium.test (Proxy).launch +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/java/com.sap.sailing.server.replication.test/ReplicationTests (Proxy).launch b/java/com.sap.sailing.server.replication.test/ReplicationTests (Proxy).launch deleted file mode 100644 index a152a7fd218..00000000000 --- a/java/com.sap.sailing.server.replication.test/ReplicationTests (Proxy).launch +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/java/com.sap.sailing.server.trackfiles.test/com.sap.sailing.server.trackfiles.test (Proxy).launch b/java/com.sap.sailing.server.trackfiles.test/com.sap.sailing.server.trackfiles.test (Proxy).launch deleted file mode 100644 index 375504975e1..00000000000 --- a/java/com.sap.sailing.server.trackfiles.test/com.sap.sailing.server.trackfiles.test (Proxy).launch +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - 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 a43cbbed654..3e59c3e8796 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 @@ -16,7 +16,9 @@ import com.sap.sailing.domain.base.RaceColumnListener; import com.sap.sailing.domain.base.impl.RaceColumnListenerWithDefaultAction; import com.sap.sailing.domain.common.LeaderboardNameConstants; import com.sap.sailing.domain.common.MaxPointsReason; +import com.sap.sailing.domain.common.RankingMetrics; import com.sap.sailing.domain.leaderboard.Leaderboard; +import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; import com.sap.sailing.domain.leaderboard.ScoringScheme; import com.sap.sailing.domain.racelog.RaceLogIdentifier; import com.sap.sailing.server.interfaces.RacingEventService; @@ -176,8 +178,17 @@ public class RaceLogScoringReplicator implements RaceColumnListenerWithDefaultAc // let scoring scheme calculate the penalty score dynamically scoreByRaceCommittee = null; } else { - scoreByRaceCommittee = leaderboard.getScoringScheme().getScoreForRank(leaderboard, raceColumn, competitor, - oneBasedRankByRaceCommittee, ()->numberOfCompetitorsInRace, leaderboard.getNumberOfCompetitorsInLeaderboardFetcher(), timePoint); + // Check whether it's a non-one-design regatta. In that case, "rank by race committee" just means finish line + // crossing order, but that's not relevant for the actual *ranking* in a handicapped regatta. Corrected/calculated + // times are. Therefore, for handicap regattas, don't ask the scoring scheme to map the finishing order + // to a score. + if (leaderboard instanceof RegattaLeaderboard && + ((RegattaLeaderboard) leaderboard).getRegatta().getRankingMetricType() != RankingMetrics.ONE_DESIGN) { + scoreByRaceCommittee = null; + } else { + scoreByRaceCommittee = leaderboard.getScoringScheme().getScoreForRank(leaderboard, raceColumn, competitor, + oneBasedRankByRaceCommittee, ()->numberOfCompetitorsInRace, leaderboard.getNumberOfCompetitorsInLeaderboardFetcher(), timePoint); + } } } else { scoreByRaceCommittee = optionalExplicitScore; diff --git a/java/com.sap.sailing.www/release_notes_admin.html b/java/com.sap.sailing.www/release_notes_admin.html index dccb8b9490f..62b9ecc88ad 100755 --- a/java/com.sap.sailing.www/release_notes_admin.html +++ b/java/com.sap.sailing.www/release_notes_admin.html @@ -21,6 +21,15 @@

    Release Notes - Administration Console

    +

    May 2019

    +
      +
    • Ranks (finishing orders) entered by the Race Manager app user are no longer + considered for handicap regattas. Only an explicit score will be accepted, + or a finishing time can be provided which will then let the handicap ranking + metric infer the correct rank from that. +
    • +
    +

    April 2019

    • Filtering the leaderboard groups during a Master Data Import now also diff --git a/translation_v2.json b/translation_v2.json index bcb4436a3b7..1f487c83550 100644 --- a/translation_v2.json +++ b/translation_v2.json @@ -18,6 +18,23 @@ } ] }, + { + "collectionName":"DataMiningGwtUi", + "folders":[ + { + "startingFolderPath":"java/com.sap.sse.datamining.ui/java/com/sap/sse/datamining/ui/client", + "sourceFilters":[ + "**/StringMessages*.properties" + ], + "excludedSourceFilters":[ + "**/*_zh.properties" + ], + "xlfFolderPath":"/XLF/[fullPath]/[targetFile]", + "targetFolderPath":".", + "pseudoLocFolderPath":"." + } + ] + }, { "collectionName":"RacelogTracking", "folders":[ @@ -85,7 +102,7 @@ "collectionName":"FileStorage", "folders":[ { - "startingFolderPath":"java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage", + "startingFolderPath":"java/com.sap.sse.gwt/src/com/sap/sse/gwt/client", "sourceFilters":[ "**/StringMessages*.properties" ],