diff --git a/doc/SAPSailingAnalytics_Administrator_Training.pptx b/doc/SAPSailingAnalytics_Administrator_Training.pptx index b7b81e9293a..2a9148eb5e6 100644 Binary files a/doc/SAPSailingAnalytics_Administrator_Training.pptx and b/doc/SAPSailingAnalytics_Administrator_Training.pptx differ diff --git a/java/com.sap.sailing.dashboards.gwt/.classpath b/java/com.sap.sailing.dashboards.gwt/.classpath index 34d9ee0253f..874972618da 100644 --- a/java/com.sap.sailing.dashboards.gwt/.classpath +++ b/java/com.sap.sailing.dashboards.gwt/.classpath @@ -1,5 +1,6 @@ + @@ -7,6 +8,5 @@ - diff --git a/java/com.sap.sailing.dashboards.gwt/.settings/com.google.gwt.eclipse.core.prefs b/java/com.sap.sailing.dashboards.gwt/.settings/com.google.gwt.eclipse.core.prefs index f416a9822d7..e4f2807cc68 100644 --- a/java/com.sap.sailing.dashboards.gwt/.settings/com.google.gwt.eclipse.core.prefs +++ b/java/com.sap.sailing.dashboards.gwt/.settings/com.google.gwt.eclipse.core.prefs @@ -1,4 +1,4 @@ eclipse.preferences.version=1 entryPointModules= -filesCopiedToWebInfLib=gwt-servlet.jar +filesCopiedToWebInfLib= gwtCompileSettings=PGd3dC1jb21waWxlLXNldHRpbmdzPjxsb2ctbGV2ZWw+SU5GTzwvbG9nLWxldmVsPjxvdXRwdXQtc3R5bGU+T0JGVVNDQVRFRDwvb3V0cHV0LXN0eWxlPjxleHRyYS1hcmdzPjwhW0NEQVRBWy13YXIgLiAtbG9jYWxXb3JrZXJzIDMgLXN0cmljdCAtbG9nTGV2ZWwgVFJBQ0VdXT48L2V4dHJhLWFyZ3M+PHZtLWFyZ3M+PCFbQ0RBVEFbLVhteDIwMjRtXV0+PC92bS1hcmdzPjxlbnRyeS1wb2ludC1tb2R1bGU+Y29tLnNhcC5zYWlsaW5nLmRhc2hib2FyZHMuZ3d0LlJpYkRhc2hib2FyZDwvZW50cnktcG9pbnQtbW9kdWxlPjwvZ3d0LWNvbXBpbGUtc2V0dGluZ3M+ diff --git a/java/com.sap.sailing.dashboards.gwt/SailingDashboardsGWT sdm.launch b/java/com.sap.sailing.dashboards.gwt/SailingDashboardsGWT sdm.launch old mode 100755 new mode 100644 index aefd1e3ffb6..e6230b9950b --- a/java/com.sap.sailing.dashboards.gwt/SailingDashboardsGWT sdm.launch +++ b/java/com.sap.sailing.dashboards.gwt/SailingDashboardsGWT sdm.launch @@ -1,47 +1,47 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com.sap.sailing.dashboards.gwt/SailingDashboardsGWT.launch b/java/com.sap.sailing.dashboards.gwt/SailingDashboardsGWT.launch old mode 100755 new mode 100644 index 463a6cf721f..20731950d47 --- a/java/com.sap.sailing.dashboards.gwt/SailingDashboardsGWT.launch +++ b/java/com.sap.sailing.dashboards.gwt/SailingDashboardsGWT.launch @@ -1,47 +1,47 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/AbstractDistance.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/AbstractDistance.java index f8af9f7472d..456a7ad7a2a 100755 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/AbstractDistance.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/AbstractDistance.java @@ -79,4 +79,20 @@ public abstract class AbstractDistance implements Distance { public Distance add(Distance d) { return new NauticalMileDistance(getNauticalMiles()+d.getNauticalMiles()); } + + @Override + public double divide(Distance other) { + return getMeters() / other.getMeters(); + } + + @Override + public Distance abs() { + final Distance result; + if (getMeters() >= 0) { + result = this; + } else { + result = scale(-1); + } + return result; + } } diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/AbstractPosition.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/AbstractPosition.java index 6f8be9c57b8..082b654c88b 100755 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/AbstractPosition.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/AbstractPosition.java @@ -164,7 +164,8 @@ public class AbstractPosition implements Position { @Override public Distance getDistanceToLine(Position left, Position right) { final Distance result; - final int factor = this.crossTrackError(left, left.getBearingGreatCircle(right)).getMeters()>0?1:-1; + final Distance crossTrackError = this.crossTrackError(left, left.getBearingGreatCircle(right)); + final int factor = crossTrackError.getMeters()>0?1:-1; double toLeft = Math.abs(left.getBearingGreatCircle(this).getDifferenceTo(left.getBearingGreatCircle(right)) .getDegrees()); double toRight = Math.abs(right.getBearingGreatCircle(this).getDifferenceTo(right.getBearingGreatCircle(left)) @@ -172,9 +173,9 @@ public class AbstractPosition implements Position { if (toLeft > 90) { result = this.getDistance(left).scale(factor); } else if (toRight > 90) { - result = this.getDistance(right).scale(factor); - } else { - result = this.crossTrackError(left, left.getBearingGreatCircle(right)); + result = this.getDistance(right).scale(factor); + } else { + result = crossTrackError; } return result; } diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/Distance.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/Distance.java index 010a7df8ecb..ddf0678164e 100755 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/Distance.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/Distance.java @@ -80,6 +80,22 @@ public interface Distance extends Comparable, Serializable { public String toString() { return "0m"; } + + @Override + public double divide(Distance other) { + final double result; + if (other.getMeters() == 0) { + result = 0./0.; + } else { + result = 0; + } + return result; + } + + @Override + public Distance abs() { + return this; + } }; double getGeographicalMiles(); @@ -108,4 +124,16 @@ public interface Distance extends Comparable, Serializable { Speed inTime(Duration duration); Distance add(Distance d); + + /** + * Divides this distance by the {@code other} distance. Should the {@code other} distance + * be {@link #null}, the result will be {@code NaN}. + */ + double divide(Distance other); + + /** + * If this distance is negative, returns a positive difference with the same absolute value; for positive + * distances, this distance is returned. + */ + Distance abs(); } diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/PassingInstruction.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/PassingInstruction.java index 6a1a4cf576e..d8d11bf4472 100644 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/PassingInstruction.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/PassingInstruction.java @@ -14,6 +14,7 @@ package com.sap.sailing.domain.common; public enum PassingInstruction { None(1, 2), Port(1), + Single_Unknown(1), Starboard(1), Gate(2), Line(2), diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/Position.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/Position.java index b25873ad0c1..b2fb909baec 100755 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/Position.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/Position.java @@ -69,8 +69,8 @@ public interface Position extends Serializable { Distance alongTrackDistance(Position from, Bearing bearing); /** - * Computes the distance from this position to the line between left and right. This distance - * is positive if left is actually the position farther to the left, as seen from this position. + * Computes the distance from this position to the line between left and right. This distance + * is positive if left is actually the position farther to the left, as seen from this position. */ Distance getDistanceToLine(Position left, Position right); diff --git a/java/com.sap.sailing.domain.test/resources/event_20130424_IDMStarboo-Race_5.mtb b/java/com.sap.sailing.domain.test/resources/event_20130424_IDMStarboo-Race_5.mtb index adae6c1cef3..e9912906b44 100755 Binary files a/java/com.sap.sailing.domain.test/resources/event_20130424_IDMStarboo-Race_5.mtb and b/java/com.sap.sailing.domain.test/resources/event_20130424_IDMStarboo-Race_5.mtb differ diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateFinderImplWhiteBoxTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateFinderImplWhiteBoxTest.java new file mode 100644 index 00000000000..23788331f99 --- /dev/null +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateFinderImplWhiteBoxTest.java @@ -0,0 +1,109 @@ +package com.sap.sailing.domain.markpassingcalculation.impl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.base.DomainFactory; +import com.sap.sailing.domain.base.RaceDefinition; +import com.sap.sailing.domain.common.impl.MeterDistance; +import com.sap.sailing.domain.markpassingcalculation.impl.CandidateFinderImpl.AbsoluteGeometricDistanceAndSignedProjectedDistanceToStartLine; +import com.sap.sailing.domain.tracking.DynamicTrackedRace; + +public class CandidateFinderImplWhiteBoxTest { + private CandidateFinderImpl finder; + + @Before + public void setUp() { + DynamicTrackedRace trackedRace = mock(DynamicTrackedRace.class); + final BoatClass boatClass = DomainFactory.INSTANCE.getOrCreateBoatClass("29er"); + RaceDefinition race = mock(RaceDefinition.class); + when(trackedRace.getRace()).thenReturn(race); + when(race.getBoatClass()).thenReturn(boatClass); + when(race.getCompetitors()).thenReturn(Collections.emptySet()); + finder = new CandidateFinderImpl(trackedRace); + } + + @Test + public void testOneProbabilityForEmptyList() { + List emptyDistancesToStartLineOfOtherCompetitors = new ArrayList<>(); + final double probability = finder.getProbabilityOfStartBasedOnOtherCompetitorsStartLineDistances(emptyDistancesToStartLineOfOtherCompetitors, + /* startIsLine */ true); + assertEquals(1.0, probability, 0.00001); + } + + @Test + public void testHighProbabilityForCloseProximityAndNoLine() { + List distancesToStartLineOfOtherCompetitors = new ArrayList<>(); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(3.0, null)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(5.0, null)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(2.0, null)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(8.0, null)); + final double probability = finder.getProbabilityOfStartBasedOnOtherCompetitorsStartLineDistances(distancesToStartLineOfOtherCompetitors, + /* startIsLine */ false); + assertTrue(probability > 0.95); + } + + @Test + public void testHighProbabilityForCloseProximityExceptOneAndNoLine() { + List distancesToStartLineOfOtherCompetitors = new ArrayList<>(); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(3.0, null)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(5.0, null)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(2.0, null)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(8.0, null)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(1.0, null)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(2.5, null)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(3.2, null)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(2739.0, null)); + final double probability = finder.getProbabilityOfStartBasedOnOtherCompetitorsStartLineDistances(distancesToStartLineOfOtherCompetitors, + /* startIsLine */ false); + assertTrue("Expected probability to exceed 55% but got "+probability, probability > 0.55); + } + + @Test + public void testLowProbabilityForMostFarAwayAndNoLine() { + List distancesToStartLineOfOtherCompetitors = new ArrayList<>(); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(50.0, null)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(250.0, null)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(120.0, null)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(200.0, null)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(300.0, null)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(2.5, null)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(3.2, null)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(2739.0, null)); + final double probability = finder.getProbabilityOfStartBasedOnOtherCompetitorsStartLineDistances(distancesToStartLineOfOtherCompetitors, + /* startIsLine */ false); + assertTrue("Expected probability to be below 10% but got "+probability, probability <= 0.1); + } + + @Test + public void testHighProbabilityForLateStartAndStartIsLine() { + List distancesToStartLineOfOtherCompetitors = new ArrayList<>(); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(100.0, -80.)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(120.0, -82.)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(130.0, -81.)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(90.0, -79.)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(140.0, -75.)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(150, -88.)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(135, -77.)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(20.0, -10.)); // another late starter just across the line + final double probability = finder.getProbabilityOfStartBasedOnOtherCompetitorsStartLineDistances(distancesToStartLineOfOtherCompetitors, + /* startIsLine */ true); + assertTrue("Expected probability to be above 85% but got "+probability, probability >= 0.85); + } + + private AbsoluteGeometricDistanceAndSignedProjectedDistanceToStartLine createDistancePair(double absoluteDistanceToLine, + Double signedXTEDistanceFromLine) { + return new AbsoluteGeometricDistanceAndSignedProjectedDistanceToStartLine( + new MeterDistance(absoluteDistanceToLine), signedXTEDistanceFromLine == null ? null : new MeterDistance(signedXTEDistanceFromLine)); + } +} diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/AbstractTracTracLiveTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/AbstractTracTracLiveTest.java index 8a8a930bf72..9849a0e3322 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/AbstractTracTracLiveTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/AbstractTracTracLiveTest.java @@ -55,7 +55,8 @@ public abstract class AbstractTracTracLiveTest extends StoredTrackBasedTest { private IRaceSubscriber raceSubscriber; private final Collection receivers; - @Rule public Timeout AbstractTracTracLiveTestTimeout = new Timeout(3 * 60 * 1000); + // TODO uncomment when done testing +// @Rule public Timeout AbstractTracTracLiveTestTimeout = new Timeout(3 * 60 * 1000); protected AbstractTracTracLiveTest() throws URISyntaxException, MalformedURLException { receivers = new HashSet(); diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/markpassing/AbstractMarkPassingTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/markpassing/AbstractMarkPassingTest.java index 4d2bdf4f4e2..bcab5fcf473 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/markpassing/AbstractMarkPassingTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/markpassing/AbstractMarkPassingTest.java @@ -64,7 +64,7 @@ public abstract class AbstractMarkPassingTest extends OnlineTracTracBasedTest { simpleName = getClass().getSimpleName(); } - private void setUp(String raceNumber) throws IOException, InterruptedException, URISyntaxException, ParseException, SubscriberInitializationException, CreateModelException { + protected void setUp(String raceNumber) throws IOException, InterruptedException, URISyntaxException, ParseException, SubscriberInitializationException, CreateModelException { super.setUp(); URI storedUri = new URI("file:///" + new File("resources/" + getFileName() + raceNumber + ".mtb").getCanonicalPath().replace('\\', '/')); @@ -90,6 +90,21 @@ public abstract class AbstractMarkPassingTest extends OnlineTracTracBasedTest { } } + protected Iterable getWaypoints() { + return waypoints; + } + + protected Competitor getCompetitorByName(final String name) { + Competitor result = null; + for (Competitor c : getTrackedRace().getRace().getCompetitors()) { + if (c.getName().equals(name)) { + result = c; + break; + } + } + return result; + } + protected abstract String getFileName(); protected void testRace(String raceNumber) throws IOException, InterruptedException, URISyntaxException, ParseException, SubscriberInitializationException, CreateModelException { @@ -214,29 +229,19 @@ public abstract class AbstractMarkPassingTest extends OnlineTracTracBasedTest { } private void testMiddleOfRace(int zeroBasedIndexOfLastWaypointToBePassed) { + int mistakes = 0; CandidateFinder finder = new CandidateFinderImpl(getTrackedRace()); CandidateChooser chooser = new CandidateChooserImpl(getTrackedRace()); - int mistakes = 0; Waypoint lastWaypointToBePassed = waypoints.get(zeroBasedIndexOfLastWaypointToBePassed); Waypoint wayPointAfterwards = waypoints.get(zeroBasedIndexOfLastWaypointToBePassed + 1); for (Competitor c : getRace().getCompetitors()) { MarkPassing markPassingAfter = givenPasses.get(c).get(wayPointAfterwards); if (markPassingAfter != null) { if (givenPasses.get(c).get(lastWaypointToBePassed) != null) { - TimePoint beforeNextPassing = markPassingAfter.getTimePoint().minus(20000); - List fixes = new ArrayList(); - try { - getTrackedRace().getTrack(c).lockForRead(); - for (GPSFixMoving fix : getTrackedRace().getTrack(c).getFixes()) { - if (fix.getTimePoint().before(beforeNextPassing)) { - fixes.add(fix); - } - } - } finally { - getTrackedRace().getTrack(c).unlockAfterRead(); - } - Util.Pair, Iterable> f = finder.getCandidateDeltas(c, fixes); - chooser.calculateMarkPassDeltas(c, f.getA(), f.getB()); + final TimePoint lastGivenPassingToDetectAt = givenPasses.get(c).get(lastWaypointToBePassed).getTimePoint(); + // add fixes up to 50% into the leg starting after the last passing to be detected + final TimePoint beforeNextPassing = lastGivenPassingToDetectAt.plus(lastGivenPassingToDetectAt.until(markPassingAfter.getTimePoint()).times(0.5)); + calculateMarkPassingsForPartialTrack(c, beforeNextPassing, finder, chooser); boolean gotPassed = true; boolean gotOther = false; System.out.println(c); @@ -270,6 +275,23 @@ public abstract class AbstractMarkPassingTest extends OnlineTracTracBasedTest { } + protected void calculateMarkPassingsForPartialTrack(Competitor c, final TimePoint upToTimePoint, + CandidateFinder finder, CandidateChooser chooser) { + List fixes = new ArrayList(); + try { + getTrackedRace().getTrack(c).lockForRead(); + for (GPSFixMoving fix : getTrackedRace().getTrack(c).getFixes()) { + if (fix.getTimePoint().before(upToTimePoint)) { + fixes.add(fix); + } + } + } finally { + getTrackedRace().getTrack(c).unlockAfterRead(); + } + Util.Pair, Iterable> f = finder.getCandidateDeltas(c, fixes); + chooser.calculateMarkPassDeltas(c, f.getA(), f.getB()); + } + @AfterClass public static void createXML() throws IOException { double accuracy = correct / totalPasses; diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/markpassing/Kiel2013505Test.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/markpassing/Kiel2013505Test.java index 9844a4266f7..42c1801ec38 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/markpassing/Kiel2013505Test.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/markpassing/Kiel2013505Test.java @@ -7,17 +7,33 @@ import java.text.ParseException; import org.junit.Test; +import com.sap.sailing.domain.abstractlog.impl.LogEventAuthorImpl; +import com.sap.sailing.domain.abstractlog.race.RaceLog; +import com.sap.sailing.domain.abstractlog.race.impl.RaceLogImpl; +import com.sap.sailing.domain.abstractlog.race.impl.RaceLogStartProcedureChangedEventImpl; +import com.sap.sailing.domain.common.racelog.RacingProcedureType; +import com.sap.sse.common.impl.MillisecondsTimePoint; import com.tractrac.model.lib.api.event.CreateModelException; import com.tractrac.subscription.lib.api.SubscriberInitializationException; public class Kiel2013505Test extends AbstractMarkPassingTest { - public Kiel2013505Test() throws MalformedURLException, URISyntaxException { + public Kiel2013505Test() throws MalformedURLException, URISyntaxException { super(); } - //Takes longer than 2 minutes, so the rule in AbstractTracTracLiveTest needs to be deactivated first. - + /** + * Marks the race as using a gate start + */ + @Override + protected void setUp(String raceNumber) throws IOException, InterruptedException, URISyntaxException, + ParseException, SubscriberInitializationException, CreateModelException { + super.setUp(raceNumber); + final RaceLog raceLog = new RaceLogImpl("505 Race Log"); + getTrackedRace().attachRaceLog(raceLog); + raceLog.add(new RaceLogStartProcedureChangedEventImpl(MillisecondsTimePoint.now(), new LogEventAuthorImpl("Me", 0), 0, RacingProcedureType.GateStart)); + } + @Test public void testRace5() throws IOException, InterruptedException, URISyntaxException, ParseException, SubscriberInitializationException, CreateModelException { testRace("5"); diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/markpassing/StarIDM2013MarkPassingTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/markpassing/StarIDM2013MarkPassingTest.java index 600111324cb..6a3d28e5684 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/markpassing/StarIDM2013MarkPassingTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/markpassing/StarIDM2013MarkPassingTest.java @@ -1,12 +1,31 @@ package com.sap.sailing.domain.test.markpassing; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + import java.io.IOException; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.text.ParseException; +import java.util.Collections; +import java.util.Date; import org.junit.Test; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.Waypoint; +import com.sap.sailing.domain.markpassingcalculation.CandidateChooser; +import com.sap.sailing.domain.markpassingcalculation.CandidateFinder; +import com.sap.sailing.domain.markpassingcalculation.MarkPassingCalculator; +import com.sap.sailing.domain.markpassingcalculation.impl.CandidateChooserImpl; +import com.sap.sailing.domain.markpassingcalculation.impl.CandidateFinderImpl; +import com.sap.sailing.domain.tracking.MarkPassing; +import com.sap.sse.InvalidDateException; +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; +import com.sap.sse.util.DateParser; import com.tractrac.model.lib.api.event.CreateModelException; import com.tractrac.subscription.lib.api.SubscriberInitializationException; @@ -18,8 +37,82 @@ public class StarIDM2013MarkPassingTest extends AbstractMarkPassingTest { @Test public void testRace5() throws IOException, InterruptedException, URISyntaxException, ParseException, - SubscriberInitializationException, CreateModelException { - testRace("5"); + SubscriberInitializationException, CreateModelException, InvalidDateException { + // Race 5 is really broken on TracTrac's side. I've tried re-calculating the mark passing to no avail. + // Now it's even worse, and only the windward mark passings and the start mark passings are recognized + // half-way reasonably, but no mark passings seem to have been detected for subsequent waypoints. + // To still use this race for testing purposes, let's do spot checks based on manually identified mark + // passings, based on https://www.sapsailing.com/gwt/RaceBoard.html?eventId=a43b8f96-1e77-448a-9b8a-0f586f1bb18f&leaderboardName=Star%20IDM%202013&leaderboardGroupName=Star%20IDM%202013&raceName=Race%205&viewShowMapControls=true&viewShowNavigationPanel=true®attaName=IDM%20Starboot%202013%20%28Star%29 + setUp("5"); + synchronized (getSemaphor()) { + while (!isStoredDataLoaded()) { + getSemaphor().wait(); + } + } + testWholeRace5(); + testOnlyStartOfRace5(); + } + + private void testWholeRace5() throws InvalidDateException { + new MarkPassingCalculator(getTrackedRace(), false); // do our calculation, injecting into TrackedRace + final Competitor GER7897 = getCompetitorByName("Tusch, Frank,Winkelmann, Sven"); + assertMarkPassing(GER7897, Util.get(getWaypoints(), 0), DateParser.parse("2013-05-04T14:50:10+0200")); + assertMarkPassing(GER7897, Util.get(getWaypoints(), 1), DateParser.parse("2013-05-04T14:57:12+0200")); + assertMarkPassing(GER7897, Util.get(getWaypoints(), 3), DateParser.parse("2013-05-04T15:06:32+0200")); + assertMarkPassing(GER7897, Util.get(getWaypoints(), 4), DateParser.parse("2013-05-04T15:15:01+0200")); + assertMarkPassing(GER7897, Util.get(getWaypoints(), 6), DateParser.parse("2013-05-04T15:21:17+0200")); + assertMarkPassing(GER7897, Util.get(getWaypoints(), 7), DateParser.parse("2013-05-04T15:29:18+0200")); + assertMarkPassing(GER7897, Util.get(getWaypoints(), 9), DateParser.parse("2013-05-04T15:36:36+0200")); + assertMarkPassing(GER7897, Util.get(getWaypoints(), 10), DateParser.parse("2013-05-04T15:38:09+0200")); + + final Competitor GER7616 = getCompetitorByName("Seefelder, Jürgen,Seefelder, N"); // starts late + assertMarkPassing(GER7616, Util.get(getWaypoints(), 0), DateParser.parse("2013-05-04T14:50:57+0200")); + assertMarkPassing(GER7616, Util.get(getWaypoints(), 1), DateParser.parse("2013-05-04T14:59:05+0200")); + assertMarkPassing(GER7616, Util.get(getWaypoints(), 3), DateParser.parse("2013-05-04T15:07:32+0200")); + assertMarkPassing(GER7616, Util.get(getWaypoints(), 4), DateParser.parse("2013-05-04T15:16:40+0200")); + assertMarkPassing(GER7616, Util.get(getWaypoints(), 6), DateParser.parse("2013-05-04T15:23:26+0200")); + assertMarkPassing(GER7616, Util.get(getWaypoints(), 7), DateParser.parse("2013-05-04T15:33:01+0200")); + assertMarkPassing(GER7616, Util.get(getWaypoints(), 9), DateParser.parse("2013-05-04T15:39:25+0200")); + assertMarkPassing(GER7616, Util.get(getWaypoints(), 10), DateParser.parse("2013-05-04T15:40:51+0200")); + + final Competitor GER8340 = getCompetitorByName("Griese, Achim,Marcour, Michael"); // starts rather early + assertMarkPassing(GER8340, Util.get(getWaypoints(), 0), DateParser.parse("2013-05-04T14:50:04+0200")); + assertMarkPassing(GER8340, Util.get(getWaypoints(), 1), DateParser.parse("2013-05-04T14:56:57+0200")); + assertMarkPassing(GER8340, Util.get(getWaypoints(), 3), DateParser.parse("2013-05-04T15:06:33+0200")); + assertMarkPassing(GER8340, Util.get(getWaypoints(), 4), DateParser.parse("2013-05-04T15:14:31+0200")); + assertMarkPassing(GER8340, Util.get(getWaypoints(), 6), DateParser.parse("2013-05-04T15:20:29+0200")); + assertMarkPassing(GER8340, Util.get(getWaypoints(), 7), DateParser.parse("2013-05-04T15:28:34+0200")); + assertMarkPassing(GER8340, Util.get(getWaypoints(), 9), DateParser.parse("2013-05-04T15:35:52+0200")); + assertMarkPassing(GER8340, Util.get(getWaypoints(), 10), DateParser.parse("2013-05-04T15:37:22+0200")); + } + + private void testOnlyStartOfRace5() throws InvalidDateException { + // clear mark passings + for (Competitor c : getRace().getCompetitors()) { + getTrackedRace().updateMarkPassings(c, Collections.emptySet()); + } + CandidateFinder finder = new CandidateFinderImpl(getTrackedRace()); + CandidateChooser chooser = new CandidateChooserImpl(getTrackedRace()); + final TimePoint afterStart = new MillisecondsTimePoint(DateParser.parse("2013-05-04T14:52:30+0200")); + for (Competitor c : getRace().getCompetitors()) { + calculateMarkPassingsForPartialTrack(c, afterStart, finder, chooser); + } + final Competitor GER7897 = getCompetitorByName("Tusch, Frank,Winkelmann, Sven"); + assertMarkPassing(GER7897, Util.get(getWaypoints(), 0), DateParser.parse("2013-05-04T14:50:10+0200")); + final Competitor GER7616 = getCompetitorByName("Seefelder, Jürgen,Seefelder, N"); // starts late + assertMarkPassing(GER7616, Util.get(getWaypoints(), 0), DateParser.parse("2013-05-04T14:50:57+0200")); + final Competitor GER8340 = getCompetitorByName("Griese, Achim,Marcour, Michael"); // starts rather early + assertMarkPassing(GER8340, Util.get(getWaypoints(), 0), DateParser.parse("2013-05-04T14:50:04+0200")); + } + + private void assertMarkPassing(Competitor competitor, Waypoint waypoint, Date when) { + final MarkPassing markPassing = getTrackedRace().getMarkPassing(competitor, waypoint); + if (markPassing == null) { + fail("Expected mark passing for "+competitor+" and "+waypoint+" around "+when+" but no mark passing found"); + } + final Duration offset = new MillisecondsTimePoint(when).until(markPassing.getTimePoint()).abs(); + assertTrue("Expected mark passing for "+competitor+" and "+waypoint+" around "+when+ + " but found one at "+markPassing.getTimePoint()+" which is "+offset+" off.", offset.asSeconds() < 10); } @Test diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateChooserImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateChooserImpl.java index e96ad5ba319..093f167be45 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateChooserImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateChooserImpl.java @@ -67,7 +67,7 @@ public class CandidateChooserImpl implements CandidateChooser { */ private static final Duration DELAY_AFTER_WHICH_PROBABILITY_OF_START_HALVES = Duration.ONE_MINUTE; - private final static double MINIMUM_PROBABILITY = 1 - Edge.getPenaltyForSkipping(); + private final static double MINIMUM_PROBABILITY = Edge.getPenaltyForSkipping(); private static final Logger logger = Logger.getLogger(CandidateChooserImpl.class.getName()); 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 98abb9c3b45..447b99753cf 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 @@ -2,6 +2,7 @@ package com.sap.sailing.domain.markpassingcalculation.impl; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; @@ -35,6 +36,7 @@ import com.sap.sailing.domain.tracking.GPSFixTrack; import com.sap.sse.common.TimePoint; import com.sap.sse.common.TimeRange; import com.sap.sse.common.Util; +import com.sap.sse.common.Util.Pair; /** * The standard implemantation of {@link CandidateFinder}. There are two kinds of {@link Candidate}s. First of all, @@ -53,11 +55,36 @@ public class CandidateFinderImpl implements CandidateFinder { // All of the penalties are multiplied onto the probability of a Candidate. A value of 0 excludes Candidates that do // not fit, a value of 1 imposes no penalty on each criteria - private final double PENALTY_FOR_WRONG_SIDE = 0.8; - private final double PENALTY_FOR_WRONG_DIRECTION = 0.8; - private final double PENALTY_FOR_DISTANCE_CANDIDATES = 0.8; + private static final double PENALTY_FOR_WRONG_SIDE = 0.8; + + /** + * The penalty factor multiplied with a candidate's probability in case the competitor passes the line of an XTE + * candidate with passing instructions other than {@link PassingInstruction#Line} in the wrong direction, e.g., + * passing a single mark that is to be passed to port in the wrong direction. + */ + private static final double PENALTY_FOR_WRONG_DIRECTION = 0.7; + + /** + * The penalty factor multiplied with a candidate's probability in case the competitor passes an XTE candidate for a + * {@link PassingInstruction#Line line} in the wrong direction, e.g., passing a start line the wrong way. + */ + private static final double PENALTY_FOR_LINE_PASSED_IN_WRONG_DIRECTION = 0.3; + + /** + * Normally, for each distance candidate there should also be a proper XTE candidate that also tells about the + * direction in which the boat crosses the line. XTE candidates are more precise and get a penalty in case the competitor + * crosses the virtual line in the wrong direction: {@link #PENALTY_FOR_WRONG_DIRECTION}. The penalty a competitor should + * get for not triggering a proper XTE candidate and only producing a candidate by getting near a mark and then + * moving away again should be penalized at least as badly as {@link #PENALTY_FOR_WRONG_DIRECTION}. + */ + private static final double PENALTY_FOR_DISTANCE_CANDIDATES = 0.9 * PENALTY_FOR_WRONG_DIRECTION; + + private static final double WORST_PENALTY_FOR_OTHER_COMPETITORS_BEING_FAR_FROM_START = 0.1; + private static final double NUMBER_OF_HULL_LENGTHS_DISTANCE_FROM_START_AT_WHICH_WORST_PENALTY_APPLIES = 10; private static final Logger logger = Logger.getLogger(CandidateFinderImpl.class.getName()); + + private Map>>> distanceCache = new LinkedHashMap<>(); private Map>>> xteCache = new LinkedHashMap<>(); @@ -291,7 +318,7 @@ public class CandidateFinderImpl implements CandidateFinder { if (numberofMarks == 2) { instruction = PassingInstruction.Gate; } else if (numberofMarks == 1) { - instruction = PassingInstruction.Port; + instruction = PassingInstruction.Single_Unknown; } else { instruction = PassingInstruction.None; } @@ -368,7 +395,9 @@ public class CandidateFinderImpl implements CandidateFinder { Boolean isCan = false; Candidate oldCan = null; Double probability = null; + double sidePenalty = 1; Distance distance = null; + Double startProbabilityBasedOnOtherCompetitors = null; double onCorrectSideOfWaypoint = 0.8; List waypointDistances = fixDistances.get(w); List waypointDistancesBefore = fixDistancesBefore.get(w); @@ -397,12 +426,23 @@ public class CandidateFinderImpl implements CandidateFinder { // FIXME why not generate the candidate here where we have all information at hand? final double newOnCorrectSideOfWaypointPenalty = getSidePenalty(w, p, t, portMark); newProbability *= newOnCorrectSideOfWaypointPenalty * PENALTY_FOR_DISTANCE_CANDIDATES; + final Double newStartProbabilityBasedOnOtherCompetitors; + if (isStartWaypoint(w)) { + newStartProbabilityBasedOnOtherCompetitors = getProbabilityForStartCandidateBasedOnOtherCompetitorsBehavior(c, t); + if (newStartProbabilityBasedOnOtherCompetitors != null) { + newProbability *= newStartProbabilityBasedOnOtherCompetitors; + } + } else { + newStartProbabilityBasedOnOtherCompetitors = null; + } if (newProbability > penaltyForSkipping && (probability == null || newProbability > probability)) { isCan = true; probability = newProbability; + sidePenalty = newOnCorrectSideOfWaypointPenalty; onCorrectSideOfWaypoint = newOnCorrectSideOfWaypointPenalty; distance = dis; + startProbabilityBasedOnOtherCompetitors = newStartProbabilityBasedOnOtherCompetitors; } } } @@ -415,7 +455,7 @@ public class CandidateFinderImpl implements CandidateFinder { } if (!wasCan && isCan) { Candidate newCan = new DistanceCandidateImpl(race.getRace().getCourse().getIndexOfWaypoint(w) + 1, - t, probability, w, onCorrectSideOfWaypoint, distance); + t, probability, startProbabilityBasedOnOtherCompetitors, w, sidePenalty, distance); getDistanceCandidates(c, w).put(fix, newCan); result.getA().add(newCan); logger.finest("Added distance" + newCan.toString() + "for " + c); @@ -424,7 +464,7 @@ public class CandidateFinderImpl implements CandidateFinder { result.getB().add(oldCan); } else if (wasCan && isCan && oldCan.getProbability() != probability) { Candidate newCan = new DistanceCandidateImpl(race.getRace().getCourse().getIndexOfWaypoint(w) + 1, - t, probability, w, onCorrectSideOfWaypoint, distance); + t, probability, startProbabilityBasedOnOtherCompetitors, w, onCorrectSideOfWaypoint, distance); getDistanceCandidates(c, w).put(fix, newCan); result.getA().add(newCan); logger.finest("Added distance" + newCan.toString() + "for " + c); @@ -632,11 +672,229 @@ public class CandidateFinderImpl implements CandidateFinder { final List distances = calculateDistance(p, w, t); final Distance d = portMark ? distances.get(0) : distances.get(1); final double sidePenalty = getSidePenalty(w, p, t, portMark); - final boolean passesInRightDirection = passesInTheRightDirection(w, xte1, xte2, portMark); double probability = getDistanceBasedProbability(w, t, d) * sidePenalty; - probability = passesInRightDirection ? probability: probability* PENALTY_FOR_WRONG_DIRECTION ; - - return new XTECandidateImpl(race.getRace().getCourse().getIndexOfWaypoint(w) + 1, t, probability, w, sidePenalty, passesInRightDirection); + final Double passesInTheRightDirectionProbability = passesInTheRightDirection(w, xte1, xte2, portMark); + // null would mean "unknown"; no penalty for those cases + probability = passesInTheRightDirectionProbability == null ? probability : probability * passesInTheRightDirectionProbability; + final Double startProbabilityBasedOnOtherCompetitors; + if (isStartWaypoint(w)) { + // add a penalty for a start candidate if we don't know the start time, it's not a gate start and + // at time point t the other competitors are largely not even close to the start waypoint; + // this will make start candidates much more likely if many other competitors are also very close to the + // start, and it will help ruling out those candidates where someone is practicing a start just for themselves + startProbabilityBasedOnOtherCompetitors = getProbabilityForStartCandidateBasedOnOtherCompetitorsBehavior(c, t); + if (startProbabilityBasedOnOtherCompetitors != null) { + probability *= startProbabilityBasedOnOtherCompetitors; + } + } else { + startProbabilityBasedOnOtherCompetitors = null; + } + return new XTECandidateImpl(race.getRace().getCourse().getIndexOfWaypoint(w) + 1, t, probability, + startProbabilityBasedOnOtherCompetitors, w, sidePenalty, passesInTheRightDirectionProbability); + } + + static class AbsoluteGeometricDistanceAndSignedProjectedDistanceToStartLine { + private final Distance absoluteGeometricDistance; + private final Distance signedProjectedDistance; + public AbsoluteGeometricDistanceAndSignedProjectedDistanceToStartLine(Distance absoluteGeometricDistance, + Distance signedProjectedDistance) { + super(); + this.absoluteGeometricDistance = absoluteGeometricDistance; + this.signedProjectedDistance = signedProjectedDistance; + } + public Distance getAbsoluteGeometricDistance() { + return absoluteGeometricDistance; + } + public Distance getSignedProjectedDistance() { + return signedProjectedDistance; + } + @Override + public String toString() { + return "AbsoluteGeometricDistanceAndSignedProjectedDistanceToStartLine [absoluteGeometricDistance=" + + absoluteGeometricDistance + ", signedProjectedDistance=" + signedProjectedDistance + "]"; + } + } + + /** + * For a fixed line start it is very likely that all competitors pass the start waypoint at about the same time, or + * that only a few competitors lag behind, e.g., because of start time penalties. This requires everyone to at least + * be pretty close to the start waypoint at the start time, of if we're looking at a competitor starting late, the + * field would at least have little variance and be consistently on the course side of the start line. + *

+ * + * Therefore, a start mark passing candidate is very likely when a large share of competitors is very close to the + * start line. If the other competitors are not very close to the start line, a start may still be likely if the + * others are on the course side and have similar distance to the start line, whereas it becomes pretty unlikely + * when everyone is sailing around anywhere on the course or behind the line but not being in close proximity to the + * line and not having similar distances to the start line while being on course side. + * + *

    + *
  • check distance variance of other competitors; low variance could mean they started jointly
  • + *
  • for start *line*, consider the side on which the other competitors are; don't rule out a late start if other + * competitors have low distance variance and are on course side of the line
  • + *
  • generally consider distance of other competitors to line
  • + *
  • dampen "outliers" such as trackers forgotten in harbor or trackers not currently sending (80% of competitors + * that look like they are starting seems like a good guess)
  • + *
+ * + * Approach: compute signed distances to start line (positive meaning on course side). Sort by absolute distance and + * compute a {@link #weight(double) weighted} average where the boats farthest away are considered less relevant than + * the ones closer, leading to a natural outlier removal.

+ * + * For the special case of a late starter, consider the weighted variance of the signed distance from the start line. + * A low variance with a positive average (meaning on course side) indicates that everyone else may have started around the + * same time, making this a probable late starter's candidate. Therefore, low variance with positive average shall increase + * a probability that was low because of a large weighted average distance. + * + * @param t + * the time point at which to consider the other competitors behavior relative to the start waypoint + * @return {@code null} if the {@link #race} has a gate start or the start time is known; a probability between 0..1 + * (inclusive) where 0 means that based on all but {@code c}'s relation to the start line it seems + * completely unlikely that a candidate for {@code c} at time {@code t} could have been a start candidate; 1 + * meaning that based on the other competitors' relation to the start line at time point {@code t} is seems + * a fact that this must have been the start. + * + */ + private Double getProbabilityForStartCandidateBasedOnOtherCompetitorsBehavior(Competitor c, TimePoint t) { + final Double result; + if (race.getStartOfRace(/* inferred */ false) == null && race.isGateStart() != Boolean.TRUE) { + final Waypoint start = race.getRace().getCourse().getFirstWaypoint(); + final Iterable> crossingInformationForStart = getCrossingInformation(start, t); + if (start == null) { + result = 1.0; + } else { + // if the start waypoint's passing instructions are unknown, assume it's a line + final boolean startIsLine = start.getPassingInstructions() == PassingInstruction.Line || start.getPassingInstructions() == PassingInstruction.None; + final List distancesToStartLineOfOtherCompetitors = new ArrayList<>(); + for (final Competitor otherCompetitor : race.getRace().getCompetitors()) { + if (otherCompetitor != c) { + final DynamicGPSFixTrack track = race.getTrack(otherCompetitor); + if (track != null) { + final Position estimatedPositionAtT = track.getEstimatedPosition(t, /* extrapolate */ true); + final Distance otherCompetitorsDistanceToStartAtT = getMinDistanceOrNull(calculateDistance(estimatedPositionAtT, start, t)); + if (otherCompetitorsDistanceToStartAtT != null) { + final Distance crossTrackError; + if (startIsLine) { + Pair crossingInformationForStartLine = crossingInformationForStart.iterator().next(); + crossTrackError = estimatedPositionAtT.crossTrackError(crossingInformationForStartLine.getA(), + crossingInformationForStartLine.getB()); + } else { + crossTrackError = null; + } + distancesToStartLineOfOtherCompetitors.add(new + AbsoluteGeometricDistanceAndSignedProjectedDistanceToStartLine(otherCompetitorsDistanceToStartAtT, crossTrackError)); + } + } + } + } + result = getProbabilityOfStartBasedOnOtherCompetitorsStartLineDistances(distancesToStartLineOfOtherCompetitors, startIsLine); + } + } else { + result = null; + } + return result; + } + + /** + * The method computes two probabilities of the candidate being a start: one based on the other competitors largely + * being close to the start line, meaning a regular start where the competitor starts roughly at the same time as + * everybody else; the other probability computed is based on the other competitors largely being in a similar + * distance to the start line on the course side, suggesting that the competitor is starting late, but mostly + * everybody else did start earlier and around the same time. The probability returned is the probability of + * the start being the one or the other scenario which mathematically is represented by using the inverted + * probabilities of the two start options, multiplying them and inverting the resulting probability. + */ + Double getProbabilityOfStartBasedOnOtherCompetitorsStartLineDistances( + final List distancesToStartLineOfOtherCompetitors, boolean startIsLine) { + final Double result; + // sort by the absolute distance to start line + // boats within one hull length of the start line are great indicators that we're at the start: + final Distance hullLength = race.getRace().getBoatClass().getHullLength(); + final Distance weightedAverageAbsoluteDistance; + if (!distancesToStartLineOfOtherCompetitors.isEmpty()) { + Collections.sort(distancesToStartLineOfOtherCompetitors, (a, b)->a.getAbsoluteGeometricDistance().compareTo(b.getAbsoluteGeometricDistance())); + Distance weightedAbsoluteDistanceSum = Distance.NULL; + int i=0; + double weightSum = 0; + for (final AbsoluteGeometricDistanceAndSignedProjectedDistanceToStartLine d : distancesToStartLineOfOtherCompetitors) { + final double weight = weight(((double) i)/distancesToStartLineOfOtherCompetitors.size()); + final Distance weightedDistance = d.getAbsoluteGeometricDistance().scale(weight); + weightedAbsoluteDistanceSum = weightedAbsoluteDistanceSum.add(weightedDistance); + weightSum += weight; + i++; + } + weightedAverageAbsoluteDistance = weightedAbsoluteDistanceSum.scale(1. / weightSum); + final double probabilityOfStartWithOthers = Math.max(WORST_PENALTY_FOR_OTHER_COMPETITORS_BEING_FAR_FROM_START, Math.min(1.0, + (1.-(1.-WORST_PENALTY_FOR_OTHER_COMPETITORS_BEING_FAR_FROM_START)/NUMBER_OF_HULL_LENGTHS_DISTANCE_FROM_START_AT_WHICH_WORST_PENALTY_APPLIES + * (weightedAverageAbsoluteDistance.divide(hullLength)-1)))); + if (startIsLine) { + // Now look at the variance, particularly on course side: low variance may mean we are looking at a late starter + // sort such that the boats with the least value (greatest negative amount) comes first and the last ~20% are largely ignored + Collections.sort(distancesToStartLineOfOtherCompetitors, (a, b)->a.getSignedProjectedDistance().compareTo(b.getSignedProjectedDistance())); + Distance weightedSignedDistanceSum = Distance.NULL; + weightSum = 0; + i=0; + for (final AbsoluteGeometricDistanceAndSignedProjectedDistanceToStartLine d : distancesToStartLineOfOtherCompetitors) { + final double weight = weight(((double) i)/distancesToStartLineOfOtherCompetitors.size()); + weightedSignedDistanceSum = weightedSignedDistanceSum.add(d.getSignedProjectedDistance().scale( + weight)); + weightSum += weight; + i++; + } + final Distance weightedAverageSignedDistance = weightedSignedDistanceSum.scale(1. / weightSum); + i=0; + double signedDistanceVarianceSumInSquareMeters = 0; + weightSum = 0; + for (final AbsoluteGeometricDistanceAndSignedProjectedDistanceToStartLine d : distancesToStartLineOfOtherCompetitors) { + final double differenceFromWeightedAverageInMeters = d.getSignedProjectedDistance().getMeters() - weightedAverageSignedDistance.getMeters(); + final double weight = weight(((double) i)/distancesToStartLineOfOtherCompetitors.size()); + signedDistanceVarianceSumInSquareMeters += weight*differenceFromWeightedAverageInMeters*differenceFromWeightedAverageInMeters; + weightSum += weight; + i++; + } + final double weightedVarianceInSquareMeters = signedDistanceVarianceSumInSquareMeters / weightSum; + final double weightedVarianceRatioToSquaredHullLength = weightedVarianceInSquareMeters / hullLength.getMeters() / hullLength.getMeters(); + final double probabilityOfLateStart = Math.max(WORST_PENALTY_FOR_OTHER_COMPETITORS_BEING_FAR_FROM_START, Math.min(1.0, + (1.-(1.-WORST_PENALTY_FOR_OTHER_COMPETITORS_BEING_FAR_FROM_START)/NUMBER_OF_HULL_LENGTHS_DISTANCE_FROM_START_AT_WHICH_WORST_PENALTY_APPLIES + * (weightedVarianceRatioToSquaredHullLength-1)))); + result = 1. - (1.-probabilityOfLateStart) * (1.-probabilityOfStartWithOthers); + } else { + result = probabilityOfStartWithOthers; + } + } else { + // no distance for any other competitor from the start line was available + result = 1.0; + } + return result; + } + + /** + * Idea: sort and cut off the worst 20%. We played with weight functions for averaging the distances such that after sorting them + * in increasing order the smaller ones get greater weight and the "outlier" ones towards the end of the sorted set receive small + * weights. Still, if even the "best" (least) distances are far away, the result will be a low probability. + * A function that could be helpful is this: -atan(20*(x-0.8)) + Pi/2)/Pi. The 0.8 represents the 80% starting from which the + * weight declines sharply. The factor of 20 represents how sharp the decline is. x is between 0 and 1, representing the start + * and the end of the sorted distance collection, respectively. Outputs are between 0 and 1, representing the weight to assign + * to a distance while averaging. + */ + private double weight(double relativePositionInDistanceCollectionInIncreasingOrder) { + final double SHARPNESS_OF_DECLINE = 100; + final double CENTER_OF_DECLINE = 0.8; + return (-Math.atan(SHARPNESS_OF_DECLINE*(relativePositionInDistanceCollectionInIncreasingOrder-CENTER_OF_DECLINE))+Math.PI/2) / Math.PI; + } + + private Distance getMinDistanceOrNull(List distances) { + Distance result = null; + for (Distance d : distances) { + if (d != null && (result == null || result.compareTo(d) > 0)) { + result = d; + } + } + return result; + } + + private boolean isStartWaypoint(Waypoint w) { + return race.getRace().getCourse().getFirstWaypoint() == w; } /** @@ -678,7 +936,7 @@ public class CandidateFinderImpl implements CandidateFinder { } else { Mark m = null; - if (instruction == PassingInstruction.Port || instruction == PassingInstruction.Starboard + if (instruction == PassingInstruction.Single_Unknown || instruction == PassingInstruction.Port || instruction == PassingInstruction.Starboard || instruction == PassingInstruction.FixedBearing || instruction == PassingInstruction.Offset) { m = w.getMarks().iterator().next(); } else if (instruction == PassingInstruction.Gate) { @@ -706,19 +964,31 @@ public class CandidateFinderImpl implements CandidateFinder { } /** - * Determines whether a candidate passes a waypoint in the right direction. Can only be applied to XTE-Candidates. - * For marks passed on port, the cross-track error should switch from positive to negative and vice versa. Lines are - * also from positive to negative as the cross-track error to a line is always positive when approaching it from the - * correct side. + * Determines whether a candidate passes a waypoint in the right direction. A probability of 1 is assigned in case + * it does. Otherwise, depending on the passing instructions, {@link #PENALTY_FOR_WRONG_DIRECTION} or + * {@link #PENALTY_FOR_LINE_PASSED_IN_WRONG_DIRECTION} is returned as a "probability penalty" for passing the + * waypoint in the wrong direction. The penalty is worse if {@link PassingInstruction#Line lines} are passed in the + * wrong direction. Can only be applied to XTE-Candidates. For marks passed on port, the cross-track error should + * switch from positive to negative and vice versa. Lines are also from positive to negative as the cross-track + * error to a line is always positive when approaching it from the correct side. + * + * @return {@code null} in case the passing instructions aren't defined and therefore the correct direction is not + * known */ - private boolean passesInTheRightDirection(Waypoint w, double xte1, double xte2, boolean portMark) { - boolean result = true; + private Double passesInTheRightDirection(Waypoint w, double xte1, double xte2, boolean portMark) { + final Double result; PassingInstruction instruction = getPassingInstructions(w); - if (instruction == PassingInstruction.Port || instruction == PassingInstruction.Line + if (instruction == PassingInstruction.Single_Unknown) { + result = null; + } else if (instruction == PassingInstruction.Port || (instruction == PassingInstruction.Gate && portMark)) { - result = xte1 > xte2 ? true : false; + result = xte1 > xte2 ? 1 : PENALTY_FOR_WRONG_DIRECTION; } else if (instruction == PassingInstruction.Starboard || (instruction == PassingInstruction.Gate && !portMark)) { - result = xte1 < xte2 ? true : false; + result = xte1 < xte2 ? 1 : PENALTY_FOR_WRONG_DIRECTION; + } else if (instruction == PassingInstruction.Line) { + result = xte1 > xte2 ? 1 : PENALTY_FOR_LINE_PASSED_IN_WRONG_DIRECTION; + } else { + result = null; } return result; } @@ -761,11 +1031,12 @@ public class CandidateFinderImpl implements CandidateFinder { * Calculates the distance from p to each mark of w at the timepoint t. */ private List calculateDistance(Position p, Waypoint w, TimePoint t) { - List distances = new ArrayList<>(); + final List distances = new ArrayList<>(); PassingInstruction instruction = getPassingInstructions(w); boolean singleMark = false; switch (instruction) { case Port: + case Single_Unknown: case Starboard: case FixedBearing: singleMark = true; @@ -791,7 +1062,7 @@ public class CandidateFinderImpl implements CandidateFinder { Position portLinePosition = race.getOrCreateTrack(posLine.getA()).getEstimatedPosition(t, false); Position starboardLinePosition = race.getOrCreateTrack(posLine.getB()).getEstimatedPosition(t, false); distances.add((portLinePosition != null && starboardLinePosition != null) ? p.getDistanceToLine( - portLinePosition, starboardLinePosition) : null); + portLinePosition, starboardLinePosition).abs() : null); } break; case Offset: diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateWithSide.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateWithSide.java index 9e80f01f60e..cf82ee24fc6 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateWithSide.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateWithSide.java @@ -5,15 +5,18 @@ import com.sap.sse.common.TimePoint; public abstract class CandidateWithSide extends CandidateImpl { private final double onCorrectSideOfWaypoint; + private final Double startProbabilityBasedOnOtherCompetitors; - public CandidateWithSide(int oneBasedIndexOfWaypoint, TimePoint p, double probability, Waypoint w, - double onCorrectSideOfWaypoint) { + public CandidateWithSide(int oneBasedIndexOfWaypoint, TimePoint p, double probability, Double startProbabilityBasedOnOtherCompetitors, + Waypoint w, double onCorrectSideOfWaypoint) { super(oneBasedIndexOfWaypoint, p, probability, w); + this.startProbabilityBasedOnOtherCompetitors = startProbabilityBasedOnOtherCompetitors; this.onCorrectSideOfWaypoint = onCorrectSideOfWaypoint; } @Override public String toString() { - return ""+getClass().getSimpleName()+" "+super.toString()+", onCorrectSideOfWaypoint: "+onCorrectSideOfWaypoint; + return ""+getClass().getSimpleName()+" "+super.toString()+", onCorrectSideOfWaypoint: "+onCorrectSideOfWaypoint+ + (startProbabilityBasedOnOtherCompetitors==null?"":", startProbabilityBasedOnOtherCompetitors="+startProbabilityBasedOnOtherCompetitors); } } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/DistanceCandidateImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/DistanceCandidateImpl.java index d550cab17a4..c9cca5c756c 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/DistanceCandidateImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/DistanceCandidateImpl.java @@ -7,9 +7,9 @@ import com.sap.sse.common.TimePoint; public class DistanceCandidateImpl extends CandidateWithSide { private final Distance distance; - public DistanceCandidateImpl(int oneBasedIndexOfWaypoint, TimePoint p, double probability, Waypoint w, - double onCorrectSideOfWaypoint, Distance distance) { - super(oneBasedIndexOfWaypoint, p, probability, w, onCorrectSideOfWaypoint); + public DistanceCandidateImpl(int oneBasedIndexOfWaypoint, TimePoint p, double probability, Double startProbabilityBasedOnOtherCompetitors, + Waypoint w, double onCorrectSideOfWaypoint, Distance distance) { + super(oneBasedIndexOfWaypoint, p, probability, startProbabilityBasedOnOtherCompetitors, w, onCorrectSideOfWaypoint); this.distance = distance; } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/Edge.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/Edge.java index 1b8d76c3f3a..20589f0c145 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/Edge.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/Edge.java @@ -13,14 +13,30 @@ import com.sap.sailing.domain.markpassingcalculation.Candidate; * @author Nicolas Klose * */ - public class Edge implements Comparable { private final Candidate start; private final Candidate end; - // TODO what is the meaning of this constant? - private final static double PENALTY_FOR_SKIPPED = 0.16; - // TODO what is the meaning of this constant? - private final static double PENALTY_FOR_SKIPPED_TO_END = 0.25; + + /** + * The penalty for an edge's probability in case the edge skips one or more waypoints and does not + * end at the end proxy node. Skipping a waypoint that is not the last means that either the waypoint + * was passed in a very strange, unrecognized way or we got a tracker outage. This is both pretty + * unlikely and is penalized by this factor being multiplied to the edge's general probability.

+ * + * This factor is raised to the n-th power for n waypoints skipped. + */ + private final static double PENALTY_FOR_SKIPPED = 0.1; + + /** + * Similar to {@link #PENALTY_FOR_SKIPPED}, but applied to edges that skip to the end proxy node. Such skips + * are regular business while the race is still going on, and paths that are otherwise likely but don't lead + * up to the end shall be possible and therefore must not be penalized harshly, receiving less of a penalty + * (greater factor).

+ * + * As with {@link #PENALTY_FOR_SKIPPED}, this factor is raised to the n-th power for n waypoints skipped. + */ + private final static double PENALTY_FOR_SKIPPED_TO_END = 0.5; + private final double estimatedDistanceAndStartTimingProbability; private final int numberOfWaypoints; @@ -44,8 +60,10 @@ public class Edge implements Comparable { */ public Double getProbability() { final double penalty = end.getOneBasedIndexOfWaypoint() == numberOfWaypoints + 1 ? PENALTY_FOR_SKIPPED_TO_END : PENALTY_FOR_SKIPPED; - return start.getProbability() * end.getProbability() * estimatedDistanceAndStartTimingProbability * Math.pow(penalty, (end.getOneBasedIndexOfWaypoint() - start.getOneBasedIndexOfWaypoint() - 1)); - + // See bug 3241 comment #38: only use the edge's end candidate's probability; the start candidate's probability is the + // previous edge's end probability. The only probability we'll miss this way is that of the start proxy node and that is always 1. + return end.getProbability() * estimatedDistanceAndStartTimingProbability * + Math.pow(penalty, (end.getOneBasedIndexOfWaypoint() - start.getOneBasedIndexOfWaypoint() - 1)); } public Candidate getStart() { diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/XTECandidateImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/XTECandidateImpl.java index 0b4168b8b9f..e478568d72b 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/XTECandidateImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/XTECandidateImpl.java @@ -4,15 +4,17 @@ import com.sap.sailing.domain.base.Waypoint; import com.sap.sse.common.TimePoint; public class XTECandidateImpl extends CandidateWithSide { - private final boolean passesInTheRightDirection; + private final Double passesInTheRightDirectionProbability; - public XTECandidateImpl(int oneBasedIndexOfWaypoint, TimePoint p, double probability, Waypoint w, double onCorrectSideOfWaypoint, boolean passesInTheRightDirection) { - super(oneBasedIndexOfWaypoint, p, probability, w, onCorrectSideOfWaypoint); - this.passesInTheRightDirection = passesInTheRightDirection; + public XTECandidateImpl(int oneBasedIndexOfWaypoint, TimePoint p, double probability, + Double startProbabilityBasedOnOtherCompetitors, Waypoint w, double onCorrectSideOfWaypoint, + Double passesInTheRightDirectionProbability) { + super(oneBasedIndexOfWaypoint, p, probability, startProbabilityBasedOnOtherCompetitors, w, onCorrectSideOfWaypoint); + this.passesInTheRightDirectionProbability = passesInTheRightDirectionProbability; } @Override public String toString() { - return super.toString()+", passesInTheRightDirection: "+passesInTheRightDirection; + return super.toString()+", passesInTheRightDirection: "+passesInTheRightDirectionProbability; } } 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 c7199a2a5ed..c6b2997f97c 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 @@ -575,7 +575,7 @@ DynamicTrackedRace, GPSTrackListener { neededToCreateOrUpdateFinishMarkPassing = true; } else { finishMarkPassingToUse = originalMarkPassing.getOriginal(); - if (copyOfMarkPassings != originalMarkPassing) { + if (finishMarkPassingToUse != 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; diff --git a/java/com.sap.sailing.gwt.ui/Leaderboard.css b/java/com.sap.sailing.gwt.ui/Leaderboard.css index 92b5cf8adae..79ed640da2c 100755 --- a/java/com.sap.sailing.gwt.ui/Leaderboard.css +++ b/java/com.sap.sailing.gwt.ui/Leaderboard.css @@ -52,7 +52,7 @@ table { } .leaderboardContent td, .leaderboardContent th { - vertical-align: bottom; +/* vertical-align: bottom; */ line-height: 1.3; /* padding-top: 2px; */ } diff --git a/java/com.sap.sailing.gwt.ui/SailingGWT sdm.launch b/java/com.sap.sailing.gwt.ui/SailingGWT sdm.launch index 2bd988c6083..263825f093c 100644 --- a/java/com.sap.sailing.gwt.ui/SailingGWT sdm.launch +++ b/java/com.sap.sailing.gwt.ui/SailingGWT sdm.launch @@ -1,78 +1,78 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + diff --git a/java/com.sap.sailing.gwt.ui/SailingGWT.launch b/java/com.sap.sailing.gwt.ui/SailingGWT.launch index 6adb642328e..b14028404ea 100644 --- a/java/com.sap.sailing.gwt.ui/SailingGWT.launch +++ b/java/com.sap.sailing.gwt.ui/SailingGWT.launch @@ -1,75 +1,75 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/i18n/TextMessages.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/i18n/TextMessages.properties index cd6e7af5c5a..7365690323b 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/i18n/TextMessages.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/i18n/TextMessages.properties @@ -163,7 +163,7 @@ searchResultSortLabel=Sort by searchResultSortNewest=Recent searchResultSortOldest=Oldest searchResultSortRelevance=Relevance -searchResultCount=About 2,450 results where found for 'Schnitzel' +searchResultCount=About 2,450 results where found for ''Schnitzel'' sharingEmail=Send an email sharingFacebook=Share on Facebook sharingTwitter=Tweet about it @@ -197,7 +197,7 @@ solutionsSAPText1=By delivering real time analytics around race rankings, speeds solutionsSAPText2=SAP Sailing Analytics provide insights and transparency to the world of sailing by utilizing Cloud and In-Memory Technology, processing GPS and wind measurement data in real time and visualizing contents in various frontends accessible from anywhere. solutionsRaceText=The SAP Race Committee App delivers greater efficiency and control to race organizers by automating and simplifying their manual tasks and communication during running sailing regattas. The Race Committee App helps simplify the operation of running and managing race events to help race committees operate smarter. solutionsPostText=Access to data collected at sailing events gives sailors the opportunity to assess and analyse their performances to help develop strategies and sail better.
SAP Business Analytics solutions provide sailors, coaches, fans and media with tools to browse and analyse historical data across many regattas and races. SAP HANA allows them to learn about performance patterns, tendencies and sailor profiles in a simple, fast and efficient way in order to optimize their own performance and develop successful strategies for future regattas. -solutionsTrainingText=The STG Training Diary supports the daily documentation and analysis of an athlete\u2019s physical condition and training efforts in the areas of water training, physical training, boat work, mental, theory as well as other activities like education or travel. +solutionsTrainingText=The STG Training Diary supports the daily documentation and analysis of an athlete''s physical condition and training efforts in the areas of water training, physical training, boat work, mental, theory as well as other activities like education or travel. solutionsSimulatorText=Choosing the right strategy can provide great competitive advantage during a race. The Strategy Simulator simplifies determining the best sailing strategy for various wind conditions and water currents. Since wind and current can be changed in the simulation right away, the robustness of a strategy can be easily evaluated even for uncertain weather conditions. diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/footer/Footer.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/footer/Footer.ui.xml index 0c9ed27ee32..f90fce8f834 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/footer/Footer.ui.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/footer/Footer.ui.xml @@ -43,10 +43,10 @@ - + - + diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/header/Header.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/header/Header.ui.xml index 307e1bb0bf2..8d1b7a99c63 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/header/Header.ui.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/header/Header.ui.xml @@ -24,7 +24,7 @@ styleName="{local_res.css.sitenavigation_link}" text="{i18n.events}" /> - | - + | + + +

td:FIRST-CHILD { - border-left: 1px solid rgba(33,160,228,0.6) !important; -} - -.cellTableSelectedRow > td:LAST-CHILD { - border-right: 1px solid rgba(33,160,228,0.6) !important; -} -.cellTableSelectedRow > td { -} .cellTableCell { - overflow: inherit; font-size: 12px !important; line-height: 1.5em !important; -} -.cellTableSelectedRowCell { - border-top: 2px double #21a0e4 !important; - border-bottom: 2px double #21a0e4 !important; - padding: 7px 7px 6px 7px !important; + vertical-align: middle; } +.cellTableSelectedRowCell { + box-shadow: inset 0px -3px 3px -3px #21a0e4, inset 0px 3px 3px -3px #21a0e4; + padding: 7px !important; + } + .cellTableEvenRowCell, .cellTableOddRowCell { - border: 1px solid #c9caca; + border-top: none; + border-right: none; + border-bottom: 1px solid #d6d6d6; + border-left: 1px solid #d6d6d6; padding: 7px; white-space: nowrap; } .cellTableCheckboxColumnCell { - padding: 1px 1px 1px !important; - vertical-align: middle !important; - text-align: center; } .cellTableCheckboxSelected { - margin: auto auto; height: 13px; width: 13px; - border: 1px solid #fcbb22; - background-color: #fcbb22; - vertical-align: middle; + margin: auto auto; +/* border: 1px solid #fcbb22; */ +/* background-color: #fcbb22; */ + border: solid 1px #acacac; + background-color: #cacaca; + } .cellTableCheckboxDeselected { - margin: auto auto; height: 13px; width: 13px; + margin: auto auto; border: solid 1px #acacac; - vertical-align: middle; } .cellTableHoveredRow { @@ -135,7 +123,6 @@ .cellTableLegColumnHeader { background-color: #efefef; - } .cellTableLegDetailColumnHeader { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties index 316d07f3a69..ae1417c1b07 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties @@ -1444,7 +1444,6 @@ profile=Profile showOnlyCompetitorsOfLog=Show only competitors registered in Log confirmLosingCompetitorEditsWhenTogglingLogBasedView=This will reset your current, unsaved changes to the competitors.\nDo you really want to continue? removalOfMarkDisabledMayBeUsedInRaces=Removal deavticated, marks are used in races {0} -pleaseCreateAtLeastOneMappingBy=Plase create at list one mapping by first selecting a track on the left hand side and then selecting a mark or competitor on the right hand side createDefaultLeaderboardGroup=Would you like to create a default LeaderboardGroup for your event? pleaseCreateAtLeastOneMappingBy=Please create at least one mapping by first selecting a track on the left hand side and then selecting a mark or competitor on the right hand side matcherType=Matcher type diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties index de43e303420..e43b12f8808 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties @@ -1366,7 +1366,6 @@ scoringSchemeHighPointFirstGetsTwelveOrEight=High Point, winner gets 12 points ( scoringSchemeHighPointFirstGetsTwelveOrEightDescription=Score in points. Winner scores 12 points, 2nd - 11 points, .... A tie is broken in favour of the competitor having won most races, if tie remains then tie is broken by the rank in the last race scored. scoringSchemeHighPointEssOverall12=High Point System ESS Overall (12) scoringSchemeHighPointEssOverall12Description=Score in points. Winner of an act scores 12 points, 2nd - 11 points, .... Any tie in the overall Extreme Sailing Series is broken in favour of the competitor with the most Act wins. If that does not break the tie, the result in the last Act will be used. -pleaseCreateAtLeastOneMappingBy=Plase create at list one mapping by first selecting a track on the left hand side and then selecting a mark or competitor on the right hand side createDefaultLeaderboardGroup=Would you like to create a default LeaderboardGroup for your event? pleaseCreateAtLeastOneMappingBy=Please create at least one mapping by first selecting a track on the left hand side and then selecting a mark or competitor on the right hand side matcherType=Matcher type diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties index 72dd9f19f23..1db85e36f16 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties @@ -1381,7 +1381,6 @@ scoringSchemeHighPointFirstGetsTwelveOrEight=High Point, winner gets 12 points ( scoringSchemeHighPointFirstGetsTwelveOrEightDescription=Score in points. Winner scores 12 points, 2nd - 11 points, .... A tie is broken in favour of the competitor having won most races, if tie remains then tie is broken by the rank in the last race scored. scoringSchemeHighPointEssOverall12=High Point System ESS Overall (12) scoringSchemeHighPointEssOverall12Description=Score in points. Winner of an act scores 12 points, 2nd - 11 points, .... Any tie in the overall Extreme Sailing Series is broken in favour of the competitor with the most Act wins. If that does not break the tie, the result in the last Act will be used. -pleaseCreateAtLeastOneMappingBy=Plase create at list one mapping by first selecting a track on the left hand side and then selecting a mark or competitor on the right hand side createDefaultLeaderboardGroup=Would you like to create a default LeaderboardGroup for your event? pleaseCreateAtLeastOneMappingBy=Please create at least one mapping by first selecting a track on the left hand side and then selecting a mark or competitor on the right hand side matcherType=Matcher type diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java index d8a3d76289a..d44004fe6e7 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java @@ -2033,7 +2033,7 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe + " -> " + NumberFormat.getDecimalFormat().format(after.speedInKnots) + " " + stringMessages.knotsUnit() + ")")); if (maneuver.maneuverLossInMeters != null) { - vPanel.add(createInfoWindowLabelAndValue(stringMessages.maneuverLoss(), ""+maneuver.maneuverLossInMeters+"m")); + vPanel.add(createInfoWindowLabelAndValue(stringMessages.maneuverLoss(), NumberFormat.getFormat("0.0").format(maneuver.maneuverLossInMeters)+"m")); } return vPanel; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardTable.css b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardTable.css index 11d9c412cda..2d8c4698a2b 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardTable.css +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardTable.css @@ -1,7 +1,7 @@ .cellTableWidget { - border-left: 1px solid #c9caca; border-top: 1px solid #c9caca; - border-collapse: collapse; + border-right: 1px solid #c9caca; + border-collapse: separate; } @@ -16,20 +16,19 @@ border-right: 0px solid #c9caca !important; border-top: none !important; border-bottom: 1px solid #c9caca !important; + border-left: 1px solid #c9caca !important; padding: 3px 5px; padding-bottom: 5px; text-align: center; text-shadow: none !important; + font-weight: bold; + font-size: 12px!important; } .cellTableFirstColumnHeader { position: relative; vertical-align: bottom; background-color: #e5e5e5; - border-left: none !important; - border-right: 1px solid #c9caca !important; - border-top: none !important; - border-bottom: 1px solid #c9caca !important; text-align: center; } @@ -37,9 +36,6 @@ position: relative; vertical-align: bottom; background-color: #e5e5e5; - border-right: 1px solid #c9caca !important; - border-top: none !important; - border-bottom: 1px solid #c9caca !important; text-align: center; } @@ -58,57 +54,49 @@ .cellTableSelectedRow { color: #000000; - background-color: rgba(229,242,249,0.6); + background-color: rgba(229,242,249,0.5); } -.cellTableSelectedRow > td:FIRST-CHILD { - border-left: 1px solid rgba(33,160,228,0.6) !important; -} - -.cellTableSelectedRow > td:LAST-CHILD { - border-right: 1px solid rgba(33,160,228,0.6) !important; -} -.cellTableSelectedRow > td { -} .cellTableCell { - overflow: inherit; font-size: 12px !important; line-height: 1.5em !important; -} -.cellTableSelectedRowCell { - border-top: 2px double #21a0e4 !important; - border-bottom: 2px double #21a0e4 !important; - padding: 7px 7px 6px 7px !important; + vertical-align: middle; } +.cellTableSelectedRowCell { + box-shadow: inset 0px -3px 3px -3px #21a0e4, inset 0px 3px 3px -3px #21a0e4; + padding: 7px !important; + } + .cellTableEvenRowCell, .cellTableOddRowCell { - border: 1px solid #c9caca; + border-top: none; + border-right: none; + border-bottom: 1px solid #d6d6d6; + border-left: 1px solid #d6d6d6; padding: 7px; white-space: nowrap; } .cellTableCheckboxColumnCell { - padding: 1px 1px 1px !important; - vertical-align: middle !important; - text-align: center; } .cellTableCheckboxSelected { + height: 13px; + width: 13px; margin: auto auto; - height: 13px; - width: 13px; - border: 1px solid #fcbb22; - background-color: #fcbb22; - vertical-align: middle; + border: 1px solid #fcbb22; + background-color: #fcbb22; +/* border: solid 1px #acacac; */ +/* background-color: #cacaca; */ + } .cellTableCheckboxDeselected { - margin: auto auto; - height: 13px; - width: 13px; + height: 13px; + width: 13px; + margin: auto auto; border: solid 1px #acacac; - vertical-align: middle; } .cellTableHoveredRow { @@ -135,7 +123,6 @@ .cellTableLegColumnHeader { background-color: #efefef; - } .cellTableLegDetailColumnHeader { diff --git a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/RaceStateSerializer.java b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/RaceStateSerializer.java index b48aae155d1..0c76e3d5a3a 100644 --- a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/RaceStateSerializer.java +++ b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/RaceStateSerializer.java @@ -38,6 +38,7 @@ public class RaceStateSerializer implements JsonSerializer - - - - - - - - + + + + + + + + + diff --git a/java/com.sap.sse.gwt.test/.settings/com.google.gwt.eclipse.core.prefs b/java/com.sap.sse.gwt.test/.settings/com.google.gwt.eclipse.core.prefs index 7913a01d44c..0a706b8b4ac 100644 --- a/java/com.sap.sse.gwt.test/.settings/com.google.gwt.eclipse.core.prefs +++ b/java/com.sap.sse.gwt.test/.settings/com.google.gwt.eclipse.core.prefs @@ -1,2 +1,3 @@ eclipse.preferences.version=1 entryPointModules=com.sap.sse.gwt.test.TestSSE +filesCopiedToWebInfLib= diff --git a/java/com.sap.sse.security.ui/Security UI sdm.launch b/java/com.sap.sse.security.ui/Security UI sdm.launch old mode 100755 new mode 100644 index 63fc7dc7116..fdc04ec8bf3 --- a/java/com.sap.sse.security.ui/Security UI sdm.launch +++ b/java/com.sap.sse.security.ui/Security UI sdm.launch @@ -1,48 +1,48 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/adapters/coursedesign/CourseElementAdapter.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/adapters/coursedesign/CourseElementAdapter.java index c44c51f9a7d..18eddf8d1da 100644 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/adapters/coursedesign/CourseElementAdapter.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/adapters/coursedesign/CourseElementAdapter.java @@ -179,6 +179,9 @@ public class CourseElementAdapter extends BaseDraggableSwipeAdapter