From afc5945847cf341917065d69fcba08403514320c Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Tue, 3 Jun 2014 11:58:24 +0200 Subject: [PATCH 01/10] added start time based ranking for search results --- .../ui/adminconsole/CompetitorEditDialog.java | 2 +- .../sailing/server/RegattaSearchResult.java | 3 +- .../impl/RegattaByKeywordSearchService.java | 2 +- .../impl/RegattaSearchResultRanker.java | 62 ++++++++++++++++++- .../src/com/sap/sse/common/Util.java | 11 +++- 5 files changed, 72 insertions(+), 8 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorEditDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorEditDialog.java index ad72b440d5c..170d60f6852 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorEditDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorEditDialog.java @@ -98,7 +98,7 @@ public class CompetitorEditDialog extends DataEntryDialog { Collections.sort(ccs, new Comparator() { @Override public int compare(CountryCode o1, CountryCode o2) { - return Util.compareToWithNull(o1 == null ? null : o1.getThreeLetterIOCCode(), o2 == null ? null : o2.getThreeLetterIOCCode()); + return Util.compareToWithNull(o1 == null ? null : o1.getThreeLetterIOCCode(), o2 == null ? null : o2.getThreeLetterIOCCode(), /* nullIsLess */ true); } }); for (CountryCode cc : ccs) { diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/RegattaSearchResult.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/RegattaSearchResult.java index 13196ee8abe..1c5410b0678 100755 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/RegattaSearchResult.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/RegattaSearchResult.java @@ -1,7 +1,8 @@ package com.sap.sailing.server; +import com.sap.sailing.domain.base.Regatta; import com.sap.sse.common.search.Hit; public interface RegattaSearchResult extends Hit { - + Regatta getRegatta(); } diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RegattaByKeywordSearchService.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RegattaByKeywordSearchService.java index db8a14f1718..f8eae1e51f0 100755 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RegattaByKeywordSearchService.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RegattaByKeywordSearchService.java @@ -31,7 +31,7 @@ import com.sap.sse.common.search.ResultImpl; */ public class RegattaByKeywordSearchService { Result search(final RacingEventService racingEventService, KeywordQuery query) { - ResultImpl result = new ResultImpl<>(query, new RegattaSearchResultRanker()); + ResultImpl result = new ResultImpl<>(query, new RegattaSearchResultRanker(racingEventService)); AbstractListFilter regattaFilter = new AbstractListFilter() { @Override public Iterable getStrings(Regatta regatta) { diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RegattaSearchResultRanker.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RegattaSearchResultRanker.java index 84756dc1358..7c0c1c0d603 100755 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RegattaSearchResultRanker.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RegattaSearchResultRanker.java @@ -1,13 +1,71 @@ package com.sap.sailing.server.impl; +import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; +import java.util.List; +import com.sap.sailing.domain.base.Regatta; +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sailing.domain.tracking.TrackedRegatta; +import com.sap.sailing.domain.tracking.TrackedRegattaRegistry; +import com.sap.sailing.server.RacingEventService; import com.sap.sailing.server.RegattaSearchResult; +import com.sap.sse.common.Util; public class RegattaSearchResultRanker implements Comparator { + private final TrackedRegattaRegistry trackedRegattaRegistry; + + protected RegattaSearchResultRanker(RacingEventService racingEventService) { + this.trackedRegattaRegistry = racingEventService; + } + @Override public int compare(RegattaSearchResult o1, RegattaSearchResult o2) { - // TODO Auto-generated method stub - return 0; + Regatta r1 = o1.getRegatta(); + Regatta r2 = o2.getRegatta(); + TrackedRegatta trackedR1 = trackedRegattaRegistry.getTrackedRegatta(r1); + TrackedRegatta trackedR2 = trackedRegattaRegistry.getTrackedRegatta(r2); + final int result; + if (trackedR1 == null) { + if (trackedR2 == null) { + result = r1.getName().compareTo(r2.getName()); + } else { + result = -1; // a non-tracked regatta is greater (shown further down) than a tracked regatta + } + } else { + if (trackedR2 == null) { + result = 1; // a tracked regatta is less (shown further up) than a non-tracked regatta + } else { + // both are tracked; check times of tracked races + result = compareByEarliestStartOfATrackedRace(trackedR1, trackedR2); + } + } + return result; + } + + private int compareByEarliestStartOfATrackedRace(TrackedRegatta trackedR1, TrackedRegatta trackedR2) { + assert trackedR1 != null; + assert trackedR2 != null; + TimePoint earliestTrackedRaceInR1 = getEarliestTrackedRace(trackedR1); + TimePoint earliestTrackedRaceInR2 = getEarliestTrackedRace(trackedR2); + return Util.compareToWithNull(earliestTrackedRaceInR1, earliestTrackedRaceInR2, /* nullIsLess */ false); + } + + private TimePoint getEarliestTrackedRace(TrackedRegatta trackedRegatta) { + List startOfTrackingTimes = new ArrayList<>(); + for (TrackedRace trackedRace : trackedRegatta.getTrackedRaces()) { + if (trackedRace.getStartOfTracking() != null) { + startOfTrackingTimes.add(trackedRace.getStartOfTracking()); + } + } + final TimePoint result; + if (startOfTrackingTimes.isEmpty()) { + result = null; + } else { + result = Collections.min(startOfTrackingTimes); + } + return result; } } diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/Util.java b/java/com.sap.sse.common/src/com/sap/sse/common/Util.java index 8fc5ab15885..16f4d313f1b 100755 --- a/java/com.sap.sse.common/src/com/sap/sse/common/Util.java +++ b/java/com.sap.sse.common/src/com/sap/sse/common/Util.java @@ -270,17 +270,22 @@ public class Util { return result; } - public static int compareToWithNull(Comparable o1, T o2) { + /** + * null is permissible for both, o1 and o2, where a null value + * is considered less than a non-null value if nullIsLess is true, greater otherwise. + * @param nullIsLess TODO + */ + public static int compareToWithNull(Comparable o1, T o2, boolean nullIsLess) { final int result; if (o1 == null) { if (o2 == null) { result = 0; } else { - result = -1; + result = nullIsLess ? -1 : 1; } } else { if (o2 == null) { - result = 1; + result = nullIsLess ? 1 : -1; } else { result = o1.compareTo(o2); } From d5cf2585a1f9234da0d0930f047e3228f301c36c Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Tue, 3 Jun 2014 16:15:52 +0200 Subject: [PATCH 02/10] tried to change the spy mocking to fix a test error in Maven --- .../server/gateway/trackfiles/impl/GPSFixImportTest.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/trackfiles/impl/GPSFixImportTest.java b/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/trackfiles/impl/GPSFixImportTest.java index 1b864c8704a..602085f7b3e 100644 --- a/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/trackfiles/impl/GPSFixImportTest.java +++ b/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/trackfiles/impl/GPSFixImportTest.java @@ -3,9 +3,9 @@ package com.sap.sailing.server.gateway.trackfiles.impl; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; +import static org.mockito.Mockito.when; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; import java.io.BufferedInputStream; @@ -67,15 +67,13 @@ public class GPSFixImportTest { @Test public void testReusingImportStream() throws IOException { TrackFilesImportServlet servlet = spy(new TrackFilesImportServlet()); - doReturn(Arrays.asList((GPSFixImporter) RouteConverterGPSFixImporterFactory.INSTANCE.createRouteConverterGPSFixImporter())). - when(servlet).getGPSFixImporters(anyString()); + when(servlet.getGPSFixImporters(anyString())).thenReturn(Arrays.asList((GPSFixImporter) RouteConverterGPSFixImporterFactory.INSTANCE.createRouteConverterGPSFixImporter())); doAnswer(new Answer() { @Override public Void answer(InvocationOnMock invocation) throws Throwable { return null; } }).when(servlet).storeFix(any(GPSFix.class), any(DeviceIdentifier.class)); - InputStream in = getClass().getResourceAsStream("/Cardiff Race17 - COMPETITORS.gpx"); servlet.importFiles(Arrays.asList(new Pair<>("test.gpx", in)), new AlwaysFailingGPSFixImporter(-1)); // getting to here without errors is good enough From a01aac197ca1e05ed2816680222647e0041b0f1f Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Tue, 3 Jun 2014 16:45:35 +0200 Subject: [PATCH 03/10] started with a search test setup; a first test fails because of duplicate regattas... strange... --- .../sailing/domain/test/ReachingLegTest.java | 4 +- .../sailing/domain/test/TrackBasedTest.java | 2 +- .../META-INF/MANIFEST.MF | 3 +- .../server/test/SearchServiceTest.java | 164 ++++++++++++++++++ .../sailing/server/RacingEventService.java | 4 + .../server/impl/RacingEventServiceImpl.java | 8 + .../CreateTrackedRace.java | 5 +- .../sap/sse/common/search/KeywordQuery.java | 8 +- .../src/com/sap/sse/common/search/Result.java | 2 + 9 files changed, 193 insertions(+), 7 deletions(-) create mode 100755 java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/SearchServiceTest.java diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/ReachingLegTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/ReachingLegTest.java index 6c7ebc395c4..fafcebf1551 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/ReachingLegTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/ReachingLegTest.java @@ -76,14 +76,14 @@ public class ReachingLegTest extends TrackBasedTest { schomaeker = createCompetitor("Meike Schom�ker"); competitors.add(schomaeker); start = new MillisecondsTimePoint(new GregorianCalendar(2011, 05, 23).getTime()); - setTrackedRace(createTestTrackedRace("Kieler Woche", "505 Race 2", "505", competitors, start)); + setTrackedRace(createTrackedRace("Kieler Woche", "505 Race 2", "505", competitors, start)); List hungersMarkPassings = createMarkPassings(hunger, start); getTrackedRace().updateMarkPassings(hunger, hungersMarkPassings); List plattnersMarkPassings = createMarkPassings(plattner, start); getTrackedRace().updateMarkPassings(plattner, plattnersMarkPassings); } - protected DynamicTrackedRace createTestTrackedRace(String regattaName, String raceName, String boatClassName, + protected DynamicTrackedRace createTrackedRace(String regattaName, String raceName, String boatClassName, Iterable competitors, TimePoint timePointForFixes) { BoatClassImpl boatClass = new BoatClassImpl(boatClassName, /* typicallyStartsUpwind */ true); Regatta regatta = new RegattaImpl(EmptyRaceLogStore.INSTANCE, regattaName, boatClass, /* trackedRegattaRegistry */ null, diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/TrackBasedTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/TrackBasedTest.java index 2f31e9cde6c..252acc9fdc4 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/TrackBasedTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/TrackBasedTest.java @@ -93,7 +93,7 @@ public abstract class TrackBasedTest { * tracked race returned by this method) is important because otherwise confidences of wind readouts may * be ridiculously low. */ - protected DynamicTrackedRace createTestTrackedRace(String regattaName, String raceName, String boatClassName, + public static DynamicTrackedRace createTestTrackedRace(String regattaName, String raceName, String boatClassName, Iterable competitors, TimePoint timePointForFixes) { BoatClassImpl boatClass = new BoatClassImpl(boatClassName, /* typicallyStartsUpwind */ true); Regatta regatta = new RegattaImpl(EmptyRaceLogStore.INSTANCE, regattaName, boatClass, /* trackedRegattaRegistry */ null, diff --git a/java/com.sap.sailing.server.test/META-INF/MANIFEST.MF b/java/com.sap.sailing.server.test/META-INF/MANIFEST.MF index f2a884f5352..b2ff515e274 100755 --- a/java/com.sap.sailing.server.test/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.server.test/META-INF/MANIFEST.MF @@ -18,7 +18,8 @@ Require-Bundle: com.sap.sailing.domain, org.junit4;bundle-version="4.8.2", com.sap.sailing.domain.test, org.hamcrest;bundle-version="1.1.0", - org.mockito.mockito-core;bundle-version="1.9.5" + org.mockito.mockito-core;bundle-version="1.9.5", + com.sap.sse.common Import-Package: com.sap.sailing.server.gateway.jaxrs, com.sap.sailing.server.gateway.jaxrs.spi, org.json.simple, diff --git a/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/SearchServiceTest.java b/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/SearchServiceTest.java new file mode 100755 index 00000000000..2130c21e73c --- /dev/null +++ b/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/SearchServiceTest.java @@ -0,0 +1,164 @@ +package com.sap.sailing.server.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; + +import java.net.URL; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collections; +import java.util.GregorianCalendar; +import java.util.LinkedHashMap; +import java.util.UUID; + +import org.junit.Before; +import org.junit.Test; + +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.Event; +import com.sap.sailing.domain.base.Regatta; +import com.sap.sailing.domain.base.Venue; +import com.sap.sailing.domain.base.Waypoint; +import com.sap.sailing.domain.base.impl.CourseAreaImpl; +import com.sap.sailing.domain.base.impl.CourseImpl; +import com.sap.sailing.domain.base.impl.RaceDefinitionImpl; +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.common.dto.RegattaCreationParametersDTO; +import com.sap.sailing.domain.common.dto.SeriesCreationParametersDTO; +import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; +import com.sap.sailing.domain.leaderboard.impl.LowPoint; +import com.sap.sailing.domain.racelog.tracking.EmptyGPSFixStore; +import com.sap.sailing.domain.test.AbstractTracTracLiveTest; +import com.sap.sailing.domain.tracking.DynamicTrackedRace; +import com.sap.sailing.domain.tracking.impl.EmptyWindStore; +import com.sap.sailing.server.RacingEventService; +import com.sap.sailing.server.RegattaSearchResult; +import com.sap.sailing.server.impl.RacingEventServiceImpl; +import com.sap.sailing.server.operationaltransformation.AddRaceDefinition; +import com.sap.sailing.server.operationaltransformation.AddSpecificRegatta; +import com.sap.sailing.server.operationaltransformation.CreateEvent; +import com.sap.sailing.server.operationaltransformation.CreateTrackedRace; +import com.sap.sse.common.Util; +import com.sap.sse.common.search.KeywordQuery; +import com.sap.sse.common.search.Result; + +public class SearchServiceTest { + private RacingEventService server; + private Venue kiel; + private Venue flensburg; + private Event pfingstbusch; + private Regatta pfingstbusch29er; + private Competitor hassoPlattner; + private Competitor alexanderRies; + private Competitor antonKoch; + private Competitor tobiasSchadewaldt; + private Competitor philippBuhl; + private Competitor dennisGehrlein; + private Regatta pfingstbusch470; + private Regatta aalRegatta; + private DynamicTrackedRace pfingstbusch29erTrackedR1; + private DynamicTrackedRace pfingstbusch29erTrackedR3; + private Event aalEvent; + private DynamicTrackedRace pfingstbusch470TrackedR1; + private DynamicTrackedRace pfingstbusch470TrackedR2; + private DynamicTrackedRace aalOrcTrackedR1; + private DynamicTrackedRace aalOrcTrackedR2; + + @Before + public void setUp() { + server = new RacingEventServiceImpl(); + final Calendar cal = new GregorianCalendar(); + cal.set(2014, 5, 6, 10, 00); + final TimePoint pfingstbuschStartDate = new MillisecondsTimePoint(cal.getTime()); + cal.set(2014, 5, 8, 16, 00); + final TimePoint pfingstbuschEndDate = new MillisecondsTimePoint(cal.getTime()); + pfingstbusch = server.apply(new CreateEvent("Pfingsbusch", pfingstbuschStartDate, pfingstbuschEndDate, "Kiel", /* isPublic */ + true, UUID.randomUUID(), Collections.emptySet(), Collections.emptySet())); + kiel = pfingstbusch.getVenue(); + final CourseAreaImpl kielAlpha = new CourseAreaImpl("Alpha", UUID.randomUUID()); + kiel.addCourseArea(kielAlpha); + final CourseAreaImpl kielBravo = new CourseAreaImpl("Bravo", UUID.randomUUID()); + kiel.addCourseArea(kielBravo); + pfingstbusch29er = server.apply(new AddSpecificRegatta("Pfingstbusch", "29er", UUID.randomUUID(), + new RegattaCreationParametersDTO(new LinkedHashMap()), /* persistent */ true, new LowPoint(), kielAlpha)); + pfingstbusch470 = server.apply(new AddSpecificRegatta("Pfingstbusch", "470", UUID.randomUUID(), + new RegattaCreationParametersDTO(new LinkedHashMap()), /* persistent */ true, new LowPoint(), kielBravo)); + cal.set(2014, 5, 7, 10, 00); + final TimePoint aalStartDate = new MillisecondsTimePoint(cal.getTime()); + cal.set(2014, 5, 8, 18, 00); + final TimePoint aalEndDate = new MillisecondsTimePoint(cal.getTime()); + aalEvent = server.apply(new CreateEvent("Aalregatta", aalStartDate, aalEndDate, "Flensburg", /* isPublic */ + true, UUID.randomUUID(), Collections.emptySet(), Collections.emptySet())); + flensburg = aalEvent.getVenue(); + final CourseAreaImpl flensburgStandard = new CourseAreaImpl("Standard", UUID.randomUUID()); + flensburg.addCourseArea(flensburgStandard); + aalRegatta = server.apply(new AddSpecificRegatta("Aalregatta", "ORC", UUID.randomUUID(), + new RegattaCreationParametersDTO(new LinkedHashMap()), /* persistent */ true, new LowPoint(), flensburgStandard)); + hassoPlattner = AbstractTracTracLiveTest.createCompetitor("Hasso Plattner"); + alexanderRies = AbstractTracTracLiveTest.createCompetitor("Alexander Ries"); + antonKoch = AbstractTracTracLiveTest.createCompetitor("Anton Koch"); + tobiasSchadewaldt = AbstractTracTracLiveTest.createCompetitor("Tobias Schadewaldt"); + philippBuhl = AbstractTracTracLiveTest.createCompetitor("Philipp Buhl"); + dennisGehrlein = AbstractTracTracLiveTest.createCompetitor("Dennis Gehrlein"); + final RaceDefinitionImpl pfingstbush29erR1 = new RaceDefinitionImpl("R1", new CourseImpl("up/down", Collections.emptyList()), pfingstbusch29er.getBoatClass(), + Arrays.asList(new Competitor[] { alexanderRies, tobiasSchadewaldt })); + final RaceDefinitionImpl pfingstbush29erR2 = new RaceDefinitionImpl("R2", new CourseImpl("up/down", Collections.emptyList()), pfingstbusch29er.getBoatClass(), + Arrays.asList(new Competitor[] { alexanderRies, tobiasSchadewaldt })); + final RaceDefinitionImpl pfingstbush29erR3 = new RaceDefinitionImpl("R3", new CourseImpl("up/down", Collections.emptyList()), pfingstbusch29er.getBoatClass(), + Arrays.asList(new Competitor[] { alexanderRies, tobiasSchadewaldt })); + server.apply(new AddRaceDefinition(pfingstbusch29er.getRegattaIdentifier(), pfingstbush29erR1)); + server.apply(new AddRaceDefinition(pfingstbusch29er.getRegattaIdentifier(), pfingstbush29erR2)); + server.apply(new AddRaceDefinition(pfingstbusch29er.getRegattaIdentifier(), pfingstbush29erR3)); + // track only R1 and R3 + pfingstbusch29erTrackedR1 = server.apply(new CreateTrackedRace(pfingstbusch29er.getRaceIdentifier(pfingstbush29erR1), EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, /* delayToLiveInMillis */ 3000, + /* millisecondsOverWhichToAverageWind */ 15000, /* millisecondsOverWhichToAverageSpeed */ 15000)); + pfingstbusch29erTrackedR3 = server.apply(new CreateTrackedRace(pfingstbusch29er.getRaceIdentifier(pfingstbush29erR3), EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, /* delayToLiveInMillis */ 3000, + /* millisecondsOverWhichToAverageWind */ 15000, /* millisecondsOverWhichToAverageSpeed */ 15000)); + + final RaceDefinitionImpl pfingstbush470R1 = new RaceDefinitionImpl("R1", new CourseImpl("up/down", Collections.emptyList()), pfingstbusch470.getBoatClass(), + Arrays.asList(new Competitor[] { philippBuhl, antonKoch })); + final RaceDefinitionImpl pfingstbush470R2 = new RaceDefinitionImpl("R2", new CourseImpl("up/down", Collections.emptyList()), pfingstbusch470.getBoatClass(), + Arrays.asList(new Competitor[] { philippBuhl, antonKoch })); + server.apply(new AddRaceDefinition(pfingstbusch470.getRegattaIdentifier(), pfingstbush470R1)); + server.apply(new AddRaceDefinition(pfingstbusch470.getRegattaIdentifier(), pfingstbush470R2)); + // track only R1 and R2 + pfingstbusch470TrackedR1 = server.apply(new CreateTrackedRace(pfingstbusch470.getRaceIdentifier(pfingstbush470R1), EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, /* delayToLiveInMillis */ 3000, + /* millisecondsOverWhichToAverageWind */ 15000, /* millisecondsOverWhichToAverageSpeed */ 15000)); + pfingstbusch470TrackedR2 = server.apply(new CreateTrackedRace(pfingstbusch470.getRaceIdentifier(pfingstbush470R2), EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, /* delayToLiveInMillis */ 3000, + /* millisecondsOverWhichToAverageWind */ 15000, /* millisecondsOverWhichToAverageSpeed */ 15000)); + + final RaceDefinitionImpl aalOrcR1 = new RaceDefinitionImpl("R1", new CourseImpl("up/down", Collections.emptyList()), aalRegatta.getBoatClass(), + Arrays.asList(new Competitor[] { hassoPlattner, dennisGehrlein })); + final RaceDefinitionImpl aalOrcR2 = new RaceDefinitionImpl("R2", new CourseImpl("up/down", Collections.emptyList()), aalRegatta.getBoatClass(), + Arrays.asList(new Competitor[] { hassoPlattner, dennisGehrlein })); + server.apply(new AddRaceDefinition(aalRegatta.getRegattaIdentifier(), aalOrcR1)); + server.apply(new AddRaceDefinition(aalRegatta.getRegattaIdentifier(), aalOrcR2)); + // track only R1 and R2 + aalOrcTrackedR1 = server.apply(new CreateTrackedRace(aalRegatta.getRaceIdentifier(aalOrcR1), EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, /* delayToLiveInMillis */ 3000, + /* millisecondsOverWhichToAverageWind */ 15000, /* millisecondsOverWhichToAverageSpeed */ 15000)); + aalOrcTrackedR2 = server.apply(new CreateTrackedRace(aalRegatta.getRaceIdentifier(aalOrcR2), EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, /* delayToLiveInMillis */ 3000, + /* millisecondsOverWhichToAverageWind */ 15000, /* millisecondsOverWhichToAverageSpeed */ 15000)); + } + + @Test + public void testSetup() { + assertNotNull(pfingstbusch29er); + assertNotNull(pfingstbusch29erTrackedR1); + assertNotNull(pfingstbusch29erTrackedR3); + assertNotNull(pfingstbusch470TrackedR1); + assertNotNull(pfingstbusch470TrackedR2); + assertNotNull(aalEvent); + assertNotNull(aalRegatta); + assertNotNull(aalOrcTrackedR1); + assertNotNull(aalOrcTrackedR2); + } + + @Test + public void testSimpleSearch() { + Result searchResults = server.search(new KeywordQuery(Arrays.asList(new String[] { "Tobi" }))); + assertEquals(1, Util.size(searchResults.getHits())); + Regatta foundRegatta = searchResults.getHits().iterator().next().getRegatta(); + assertSame(pfingstbusch29er, foundRegatta); + } +} diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventService.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventService.java index 99eb6fd1a27..8d83129e066 100755 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventService.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventService.java @@ -67,6 +67,8 @@ import com.sap.sailing.domain.tracking.WindStore; import com.sap.sailing.server.masterdata.DataImportLockWithProgress; import com.sap.sse.common.Util; import com.sap.sse.common.Util.Triple; +import com.sap.sse.common.search.KeywordQuery; +import com.sap.sse.common.search.Result; /** * An OSGi service that can be used to track boat races using a TracTrac connector that pushes @@ -552,4 +554,6 @@ public interface RacingEventService extends TrackedRegattaRegistry, RegattaFetch * or, if fetching the event list from the remote server did fail, the exception for which it failed. */ Util.Pair, Exception> updateRemoteServerEventCacheSynchronously(RemoteSailingServerReference ref); + + Result search(KeywordQuery query); } diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java index e875e2049d6..caee176bb29 100755 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java @@ -133,6 +133,7 @@ import com.sap.sailing.operationaltransformation.Operation; import com.sap.sailing.server.OperationExecutionListener; import com.sap.sailing.server.RacingEventService; import com.sap.sailing.server.RacingEventServiceOperation; +import com.sap.sailing.server.RegattaSearchResult; import com.sap.sailing.server.Replicator; import com.sap.sailing.server.masterdata.DataImportLockWithProgress; import com.sap.sailing.server.operationaltransformation.AddCourseArea; @@ -171,6 +172,8 @@ import com.sap.sailing.util.BuildVersion; import com.sap.sailing.util.impl.LockUtil; import com.sap.sailing.util.impl.NamedReentrantReadWriteLock; import com.sap.sse.common.Util; +import com.sap.sse.common.search.KeywordQuery; +import com.sap.sse.common.search.Result; public class RacingEventServiceImpl implements RacingEventServiceWithTestSupport, RegattaListener, LeaderboardRegistry, Replicator { private static final Logger logger = Logger.getLogger(RacingEventServiceImpl.class.getName()); @@ -2588,4 +2591,9 @@ public class RacingEventServiceImpl implements RacingEventServiceWithTestSupport public void setDataImportDeleteProgressFromMapTimerWithoutReplication(UUID importOperationId) { dataImportLock.setDeleteFromMapTimer(importOperationId); } + + @Override + public Result search(KeywordQuery query) { + return new RegattaByKeywordSearchService().search(this, query); + } } diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/CreateTrackedRace.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/CreateTrackedRace.java index 4a74ff0e59d..5e7bce313dd 100755 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/CreateTrackedRace.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/CreateTrackedRace.java @@ -6,6 +6,7 @@ import com.sap.sailing.domain.common.RaceIdentifier; import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.domain.racelog.tracking.EmptyGPSFixStore; import com.sap.sailing.domain.racelog.tracking.GPSFixStore; +import com.sap.sailing.domain.tracking.DynamicTrackedRace; import com.sap.sailing.domain.tracking.TrackedRace; import com.sap.sailing.domain.tracking.TrackedRegatta; import com.sap.sailing.domain.tracking.WindStore; @@ -22,7 +23,7 @@ import com.sap.sailing.server.RacingEventServiceOperation; * @author Axel Uhl (d043530) * */ -public class CreateTrackedRace extends AbstractRaceOperation { +public class CreateTrackedRace extends AbstractRaceOperation { private static final long serialVersionUID = 5084401060896514911L; private final long millisecondsOverWhichToAverageWind; private final long millisecondsOverWhichToAverageSpeed; @@ -73,7 +74,7 @@ public class CreateTrackedRace extends AbstractRaceOperation { } @Override - public TrackedRace internalApplyTo(RacingEventService toState) { + public DynamicTrackedRace internalApplyTo(RacingEventService toState) { return toState.createTrackedRace(getRaceIdentifier(), windStore == null ? EmptyWindStore.INSTANCE : windStore, gpsFixStore == null ? EmptyGPSFixStore.INSTANCE : gpsFixStore, delayToLiveInMillis, millisecondsOverWhichToAverageWind, millisecondsOverWhichToAverageSpeed); diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/search/KeywordQuery.java b/java/com.sap.sse.common/src/com/sap/sse/common/search/KeywordQuery.java index b83b9c4512c..44287529ca3 100755 --- a/java/com.sap.sse.common/src/com/sap/sse/common/search/KeywordQuery.java +++ b/java/com.sap.sse.common/src/com/sap/sse/common/search/KeywordQuery.java @@ -1,9 +1,15 @@ package com.sap.sse.common.search; +import java.util.Arrays; + public class KeywordQuery implements Query { private final Iterable keywords; - protected KeywordQuery(Iterable keywords) { + public KeywordQuery(String... keywords) { + this.keywords = Arrays.asList(keywords); + } + + public KeywordQuery(Iterable keywords) { super(); this.keywords = keywords; } diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/search/Result.java b/java/com.sap.sse.common/src/com/sap/sse/common/search/Result.java index 0891c17ec04..5fbfc52b23a 100644 --- a/java/com.sap.sse.common/src/com/sap/sse/common/search/Result.java +++ b/java/com.sap.sse.common/src/com/sap/sse/common/search/Result.java @@ -11,4 +11,6 @@ public interface Result { * The query for which this is the result. Useful when multiple queries have been fired concurrently. */ Query getQuery(); + + Iterable getHits(); } From c0eb502f0948f81da909427e77a25e780b0b4af1 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Wed, 4 Jun 2014 00:11:39 +0200 Subject: [PATCH 04/10] removed redundant import --- .../sailing/server/gateway/trackfiles/impl/GPSFixImportTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/trackfiles/impl/GPSFixImportTest.java b/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/trackfiles/impl/GPSFixImportTest.java index 13cf19ac5e3..34c3e503580 100755 --- a/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/trackfiles/impl/GPSFixImportTest.java +++ b/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/trackfiles/impl/GPSFixImportTest.java @@ -2,7 +2,6 @@ package com.sap.sailing.server.gateway.trackfiles.impl; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.when; import java.io.BufferedInputStream; import java.io.IOException; From dd2785c0f03c449828fa3fab3c8b5275c6c9e77e Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Wed, 4 Jun 2014 00:19:03 +0200 Subject: [PATCH 05/10] fixed search test by removing all regattas before creating them to avoid duplicate regatta names --- .../src/com/sap/sailing/server/test/SearchServiceTest.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/SearchServiceTest.java b/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/SearchServiceTest.java index 2130c21e73c..89ae39bdd87 100755 --- a/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/SearchServiceTest.java +++ b/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/SearchServiceTest.java @@ -23,6 +23,7 @@ import com.sap.sailing.domain.base.Waypoint; import com.sap.sailing.domain.base.impl.CourseAreaImpl; import com.sap.sailing.domain.base.impl.CourseImpl; import com.sap.sailing.domain.base.impl.RaceDefinitionImpl; +import com.sap.sailing.domain.common.RegattaName; import com.sap.sailing.domain.common.TimePoint; import com.sap.sailing.domain.common.dto.RegattaCreationParametersDTO; import com.sap.sailing.domain.common.dto.SeriesCreationParametersDTO; @@ -39,6 +40,7 @@ import com.sap.sailing.server.operationaltransformation.AddRaceDefinition; import com.sap.sailing.server.operationaltransformation.AddSpecificRegatta; import com.sap.sailing.server.operationaltransformation.CreateEvent; import com.sap.sailing.server.operationaltransformation.CreateTrackedRace; +import com.sap.sailing.server.operationaltransformation.RemoveRegatta; import com.sap.sse.common.Util; import com.sap.sse.common.search.KeywordQuery; import com.sap.sse.common.search.Result; @@ -68,6 +70,9 @@ public class SearchServiceTest { @Before public void setUp() { server = new RacingEventServiceImpl(); + server.apply(new RemoveRegatta(new RegattaName("Pfingstbusch (29er)"))); + server.apply(new RemoveRegatta(new RegattaName("Pfingstbusch (470)"))); + server.apply(new RemoveRegatta(new RegattaName("Aalregatta (ORC)"))); final Calendar cal = new GregorianCalendar(); cal.set(2014, 5, 6, 10, 00); final TimePoint pfingstbuschStartDate = new MillisecondsTimePoint(cal.getTime()); From d7cf30f9f7dd1598c183da85da906597565fb6a7 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Wed, 4 Jun 2014 00:36:24 +0200 Subject: [PATCH 06/10] fixed test setup and added tests for searching by venue name --- .../server/test/SearchServiceTest.java | 81 +++++++++++++++---- 1 file changed, 67 insertions(+), 14 deletions(-) diff --git a/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/SearchServiceTest.java b/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/SearchServiceTest.java index 89ae39bdd87..521d9980de5 100755 --- a/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/SearchServiceTest.java +++ b/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/SearchServiceTest.java @@ -5,11 +5,14 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertSame; import java.net.URL; +import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; import java.util.GregorianCalendar; +import java.util.Iterator; import java.util.LinkedHashMap; +import java.util.List; import java.util.UUID; import org.junit.Before; @@ -40,6 +43,7 @@ import com.sap.sailing.server.operationaltransformation.AddRaceDefinition; import com.sap.sailing.server.operationaltransformation.AddSpecificRegatta; import com.sap.sailing.server.operationaltransformation.CreateEvent; import com.sap.sailing.server.operationaltransformation.CreateTrackedRace; +import com.sap.sailing.server.operationaltransformation.RemoveEvent; import com.sap.sailing.server.operationaltransformation.RemoveRegatta; import com.sap.sse.common.Util; import com.sap.sse.common.search.KeywordQuery; @@ -70,6 +74,11 @@ public class SearchServiceTest { @Before public void setUp() { server = new RacingEventServiceImpl(); + List allEvents = new ArrayList<>(); + Util.addAll(server.getAllEvents(), allEvents); + for (Event e : allEvents) { + server.apply(new RemoveEvent(e.getId())); + } server.apply(new RemoveRegatta(new RegattaName("Pfingstbusch (29er)"))); server.apply(new RemoveRegatta(new RegattaName("Pfingstbusch (470)"))); server.apply(new RemoveRegatta(new RegattaName("Aalregatta (ORC)"))); @@ -86,9 +95,11 @@ public class SearchServiceTest { final CourseAreaImpl kielBravo = new CourseAreaImpl("Bravo", UUID.randomUUID()); kiel.addCourseArea(kielBravo); pfingstbusch29er = server.apply(new AddSpecificRegatta("Pfingstbusch", "29er", UUID.randomUUID(), - new RegattaCreationParametersDTO(new LinkedHashMap()), /* persistent */ true, new LowPoint(), kielAlpha)); + new RegattaCreationParametersDTO(new LinkedHashMap()), /* persistent */ + true, new LowPoint(), kielAlpha.getId())); pfingstbusch470 = server.apply(new AddSpecificRegatta("Pfingstbusch", "470", UUID.randomUUID(), - new RegattaCreationParametersDTO(new LinkedHashMap()), /* persistent */ true, new LowPoint(), kielBravo)); + new RegattaCreationParametersDTO(new LinkedHashMap()), /* persistent */ + true, new LowPoint(), kielBravo.getId())); cal.set(2014, 5, 7, 10, 00); final TimePoint aalStartDate = new MillisecondsTimePoint(cal.getTime()); cal.set(2014, 5, 8, 18, 00); @@ -99,26 +110,28 @@ public class SearchServiceTest { final CourseAreaImpl flensburgStandard = new CourseAreaImpl("Standard", UUID.randomUUID()); flensburg.addCourseArea(flensburgStandard); aalRegatta = server.apply(new AddSpecificRegatta("Aalregatta", "ORC", UUID.randomUUID(), - new RegattaCreationParametersDTO(new LinkedHashMap()), /* persistent */ true, new LowPoint(), flensburgStandard)); + new RegattaCreationParametersDTO(new LinkedHashMap()), /* persistent */ + true, new LowPoint(), flensburgStandard.getId())); hassoPlattner = AbstractTracTracLiveTest.createCompetitor("Hasso Plattner"); alexanderRies = AbstractTracTracLiveTest.createCompetitor("Alexander Ries"); antonKoch = AbstractTracTracLiveTest.createCompetitor("Anton Koch"); tobiasSchadewaldt = AbstractTracTracLiveTest.createCompetitor("Tobias Schadewaldt"); philippBuhl = AbstractTracTracLiveTest.createCompetitor("Philipp Buhl"); dennisGehrlein = AbstractTracTracLiveTest.createCompetitor("Dennis Gehrlein"); - final RaceDefinitionImpl pfingstbush29erR1 = new RaceDefinitionImpl("R1", new CourseImpl("up/down", Collections.emptyList()), pfingstbusch29er.getBoatClass(), + final RaceDefinitionImpl pfingstbusch29erR1 = new RaceDefinitionImpl("R1", new CourseImpl("up/down", Collections.emptyList()), pfingstbusch29er.getBoatClass(), Arrays.asList(new Competitor[] { alexanderRies, tobiasSchadewaldt })); - final RaceDefinitionImpl pfingstbush29erR2 = new RaceDefinitionImpl("R2", new CourseImpl("up/down", Collections.emptyList()), pfingstbusch29er.getBoatClass(), + final RaceDefinitionImpl pfingstbusch29erR2 = new RaceDefinitionImpl("R2", new CourseImpl("up/down", Collections.emptyList()), pfingstbusch29er.getBoatClass(), Arrays.asList(new Competitor[] { alexanderRies, tobiasSchadewaldt })); - final RaceDefinitionImpl pfingstbush29erR3 = new RaceDefinitionImpl("R3", new CourseImpl("up/down", Collections.emptyList()), pfingstbusch29er.getBoatClass(), + final RaceDefinitionImpl pfingstbusch29erR3 = new RaceDefinitionImpl("R3", new CourseImpl("up/down", Collections.emptyList()), pfingstbusch29er.getBoatClass(), Arrays.asList(new Competitor[] { alexanderRies, tobiasSchadewaldt })); - server.apply(new AddRaceDefinition(pfingstbusch29er.getRegattaIdentifier(), pfingstbush29erR1)); - server.apply(new AddRaceDefinition(pfingstbusch29er.getRegattaIdentifier(), pfingstbush29erR2)); - server.apply(new AddRaceDefinition(pfingstbusch29er.getRegattaIdentifier(), pfingstbush29erR3)); + server.apply(new AddRaceDefinition(pfingstbusch29er.getRegattaIdentifier(), pfingstbusch29erR1)); + server.apply(new AddRaceDefinition(pfingstbusch29er.getRegattaIdentifier(), pfingstbusch29erR2)); + server.apply(new AddRaceDefinition(pfingstbusch29er.getRegattaIdentifier(), pfingstbusch29erR3)); // track only R1 and R3 - pfingstbusch29erTrackedR1 = server.apply(new CreateTrackedRace(pfingstbusch29er.getRaceIdentifier(pfingstbush29erR1), EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, /* delayToLiveInMillis */ 3000, + pfingstbusch29erTrackedR1 = server.apply(new CreateTrackedRace(pfingstbusch29er.getRaceIdentifier(pfingstbusch29erR1), EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, /* delayToLiveInMillis */ 3000, /* millisecondsOverWhichToAverageWind */ 15000, /* millisecondsOverWhichToAverageSpeed */ 15000)); - pfingstbusch29erTrackedR3 = server.apply(new CreateTrackedRace(pfingstbusch29er.getRaceIdentifier(pfingstbush29erR3), EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, /* delayToLiveInMillis */ 3000, + pfingstbusch29erTrackedR1.setStartOfTrackingReceived(pfingstbuschStartDate.plus(1)); + pfingstbusch29erTrackedR3 = server.apply(new CreateTrackedRace(pfingstbusch29er.getRaceIdentifier(pfingstbusch29erR3), EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, /* delayToLiveInMillis */ 3000, /* millisecondsOverWhichToAverageWind */ 15000, /* millisecondsOverWhichToAverageSpeed */ 15000)); final RaceDefinitionImpl pfingstbush470R1 = new RaceDefinitionImpl("R1", new CourseImpl("up/down", Collections.emptyList()), pfingstbusch470.getBoatClass(), @@ -130,18 +143,20 @@ public class SearchServiceTest { // track only R1 and R2 pfingstbusch470TrackedR1 = server.apply(new CreateTrackedRace(pfingstbusch470.getRaceIdentifier(pfingstbush470R1), EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, /* delayToLiveInMillis */ 3000, /* millisecondsOverWhichToAverageWind */ 15000, /* millisecondsOverWhichToAverageSpeed */ 15000)); + pfingstbusch470TrackedR1.setStartOfTrackingReceived(pfingstbuschStartDate.plus(2)); // starts later than 29er pfingstbusch470TrackedR2 = server.apply(new CreateTrackedRace(pfingstbusch470.getRaceIdentifier(pfingstbush470R2), EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, /* delayToLiveInMillis */ 3000, /* millisecondsOverWhichToAverageWind */ 15000, /* millisecondsOverWhichToAverageSpeed */ 15000)); final RaceDefinitionImpl aalOrcR1 = new RaceDefinitionImpl("R1", new CourseImpl("up/down", Collections.emptyList()), aalRegatta.getBoatClass(), - Arrays.asList(new Competitor[] { hassoPlattner, dennisGehrlein })); + Arrays.asList(new Competitor[] { hassoPlattner, dennisGehrlein, philippBuhl })); final RaceDefinitionImpl aalOrcR2 = new RaceDefinitionImpl("R2", new CourseImpl("up/down", Collections.emptyList()), aalRegatta.getBoatClass(), - Arrays.asList(new Competitor[] { hassoPlattner, dennisGehrlein })); + Arrays.asList(new Competitor[] { hassoPlattner, dennisGehrlein, philippBuhl })); server.apply(new AddRaceDefinition(aalRegatta.getRegattaIdentifier(), aalOrcR1)); server.apply(new AddRaceDefinition(aalRegatta.getRegattaIdentifier(), aalOrcR2)); // track only R1 and R2 aalOrcTrackedR1 = server.apply(new CreateTrackedRace(aalRegatta.getRaceIdentifier(aalOrcR1), EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, /* delayToLiveInMillis */ 3000, /* millisecondsOverWhichToAverageWind */ 15000, /* millisecondsOverWhichToAverageSpeed */ 15000)); + aalOrcTrackedR1.setStartOfTrackingReceived(aalStartDate); aalOrcTrackedR2 = server.apply(new CreateTrackedRace(aalRegatta.getRaceIdentifier(aalOrcR2), EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, /* delayToLiveInMillis */ 3000, /* millisecondsOverWhichToAverageWind */ 15000, /* millisecondsOverWhichToAverageSpeed */ 15000)); } @@ -160,10 +175,48 @@ public class SearchServiceTest { } @Test - public void testSimpleSearch() { + public void testSimpleSearchByCompetitorName() { Result searchResults = server.search(new KeywordQuery(Arrays.asList(new String[] { "Tobi" }))); assertEquals(1, Util.size(searchResults.getHits())); Regatta foundRegatta = searchResults.getHits().iterator().next().getRegatta(); assertSame(pfingstbusch29er, foundRegatta); } + + @Test + public void testSimpleSearchByCompetitorName2() { + Result searchResults = server.search(new KeywordQuery(Arrays.asList(new String[] { "Hasso" }))); + assertEquals(1, Util.size(searchResults.getHits())); + Regatta foundRegatta = searchResults.getHits().iterator().next().getRegatta(); + assertSame(aalRegatta, foundRegatta); + } + + @Test + public void testSimpleSearchByVenueName() { + Result searchResults = server.search(new KeywordQuery(Arrays.asList(new String[] { "Flensburg" }))); + assertEquals(1, Util.size(searchResults.getHits())); + Regatta foundRegatta = searchResults.getHits().iterator().next().getRegatta(); + assertSame(aalRegatta, foundRegatta); + } + + @Test + public void testSimpleSearchByVenueName2() { + Result searchResults = server.search(new KeywordQuery(Arrays.asList(new String[] { "Kiel" }))); + assertEquals(2, Util.size(searchResults.getHits())); + final Iterator iterator = searchResults.getHits().iterator(); + Regatta firstFoundRegatta = iterator.next().getRegatta(); + assertSame(pfingstbusch29er, firstFoundRegatta); + Regatta secondFoundRegatta = iterator.next().getRegatta(); + assertSame(pfingstbusch470, secondFoundRegatta); + } + + @Test + public void testMultipleMatchesSortedCorrectly() { + Result searchResults = server.search(new KeywordQuery(Arrays.asList(new String[] { "Buhl" }))); + assertEquals(2, Util.size(searchResults.getHits())); + final Iterator iter = searchResults.getHits().iterator(); + Regatta earlierStartRegatta = iter.next().getRegatta(); + assertSame(pfingstbusch470, earlierStartRegatta); + Regatta laterStartRegatta = iter.next().getRegatta(); + assertSame(aalRegatta, laterStartRegatta); + } } From c204bece9caaecc52c34c4972760b11112392936 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Wed, 4 Jun 2014 12:07:31 +0200 Subject: [PATCH 07/10] search service now works based on leaderboards, not regattas --- .../server/test/SearchServiceTest.java | 60 ++++++++++++--- ...sult.java => LeaderboardSearchResult.java} | 4 +- .../sailing/server/RacingEventService.java | 2 +- .../impl/LeaderboardSearchResultImpl.java | 29 +++++++ .../server/impl/RacingEventServiceImpl.java | 4 +- .../impl/RegattaByKeywordSearchService.java | 75 ++++++++----------- .../server/impl/RegattaSearchResultImpl.java | 16 ---- .../impl/RegattaSearchResultRanker.java | 12 +-- 8 files changed, 123 insertions(+), 79 deletions(-) rename java/com.sap.sailing.server/src/com/sap/sailing/server/{RegattaSearchResult.java => LeaderboardSearchResult.java} (50%) create mode 100755 java/com.sap.sailing.server/src/com/sap/sailing/server/impl/LeaderboardSearchResultImpl.java delete mode 100755 java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RegattaSearchResultImpl.java diff --git a/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/SearchServiceTest.java b/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/SearchServiceTest.java index 521d9980de5..a714ce12874 100755 --- a/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/SearchServiceTest.java +++ b/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/SearchServiceTest.java @@ -10,9 +10,11 @@ import java.util.Arrays; import java.util.Calendar; import java.util.Collections; import java.util.GregorianCalendar; +import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.UUID; import org.junit.Before; @@ -26,24 +28,31 @@ import com.sap.sailing.domain.base.Waypoint; import com.sap.sailing.domain.base.impl.CourseAreaImpl; import com.sap.sailing.domain.base.impl.CourseImpl; import com.sap.sailing.domain.base.impl.RaceDefinitionImpl; +import com.sap.sailing.domain.common.Color; import com.sap.sailing.domain.common.RegattaName; import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.common.dto.FleetDTO; import com.sap.sailing.domain.common.dto.RegattaCreationParametersDTO; import com.sap.sailing.domain.common.dto.SeriesCreationParametersDTO; import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; +import com.sap.sailing.domain.leaderboard.Leaderboard; +import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; import com.sap.sailing.domain.leaderboard.impl.LowPoint; import com.sap.sailing.domain.racelog.tracking.EmptyGPSFixStore; import com.sap.sailing.domain.test.AbstractTracTracLiveTest; import com.sap.sailing.domain.tracking.DynamicTrackedRace; import com.sap.sailing.domain.tracking.impl.EmptyWindStore; +import com.sap.sailing.server.LeaderboardSearchResult; import com.sap.sailing.server.RacingEventService; -import com.sap.sailing.server.RegattaSearchResult; import com.sap.sailing.server.impl.RacingEventServiceImpl; +import com.sap.sailing.server.operationaltransformation.AddColumnToSeries; import com.sap.sailing.server.operationaltransformation.AddRaceDefinition; import com.sap.sailing.server.operationaltransformation.AddSpecificRegatta; import com.sap.sailing.server.operationaltransformation.CreateEvent; +import com.sap.sailing.server.operationaltransformation.CreateRegattaLeaderboard; import com.sap.sailing.server.operationaltransformation.CreateTrackedRace; import com.sap.sailing.server.operationaltransformation.RemoveEvent; +import com.sap.sailing.server.operationaltransformation.RemoveLeaderboard; import com.sap.sailing.server.operationaltransformation.RemoveRegatta; import com.sap.sse.common.Util; import com.sap.sse.common.search.KeywordQuery; @@ -79,6 +88,10 @@ public class SearchServiceTest { for (Event e : allEvents) { server.apply(new RemoveEvent(e.getId())); } + Map allLeaderboards = new HashMap<>(server.getLeaderboards()); + for (String leaderboardName : allLeaderboards.keySet()) { + server.apply(new RemoveLeaderboard(leaderboardName)); + } server.apply(new RemoveRegatta(new RegattaName("Pfingstbusch (29er)"))); server.apply(new RemoveRegatta(new RegattaName("Pfingstbusch (470)"))); server.apply(new RemoveRegatta(new RegattaName("Aalregatta (ORC)"))); @@ -94,12 +107,27 @@ public class SearchServiceTest { kiel.addCourseArea(kielAlpha); final CourseAreaImpl kielBravo = new CourseAreaImpl("Bravo", UUID.randomUUID()); kiel.addCourseArea(kielBravo); + final LinkedHashMap seriesCreationParams = new LinkedHashMap(); + seriesCreationParams.put("Default", + new SeriesCreationParametersDTO(Collections.singletonList(new FleetDTO("Default", /* order */-1, Color.RED)), + /* medal */false, /* startsWithZero */false, /* firstColumnIsNonDiscardableCarryForward */false, + /* discardingThresholds */null, /* hasSplitFleetContiguousScoring */false)); pfingstbusch29er = server.apply(new AddSpecificRegatta("Pfingstbusch", "29er", UUID.randomUUID(), - new RegattaCreationParametersDTO(new LinkedHashMap()), /* persistent */ + new RegattaCreationParametersDTO(seriesCreationParams), /* persistent */ true, new LowPoint(), kielAlpha.getId())); + server.apply(new AddColumnToSeries(pfingstbusch29er.getRegattaIdentifier(), "Default", "R1")); + server.apply(new AddColumnToSeries(pfingstbusch29er.getRegattaIdentifier(), "Default", "R2")); + server.apply(new AddColumnToSeries(pfingstbusch29er.getRegattaIdentifier(), "Default", "R3")); + RegattaLeaderboard pfingstbusch29erLeaderboard = server.apply(new CreateRegattaLeaderboard(pfingstbusch29er.getRegattaIdentifier(), + /* leaderboardDisplayName */ null, /* discardThresholds */ new int[0])); pfingstbusch470 = server.apply(new AddSpecificRegatta("Pfingstbusch", "470", UUID.randomUUID(), - new RegattaCreationParametersDTO(new LinkedHashMap()), /* persistent */ + new RegattaCreationParametersDTO(seriesCreationParams), /* persistent */ true, new LowPoint(), kielBravo.getId())); + server.apply(new AddColumnToSeries(pfingstbusch470.getRegattaIdentifier(), "Default", "R1")); + server.apply(new AddColumnToSeries(pfingstbusch470.getRegattaIdentifier(), "Default", "R2")); + server.apply(new AddColumnToSeries(pfingstbusch470.getRegattaIdentifier(), "Default", "R3")); + RegattaLeaderboard pfingstbusch470Leaderboard = server.apply(new CreateRegattaLeaderboard(pfingstbusch470.getRegattaIdentifier(), + /* leaderboardDisplayName */ null, /* discardThresholds */ new int[0])); cal.set(2014, 5, 7, 10, 00); final TimePoint aalStartDate = new MillisecondsTimePoint(cal.getTime()); cal.set(2014, 5, 8, 18, 00); @@ -110,8 +138,12 @@ public class SearchServiceTest { final CourseAreaImpl flensburgStandard = new CourseAreaImpl("Standard", UUID.randomUUID()); flensburg.addCourseArea(flensburgStandard); aalRegatta = server.apply(new AddSpecificRegatta("Aalregatta", "ORC", UUID.randomUUID(), - new RegattaCreationParametersDTO(new LinkedHashMap()), /* persistent */ + new RegattaCreationParametersDTO(seriesCreationParams), /* persistent */ true, new LowPoint(), flensburgStandard.getId())); + server.apply(new AddColumnToSeries(aalRegatta.getRegattaIdentifier(), "Default", "R1")); + server.apply(new AddColumnToSeries(aalRegatta.getRegattaIdentifier(), "Default", "R2")); + RegattaLeaderboard aalRegattaLeaderboard = server.apply(new CreateRegattaLeaderboard(aalRegatta.getRegattaIdentifier(), + /* leaderboardDisplayName */ null, /* discardThresholds */ new int[0])); hassoPlattner = AbstractTracTracLiveTest.createCompetitor("Hasso Plattner"); alexanderRies = AbstractTracTracLiveTest.createCompetitor("Alexander Ries"); antonKoch = AbstractTracTracLiveTest.createCompetitor("Anton Koch"); @@ -131,8 +163,10 @@ public class SearchServiceTest { pfingstbusch29erTrackedR1 = server.apply(new CreateTrackedRace(pfingstbusch29er.getRaceIdentifier(pfingstbusch29erR1), EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, /* delayToLiveInMillis */ 3000, /* millisecondsOverWhichToAverageWind */ 15000, /* millisecondsOverWhichToAverageSpeed */ 15000)); pfingstbusch29erTrackedR1.setStartOfTrackingReceived(pfingstbuschStartDate.plus(1)); + pfingstbusch29erLeaderboard.getRaceColumnByName("R1").setTrackedRace(pfingstbusch29erLeaderboard.getRaceColumnByName("R1").getFleetByName("Default"), pfingstbusch29erTrackedR1); pfingstbusch29erTrackedR3 = server.apply(new CreateTrackedRace(pfingstbusch29er.getRaceIdentifier(pfingstbusch29erR3), EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, /* delayToLiveInMillis */ 3000, /* millisecondsOverWhichToAverageWind */ 15000, /* millisecondsOverWhichToAverageSpeed */ 15000)); + pfingstbusch29erLeaderboard.getRaceColumnByName("R3").setTrackedRace(pfingstbusch29erLeaderboard.getRaceColumnByName("R3").getFleetByName("Default"), pfingstbusch29erTrackedR3); final RaceDefinitionImpl pfingstbush470R1 = new RaceDefinitionImpl("R1", new CourseImpl("up/down", Collections.emptyList()), pfingstbusch470.getBoatClass(), Arrays.asList(new Competitor[] { philippBuhl, antonKoch })); @@ -144,8 +178,10 @@ public class SearchServiceTest { pfingstbusch470TrackedR1 = server.apply(new CreateTrackedRace(pfingstbusch470.getRaceIdentifier(pfingstbush470R1), EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, /* delayToLiveInMillis */ 3000, /* millisecondsOverWhichToAverageWind */ 15000, /* millisecondsOverWhichToAverageSpeed */ 15000)); pfingstbusch470TrackedR1.setStartOfTrackingReceived(pfingstbuschStartDate.plus(2)); // starts later than 29er + pfingstbusch470Leaderboard.getRaceColumnByName("R1").setTrackedRace(pfingstbusch470Leaderboard.getRaceColumnByName("R1").getFleetByName("Default"), pfingstbusch470TrackedR1); pfingstbusch470TrackedR2 = server.apply(new CreateTrackedRace(pfingstbusch470.getRaceIdentifier(pfingstbush470R2), EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, /* delayToLiveInMillis */ 3000, /* millisecondsOverWhichToAverageWind */ 15000, /* millisecondsOverWhichToAverageSpeed */ 15000)); + pfingstbusch470Leaderboard.getRaceColumnByName("R2").setTrackedRace(pfingstbusch470Leaderboard.getRaceColumnByName("R2").getFleetByName("Default"), pfingstbusch470TrackedR2); final RaceDefinitionImpl aalOrcR1 = new RaceDefinitionImpl("R1", new CourseImpl("up/down", Collections.emptyList()), aalRegatta.getBoatClass(), Arrays.asList(new Competitor[] { hassoPlattner, dennisGehrlein, philippBuhl })); @@ -157,8 +193,10 @@ public class SearchServiceTest { aalOrcTrackedR1 = server.apply(new CreateTrackedRace(aalRegatta.getRaceIdentifier(aalOrcR1), EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, /* delayToLiveInMillis */ 3000, /* millisecondsOverWhichToAverageWind */ 15000, /* millisecondsOverWhichToAverageSpeed */ 15000)); aalOrcTrackedR1.setStartOfTrackingReceived(aalStartDate); + aalRegattaLeaderboard.getRaceColumnByName("R1").setTrackedRace(aalRegattaLeaderboard.getRaceColumnByName("R1").getFleetByName("Default"), aalOrcTrackedR1); aalOrcTrackedR2 = server.apply(new CreateTrackedRace(aalRegatta.getRaceIdentifier(aalOrcR2), EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, /* delayToLiveInMillis */ 3000, /* millisecondsOverWhichToAverageWind */ 15000, /* millisecondsOverWhichToAverageSpeed */ 15000)); + aalRegattaLeaderboard.getRaceColumnByName("R2").setTrackedRace(aalRegattaLeaderboard.getRaceColumnByName("R2").getFleetByName("Default"), aalOrcTrackedR2); } @Test @@ -176,7 +214,7 @@ public class SearchServiceTest { @Test public void testSimpleSearchByCompetitorName() { - Result searchResults = server.search(new KeywordQuery(Arrays.asList(new String[] { "Tobi" }))); + Result searchResults = server.search(new KeywordQuery(Arrays.asList(new String[] { "Tobi" }))); assertEquals(1, Util.size(searchResults.getHits())); Regatta foundRegatta = searchResults.getHits().iterator().next().getRegatta(); assertSame(pfingstbusch29er, foundRegatta); @@ -184,7 +222,7 @@ public class SearchServiceTest { @Test public void testSimpleSearchByCompetitorName2() { - Result searchResults = server.search(new KeywordQuery(Arrays.asList(new String[] { "Hasso" }))); + Result searchResults = server.search(new KeywordQuery(Arrays.asList(new String[] { "Hasso" }))); assertEquals(1, Util.size(searchResults.getHits())); Regatta foundRegatta = searchResults.getHits().iterator().next().getRegatta(); assertSame(aalRegatta, foundRegatta); @@ -192,7 +230,7 @@ public class SearchServiceTest { @Test public void testSimpleSearchByVenueName() { - Result searchResults = server.search(new KeywordQuery(Arrays.asList(new String[] { "Flensburg" }))); + Result searchResults = server.search(new KeywordQuery(Arrays.asList(new String[] { "Flensburg" }))); assertEquals(1, Util.size(searchResults.getHits())); Regatta foundRegatta = searchResults.getHits().iterator().next().getRegatta(); assertSame(aalRegatta, foundRegatta); @@ -200,9 +238,9 @@ public class SearchServiceTest { @Test public void testSimpleSearchByVenueName2() { - Result searchResults = server.search(new KeywordQuery(Arrays.asList(new String[] { "Kiel" }))); + Result searchResults = server.search(new KeywordQuery(Arrays.asList(new String[] { "Kiel" }))); assertEquals(2, Util.size(searchResults.getHits())); - final Iterator iterator = searchResults.getHits().iterator(); + final Iterator iterator = searchResults.getHits().iterator(); Regatta firstFoundRegatta = iterator.next().getRegatta(); assertSame(pfingstbusch29er, firstFoundRegatta); Regatta secondFoundRegatta = iterator.next().getRegatta(); @@ -211,9 +249,9 @@ public class SearchServiceTest { @Test public void testMultipleMatchesSortedCorrectly() { - Result searchResults = server.search(new KeywordQuery(Arrays.asList(new String[] { "Buhl" }))); + Result searchResults = server.search(new KeywordQuery(Arrays.asList(new String[] { "Buhl" }))); assertEquals(2, Util.size(searchResults.getHits())); - final Iterator iter = searchResults.getHits().iterator(); + final Iterator iter = searchResults.getHits().iterator(); Regatta earlierStartRegatta = iter.next().getRegatta(); assertSame(pfingstbusch470, earlierStartRegatta); Regatta laterStartRegatta = iter.next().getRegatta(); diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/RegattaSearchResult.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/LeaderboardSearchResult.java similarity index 50% rename from java/com.sap.sailing.server/src/com/sap/sailing/server/RegattaSearchResult.java rename to java/com.sap.sailing.server/src/com/sap/sailing/server/LeaderboardSearchResult.java index 1c5410b0678..9ce632d1f08 100755 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/RegattaSearchResult.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/LeaderboardSearchResult.java @@ -1,8 +1,10 @@ package com.sap.sailing.server; import com.sap.sailing.domain.base.Regatta; +import com.sap.sailing.domain.leaderboard.Leaderboard; import com.sap.sse.common.search.Hit; -public interface RegattaSearchResult extends Hit { +public interface LeaderboardSearchResult extends Hit { Regatta getRegatta(); + Leaderboard getLeaderboard(); } diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventService.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventService.java index 8d83129e066..3e1c90db23e 100755 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventService.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventService.java @@ -555,5 +555,5 @@ public interface RacingEventService extends TrackedRegattaRegistry, RegattaFetch */ Util.Pair, Exception> updateRemoteServerEventCacheSynchronously(RemoteSailingServerReference ref); - Result search(KeywordQuery query); + Result search(KeywordQuery query); } diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/LeaderboardSearchResultImpl.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/LeaderboardSearchResultImpl.java new file mode 100755 index 00000000000..ad44474869d --- /dev/null +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/LeaderboardSearchResultImpl.java @@ -0,0 +1,29 @@ +package com.sap.sailing.server.impl; + +import com.sap.sailing.domain.base.Regatta; +import com.sap.sailing.domain.leaderboard.Leaderboard; +import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; +import com.sap.sailing.server.LeaderboardSearchResult; + +public class LeaderboardSearchResultImpl implements LeaderboardSearchResult { + private final Leaderboard leaderboard; + + public LeaderboardSearchResultImpl(Leaderboard leaderboard) { + this.leaderboard = leaderboard; + } + + public Regatta getRegatta() { + final Regatta regatta; + if (leaderboard instanceof RegattaLeaderboard) { + regatta = ((RegattaLeaderboard) leaderboard).getRegatta(); + } else { + regatta = null; + } + return regatta; + } + + @Override + public Leaderboard getLeaderboard() { + return leaderboard; + } +} diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java index caee176bb29..1f8f29d0932 100755 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java @@ -133,7 +133,7 @@ import com.sap.sailing.operationaltransformation.Operation; import com.sap.sailing.server.OperationExecutionListener; import com.sap.sailing.server.RacingEventService; import com.sap.sailing.server.RacingEventServiceOperation; -import com.sap.sailing.server.RegattaSearchResult; +import com.sap.sailing.server.LeaderboardSearchResult; import com.sap.sailing.server.Replicator; import com.sap.sailing.server.masterdata.DataImportLockWithProgress; import com.sap.sailing.server.operationaltransformation.AddCourseArea; @@ -2593,7 +2593,7 @@ public class RacingEventServiceImpl implements RacingEventServiceWithTestSupport } @Override - public Result search(KeywordQuery query) { + public Result search(KeywordQuery query) { return new RegattaByKeywordSearchService().search(this, query); } } diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RegattaByKeywordSearchService.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RegattaByKeywordSearchService.java index f8eae1e51f0..0cb2590eda6 100755 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RegattaByKeywordSearchService.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RegattaByKeywordSearchService.java @@ -7,12 +7,10 @@ import java.util.Set; import com.sap.sailing.domain.base.Competitor; import com.sap.sailing.domain.base.Event; -import com.sap.sailing.domain.base.Regatta; import com.sap.sailing.domain.leaderboard.Leaderboard; import com.sap.sailing.domain.leaderboard.LeaderboardGroup; -import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; +import com.sap.sailing.server.LeaderboardSearchResult; import com.sap.sailing.server.RacingEventService; -import com.sap.sailing.server.RegattaSearchResult; import com.sap.sse.common.Util; import com.sap.sse.common.filter.AbstractListFilter; import com.sap.sse.common.search.KeywordQuery; @@ -30,53 +28,46 @@ import com.sap.sse.common.search.ResultImpl; * */ public class RegattaByKeywordSearchService { - Result search(final RacingEventService racingEventService, KeywordQuery query) { - ResultImpl result = new ResultImpl<>(query, new RegattaSearchResultRanker(racingEventService)); - AbstractListFilter regattaFilter = new AbstractListFilter() { + Result search(final RacingEventService racingEventService, KeywordQuery query) { + ResultImpl result = new ResultImpl<>(query, new RegattaSearchResultRanker(racingEventService)); + AbstractListFilter leaderboardFilter = new AbstractListFilter() { @Override - public Iterable getStrings(Regatta regatta) { - List regattaStrings = new ArrayList<>(); - regattaStrings.add(regatta.getBaseName()); - regattaStrings.add(regatta.getBoatClass().getName()); - for (Competitor competitor : regatta.getCompetitors()) { - regattaStrings.add(competitor.getName()); - regattaStrings.add(competitor.getBoat().getSailID()); - } - final Iterable leaderboardGroupsHostingRegatta = getLeaderboardGroupsHostingRegatta(regatta, racingEventService); - for (LeaderboardGroup leaderboardGroup : leaderboardGroupsHostingRegatta) { - regattaStrings.add(leaderboardGroup.getName()); - regattaStrings.add(leaderboardGroup.getDescription()); - for (Leaderboard leaderboard : leaderboardGroup.getLeaderboards()) { - if (leaderboard instanceof RegattaLeaderboard && ((RegattaLeaderboard) leaderboard).getRegatta() == regatta) { - regattaStrings.add(leaderboard.getName()); - regattaStrings.add(leaderboard.getDisplayName()); - for (Competitor competitor : leaderboard.getCompetitors()) { - String competitorDisplayName = leaderboard.getDisplayName(competitor); - if (competitorDisplayName != null) { - regattaStrings.add(competitorDisplayName); - } - } - } + public Iterable getStrings(Leaderboard leaderboard) { + List leaderboardStrings = new ArrayList<>(); + leaderboardStrings.add(leaderboard.getName()); + leaderboardStrings.add(leaderboard.getDisplayName()); + for (Competitor competitor : leaderboard.getCompetitors()) { + leaderboardStrings.add(competitor.getName()); + leaderboardStrings.add(competitor.getBoat().getSailID()); + String competitorDisplayName = leaderboard.getDisplayName(competitor); + if (competitorDisplayName != null) { + leaderboardStrings.add(competitorDisplayName); } } - for (Event event : getEventsHostingRegatta(regatta, racingEventService, leaderboardGroupsHostingRegatta)) { - regattaStrings.add(event.getName()); - regattaStrings.add(event.getVenue().getName()); + final Iterable leaderboardGroupsHostingLeaderboard = getLeaderboardGroupsHostingLeaderboard( + leaderboard, racingEventService); + for (LeaderboardGroup leaderboardGroup : leaderboardGroupsHostingLeaderboard) { + leaderboardStrings.add(leaderboardGroup.getName()); + leaderboardStrings.add(leaderboardGroup.getDescription()); } - return regattaStrings; + for (Event event : getEventsHostingLeaderboard(leaderboard, racingEventService, leaderboardGroupsHostingLeaderboard)) { + leaderboardStrings.add(event.getName()); + leaderboardStrings.add(event.getVenue().getName()); + } + return leaderboardStrings; } }; - for (Regatta matchingRegatta : regattaFilter.applyFilter(query.getKeywords(), racingEventService.getAllRegattas())) { - result.addHit(new RegattaSearchResultImpl(matchingRegatta)); + for (Leaderboard matchingLeaderboard : leaderboardFilter.applyFilter(query.getKeywords(), racingEventService.getLeaderboards().values())) { + result.addHit(new LeaderboardSearchResultImpl(matchingLeaderboard)); } return result; } - private Iterable getLeaderboardGroupsHostingRegatta(Regatta regatta, RacingEventService racingEventService) { + private Iterable getLeaderboardGroupsHostingLeaderboard(Leaderboard leaderboard, RacingEventService racingEventService) { Set result = new LinkedHashSet<>(); for (LeaderboardGroup leaderboardGroup : racingEventService.getLeaderboardGroups().values()) { - for (Leaderboard leaderboard : leaderboardGroup.getLeaderboards()) { - if (leaderboard instanceof RegattaLeaderboard && ((RegattaLeaderboard) leaderboard).getRegatta() == regatta) { + for (Leaderboard lgLeaderboard : leaderboardGroup.getLeaderboards()) { + if (lgLeaderboard == leaderboard) { result.add(leaderboardGroup); } } @@ -84,14 +75,14 @@ public class RegattaByKeywordSearchService { return result; } - private Iterable getEventsHostingRegatta(Regatta regatta, RacingEventService racingEventService, - Iterable leaderboardGroupsHostingRegatta) { + private Iterable getEventsHostingLeaderboard(Leaderboard leaderboard, RacingEventService racingEventService, + Iterable leaderboardGroupsHostingLeaderboard) { Set result = new LinkedHashSet<>(); for (Event event : racingEventService.getAllEvents()) { - if (Util.contains(event.getVenue().getCourseAreas(), regatta.getDefaultCourseArea())) { + if (Util.contains(event.getVenue().getCourseAreas(), leaderboard.getDefaultCourseArea())) { result.add(event); } else { - for (LeaderboardGroup leaderboardGroupHostingRegatta : leaderboardGroupsHostingRegatta) { + for (LeaderboardGroup leaderboardGroupHostingRegatta : leaderboardGroupsHostingLeaderboard) { if (Util.contains(event.getLeaderboardGroups(), leaderboardGroupHostingRegatta)) { result.add(event); break; diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RegattaSearchResultImpl.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RegattaSearchResultImpl.java deleted file mode 100755 index f44a9584dfc..00000000000 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RegattaSearchResultImpl.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.sap.sailing.server.impl; - -import com.sap.sailing.domain.base.Regatta; -import com.sap.sailing.server.RegattaSearchResult; - -public class RegattaSearchResultImpl implements RegattaSearchResult { - private final Regatta regatta; - - public RegattaSearchResultImpl(Regatta regatta) { - this.regatta = regatta; - } - - public Regatta getRegatta() { - return regatta; - } -} diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RegattaSearchResultRanker.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RegattaSearchResultRanker.java index 7c0c1c0d603..35e2d046f09 100755 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RegattaSearchResultRanker.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RegattaSearchResultRanker.java @@ -11,10 +11,10 @@ import com.sap.sailing.domain.tracking.TrackedRace; import com.sap.sailing.domain.tracking.TrackedRegatta; import com.sap.sailing.domain.tracking.TrackedRegattaRegistry; import com.sap.sailing.server.RacingEventService; -import com.sap.sailing.server.RegattaSearchResult; +import com.sap.sailing.server.LeaderboardSearchResult; import com.sap.sse.common.Util; -public class RegattaSearchResultRanker implements Comparator { +public class RegattaSearchResultRanker implements Comparator { private final TrackedRegattaRegistry trackedRegattaRegistry; protected RegattaSearchResultRanker(RacingEventService racingEventService) { @@ -22,15 +22,15 @@ public class RegattaSearchResultRanker implements Comparator Date: Wed, 4 Jun 2014 17:34:46 +0200 Subject: [PATCH 08/10] use a smaller buffer size for RouteConverter again; the new 2.12 release doesn't reset() the BufferedInputStream anymore upon success --- .../trackfiles/impl/BaseRouteConverterGPSFixImporterImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/java/com.sap.sailing.server.trackfiles/src/com/sap/sailing/server/trackfiles/impl/BaseRouteConverterGPSFixImporterImpl.java b/java/com.sap.sailing.server.trackfiles/src/com/sap/sailing/server/trackfiles/impl/BaseRouteConverterGPSFixImporterImpl.java index 93a53cbb587..07e4c5875ae 100644 --- a/java/com.sap.sailing.server.trackfiles/src/com/sap/sailing/server/trackfiles/impl/BaseRouteConverterGPSFixImporterImpl.java +++ b/java/com.sap.sailing.server.trackfiles/src/com/sap/sailing/server/trackfiles/impl/BaseRouteConverterGPSFixImporterImpl.java @@ -62,8 +62,8 @@ public abstract class BaseRouteConverterGPSFixImporterImpl extends BaseGPSFixImp Method m = NavigationFormatParser.class.getDeclaredMethod("read", InputStream.class, Integer.TYPE, CompactCalendar.class, List.class); m.setAccessible(true); - ParserResult result = parser.read(inputStream, /* read buffer size; 1GB max size as - RouteConverter currently buffers all streams, also the successful ones... */ 1024 * 1024 * 1024, null, supportedReadFormats); + ParserResult result = parser.read(inputStream, /* read buffer size; 1MB max should be sufficient to + determine that RouteConverter cannot parse the format */ 1024 * 1024, null, supportedReadFormats); if (result == null) { throw new FormatNotSupportedException(); } From a55cb5c4d2ca5bf40ffe3ea9abd2f2bdddde6767 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Wed, 4 Jun 2014 18:29:09 +0200 Subject: [PATCH 09/10] fixed serialization issue and synchronization for GPS fix store state --- .../domain/tracking/impl/TrackedRaceImpl.java | 5751 +++++++++-------- 1 file changed, 2877 insertions(+), 2874 deletions(-) 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 1e3dfd21ca9..e9c3c2dcefd 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 @@ -1,2874 +1,2877 @@ -package com.sap.sailing.domain.tracking.impl; - -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.ConcurrentModificationException; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.IdentityHashMap; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.NavigableSet; -import java.util.Set; -import java.util.Timer; -import java.util.TimerTask; -import java.util.concurrent.Callable; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentSkipListSet; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.FutureTask; -import java.util.logging.Level; -import java.util.logging.Logger; - -import com.sap.sailing.domain.base.BearingWithConfidence; -import com.sap.sailing.domain.base.BoatClass; -import com.sap.sailing.domain.base.Competitor; -import com.sap.sailing.domain.base.Course; -import com.sap.sailing.domain.base.CourseListener; -import com.sap.sailing.domain.base.Leg; -import com.sap.sailing.domain.base.Mark; -import com.sap.sailing.domain.base.RaceDefinition; -import com.sap.sailing.domain.base.Sideline; -import com.sap.sailing.domain.base.SpeedWithBearingWithConfidence; -import com.sap.sailing.domain.base.SpeedWithConfidence; -import com.sap.sailing.domain.base.Timed; -import com.sap.sailing.domain.base.Waypoint; -import com.sap.sailing.domain.base.impl.BearingWithConfidenceImpl; -import com.sap.sailing.domain.base.impl.DouglasPeucker; -import com.sap.sailing.domain.base.impl.SpeedWithConfidenceImpl; -import com.sap.sailing.domain.common.Bearing; -import com.sap.sailing.domain.common.BearingChangeAnalyzer; -import com.sap.sailing.domain.common.CourseChange; -import com.sap.sailing.domain.common.Distance; -import com.sap.sailing.domain.common.LegType; -import com.sap.sailing.domain.common.ManeuverType; -import com.sap.sailing.domain.common.NauticalSide; -import com.sap.sailing.domain.common.NoWindError; -import com.sap.sailing.domain.common.NoWindException; -import com.sap.sailing.domain.common.Position; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sailing.domain.common.RegattaNameAndRaceName; -import com.sap.sailing.domain.common.Speed; -import com.sap.sailing.domain.common.SpeedWithBearing; -import com.sap.sailing.domain.common.Tack; -import com.sap.sailing.domain.common.TimePoint; -import com.sap.sailing.domain.common.TimingConstants; -import com.sap.sailing.domain.common.TrackedRaceStatusEnum; -import com.sap.sailing.domain.common.WindSource; -import com.sap.sailing.domain.common.WindSourceType; -import com.sap.sailing.domain.common.impl.CentralAngleDistance; -import com.sap.sailing.domain.common.impl.KnotSpeedImpl; -import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; -import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; -import com.sap.sailing.domain.common.impl.NauticalMileDistance; -import com.sap.sailing.domain.common.impl.WindSourceImpl; -import com.sap.sailing.domain.common.racelog.tracking.NoCorrespondingServiceRegisteredException; -import com.sap.sailing.domain.common.racelog.tracking.TransformationException; -import com.sap.sailing.domain.confidence.ConfidenceBasedWindAverager; -import com.sap.sailing.domain.confidence.ConfidenceFactory; -import com.sap.sailing.domain.confidence.HasConfidence; -import com.sap.sailing.domain.confidence.Weigher; -import com.sap.sailing.domain.confidence.impl.HyperbolicTimeDifferenceWeigher; -import com.sap.sailing.domain.confidence.impl.PositionAndTimePointWeigher; -import com.sap.sailing.domain.racelog.RaceLog; -import com.sap.sailing.domain.racelog.analyzing.impl.StartTimeFinder; -import com.sap.sailing.domain.racelog.tracking.GPSFixStore; -import com.sap.sailing.domain.tracking.DynamicGPSFixTrack; -import com.sap.sailing.domain.tracking.GPSFix; -import com.sap.sailing.domain.tracking.GPSFixMoving; -import com.sap.sailing.domain.tracking.GPSFixTrack; -import com.sap.sailing.domain.tracking.GPSTrackListener; -import com.sap.sailing.domain.tracking.LineDetails; -import com.sap.sailing.domain.tracking.Maneuver; -import com.sap.sailing.domain.tracking.MarkPassing; -import com.sap.sailing.domain.tracking.Track; -import com.sap.sailing.domain.tracking.TrackedLeg; -import com.sap.sailing.domain.tracking.TrackedLegOfCompetitor; -import com.sap.sailing.domain.tracking.TrackedRaceStatus; -import com.sap.sailing.domain.tracking.TrackedRaceWithWindEssentials; -import com.sap.sailing.domain.tracking.TrackedRegatta; -import com.sap.sailing.domain.tracking.Wind; -import com.sap.sailing.domain.tracking.WindStore; -import com.sap.sailing.domain.tracking.WindTrack; -import com.sap.sailing.domain.tracking.WindWithConfidence; -import com.sap.sailing.util.SmartFutureCache; -import com.sap.sailing.util.SmartFutureCache.AbstractCacheUpdater; -import com.sap.sailing.util.SmartFutureCache.EmptyUpdateInterval; -import com.sap.sailing.util.impl.ArrayListNavigableSet; -import com.sap.sailing.util.impl.LockUtil; -import com.sap.sailing.util.impl.NamedReentrantReadWriteLock; -import com.sap.sse.common.Util; - -public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials implements CourseListener { - private static final long serialVersionUID = -4825546964220003507L; - - private static final Logger logger = Logger.getLogger(TrackedRaceImpl.class.getName()); - - // TODO make this variable - private static final long DELAY_FOR_CACHE_CLEARING_IN_MILLISECONDS = 7500; - - public static final long TIME_BEFORE_START_TO_TRACK_WIND_MILLIS = 4 * 60 * 1000l; // let wind start four minutes before race - - private TrackedRaceStatus status; - - private final Object statusNotifier; - - /** - * By default, all wind sources are used, none are excluded. However, e.g., for performance reasons, particular wind - * sources such as the track-based estimation wind source, may be excluded by adding them to this set. - */ - private final Set windSourcesToExclude; - - /** - * Keeps the oldest timestamp that is fed into this tracked race, either from a boat fix, a mark fix, a race - * start/finish or a course definition. - */ - private TimePoint timePointOfOldestEvent; - - /** - * The start of tracking time as announced by the tracking infrastructure. - */ - private TimePoint startOfTrackingReceived; - - /** - * The end of tracking time as announced by the tracking infrastructure. - */ - private TimePoint endOfTrackingReceived; - - /** - * Race start time as announced by the tracking infrastructure - */ - private TimePoint startTimeReceived; - - /** - * The calculated race start time - */ - private TimePoint startTime; - - /** - * The calculated race end time - */ - private TimePoint endTime; - - /** - * The first and last passing times of all course waypoints - */ - private transient List>> markPassingsTimes; - - /** - * The latest time point contained by any of the events received and processed - */ - private TimePoint timePointOfNewestEvent; - - /** - * Time stamp that the event received last from the underlying push service carried on it - */ - private TimePoint timePointOfLastEvent; - - private long updateCount; - - private transient Map> competitorRankings; - - /** - * The locks managed here correspond with the {@link #competitorRankings} structure. When - * {@link #getCompetitorsFromBestToWorst(TimePoint)} starts to compute rankings, it locks the write lock for the - * time point. Readers use the read lock. Checking / entering a lock into this map uses synchronized on - * the map itself. - */ - private transient Map competitorRankingsLocks; - - /** - * legs appear in the order in which they appear in the race's course - */ - private final LinkedHashMap trackedLegs; - - private final Map> tracks; - - private final Map> markPassingsForCompetitor; - - /** - * The mark passing sets used as values are ordered by time stamp. - */ - private final Map> markPassingsForWaypoint; - - /** - * Values are the from and to time points between which the maneuvers have been previously - * computed. Clients wanting to know maneuvers for the competitor outside of this time interval need to (re-)compute - * them. - */ - private transient SmartFutureCache>, EmptyUpdateInterval> maneuverCache; - - private transient Map> directionFromStartToNextMarkCache; - - private final ConcurrentHashMap> markTracks; - - private final Map courseSidelines; - - protected long millisecondsOverWhichToAverageSpeed; - - private final Map startToNextMarkCacheInvalidationListeners; - - private transient Timer cacheInvalidationTimer; - private transient Object cacheInvalidationTimerLock; - - protected transient HashMap attachedRaceLogs; - - /** - * The time delay to the current point in time in milliseconds. - */ - private long delayToLiveInMillis; - - private enum LoadingFromStoresState { NOT_STARTED, RUNNING, FINISHED }; - - /** - * The constructor loads wind fixes from the {@link #windStore} asynchronously. - * When completed all threads currently waiting on this object are notified. - */ - private LoadingFromStoresState loadingFromWindStoreState = LoadingFromStoresState.NOT_STARTED; - - /** - * @see #loadingFromWindStoreState but for GPSFixStore - */ - private LoadingFromStoresState loadingFromGPSFixStoreState = LoadingFromStoresState.NOT_STARTED; - - private transient CrossTrackErrorCache crossTrackErrorCache; - - private final GPSFixStore gpsFixStore; - - /** - * Wind and loading is started in a background thread during object construction. If a client needs to - * ensure that wind loading either has terminated or has not yet begun, it can obtain the read lock of - * this lock. The wind loading procedure will obtain the write lock before it starts loading wind fixes. - */ - private final NamedReentrantReadWriteLock loadingFromWindStoreLock; - - /** - * @see #loadingFromWindStoreLock but for GPSFixStore - */ - private final NamedReentrantReadWriteLock loadingFromGPSFixStoreLock; - - private final Map, NamedReentrantReadWriteLock> locksForMarkPassings; - - public TrackedRaceImpl(final TrackedRegatta trackedRegatta, RaceDefinition race, final Iterable sidelines, final WindStore windStore, final GPSFixStore gpsFixStore, - long delayToLiveInMillis, final long millisecondsOverWhichToAverageWind, - long millisecondsOverWhichToAverageSpeed, long delayForWindEstimationCacheInvalidation) { - super(race, trackedRegatta, windStore, millisecondsOverWhichToAverageWind); - locksForMarkPassings = new IdentityHashMap<>(); - attachedRaceLogs = new HashMap<>(); - this.status = new TrackedRaceStatusImpl(TrackedRaceStatusEnum.PREPARED, 0.0); - this.statusNotifier = new Object[0]; - this.loadingFromWindStoreLock = new NamedReentrantReadWriteLock("Loading from wind store lock for tracked race " - + race.getName(), /* fair */ false); - this.loadingFromGPSFixStoreLock = new NamedReentrantReadWriteLock("Loading from GPSFix store lock for tracked race " - + race.getName(), /* fair */ false); - this.cacheInvalidationTimerLock = new Object(); - this.updateCount = 0; - this.windSourcesToExclude = new HashSet(); - this.directionFromStartToNextMarkCache = new HashMap>(); - this.millisecondsOverWhichToAverageSpeed = millisecondsOverWhichToAverageSpeed; - this.delayToLiveInMillis = delayToLiveInMillis; - this.startToNextMarkCacheInvalidationListeners = new ConcurrentHashMap(); - this.maneuverCache = createManeuverCache(); - this.markTracks = new ConcurrentHashMap>(); - this.crossTrackErrorCache = new CrossTrackErrorCache(this); - this.gpsFixStore = gpsFixStore; - int i = 0; - for (Waypoint waypoint : race.getCourse().getWaypoints()) { - for (Mark mark : waypoint.getMarks()) { - getOrCreateTrack(mark); - if (i < 2) { - // add cache invalidation listeners for first and second waypoint's marks for - // directionFromStartToNextMarkCache - addStartToNextMarkCacheInvalidationListener(mark); - } - } - i++; - } - courseSidelines = new LinkedHashMap(); - for (Sideline sideline : sidelines) { - courseSidelines.put(sideline.getName(), sideline); - for (Mark mark : sideline.getMarks()) { - getOrCreateTrack(mark); - } - } - - trackedLegs = new LinkedHashMap(); - race.getCourse().lockForRead(); - try { - for (Leg leg : race.getCourse().getLegs()) { - trackedLegs.put(leg, createTrackedLeg(leg)); - } - getRace().getCourse().addCourseListener(this); - } finally { - race.getCourse().unlockAfterRead(); - } - markPassingsForCompetitor = new HashMap>(); - tracks = new HashMap>(); - for (Competitor competitor : race.getCompetitors()) { - markPassingsForCompetitor.put(competitor, new ConcurrentSkipListSet( - MarkPassingByTimeComparator.INSTANCE)); - tracks.put(competitor, new DynamicGPSFixMovingTrackImpl(competitor, - millisecondsOverWhichToAverageSpeed)); - } - markPassingsForWaypoint = new ConcurrentHashMap>(); - for (Waypoint waypoint : race.getCourse().getWaypoints()) { - markPassingsForWaypoint.put(waypoint, new ConcurrentSkipListSet( - MarkPassingByTimeComparator.INSTANCE)); - } - markPassingsTimes = new ArrayList>>(); - loadingFromWindStoreState = LoadingFromStoresState.NOT_STARTED; - // When this tracked race is to be serialized, wait for the loading from stores to complete. - new Thread("Mongo wind loader for tracked race " + getRace().getName()) { - @Override - public void run() { - LockUtil.lockForRead(getSerializationLock()); - LockUtil.lockForWrite(getLoadingFromWindStoreLock()); - synchronized (TrackedRaceImpl.this) { - loadingFromWindStoreState = LoadingFromStoresState.RUNNING; // indicates that the serialization lock is now safely held - TrackedRaceImpl.this.notifyAll(); - } - try { - logger.info("Started loading wind tracks for " + getRace().getName()); - final Map loadedWindTracks = windStore.loadWindTracks( - trackedRegatta.getRegatta().getName(), TrackedRaceImpl.this, millisecondsOverWhichToAverageWind); - windTracks.putAll(loadedWindTracks); - updateEventTimePoints(loadedWindTracks.values()); - logger.info("Finished loading wind tracks for " + getRace().getName() + "! Found " + windTracks.size() + " wind tracks for this race!"); - - } finally { - synchronized (TrackedRaceImpl.this) { - loadingFromWindStoreState = LoadingFromStoresState.FINISHED; - TrackedRaceImpl.this.notifyAll(); - } - synchronized (loadingFromWindStoreState) { - loadingFromWindStoreState.notifyAll(); - } - LockUtil.unlockAfterWrite(getLoadingFromWindStoreLock()); - LockUtil.unlockAfterRead(getSerializationLock()); - } - } - }.start(); - // by default, a tracked race offers one course-based wind estimation and one track-based wind estimation track; - // other wind tracks may be added as fixes are received for them and as they are loaded from the persistent - // store - WindSource courseBasedWindSource = new WindSourceImpl(WindSourceType.COURSE_BASED); - windTracks.put(courseBasedWindSource, - getOrCreateWindTrack(courseBasedWindSource, delayForWindEstimationCacheInvalidation)); - WindSource trackBasedWindSource = new WindSourceImpl(WindSourceType.TRACK_BASED_ESTIMATION); - windTracks.put(trackBasedWindSource, - getOrCreateWindTrack(trackBasedWindSource, delayForWindEstimationCacheInvalidation)); - competitorRankings = new HashMap>(); - competitorRankingsLocks = new HashMap(); - // now wait until wind loading has at least started; then we know that the serialization lock is safely held by the loader - try { - waitUntilLoadingFromWindStoreComplete(); - } catch (InterruptedException e) { - logger.log(Level.SEVERE, "Waiting for loading from stores to finish was interrupted", e); - } - } - - /** - * Assuming that the tracks were loaded from the persistent store, this method updates - * the time stamps that frame the data held by this tracked race. See {@link #timePointOfLastEvent}, {@link #timePointOfNewestEvent} - * and {@link #timePointOfOldestEvent}. - */ - private void updateEventTimePoints(Iterable> tracks) { - for (Track track : tracks) { - track.lockForRead(); - - try { - for (Timed fix : track.getRawFixes()) { - updated(fix.getTimePoint()); - } - } finally { - track.unlockAfterRead(); - } - } - } - - /** - * Object serialization obtains a read lock for the course so that in cannot change while serializing this object. - */ - private void writeObject(ObjectOutputStream s) throws IOException { - LockUtil.lockForWrite(getSerializationLock()); - try { - s.defaultWriteObject(); - } finally { - LockUtil.unlockAfterWrite(getSerializationLock()); - } - } - - /** - * Deserialization has to be maintained in lock-step with {@link #writeObject(ObjectOutputStream) serialization}. - * When de-serializing, a possibly remote {@link #windStore} is ignored because it is transient. Instead, an - * {@link EmptyWindStore} is used for the de-serialized instance. - */ - private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { - ois.defaultReadObject(); - attachedRaceLogs = new HashMap<>(); - markPassingsTimes = new ArrayList>>(); - cacheInvalidationTimerLock = new Object(); - windStore = EmptyWindStore.INSTANCE; - competitorRankings = new HashMap>(); - competitorRankingsLocks = new HashMap(); - directionFromStartToNextMarkCache = new HashMap>(); - crossTrackErrorCache = new CrossTrackErrorCache(this); - maneuverCache = createManeuverCache(); - logger.info("Deserialized race " + getRace().getName()); - } - - @Override - public synchronized void waitUntilLoadingFromWindStoreComplete() throws InterruptedException { - while (loadingFromWindStoreState != LoadingFromStoresState.FINISHED) { - wait(); - } - } - - @Override - public synchronized void waitUntilLoadingFromGPSFixStoreComplete() throws InterruptedException { - while (loadingFromGPSFixStoreState != LoadingFromStoresState.FINISHED) { - wait(); - } - } - - private SmartFutureCache>, EmptyUpdateInterval> createManeuverCache() { - return new SmartFutureCache>, EmptyUpdateInterval>( - new AbstractCacheUpdater>, EmptyUpdateInterval>() { - @Override - public com.sap.sse.common.Util.Triple> computeCacheUpdate(Competitor competitor, - EmptyUpdateInterval updateInterval) throws NoWindException { - return computeManeuvers(competitor); - } - }, /* nameForLocks */"Maneuver cache for race " + getRace().getName()); - } - - /** - * Precondition: race has already been set, e.g., in constructor before this method is called - */ - abstract protected TrackedLeg createTrackedLeg(Leg leg); - - public RegattaAndRaceIdentifier getRaceIdentifier() { - return new RegattaNameAndRaceName(getTrackedRegatta().getRegatta().getName(), getRace().getName()); - } - - @Override - public NavigableSet getMarkPassings(Competitor competitor) { - return markPassingsForCompetitor.get(competitor); - } - - protected NavigableSet getMarkPassingsInOrderAsNavigableSet(Waypoint waypoint) { - return markPassingsForWaypoint.get(waypoint); - } - - @Override - public WindStore getWindStore() { - return windStore; - } - - @Override - public Iterable getMarkPassingsInOrder(Waypoint waypoint) { - return getMarkPassingsInOrderAsNavigableSet(waypoint); - } - - protected NavigableSet getOrCreateMarkPassingsInOrderAsNavigableSet(Waypoint waypoint) { - NavigableSet result = getMarkPassingsInOrderAsNavigableSet(waypoint); - if (result == null) { - result = createMarkPassingsCollectionForWaypoint(waypoint); - } - return result; - } - - protected NavigableSet createMarkPassingsCollectionForWaypoint(Waypoint waypoint) { - final ConcurrentSkipListSet result = new ConcurrentSkipListSet( - MarkPassingByTimeComparator.INSTANCE); - LockUtil.lockForRead(getSerializationLock()); - try { - markPassingsForWaypoint.put(waypoint, result); - } finally { - LockUtil.unlockAfterRead(getSerializationLock()); - } - return result; - } - - @Override - public TimePoint getStartOfTracking() { - return startOfTrackingReceived; - } - - @Override - public TimePoint getEndOfTracking() { - return endOfTrackingReceived; - } - - public void invalidateStartTime() { - startTime = null; - } - - public void invalidateEndTime() { - endTime = null; - } - - protected void invalidateMarkPassingTimes() { - synchronized (markPassingsTimes) { - markPassingsTimes.clear(); - } - } - - /** - * Calculates the start time of the race from various sources. The highest precedence take the {@link #attachedRaceLogs race logs}, - * followed by the field {@link #startTimeReceived} which can explicitly be set using {@link #setStartTimeReceived(TimePoint)}. - * If that does not provide any start time either, a start time is attempted to be inferred from the time points - * of the start mark passing events. - */ - @Override - public TimePoint getStartOfRace() { - if (startTime == null) { - for (RaceLog raceLog : attachedRaceLogs.values()) { - startTime = new StartTimeFinder(raceLog).analyze(); - if (startTime != null) { - break; - } - } - if (startTime == null) { - startTime = getStartTimeReceived(); - // If not null, check if the first mark passing for the start line is too much after the - // startTimeReceived; if so, return an adjusted, later start time. - // If no official start time was received, try to estimate the start time using the mark passings for - // the start line. - final Waypoint firstWaypoint = getRace().getCourse().getFirstWaypoint(); - if (firstWaypoint != null) { - if (startTimeReceived != null) { - TimePoint timeOfFirstMarkPassing = getFirstPassingTime(firstWaypoint); - if (timeOfFirstMarkPassing != null) { - long startTimeReceived2timeOfFirstMarkPassingFirstMark = timeOfFirstMarkPassing.asMillis() - - startTimeReceived.asMillis(); - if (startTimeReceived2timeOfFirstMarkPassingFirstMark > MAX_TIME_BETWEEN_START_AND_FIRST_MARK_PASSING_IN_MILLISECONDS) { - startTime = new MillisecondsTimePoint(timeOfFirstMarkPassing.asMillis() - - MAX_TIME_BETWEEN_START_AND_FIRST_MARK_PASSING_IN_MILLISECONDS); - } else { - startTime = startTimeReceived; - } - } - } else { - final NavigableSet markPassingsForFirstWaypointInOrder = getMarkPassingsInOrderAsNavigableSet(firstWaypoint); - if (markPassingsForFirstWaypointInOrder != null) { - startTime = calculateStartOfRaceFromMarkPassings(markPassingsForFirstWaypointInOrder, - getRace().getCompetitors()); - } - } - } - } - } - return startTime; - } - - /** - * Calculates the end time of the race from the mark passings of the last course waypoint - */ - @Override - public TimePoint getEndOfRace() { - if (endTime == null) { - endTime = getLastPassingOfFinishLine(); - } - return endTime; - } - - private TimePoint getLastPassingOfFinishLine() { - TimePoint passingTime = null; - final Waypoint lastWaypoint = getRace().getCourse().getLastWaypoint(); - if (lastWaypoint != null) { - Iterable markPassingsInOrder = getMarkPassingsInOrder(lastWaypoint); - if (markPassingsInOrder != null) { - lockForRead(markPassingsInOrder); - try { - for (MarkPassing passingFinishLine : markPassingsInOrder) { - passingTime = passingFinishLine.getTimePoint(); - } - } finally { - unlockAfterRead(markPassingsInOrder); - } - } - } - return passingTime; - } - - private TimePoint getFirstPassingTime(Waypoint waypoint) { - NavigableSet markPassingsInOrder = getMarkPassingsInOrderAsNavigableSet(waypoint); - MarkPassing firstMarkPassing = null; - if (markPassingsInOrder != null) { - lockForRead(markPassingsInOrder); - try { - if (!markPassingsInOrder.isEmpty()) { - firstMarkPassing = markPassingsInOrder.first(); - } - } finally { - unlockAfterRead(markPassingsInOrder); - } - } - TimePoint timeOfFirstMarkPassing = null; - if (firstMarkPassing != null) { - timeOfFirstMarkPassing = firstMarkPassing.getTimePoint(); - } - return timeOfFirstMarkPassing; - } - - private TimePoint calculateStartOfRaceFromMarkPassings(NavigableSet markPassings, - Iterable competitors) { - TimePoint startOfRace = null; - // Find the first mark passing within the largest cluster crossing the line within one minute. - final long ONE_MINUTE_IN_MILLIS = 60 * 1000; - lockForRead(markPassings); - try { - if (markPassings != null) { - int largestStartGroupWithinOneMinuteSize = 0; - MarkPassing startOfLargestGroupSoFar = null; - int candiateGroupSize = 0; - MarkPassing candidateForStartOfLargestGroupSoFar = null; - Iterator iterator = markPassings.iterator(); - // sweep over all start mark passings and for each element find the number of competitors that passed - // the start up to one minute later; - // pick the start mark passing of the competitor leading the largest such group - while (iterator.hasNext()) { - MarkPassing currentMarkPassing = iterator.next(); - if (candidateForStartOfLargestGroupSoFar == null) { - // first start mark passing - candidateForStartOfLargestGroupSoFar = currentMarkPassing; - candiateGroupSize = 1; - startOfLargestGroupSoFar = currentMarkPassing; - largestStartGroupWithinOneMinuteSize = 1; - } else { - if (currentMarkPassing.getTimePoint().asMillis() - - candidateForStartOfLargestGroupSoFar.getTimePoint().asMillis() <= ONE_MINUTE_IN_MILLIS) { - // currentMarkPassing is within one minute of candidateForStartOfLargestGroupSoFar; extend - // candidate group... - candiateGroupSize++; - if (candiateGroupSize > largestStartGroupWithinOneMinuteSize) { - // ...and remember as best fit if greater than largest group so far - startOfLargestGroupSoFar = candidateForStartOfLargestGroupSoFar; - largestStartGroupWithinOneMinuteSize = candiateGroupSize; - } - } else { - // currentMarkPassing is more than a minute after candidateForStartOfLargestGroupSoFar; - // advance - // candidateForStartOfLargestGroupSoFar and reduce group size counter, until - // candidateForStartOfLargestGroupSoFar - // is again within the one-minute interval; may catch up all the way to currentMarkPassing - // if that was - // more than a minute after its predecessor - while (currentMarkPassing.getTimePoint().asMillis() - - candidateForStartOfLargestGroupSoFar.getTimePoint().asMillis() > ONE_MINUTE_IN_MILLIS) { - candidateForStartOfLargestGroupSoFar = markPassings - .higher(candidateForStartOfLargestGroupSoFar); - candiateGroupSize--; - } - } - } - } - startOfRace = startOfLargestGroupSoFar == null ? null : startOfLargestGroupSoFar.getTimePoint(); - } - } finally { - unlockAfterRead(markPassings); - } - return startOfRace; - } - - @Override - public boolean hasStarted(TimePoint at) { - return getStartOfRace() != null && getStartOfRace().compareTo(at) <= 0; - } - - @Override - public boolean isLive(TimePoint at) { - final Date startOfLivePeriod; - final Date endOfLivePeriod; - if (!hasGPSData() || !hasWindData()) { - startOfLivePeriod = null; - endOfLivePeriod = null; - } else { - if (getStartOfRace() == null) { - startOfLivePeriod = getStartOfTracking().asDate(); - } else { - startOfLivePeriod = new Date(getStartOfRace().asMillis() - TimingConstants.PRE_START_PHASE_DURATION_IN_MILLIS); - } - if (getEndOfRace() == null) { - if (getTimePointOfNewestEvent() != null) { - endOfLivePeriod = new Date(getTimePointOfNewestEvent().asMillis() - + TimingConstants.IS_LIVE_GRACE_PERIOD_IN_MILLIS); - } else { - endOfLivePeriod = null; - } - } else { - endOfLivePeriod = new Date(getEndOfRace().asMillis() + TimingConstants.IS_LIVE_GRACE_PERIOD_IN_MILLIS); - } - } - - // if an empty timepoint is given then take the start of the race - if (at == null) { - at = new MillisecondsTimePoint(startOfLivePeriod.getTime()+1); - } - - // whenLastTrackedRaceWasLive is null if there is no tracked race for fleet, or the tracked race hasn't started yet at the server time - // when this DTO was assembled, or there were no GPS or wind data - final boolean result = - startOfLivePeriod != null && - endOfLivePeriod != null && - startOfLivePeriod.getTime() <= at.asMillis() && - at.asMillis() <= endOfLivePeriod.getTime(); - return result; - } - - @Override - public RaceDefinition getRace() { - return race; - } - - @Override - public Iterable getTrackedLegs() { - // ensure that no course modification is carried out while copying the tracked legs - getRace().getCourse().lockForRead(); - try { - return new ArrayList(trackedLegs.values()); - } finally { - getRace().getCourse().unlockAfterRead(); - } - } - - @Override - public Iterable>> getMarkPassingsTimes() { - getRace().getCourse().lockForRead(); // ensure the list of waypoints doesn't change while we're updating the - // markPassingTimes structure - try { - synchronized (markPassingsTimes) { - if (markPassingsTimes.isEmpty()) { - // Remark: sometimes it can happen that a mark passing with a wrong time stamp breaks the right time - // order of the waypoint times - Date previousLegPassingTime = null; - for (Waypoint waypoint : getRace().getCourse().getWaypoints()) { - TimePoint firstPassingTime = null; - TimePoint lastPassingTime = null; - NavigableSet markPassings = getMarkPassingsInOrderAsNavigableSet(waypoint); - if (markPassings != null && !markPassings.isEmpty()) { - // ensure the leg times are in the right time order; there may perhaps be left-overs for - // marks to be reached later that - // claim it has been passed in the past which may have been an accidental tracker - // read-out; - // the results of getMarkPassingsInOrder(to) has by definition an ascending time-point - // ordering - lockForRead(markPassings); - try { - for (MarkPassing currentMarkPassing : markPassings) { - Date currentPassingDate = currentMarkPassing.getTimePoint().asDate(); - if (previousLegPassingTime == null - || currentPassingDate.after(previousLegPassingTime)) { - firstPassingTime = currentMarkPassing.getTimePoint(); - previousLegPassingTime = currentPassingDate; - break; - } - } - } finally { - unlockAfterRead(markPassings); - } - } - com.sap.sse.common.Util.Pair timesPair = new com.sap.sse.common.Util.Pair(firstPassingTime, - lastPassingTime); - markPassingsTimes.add(new com.sap.sse.common.Util.Pair>(waypoint, timesPair)); - } - } - return markPassingsTimes; - } - } finally { - getRace().getCourse().unlockAfterRead(); - } - } - - @Override - public Distance getDistanceTraveled(Competitor competitor, TimePoint timePoint) { - final Distance result; - NavigableSet markPassings = getMarkPassings(competitor); - try { - lockForRead(markPassings); - if (markPassings.isEmpty()) { - result = null; - } else { - TimePoint end = timePoint; - if (markPassings.last().getWaypoint() == getRace().getCourse().getLastWaypoint() - && timePoint.compareTo(markPassings.last().getTimePoint()) > 0) { - // competitor has finished race; use time point of crossing the finish line - end = markPassings.last().getTimePoint(); - } else if (markPassings.last().getWaypoint() != getRace().getCourse().getLastWaypoint() - && ((getEndOfTracking() != null && timePoint.after(getEndOfTracking())) - || getStatus().getStatus() == TrackedRaceStatusEnum.FINISHED)) { - // If the race is no longer tracking and hence no more data can be expected, and the competitor - // hasn't finished the race, no valid distance traveled can be determined - // for the competitor in this race. - end = null; - } - if (end == null) { - result = null; - } else { - result = getTrack(competitor).getDistanceTraveled(markPassings.first().getTimePoint(), end); - } - } - return result; - } finally { - unlockAfterRead(markPassings); - } - } - - @Override - public GPSFixTrack getTrack(Competitor competitor) { - return tracks.get(competitor); - } - - @Override - public TrackedLeg getTrackedLegFinishingAt(Waypoint endOfLeg) { - getRace().getCourse().lockForRead(); - try { - int indexOfWaypoint = getRace().getCourse().getIndexOfWaypoint(endOfLeg); - if (indexOfWaypoint == -1) { - throw new IllegalArgumentException("Waypoint " + endOfLeg + " not found in " + getRace().getCourse()); - } else if (indexOfWaypoint == 0) { - throw new IllegalArgumentException("Waypoint " + endOfLeg + " isn't start of any leg in " - + getRace().getCourse()); - } - return trackedLegs.get(race.getCourse().getLegs().get(indexOfWaypoint - 1)); - } finally { - getRace().getCourse().unlockAfterRead(); - } - } - - @Override - public TrackedLeg getTrackedLegStartingAt(Waypoint startOfLeg) { - getRace().getCourse().lockForRead(); - try { - int indexOfWaypoint = getRace().getCourse().getIndexOfWaypoint(startOfLeg); - if (indexOfWaypoint == -1) { - throw new IllegalArgumentException("Waypoint " + startOfLeg + " not found in " + getRace().getCourse()); - } else if (indexOfWaypoint == Util.size(getRace().getCourse().getWaypoints()) - 1) { - throw new IllegalArgumentException("Waypoint " + startOfLeg + " isn't start of any leg in " - + getRace().getCourse()); - } - return trackedLegs.get(race.getCourse().getLegs().get(indexOfWaypoint)); - } finally { - getRace().getCourse().unlockAfterRead(); - } - } - - @Override - public TrackedLegOfCompetitor getTrackedLeg(Competitor competitor, TimePoint at) { - NavigableSet roundings = getMarkPassings(competitor); - lockForRead(roundings); - try { - NavigableSet localRoundings = new ArrayListNavigableSet<>(roundings.size(), - new TimedComparator()); - localRoundings.addAll(roundings); - } finally { - unlockAfterRead(roundings); - } - TrackedLegOfCompetitor result = null; - if (roundings != null) { - TrackedLeg trackedLeg; - // obtain last waypoint before obtaining mark passings monitor because obtaining the last waypoint - // obtains the read lock for the course - final Waypoint lastWaypoint = getRace().getCourse().getLastWaypoint(); - MarkPassing lastBeforeOrAt = roundings.floor(new DummyMarkPassingWithTimePointOnly(at)); - // already finished the race? - if (lastBeforeOrAt != null) { - // and not at or after last mark passing - if (lastWaypoint != lastBeforeOrAt.getWaypoint()) { - trackedLeg = getTrackedLegStartingAt(lastBeforeOrAt.getWaypoint()); - } else { - // exactly *at* last mark passing? - if (!roundings.isEmpty() && at.equals(roundings.last().getTimePoint())) { - // exactly at finish line; return last leg - trackedLeg = getTrackedLegFinishingAt(lastBeforeOrAt.getWaypoint()); - } else { - // no, then we're after the last mark passing - trackedLeg = null; - } - } - } else { - // before beginning of race - trackedLeg = null; - } - if (trackedLeg != null) { - result = trackedLeg.getTrackedLeg(competitor); - } - } - return result; - } - - public TrackedLeg getTrackedLeg(Leg leg) { - getRace().getCourse().lockForRead(); - try { - return trackedLegs.get(leg); - } finally { - getRace().getCourse().unlockAfterRead(); - } - } - - @Override - public TrackedLegOfCompetitor getTrackedLeg(Competitor competitor, Leg leg) { - final TrackedLeg trackedLeg = getTrackedLeg(leg); - return trackedLeg == null ? null : trackedLeg.getTrackedLeg(competitor); - } - - @Override - public long getUpdateCount() { - return updateCount; - } - - @Override - public int getRankDifference(Competitor competitor, Leg leg, TimePoint timePoint) { - int previousRank; - if (leg == getRace().getCourse().getFirstLeg()) { - // first leg; report rank difference from 0 - previousRank = 0; - } else { - TrackedLeg previousLeg = getTrackedLegFinishingAt(leg.getFrom()); - previousRank = previousLeg.getTrackedLeg(competitor).getRank(timePoint); - } - int currentRank = getTrackedLeg(competitor, leg).getRank(timePoint); - return currentRank - previousRank; - } - - @Override - public int getRank(Competitor competitor) throws NoWindException { - return getRank(competitor, MillisecondsTimePoint.now()); - } - - @Override - public Competitor getOverallLeader(TimePoint timePoint) throws NoWindException { - try { - Competitor result = null; - List ranks = getCompetitorsFromBestToWorst(timePoint); - if (ranks != null && !ranks.isEmpty()) { - result = ranks.iterator().next(); - } - return result; - } catch (NoWindError e) { - throw e.getCause(); - } - } - - @Override - public int getRank(Competitor competitor, TimePoint timePoint) throws NoWindException { - try { - int result; - if (getMarkPassings(competitor).isEmpty()) { - result = 0; - } else { - result = getCompetitorsFromBestToWorst(timePoint).indexOf(competitor) + 1; - } - return result; - } catch (NoWindError e) { - throw e.getCause(); - } - } - - @Override - public List getCompetitorsFromBestToWorst(TimePoint timePoint) throws NoWindException { - NamedReentrantReadWriteLock readWriteLock; - synchronized (competitorRankingsLocks) { - readWriteLock = competitorRankingsLocks.get(timePoint); - if (readWriteLock == null) { - readWriteLock = new NamedReentrantReadWriteLock("competitor rankings for race " + getRace().getName() - + " for time point " + timePoint, /* fair */false); - competitorRankingsLocks.put(timePoint, readWriteLock); - } - } - List rankedCompetitors; - final boolean lockForWrite; - synchronized (competitorRankings) { - rankedCompetitors = competitorRankings.get(timePoint); - if (rankedCompetitors == null) { - lockForWrite = true; - } else { - lockForWrite = false; - } - } - if (lockForWrite) { - LockUtil.lockForWrite(readWriteLock); - } else { - LockUtil.lockForRead(readWriteLock); - } - try { - if (rankedCompetitors == null) { - rankedCompetitors = competitorRankings.get(timePoint); // try again; maybe a writer released the write - // lock after updating the cache - if (rankedCompetitors == null) { - RaceRankComparator comparator = new RaceRankComparator(this, timePoint); - rankedCompetitors = new ArrayList(); - for (Competitor c : getRace().getCompetitors()) { - rankedCompetitors.add(c); - } - Collections.sort(rankedCompetitors, comparator); - synchronized (competitorRankings) { - competitorRankings.put(timePoint, rankedCompetitors); - } - } - } - return rankedCompetitors; - } finally { - if (lockForWrite) { - LockUtil.unlockAfterWrite(readWriteLock); - } else { - LockUtil.unlockAfterRead(readWriteLock); - } - } - } - - @Override - public Distance getAverageAbsoluteCrossTrackError(Competitor competitor, TimePoint timePoint, boolean waitForLatestAnalysis) - throws NoWindException { - NavigableSet markPassings = getMarkPassings(competitor); - TimePoint from = null; - lockForRead(markPassings); - try { - if (markPassings != null && !markPassings.isEmpty()) { - from = markPassings.iterator().next().getTimePoint(); - } - } finally { - unlockAfterRead(markPassings); - } - Distance result; - if (from != null) { - result = getAverageAbsoluteCrossTrackError(competitor, from, timePoint, /* upwindOnly */true, waitForLatestAnalysis); - } else { - result = null; - } - return result; - } - - @Override - public Distance getAverageSignedCrossTrackError(Competitor competitor, TimePoint timePoint, boolean waitForLatestAnalysis) - throws NoWindException { - NavigableSet markPassings = getMarkPassings(competitor); - TimePoint from = null; - lockForRead(markPassings); - try { - if (markPassings != null && !markPassings.isEmpty()) { - from = markPassings.iterator().next().getTimePoint(); - } - } finally { - unlockAfterRead(markPassings); - } - Distance result; - if (from != null) { - result = getAverageSignedCrossTrackError(competitor, from, timePoint, /* upwindOnly */true, waitForLatestAnalysis); - } else { - result = null; - } - return result; - } - - @Override - public Distance getAverageAbsoluteCrossTrackError(Competitor competitor, TimePoint from, TimePoint to, boolean upwindOnly, - boolean waitForLatestAnalysis) throws NoWindException { - Distance result; - result = crossTrackErrorCache - .getAverageAbsoluteCrossTrackError(competitor, from, to, upwindOnly, waitForLatestAnalysis); - return result; - } - - @Override - public Distance getAverageSignedCrossTrackError(Competitor competitor, TimePoint from, TimePoint to, boolean upwindOnly, - boolean waitForLatestAnalysis) throws NoWindException { - Distance result; - result = crossTrackErrorCache - .getAverageSignedCrossTrackError(competitor, from, to, upwindOnly, waitForLatestAnalysis); - return result; - } - - @Override - public TrackedLegOfCompetitor getCurrentLeg(Competitor competitor, TimePoint timePoint) { - NavigableSet competitorMarkPassings = markPassingsForCompetitor.get(competitor); - DummyMarkPassingWithTimePointOnly markPassingTimePoint = new DummyMarkPassingWithTimePointOnly(timePoint); - TrackedLegOfCompetitor result = null; - if (!competitorMarkPassings.isEmpty()) { - MarkPassing lastMarkPassingAtOfBeforeTimePoint = competitorMarkPassings.floor(markPassingTimePoint); - if (lastMarkPassingAtOfBeforeTimePoint != null) { - Waypoint waypointPassedLastAtOrBeforeTimePoint = lastMarkPassingAtOfBeforeTimePoint.getWaypoint(); - // don't return a leg if competitor has already finished last leg and therefore the race - if (waypointPassedLastAtOrBeforeTimePoint != getRace().getCourse().getLastWaypoint()) { - result = getTrackedLegStartingAt(waypointPassedLastAtOrBeforeTimePoint).getTrackedLeg(competitor); - } - } - } - return result; - } - - @Override - public TrackedLeg getCurrentLeg(TimePoint timePoint) { - Waypoint lastWaypointPassed = null; - int indexOfLastWaypointPassed = -1; - for (Map.Entry> entry : markPassingsForWaypoint.entrySet()) { - if (!entry.getValue().isEmpty()) { - MarkPassing first = entry.getValue().first(); - // Did the mark passing happen at or before the requested time point? - if (first.getTimePoint().compareTo(timePoint) <= 0) { - int indexOfWaypoint = getRace().getCourse().getIndexOfWaypoint(entry.getKey()); - if (indexOfWaypoint > indexOfLastWaypointPassed) { - indexOfLastWaypointPassed = indexOfWaypoint; - lastWaypointPassed = entry.getKey(); - } - } - } - } - TrackedLeg result = null; - if (lastWaypointPassed != null && lastWaypointPassed != getRace().getCourse().getLastWaypoint()) { - result = getTrackedLegStartingAt(lastWaypointPassed); - } - return result; - } - - @Override - public MarkPassing getMarkPassing(Competitor competitor, Waypoint waypoint) { - final NavigableSet markPassings = getMarkPassings(competitor); - if (markPassings != null) { - lockForRead(markPassings); - try { - for (MarkPassing markPassing : markPassings) { - if (markPassing.getWaypoint() == waypoint) { - return markPassing; - } - } - } finally { - unlockAfterRead(markPassings); - } - } - return null; - } - - /** - * This method was a synchronization bottleneck when it was using a regular HashMap for {@link #markTracks}. It is - * frequently used, and the most frequent case is that the get call on {@link #markTracks} succeeds - * with a non-null result. To improve performance for this case, {@link #markTracks} now is a - * {@link ConcurrentHashMap} that can be read while writes are going on without locking or synchronization. Only if - * the get call does not provide a result, the entire procedure is repeated, this time with - * synchronization to avoid duplicate track creation for the same mark. - */ - @Override - public GPSFixTrack getOrCreateTrack(Mark mark) { - GPSFixTrack result = markTracks.get(mark); - if (result == null) { - // try again, this time with more expensive synchronization - synchronized (markTracks) { - LockUtil.lockForRead(getSerializationLock()); - try { - result = markTracks.get(mark); - if (result == null) { - result = createMarkTrack(mark); - markTracks.put(mark, result); - } - } finally { - LockUtil.unlockAfterRead(getSerializationLock()); - } - } - } - return result; - } - - protected DynamicGPSFixTrackImpl createMarkTrack(Mark mark) { - return new DynamicGPSFixTrackImpl(mark, millisecondsOverWhichToAverageSpeed); - } - - @Override - public Position getApproximatePosition(Waypoint waypoint, TimePoint timePoint) { - Position result = null; - for (Mark mark : waypoint.getMarks()) { - Position nextPos = getOrCreateTrack(mark).getEstimatedPosition(timePoint, /* extrapolate */false); - if (result == null) { - result = nextPos; - } else if (nextPos != null) { - result = result.translateGreatCircle(result.getBearingGreatCircle(nextPos), result.getDistance(nextPos) - .scale(0.5)); - } - } - return result; - } - - @Override - public boolean hasWindData() { - boolean result = false; - Course course = getRace().getCourse(); - Waypoint firstWaypoint = course.getFirstWaypoint(); - TimePoint timepoint = startTime != null ? startTime : startOfTrackingReceived; - if (firstWaypoint != null && timepoint != null) { - Position position = getApproximatePosition(firstWaypoint, timepoint); - if (position != null) { - Wind wind = getWind(position, timepoint); - if (wind != null) { - result = true; - } - } - } - return result; - } - - @Override - public boolean hasGPSData() { - boolean result = false; - if (!tracks.values().isEmpty()) { - for (GPSFixTrack gpsTrack : tracks.values()) { - if (gpsTrack.getFirstRawFix() != null) { - result = true; - break; - } - } - } - return result; - } - - @Override - public Wind getWind(Position p, TimePoint at) { - return getWind(p, at, getWindSourcesToExclude()); - } - - @Override - public Wind getWind(Position p, TimePoint at, Iterable windSourcesToExclude) { - final WindWithConfidence> windWithConfidence = getWindWithConfidence(p, at, - windSourcesToExclude); - return windWithConfidence == null ? null : windWithConfidence.getObject(); - } - - @Override - public WindWithConfidence> getWindWithConfidence(Position p, TimePoint at) { - return getWindWithConfidence(p, at, getWindSourcesToExclude()); - } - - @Override - public Iterable getWindSourcesToExclude() { - synchronized (windSourcesToExclude) { - return Collections.unmodifiableCollection(windSourcesToExclude); - } - } - - @Override - public void setWindSourcesToExclude(Iterable windSourcesToExclude) { - Set old = new HashSet<>(this.windSourcesToExclude); - synchronized (this.windSourcesToExclude) { - LockUtil.lockForRead(getSerializationLock()); - try { - this.windSourcesToExclude.clear(); - for (WindSource windSourceToExclude : windSourcesToExclude) { - this.windSourcesToExclude.add(windSourceToExclude); - } - } finally { - LockUtil.unlockAfterRead(getSerializationLock()); - } - } - if (!old.equals(this.windSourcesToExclude)) { - clearAllCachesExceptManeuvers(); - triggerManeuverCacheRecalculationForAllCompetitors(); - } - } - - @Override - public WindWithConfidence> getWindWithConfidence(Position p, TimePoint at, - Iterable windSourcesToExclude) { - boolean canUseSpeedOfAtLeastOneWindSource = false; - Weigher> timeWeigherThatPretendsToAlsoWeighPositions = new PositionAndTimePointWeigher( - /* halfConfidenceAfterMilliseconds */10000l); - ConfidenceBasedWindAverager> averager = ConfidenceFactory.INSTANCE - .createWindAverager(timeWeigherThatPretendsToAlsoWeighPositions); - List>> windFixesWithConfidences = new ArrayList>>(); - for (WindSource windSource : getWindSources()) { - // TODO consider parallelizing and consider caching - if (!Util.contains(windSourcesToExclude, windSource)) { - WindTrack track = getOrCreateWindTrack(windSource); - WindWithConfidence> windWithConfidence = track.getAveragedWindWithConfidence(p, at); - if (windWithConfidence != null) { - windFixesWithConfidences.add(windWithConfidence); - canUseSpeedOfAtLeastOneWindSource = canUseSpeedOfAtLeastOneWindSource - || windSource.getType().useSpeed(); - } - } - } - HasConfidence> average = averager.getAverage( - windFixesWithConfidences, new com.sap.sse.common.Util.Pair(p, at)); - WindWithConfidence> result = average == null ? null - : new WindWithConfidenceImpl>(average.getObject(), average.getConfidence(), - new com.sap.sse.common.Util.Pair(p, at), canUseSpeedOfAtLeastOneWindSource); - return result; - } - - @Override - public Wind getDirectionFromStartToNextMark(final TimePoint at) { - Future future; - FutureTask newFuture = null; - synchronized (directionFromStartToNextMarkCache) { - future = directionFromStartToNextMarkCache.get(at); - if (future == null) { - newFuture = new FutureTask(new Callable() { - @Override - public Wind call() { - Wind result; - Leg firstLeg = getRace().getCourse().getFirstLeg(); - if (firstLeg != null) { - Position firstLegEnd = getApproximatePosition(firstLeg.getTo(), at); - Position firstLegStart = getApproximatePosition(firstLeg.getFrom(), at); - if (firstLegStart != null && firstLegEnd != null) { - result = new WindImpl(firstLegStart, at, new KnotSpeedWithBearingImpl(1.0, - firstLegEnd.getBearingGreatCircle(firstLegStart))); - } else { - result = null; - } - } else { - result = null; - } - return result; - } - }); - directionFromStartToNextMarkCache.put(at, newFuture); - } - } - if (newFuture != null) { - newFuture.run(); - future = newFuture; - } - try { - return future.get(); - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException(e); - } - } - - @Override - public TimePoint getTimePointOfOldestEvent() { - return timePointOfOldestEvent; - } - - @Override - public TimePoint getTimePointOfNewestEvent() { - return timePointOfNewestEvent; - } - - @Override - public TimePoint getTimePointOfLastEvent() { - return timePointOfLastEvent; - } - - /** - * @param timeOfEvent - * may be null meaning to only unblock waiters but not update any time points - */ - protected void updated(TimePoint timeOfEvent) { - updateCount++; - clearAllCachesExceptManeuvers(); - if (timeOfEvent != null) { - if (timePointOfNewestEvent == null || timePointOfNewestEvent.compareTo(timeOfEvent) < 0) { - timePointOfNewestEvent = timeOfEvent; - } - if (timePointOfOldestEvent == null || timePointOfOldestEvent.compareTo(timeOfEvent) > 0) { - timePointOfOldestEvent = timeOfEvent; - } - timePointOfLastEvent = timeOfEvent; - } - synchronized (this) { - notifyAll(); - } - } - - protected void setStartTimeReceived(TimePoint start) { - if ((start == null) != (startTimeReceived == null) || (start != null && !start.equals(startTimeReceived))) { - this.startTimeReceived = start; - invalidateStartTime(); - invalidateMarkPassingTimes(); - } - } - - @Override - public TimePoint getStartTimeReceived() { - return startTimeReceived; - } - - protected void setStartOfTrackingReceived(TimePoint startOfTracking) { - this.startOfTrackingReceived = startOfTracking; - } - - protected void setEndOfTrackingReceived(TimePoint endOfTracking) { - this.endOfTrackingReceived = endOfTracking; - } - - /** - * Schedules the clearing of the caches. If a cache clearing is already scheduled, this is a no-op. - */ - private void clearAllCachesExceptManeuvers() { - synchronized (cacheInvalidationTimerLock) { - if (cacheInvalidationTimer == null) { - cacheInvalidationTimer = new Timer("Cache invalidation timer for TrackedRaceImpl " - + getRace().getName()); - cacheInvalidationTimer.schedule(new TimerTask() { - @Override - public void run() { - synchronized (cacheInvalidationTimerLock) { - cacheInvalidationTimer.cancel(); - cacheInvalidationTimer = null; - } - synchronized (competitorRankings) { - competitorRankings.clear(); - } - synchronized (competitorRankingsLocks) { - competitorRankingsLocks.clear(); - } - } - }, DELAY_FOR_CACHE_CLEARING_IN_MILLISECONDS); - } - } - } - - @Override - public synchronized void waitForNextUpdate(int sinceUpdate) throws InterruptedException { - while (updateCount <= sinceUpdate) { - wait(); // ...until updated(...) notifies us - } - } - - @Override - public void waypointAdded(int zeroBasedIndex, Waypoint waypointThatGotAdded) { - // expecting to hold the course's write lock - invalidateMarkPassingTimes(); - LockUtil.lockForRead(getSerializationLock()); - try { - // assuming that getRace().getCourse()'s write lock is held by the current thread - updateStartToNextMarkCacheInvalidationCacheListenersAfterWaypointAdded(zeroBasedIndex, waypointThatGotAdded); - getOrCreateMarkPassingsInOrderAsNavigableSet(waypointThatGotAdded); - for (Mark mark : waypointThatGotAdded.getMarks()) { - getOrCreateTrack(mark); - } - // a waypoint got added; this means that a leg got added as well; but we shouldn't claim we know where - // in the leg list of the course the leg was added; that's an implementation secret of CourseImpl. So try: - LinkedHashMap reorderedTrackedLegs = new LinkedHashMap(); - List newLegs = getRace().getCourse().getLegs(); - for (Leg leg : newLegs) { - TrackedLeg trackedLeg = trackedLegs.get(leg); - if (trackedLeg != null) { - reorderedTrackedLegs.put(leg, trackedLeg); - } else { - reorderedTrackedLegs.put(leg, createTrackedLeg(leg)); - } - } - // now ensure that the iteration order is in sync with the leg iteration order - trackedLegs.clear(); - for (Map.Entry entry : reorderedTrackedLegs.entrySet()) { - trackedLegs.put(entry.getKey(), entry.getValue()); - entry.getValue().waypointsMayHaveChanges(); - } - updated(/* time point */null); // no maneuver cache invalidation required because we don't yet have mark - // passings for new waypoint - } finally { - LockUtil.unlockAfterRead(getSerializationLock()); - } - } - - private void updateStartToNextMarkCacheInvalidationCacheListenersAfterWaypointAdded(int zeroBasedIndex, - Waypoint waypointThatGotAdded) { - if (zeroBasedIndex < 2) { - // the observing listener on any previous mark will be GCed; we need to ensure - // that the cache is recomputed - clearDirectionFromStartToNextMarkCache(); - Iterator waypointsIter = getRace().getCourse().getWaypoints().iterator(); - waypointsIter.next(); // skip first - if (waypointsIter.hasNext()) { - waypointsIter.next(); // skip second - if (waypointsIter.hasNext()) { - Waypoint oldSecond = waypointsIter.next(); - stopAndRemoveStartToNextMarkCacheInvalidationListener(oldSecond); - } - } - } - addStartToNextMarkCacheInvalidationListener(waypointThatGotAdded); - } - - private void clearDirectionFromStartToNextMarkCache() { - synchronized (directionFromStartToNextMarkCache) { - directionFromStartToNextMarkCache.clear(); - } - } - - private void addStartToNextMarkCacheInvalidationListener(Waypoint waypoint) { - for (Mark mark : waypoint.getMarks()) { - addStartToNextMarkCacheInvalidationListener(mark); - } - } - - private void addStartToNextMarkCacheInvalidationListener(Mark mark) { - GPSFixTrack track = getOrCreateTrack(mark); - StartToNextMarkCacheInvalidationListener listener = new StartToNextMarkCacheInvalidationListener(track); - LockUtil.lockForRead(getSerializationLock()); - try { - startToNextMarkCacheInvalidationListeners.put(mark, listener); - } finally { - LockUtil.unlockAfterRead(getSerializationLock()); - } - track.addListener(listener); - } - - private void stopAndRemoveStartToNextMarkCacheInvalidationListener(Waypoint waypoint) { - for (Mark mark : waypoint.getMarks()) { - stopAndRemoveStartToNextMarkCacheInvalidationListener(mark); - } - } - - private void stopAndRemoveStartToNextMarkCacheInvalidationListener(Mark mark) { - StartToNextMarkCacheInvalidationListener listener = startToNextMarkCacheInvalidationListeners.get(mark); - if (listener != null) { - listener.stopListening(); - LockUtil.lockForRead(getSerializationLock()); - try { - startToNextMarkCacheInvalidationListeners.remove(mark); - } finally { - LockUtil.unlockAfterRead(getSerializationLock()); - } - } - } - - @Override - public void waypointRemoved(int zeroBasedIndex, Waypoint waypointThatGotRemoved) { - invalidateMarkPassingTimes(); - LockUtil.lockForRead(getSerializationLock()); - try { - // assuming that getRace().getCourse()'s write lock is held by the current thread - updateStartToNextMarkCacheInvalidationCacheListenersAfterWaypointRemoved(zeroBasedIndex, - waypointThatGotRemoved); - Leg toRemove = null; - Leg last = null; - int i = 0; - for (Map.Entry e : trackedLegs.entrySet()) { - last = e.getKey(); - if (i == zeroBasedIndex) { - toRemove = e.getKey(); - break; - } - i++; - } - if (toRemove == null && !trackedLegs.isEmpty()) { - // last waypoint removed - toRemove = last; - } - if (toRemove != null) { - logger.info("Removing tracked leg at zero-based index " + zeroBasedIndex + " from tracked race " - + getRace().getName()); - LinkedHashMap newTrackedLegs = new LinkedHashMap<>(); - for (Map.Entry trackedLegsEntry : trackedLegs.entrySet()) { - if (trackedLegsEntry.getKey() == toRemove) { - break; - } else { - newTrackedLegs.put(trackedLegsEntry.getKey(), trackedLegsEntry.getValue()); - } - } - trackedLegs.clear(); - trackedLegs.putAll(newTrackedLegs); - List newLegs = getRace().getCourse().getLegs(); - for (int j = zeroBasedIndex; j < newLegs.size(); j++) { - trackedLegs.put(newLegs.get(j), createTrackedLeg(newLegs.get(j))); - } - updated(/* time point */null); - } - // remove all corresponding markpassings if a waypoint has been removed - NavigableSet markPassingsRemoved; - markPassingsRemoved = markPassingsForWaypoint.remove(waypointThatGotRemoved); - for (NavigableSet markPassingsForOneCompetitor : markPassingsForCompetitor.values()) { - if (!markPassingsForOneCompetitor.isEmpty()) { - final Competitor competitor = markPassingsForOneCompetitor.iterator().next().getCompetitor(); - LockUtil.lockForWrite(getMarkPassingsLock(markPassingsForOneCompetitor)); - try { - markPassingsForOneCompetitor.removeAll(markPassingsRemoved); - } finally { - LockUtil.unlockAfterWrite(getMarkPassingsLock(markPassingsForOneCompetitor)); - } - triggerManeuverCacheRecalculation(competitor); - } - } - } finally { - LockUtil.unlockAfterRead(getSerializationLock()); - } - } - - protected NamedReentrantReadWriteLock getMarkPassingsLock(Iterable markPassings) { - synchronized (locksForMarkPassings) { - NamedReentrantReadWriteLock lock = locksForMarkPassings.get(markPassings); - if (lock == null) { - lock = new NamedReentrantReadWriteLock("mark passings lock for tracked race " + getRace().getName(), /* fair */ - false); - locksForMarkPassings.put(markPassings, lock); - } - return lock; - } - } - - private void updateStartToNextMarkCacheInvalidationCacheListenersAfterWaypointRemoved(int zeroBasedIndex, - Waypoint waypointThatGotRemoved) { - if (zeroBasedIndex < 2) { - // the observing listener on any previous mark will be GCed; we need to ensure - // that the cache is recomputed - clearDirectionFromStartToNextMarkCache(); - stopAndRemoveStartToNextMarkCacheInvalidationListener(waypointThatGotRemoved); - Iterator waypointsIter = getRace().getCourse().getWaypoints().iterator(); - if (waypointsIter.hasNext()) { // catches the case of a course being empty - waypointsIter.next(); // skip first - if (waypointsIter.hasNext()) { - waypointsIter.next(); // skip second - if (waypointsIter.hasNext()) { - Waypoint newSecond = waypointsIter.next(); - addStartToNextMarkCacheInvalidationListener(newSecond); - } - } - } - } - } - - @Override - public TrackedRegatta getTrackedRegatta() { - return trackedRegatta; - } - - @Override - public Wind getEstimatedWindDirection(Position position, TimePoint timePoint) { - WindWithConfidence estimatedWindWithConfidence = getEstimatedWindDirectionWithConfidence(position, - timePoint); - return estimatedWindWithConfidence == null ? null : estimatedWindWithConfidence.getObject(); - } - - @Override - public WindWithConfidence getEstimatedWindDirectionWithConfidence(Position position, TimePoint timePoint) { - DummyMarkPassingWithTimePointOnly dummyMarkPassingForNow = new DummyMarkPassingWithTimePointOnly(timePoint); - Weigher weigher = ConfidenceFactory.INSTANCE.createExponentialTimeDifferenceWeigher( - // use a minimum confidence to avoid the bearing to flip to 270deg in case all is zero - getMillisecondsOverWhichToAverageSpeed(), /* minimum confidence */0.0000000001); - Map> bearings = clusterBearingsByLegType(timePoint, position, - dummyMarkPassingForNow, weigher); - // use the minimum confidence of the four "quadrants" as the result's confidence - BearingWithConfidenceImpl reversedUpwindAverage = null; - int upwindNumberOfRelevantBoats = 0; - double confidence = 0; - int numberOfBoatsRelevantForEstimate = 0; - BearingWithConfidence resultBearing = null; - if (bearings != null) { - BearingWithConfidenceCluster[] bearingClustersUpwind = bearings.get(LegType.UPWIND).splitInTwo( - getMinimumAngleBetweenDifferentTacksUpwind(), timePoint); - if (!bearingClustersUpwind[0].isEmpty() && !bearingClustersUpwind[1].isEmpty()) { - BearingWithConfidence average0 = bearingClustersUpwind[0].getAverage(timePoint); - BearingWithConfidence average1 = bearingClustersUpwind[1].getAverage(timePoint); - upwindNumberOfRelevantBoats = Math - .min(bearingClustersUpwind[0].size(), bearingClustersUpwind[1].size()); - confidence = Math.min(average0.getConfidence(), average1.getConfidence()) - * getRace().getBoatClass().getUpwindWindEstimationConfidence(upwindNumberOfRelevantBoats); - reversedUpwindAverage = new BearingWithConfidenceImpl(average0.getObject() - .middle(average1.getObject()).reverse(), confidence, timePoint); - } - BearingWithConfidenceImpl downwindAverage = null; - int downwindNumberOfRelevantBoats = 0; - BearingWithConfidenceCluster[] bearingClustersDownwind = bearings.get(LegType.DOWNWIND) - .splitInTwo(getMinimumAngleBetweenDifferentTacksDownwind(), timePoint); - if (!bearingClustersDownwind[0].isEmpty() && !bearingClustersDownwind[1].isEmpty()) { - BearingWithConfidence average0 = bearingClustersDownwind[0].getAverage(timePoint); - BearingWithConfidence average1 = bearingClustersDownwind[1].getAverage(timePoint); - downwindNumberOfRelevantBoats = Math.min(bearingClustersDownwind[0].size(), - bearingClustersDownwind[1].size()); - confidence = Math.min(average0.getConfidence(), average1.getConfidence()) - * getRace().getBoatClass().getDownwindWindEstimationConfidence(downwindNumberOfRelevantBoats); - downwindAverage = new BearingWithConfidenceImpl(average0.getObject().middle( - average1.getObject()), confidence, timePoint); - } - numberOfBoatsRelevantForEstimate = upwindNumberOfRelevantBoats + downwindNumberOfRelevantBoats; - BearingWithConfidenceCluster resultCluster = new BearingWithConfidenceCluster(weigher); - assert upwindNumberOfRelevantBoats == 0 || reversedUpwindAverage != null; - if (upwindNumberOfRelevantBoats > 0) { - resultCluster.add(reversedUpwindAverage); - } - assert downwindNumberOfRelevantBoats == 0 || downwindAverage != null; - if (downwindNumberOfRelevantBoats > 0) { - resultCluster.add(downwindAverage); - } - resultBearing = resultCluster.getAverage(timePoint); - } - return resultBearing == null ? null : new WindWithConfidenceImpl(new WindImpl(null, timePoint, - new KnotSpeedWithBearingImpl( - /* speedInKnots */numberOfBoatsRelevantForEstimate, resultBearing.getObject())), - resultBearing.getConfidence(), resultBearing.getRelativeTo(), /* useSpeed */false); - } - - // TODO confidences need to be computed not only based on timePoint but also on position: boats far away don't - // contribute as confidently as boats close by - private Map> clusterBearingsByLegType(TimePoint timePoint, - Position position, DummyMarkPassingWithTimePointOnly dummyMarkPassingForNow, Weigher weigher) { - Weigher weigherForMarkPassingProximity = new HyperbolicTimeDifferenceWeigher( - getMillisecondsOverWhichToAverageSpeed() * 5); - Map> bearings = new HashMap>(); - for (LegType legType : LegType.values()) { - bearings.put(legType, new BearingWithConfidenceCluster(weigher)); - } - Map legTypesCache = new HashMap(); - getRace().getCourse().lockForRead(); // ensure the course doesn't change, particularly lose the leg we're - // interested in, while we're running - try { - for (Competitor competitor : getRace().getCompetitors()) { - TrackedLegOfCompetitor leg; - try { - leg = getTrackedLeg(competitor, timePoint); - } catch (IllegalArgumentException iae) { - logger.warning("Caught " + iae + " during wind estimation; ignoring seemingly broken leg"); - logger.log(Level.SEVERE, "clusterBearingsByLegType", iae); - // supposedly, we got a "Waypoint X isn't start of any leg in Y" exception; leg not found - leg = null; - } - // if bearings was set to null this indicates there was an exception; no need for further calculations, - // return null - if (bearings != null && leg != null) { - TrackedLeg trackedLeg = getTrackedLeg(leg.getLeg()); - LegType legType; - try { - legType = legTypesCache.get(trackedLeg); - if (legType == null) { - legType = trackedLeg.getLegType(timePoint); - legTypesCache.put(trackedLeg, legType); - } - if (legType != LegType.REACHING) { - GPSFixTrack track = getTrack(competitor); - if (!track.hasDirectionChange(timePoint, - /* be even more conservative than maneuver detection to really try to get "straight line" behavior */ - getManeuverDegreeAngleThreshold()/2.)) { - SpeedWithBearingWithConfidence estimatedSpeedWithConfidence = track - .getEstimatedSpeed(timePoint, weigher); - if (estimatedSpeedWithConfidence != null - && estimatedSpeedWithConfidence.getObject() != null && - // Mark passings may be missing or far off. This can lead to boats apparently - // going "backwards" regarding the leg's direction; ignore those - isNavigatingForward(estimatedSpeedWithConfidence.getObject().getBearing(), - trackedLeg, timePoint)) { - // additionally to generally excluding maneuvers, reduce confidence around mark - // passings: - NavigableSet markPassings = getMarkPassings(competitor); - double markPassingProximityConfidenceReduction = 1.0; - lockForRead(markPassings); - try { - NavigableSet prevMarkPassing = markPassings.headSet( - dummyMarkPassingForNow, /* inclusive */true); - NavigableSet nextMarkPassing = markPassings.tailSet( - dummyMarkPassingForNow, /* inclusive */true); - if (prevMarkPassing != null && !prevMarkPassing.isEmpty()) { - markPassingProximityConfidenceReduction *= Math.max(0.0, - 1.0 - weigherForMarkPassingProximity.getConfidence(prevMarkPassing - .last().getTimePoint(), timePoint)); - } - if (nextMarkPassing != null && !nextMarkPassing.isEmpty()) { - markPassingProximityConfidenceReduction *= Math.max(0.0, - 1.0 - weigherForMarkPassingProximity.getConfidence(nextMarkPassing - .first().getTimePoint(), timePoint)); - } - } finally { - unlockAfterRead(markPassings); - } - BearingWithConfidence bearing = new BearingWithConfidenceImpl( - estimatedSpeedWithConfidence.getObject() == null ? null - : estimatedSpeedWithConfidence.getObject().getBearing(), - markPassingProximityConfidenceReduction - * estimatedSpeedWithConfidence.getConfidence(), - estimatedSpeedWithConfidence.getRelativeTo()); - BearingWithConfidenceCluster bearingClusterForLegType = bearings - .get(legType); - bearingClusterForLegType.add(bearing); - } - } - } - } catch (NoWindException e) { - logger.fine("Unable to determine leg type for race " + getRace().getName() - + " while trying to estimate wind (Background: I've got a NoWindException)"); - bearings = null; - } - } - } - } finally { - getRace().getCourse().unlockAfterRead(); - } - return bearings; - } - - /** - * Checks if the bearing generally moves in the direction that the trackedLeg has at time - * point at. - */ - private boolean isNavigatingForward(Bearing bearing, TrackedLeg trackedLeg, TimePoint at) { - Bearing legBearing = trackedLeg.getLegBearing(at); - return Math.abs(bearing.getDifferenceTo(legBearing).getDegrees()) < 90; - } - - /** - * This is probably best explained by example. If the wind bearing is from port to starboard, the situation looks - * like this: - * - *
-     *                                 ^
-     *                 Wind            | Boat
-     *               ----------->      |
-     *                                 |
-     * 
-     * 
- * - * In this case, the boat gets the wind from port, so the result has to be {@link Tack#PORT}. The angle between the - * boat's heading (which we can only approximate by the boat's course over ground) and the wind bearing in this case - * is 90 degrees. wind.{@link Bearing#getDifferenceTo(Bearing) getDifferenceTo}(boat) in this case will - * return a bearing representing -90 degrees. - *

- * - * If the wind is blowing the other way, the angle returned by {@link Bearing#getDifferenceTo(Bearing)} will - * correspond to +90 degrees. In other words, a negative angle means starboard tack, a positive angle represents - * port tack. - *

- * - * For the unlikely case of 0 degrees difference, {@link Tack#STARBOARD} will result. - * - * @return null in case the boat's bearing cannot be determined for timePoint - * @throws NoWindException - */ - @Override - public Tack getTack(Competitor competitor, TimePoint timePoint) throws NoWindException { - final SpeedWithBearing estimatedSpeed = getTrack(competitor).getEstimatedSpeed(timePoint); - Tack result = null; - if (estimatedSpeed != null) { - result = getTack(getTrack(competitor).getEstimatedPosition(timePoint, /* extrapolate */false), timePoint, - estimatedSpeed.getBearing()); - } - return result; - } - - /** - * Based on the wind direction at timePoint and at position where, compares the - * boatBearing to the wind's bearing at that time and place and determined the tack. - * - * @throws NoWindException - * in case the wind cannot be determined because without a wind direction, the tack cannot be determined - * either - */ - private Tack getTack(Position where, TimePoint timePoint, Bearing boatBearing) throws NoWindException { - final Wind wind = getWind(where, timePoint); - Tack result; - if (wind == null) { - throw new NoWindException("Can't determine wind direction in position " + where + " at " + timePoint - + ", therefore cannot determine tack"); - } - Bearing windBearing = wind.getBearing(); - Bearing difference = windBearing.getDifferenceTo(boatBearing); - result = difference.getDegrees() <= 0 ? Tack.PORT : Tack.STARBOARD; - return result; - } - - @Override - public String toString() { - return "TrackedRace for " + getRace(); - } - - @Override - public List approximate(Competitor competitor, Distance maxDistance, TimePoint from, TimePoint to) { - DouglasPeucker douglasPeucker = new DouglasPeucker( - getTrack(competitor)); - return douglasPeucker.approximate(maxDistance, from, to); - } - - protected void triggerManeuverCacheRecalculationForAllCompetitors() { - final List shuffledCompetitors = new ArrayList<>(); - for (Competitor competitor : (getRace().getCompetitors())) { - shuffledCompetitors.add(competitor); - } - Collections.shuffle(shuffledCompetitors); - for (Competitor competitor : shuffledCompetitors) { - triggerManeuverCacheRecalculation(competitor); - } - } - - protected void triggerManeuverCacheRecalculation(final Competitor competitor) { - maneuverCache.triggerUpdate(competitor, /* updateInterval */null); - } - - private com.sap.sse.common.Util.Triple> computeManeuvers(Competitor competitor) throws NoWindException { - logger.finest("computeManeuvers(" + competitor.getName() + ") called in tracked race " + this); - long startedAt = System.currentTimeMillis(); - // compute the maneuvers for competitor - com.sap.sse.common.Util.Triple> result = null; - NavigableSet markPassings = getMarkPassings(competitor); - boolean markPassingsNotEmpty; - TimePoint extendedFrom = null; - MarkPassing crossedFinishLine = null; - // getLastWaypoint() will wait for a read lock on the course; do this outside the synchronized block to avoid - // deadlocks - final Waypoint lastWaypoint = getRace().getCourse().getLastWaypoint(); - lockForRead(markPassings); - try { - markPassingsNotEmpty = markPassings != null && !markPassings.isEmpty(); - if (markPassingsNotEmpty) { - extendedFrom = markPassings.iterator().next().getTimePoint(); - crossedFinishLine = getMarkPassing(competitor, lastWaypoint); - } - } finally { - unlockAfterRead(markPassings); - } - if (markPassingsNotEmpty) { - TimePoint extendedTo; - if (crossedFinishLine != null) { - extendedTo = crossedFinishLine.getTimePoint(); - } else { - final GPSFixMoving lastRawFix = getTrack(competitor).getLastRawFix(); - if (lastRawFix != null) { - extendedTo = lastRawFix.getTimePoint(); - } else { - extendedTo = null; - } - } - if (extendedTo != null) { - try { - List extendedResultForCache = detectManeuvers(competitor, extendedFrom, extendedTo, /* ignoreMarkPassings */ false); - result = new com.sap.sse.common.Util.Triple>(extendedFrom, extendedTo, - extendedResultForCache); - } catch (NoWindException ex) { - // Catching the NoWindException here without letting it propagate thru other handlers. - // This is mainly to avoid having logs flooded with stack traces. It is safe to catch - // it here because we can assume that this exception does not hide any severe problem - // other than that there is no wind. Because maneuvers are mostly computed using a - // future cache and need wind (like getTack) they will often fail before the wind has been - // loaded from database. We can safely return null here because we can be sure that - // cache will be updated when new fixes are fed into the stream therefore leading to a - // recomputation. - logger.fine("NoWindException during computation of maneuvers for " + competitor.getName()); - } - } // else competitor has no fixes to consider; remove any maneuver cache entry - } // else competitor hasn't started yet; remove any maneuver cache entry - logger.finest("computeManeuvers(" + competitor.getName() + ") called in tracked race " + this + " took " - + (System.currentTimeMillis() - startedAt) + "ms"); - return result; - } - - /** - * @param ignoreMarkPassings - * When true, no {@link ManeuverType#MARK_PASSING} maneuvers will be identified, and the - * fact that a mark passing would split up what else may be a penalty circle is ignored. This is helpful - * for recursive calls, e.g., after identifying a tack and a jibe around a mark passing and trying to - * identify for the time before and after the mark passing which maneuvers exist on which side of the - * passing. - * @return a valid but possibly empty list - * @see #detectManeuvers(Competitor, List, boolean, TimePoint, TimePoint) - */ - private List detectManeuvers(Competitor competitor, TimePoint from, TimePoint to, boolean ignoreMarkPassings) throws NoWindException { - return detectManeuvers( - competitor, approximate(competitor, getRace().getBoatClass().getMaximumDistanceForCourseApproximation(), - from, to), ignoreMarkPassings, from, to); - } - - /** - * Tries to detect maneuvers on the competitor's track based on a number of approximating fixes. The - * fixes contain bearing information, but this is not the bearing leading to the next approximation fix but the - * bearing the boat had at the time of the approximating fix which is taken from the original track. - *

- * - * The time period assumed for a maneuver duration is taken from the - * {@link BoatClass#getApproximateManeuverDurationInMilliseconds() boat class}. If no maneuver is detected, an empty - * list is returned. Maneuvers can only be expected to be detected if at least three fixes are provided in - * approximatedFixesToAnalyze. For the inner approximating fixes (all except the first and the last - * approximating fix), their course changes according to the approximated path (and not the underlying actual - * tracked fixes) are computed. Subsequent course changes to the same direction are then grouped. Those in closer - * timely distance than {@link #getApproximateManeuverDurationInMilliseconds()} (including single course changes - * that have no surrounding other course changes to group) are grouped into one {@link Maneuver}. - * - * @param ignoreMarkPassings - * When true, no {@link ManeuverType#MARK_PASSING} maneuvers will be identified, and the - * fact that a mark passing would split up what else may be a penalty circle is ignored. This is helpful - * for recursive calls, e.g., after identifying a tack and a jibe around a mark passing and trying to - * identify for the time before and after the mark passing which maneuvers exist on which side of the - * passing. - * @param earliestManeuverStart - * maneuver start will not be before this time point; if a maneuver is found whose time point is at or - * after this time point, no matter how close it is, its start regarding speed and course into the - * maneuver and the leg before the maneuver is not taken from an earlier time point, even if half the - * maneuver duration before the maneuver time point were before this time point. - * @param latestManeuverEnd - * maneuver end will not be after this time point; if a maneuver is found whose time point is at or - * before this time point, no matter how close it is, its end regarding speed and course out of the - * maneuver and the leg after the maneuver is not taken from a later time point, even if half the - * maneuver duration after the maneuver time point were after this time point. - * - * @return an empty list if no maneuver is detected for competitor between from and - * to, or else the list of maneuvers detected. - */ - private List detectManeuvers(Competitor competitor, List approximatingFixesToAnalyze, - boolean ignoreMarkPassings, TimePoint earliestManeuverStart, TimePoint latestManeuverEnd) - throws NoWindException { - List result = new ArrayList(); - if (approximatingFixesToAnalyze.size() > 2) { - List> courseChangeSequenceInSameDirection = new ArrayList>(); - Iterator approximationPointsIter = approximatingFixesToAnalyze.iterator(); - GPSFixMoving previous = approximationPointsIter.next(); - GPSFixMoving current = approximationPointsIter.next(); - // the bearings in these variables are between approximation points - SpeedWithBearing speedWithBearingOnApproximationFromPreviousToCurrent = previous - .getSpeedAndBearingRequiredToReach(current); - SpeedWithBearing speedWithBearingOnApproximationAtBeginningOfUnidirectionalCourseChanges = speedWithBearingOnApproximationFromPreviousToCurrent; - SpeedWithBearing speedWithBearingOnApproximationFromCurrentToNext; // will certainly be assigned because - // iter's collection's size > 2 - do { - GPSFixMoving next = approximationPointsIter.next(); - speedWithBearingOnApproximationFromCurrentToNext = current.getSpeedAndBearingRequiredToReach(next); - // compute course change on "approximation track" - CourseChange courseChange = speedWithBearingOnApproximationFromPreviousToCurrent - .getCourseChangeRequiredToReach(speedWithBearingOnApproximationFromCurrentToNext); - com.sap.sse.common.Util.Pair courseChangeAtFix = new com.sap.sse.common.Util.Pair(current, - courseChange); - if (!courseChangeSequenceInSameDirection.isEmpty() - && Math.signum(courseChangeSequenceInSameDirection.get(0).getB().getCourseChangeInDegrees()) != Math - .signum(courseChange.getCourseChangeInDegrees())) { - // course change in different direction; cluster the course changes in same direction so far, then - // start new list - List maneuvers = groupChangesInSameDirectionIntoManeuvers(competitor, - speedWithBearingOnApproximationAtBeginningOfUnidirectionalCourseChanges, - courseChangeSequenceInSameDirection, ignoreMarkPassings, earliestManeuverStart, latestManeuverEnd); - result.addAll(maneuvers); - courseChangeSequenceInSameDirection.clear(); - speedWithBearingOnApproximationAtBeginningOfUnidirectionalCourseChanges = speedWithBearingOnApproximationFromPreviousToCurrent; - } - courseChangeSequenceInSameDirection.add(courseChangeAtFix); - previous = current; - current = next; - speedWithBearingOnApproximationFromPreviousToCurrent = speedWithBearingOnApproximationFromCurrentToNext; - } while (approximationPointsIter.hasNext()); - if (!courseChangeSequenceInSameDirection.isEmpty()) { - result.addAll(groupChangesInSameDirectionIntoManeuvers(competitor, - speedWithBearingOnApproximationAtBeginningOfUnidirectionalCourseChanges, - courseChangeSequenceInSameDirection, ignoreMarkPassings, earliestManeuverStart, latestManeuverEnd)); - } - } - return result; - } - - /** - * Fetches results from {@link #maneuverCache}. The cache is updated asynchronously after relevant updates have been - * received (see {@link #triggerManeuverCacheRecalculation(Competitor)} and - * {@link #triggerManeuverCacheRecalculationForAllCompetitors()}). Callers can choose whether to wait for any - * ongoing updates by using the waitForLatest parameter. From the cache the interval requested is then - * {@link #extractInterval(TimePoint, TimePoint, List) extracted}. - * - * @param waitForLatest - * if true, any currently ongoing maneuver recalculation for competitor is - * waited for before returning the result; otherwise, whatever is in the {@link #maneuverCache} for - * competitor, reduced to the interval requested, will be returned. - */ - @Override - public List getManeuvers(Competitor competitor, TimePoint from, TimePoint to, boolean waitForLatest) - throws NoWindException { - com.sap.sse.common.Util.Triple> allManeuvers = maneuverCache.get(competitor, waitForLatest); - List result; - if (allManeuvers == null) { - result = Collections.emptyList(); - } else { - result = extractInterval(from, to, allManeuvers.getC()); - } - return result; - } - - private List extractInterval(TimePoint from, TimePoint to, List listOfTimed) { - List result; - result = new LinkedList(); - for (T timed : listOfTimed) { - if (timed.getTimePoint().compareTo(from) >= 0 && timed.getTimePoint().compareTo(to) <= 0) { - result.add(timed); - } - } - return result; - } - - /** - * Groups the {@link CourseChange} sequence into groups where the times of the fixes at which the course changes - * took place are no further apart than {@link #getApproximateManeuverDurationInMilliseconds()} milliseconds or - * where the distances of those course changes are less than two hull lengths apart. For those, a single - * {@link Maneuver} object is created and added to the resulting list. The maneuver sums up the direction changes of - * the individual {@link CourseChange} objects. This can result in direction changes of more than 180 degrees in one - * direction which may, e.g., represent a penalty circle or a mark rounding maneuver. As the maneuver's time point, - * the average time point of the course changes that went into the maneuver construction is used. - *

- * @param speedWithBearingOnApproximationAtBeginning - * the speed/bearing before the first approximating fix passed in - * courseChangeSequenceInSameDirection - * @param courseChangeSequenceInSameDirection - * all expected to have equal {@link CourseChange#to()} values - * @param ignoreMarkPassings - * When true, no {@link ManeuverType#MARK_PASSING} maneuvers will be identified, and the - * fact that a mark passing would split up what else may be a penalty circle is ignored. This is helpful - * for recursive calls, e.g., after identifying a tack and a jibe around a mark passing and trying to - * identify for the time before and after the mark passing which maneuvers exist on which side of the - * passing. - * @param earliestManeuverStart - * maneuver start will not be before this time point; if a maneuver is found whose time point is at or - * after this time point, no matter how close it is, its start regarding speed and course into the - * maneuver and the leg before the maneuver is not taken from an earlier time point, even if half the - * maneuver duration before the maneuver time point were before this time point. - * @param latestManeuverEnd - * maneuver end will not be after this time point; if a maneuver is found whose time point is at or - * before this time point, no matter how close it is, its end regarding speed and course out of the - * maneuver and the leg after the maneuver is not taken from a later time point, even if half the - * maneuver duration after the maneuver time point were after this time point. - * - * @return a non-null list - */ - private List groupChangesInSameDirectionIntoManeuvers(Competitor competitor, - SpeedWithBearing speedWithBearingOnApproximationAtBeginning, - List> courseChangeSequenceInSameDirection, boolean ignoreMarkPassings, - TimePoint earliestManeuverStart, TimePoint latestManeuverEnd) throws NoWindException { - List result = new ArrayList(); - List> group = new ArrayList>(); - if (!courseChangeSequenceInSameDirection.isEmpty()) { - Distance threeHullLengths = competitor.getBoat().getBoatClass().getHullLength().scale(3); - SpeedWithBearing beforeGroupOnApproximation = speedWithBearingOnApproximationAtBeginning; // speed/bearing - // before group - SpeedWithBearing beforeCurrentCourseChangeOnApproximation = beforeGroupOnApproximation; // speed/bearing - // before current - // course change - Iterator> iter = courseChangeSequenceInSameDirection.iterator(); - double totalCourseChangeInDegrees = 0.0; - long totalMilliseconds = 0l; - SpeedWithBearing afterCurrentCourseChange = null; // sure to be set because iter's collection is not empty - // and the first use requires group not to be empty which can only happen after the first group.add - do { - com.sap.sse.common.Util.Pair currentFixAndCourseChange = iter.next(); - if (!group.isEmpty() - // TODO use different maneuver times for upwind / reaching / downwind / cross-leg (mark passing) - // group contains complete maneuver if the next fix is too late or too far away to belong to the - // same maneuver - // FIXME penalty circles slow down the boat so much that time limit may get exceeded although - // distance limit is matched - && currentFixAndCourseChange.getA().getTimePoint().asMillis() - - group.get(group.size() - 1).getA().getTimePoint().asMillis() > getApproximateManeuverDurationInMilliseconds() - && currentFixAndCourseChange.getA().getPosition() - .getDistance(group.get(group.size() - 1).getA().getPosition()) - .compareTo(threeHullLengths) > 0) { - // if next is more then approximate maneuver duration later or further apart than two hull lengths, - // turn the current group into a maneuver and add to result - Util.addAll(createManeuverFromGroupOfCourseChanges(competitor, beforeGroupOnApproximation, - group, afterCurrentCourseChange, totalCourseChangeInDegrees, totalMilliseconds, earliestManeuverStart, latestManeuverEnd), result); - group.clear(); - totalCourseChangeInDegrees = 0.0; - totalMilliseconds = 0l; - beforeGroupOnApproximation = beforeCurrentCourseChangeOnApproximation; - } - afterCurrentCourseChange = beforeCurrentCourseChangeOnApproximation - .applyCourseChange(currentFixAndCourseChange.getB()); - totalMilliseconds += currentFixAndCourseChange.getA().getTimePoint().asMillis(); - totalCourseChangeInDegrees += currentFixAndCourseChange.getB().getCourseChangeInDegrees(); - group.add(currentFixAndCourseChange); - beforeCurrentCourseChangeOnApproximation = afterCurrentCourseChange; // speed/bearing after course - // change - } while (iter.hasNext()); - if (!group.isEmpty()) { - Util.addAll(createManeuverFromGroupOfCourseChanges(competitor, beforeGroupOnApproximation, group, - afterCurrentCourseChange, totalCourseChangeInDegrees, totalMilliseconds, earliestManeuverStart, latestManeuverEnd), result); - } - } - return result; - } - - private Iterable createManeuverFromGroupOfCourseChanges(Competitor competitor, - SpeedWithBearing speedWithBearingOnApproximationAtBeginning, List> group, - SpeedWithBearing speedWithBearingOnApproximationAtEnd, double totalCourseChangeInDegrees, - long totalMilliseconds, TimePoint earliestManeuverStart, TimePoint latestManeuverEnd) throws NoWindException { - List result = new ArrayList<>(); - TimePoint timePointBeforeManeuver = Collections.max(Arrays.asList(new MillisecondsTimePoint(group.get(0).getA().getTimePoint() - .asMillis() - getApproximateManeuverDurationInMilliseconds() / 2), earliestManeuverStart)); - TimePoint timePointAfterManeuver = Collections.min(Arrays.asList(new MillisecondsTimePoint(group.get(group.size() - 1).getA() - .getTimePoint().asMillis() + getApproximateManeuverDurationInMilliseconds() / 2), latestManeuverEnd)); - TimePoint maneuverTimePoint = computeManeuverTimepoint(competitor, timePointBeforeManeuver, timePointAfterManeuver); - final GPSFixTrack competitorTrack = getTrack(competitor); - Position maneuverPosition = competitorTrack.getEstimatedPosition(maneuverTimePoint, /* extrapolate */false); - Tack tackAfterManeuver = getTack(maneuverPosition, timePointAfterManeuver, - speedWithBearingOnApproximationAtEnd.getBearing()); - ManeuverType maneuverType; - Distance maneuverLoss = null; - // the TrackedLegOfCompetitor variables may be null, e.g., in case the time points are before or after the race - TrackedLegOfCompetitor legBeforeManeuver = getTrackedLeg(competitor, timePointBeforeManeuver); - TrackedLegOfCompetitor legAfterManeuver = getTrackedLeg(competitor, timePointAfterManeuver); - Waypoint waypointPassed = null; // set for MARK_PASSING maneuvers only - NauticalSide sideToWhichWaypointWasPassed = null; // set for MARK_PASSING maneuvers only - final NauticalSide courseChangedTo = totalCourseChangeInDegrees < 0 ? NauticalSide.PORT : NauticalSide.STARBOARD; - // check for mask passing first; a tacking / jibe-setting mark rounding thus takes precedence over being - // detected as a penalty circle - final TimePoint markPassingTimePoint; - if (legBeforeManeuver != legAfterManeuver - // a maneuver at the start line is not to be considered a MARK_PASSING maneuver; show a tack as a tack - && legAfterManeuver != null - && legAfterManeuver.getLeg().getFrom() != getRace().getCourse().getFirstWaypoint()) { - waypointPassed = legAfterManeuver.getLeg().getFrom(); - MarkPassing markPassing = getMarkPassing(competitor, waypointPassed); - markPassingTimePoint = markPassing != null ? markPassing.getTimePoint() : maneuverTimePoint; - Position markPassingPosition = markPassing != null ? competitorTrack.getEstimatedPosition(markPassingTimePoint, /* extrapolate */false) : maneuverPosition; - sideToWhichWaypointWasPassed = courseChangedTo; - // produce an additional mark passing maneuver; continue to analyze to catch jibe sets and kiwi drops - result.add(new MarkPassingManeuverImpl(ManeuverType.MARK_PASSING, tackAfterManeuver, markPassingPosition, - markPassingTimePoint, speedWithBearingOnApproximationAtBeginning, - speedWithBearingOnApproximationAtEnd, totalCourseChangeInDegrees, maneuverLoss, waypointPassed, - sideToWhichWaypointWasPassed)); - } else { - markPassingTimePoint = null; - } - final Wind wind = getWind(maneuverPosition, maneuverTimePoint); - final SpeedWithBearing estimatedSpeedBeforeManeuver = competitorTrack.getEstimatedSpeed(timePointBeforeManeuver); - final SpeedWithBearing estimatedSpeedAfterManeuver = competitorTrack.getEstimatedSpeed(timePointAfterManeuver); - if (wind != null && estimatedSpeedBeforeManeuver != null && estimatedSpeedAfterManeuver != null) { - BearingChangeAnalyzer bearingChangeAnalyzer = BearingChangeAnalyzer.INSTANCE; - final Bearing courseBeforeManeuver = estimatedSpeedBeforeManeuver.getBearing(); - final Bearing courseAfterManeuver = estimatedSpeedAfterManeuver.getBearing(); - boolean jibed = bearingChangeAnalyzer.didPass(courseBeforeManeuver, totalCourseChangeInDegrees, - courseAfterManeuver, wind.getBearing()); - boolean tacked = bearingChangeAnalyzer.didPass(courseBeforeManeuver, totalCourseChangeInDegrees, - courseAfterManeuver, wind.getFrom()); - if (markPassingTimePoint != null && (tacked || jibed)) { - // In case of a mark passing we need to split the maneuver analysis into the phase before and after - // the mark passing. First of all, this is important to identify the correct maneuver time point for - // each tack and jibe, second it is essential to call a penalty which is only the case if the tack and - // the jibe are on the same side of the mark passing; otherwise this may have been a jibe set or a - // kiwi drop. - // Therefore, we recursively detect the maneuvers for the segment before and the segment after the - // mark passing and add the results to our result. - result.addAll(detectManeuvers(competitor, timePointBeforeManeuver, markPassingTimePoint.minus(1), /* ignoreMarkPassings */ true)); - result.addAll(detectManeuvers(competitor, markPassingTimePoint.plus(1), timePointAfterManeuver, /* ignoreMarkPassings */ true)); - } else { - // Either there was no mark passing, or the mark passing was not accompanied by a tack or a jibe - if (tacked && jibed && markPassingTimePoint == null) { - maneuverType = ManeuverType.PENALTY_CIRCLE; - if (legBeforeManeuver != null) { - maneuverLoss = legBeforeManeuver.getManeuverLoss(timePointBeforeManeuver, maneuverTimePoint, - timePointAfterManeuver); - } - } else if (tacked) { - maneuverType = ManeuverType.TACK; - if (legBeforeManeuver != null) { - maneuverLoss = legBeforeManeuver.getManeuverLoss(timePointBeforeManeuver, maneuverTimePoint, - timePointAfterManeuver); - } - } else if (jibed) { - maneuverType = ManeuverType.JIBE; - if (legBeforeManeuver != null) { - maneuverLoss = legBeforeManeuver.getManeuverLoss(timePointBeforeManeuver, maneuverTimePoint, - timePointAfterManeuver); - } - } else { - // heading up or bearing away - Bearing windBearing = wind.getBearing(); - Bearing toWindBeforeManeuver = windBearing - .getDifferenceTo(speedWithBearingOnApproximationAtBeginning.getBearing()); - Bearing toWindAfterManeuver = windBearing.getDifferenceTo(speedWithBearingOnApproximationAtEnd - .getBearing()); - maneuverType = Math.abs(toWindBeforeManeuver.getDegrees()) < Math.abs(toWindAfterManeuver - .getDegrees()) ? ManeuverType.HEAD_UP : ManeuverType.BEAR_AWAY; - } - final Maneuver maneuver = new ManeuverImpl(maneuverType, tackAfterManeuver, maneuverPosition, - maneuverTimePoint, speedWithBearingOnApproximationAtBeginning, - speedWithBearingOnApproximationAtEnd, totalCourseChangeInDegrees, maneuverLoss); - result.add(maneuver); - } - } - return result; - } - - /** - * Computes the maneuver time point as the time point along between maneuver start and end where the competitor's - * track has greatest change in course. - */ - private TimePoint computeManeuverTimepoint(Competitor competitor, TimePoint timePointBeforeManeuver, - TimePoint timePointAfterManeuver) { - TimePoint result = timePointBeforeManeuver; - GPSFixTrack track = getTrack(competitor); - GPSFixMoving lastFix = null; - double maxAngleSpeedInDegreesPerSecond = 0; - track.lockForRead(); - try { - for (Iterator i = track.getFixesIterator(timePointBeforeManeuver, /* inclusive */true); i - .hasNext();) { - GPSFixMoving fix = i.next(); - if (fix.getTimePoint().after(timePointAfterManeuver)) { - break; - } - if (lastFix != null) { - final SpeedWithBearing lastEstimatedSpeed = track.getEstimatedSpeed(lastFix.getTimePoint()); - if (lastEstimatedSpeed != null) { - Bearing courseAtLastFix = lastEstimatedSpeed.getBearing(); - final SpeedWithBearing estimatedSpeed = track.getEstimatedSpeed(fix.getTimePoint()); - if (estimatedSpeed != null) { - Bearing courseAtFix = estimatedSpeed.getBearing(); - double angleSpeedInDegreesPerSecond = Math.abs((courseAtFix - .getDifferenceTo(courseAtLastFix).getDegrees()) - / (double) (fix.getTimePoint().asMillis() - lastFix.getTimePoint().asMillis())); - if (angleSpeedInDegreesPerSecond > maxAngleSpeedInDegreesPerSecond) { - maxAngleSpeedInDegreesPerSecond = angleSpeedInDegreesPerSecond; - result = lastFix.getTimePoint(); - } - } - } - } - lastFix = fix; - } - } finally { - track.unlockAfterRead(); - } - return result; - } - - /** - * Fetches the boat class-specific parameter - */ - private double getManeuverDegreeAngleThreshold() { - return getRace().getBoatClass().getManeuverDegreeAngleThreshold(); - } - - private double getMinimumAngleBetweenDifferentTacksDownwind() { - return getRace().getBoatClass().getMinimumAngleBetweenDifferentTacksDownwind(); - } - - private double getMinimumAngleBetweenDifferentTacksUpwind() { - return getRace().getBoatClass().getMinimumAngleBetweenDifferentTacksUpwind(); - } - - private long getApproximateManeuverDurationInMilliseconds() { - return getRace().getBoatClass().getApproximateManeuverDurationInMilliseconds(); - } - - private class StartToNextMarkCacheInvalidationListener implements GPSTrackListener { - private static final long serialVersionUID = 3540278554797445085L; - private final GPSFixTrack listeningTo; - - public StartToNextMarkCacheInvalidationListener(GPSFixTrack listeningTo) { - this.listeningTo = listeningTo; - } - - public void stopListening() { - listeningTo.removeListener(this); - } - - @Override - public void gpsFixReceived(GPSFix fix, Mark mark) { - clearDirectionFromStartToNextMarkCache(); - } - - @Override - public void speedAveragingChanged(long oldMillisecondsOverWhichToAverage, long newMillisecondsOverWhichToAverage) { - } - - @Override - public boolean isTransient() { - return false; - } - } - - @Override - public Distance getWindwardDistanceToOverallLeader(Competitor competitor, TimePoint timePoint) - throws NoWindException { - final TrackedLegOfCompetitor trackedLeg = getTrackedLeg(competitor, timePoint); - return trackedLeg == null ? null : trackedLeg.getWindwardDistanceToOverallLeader(timePoint); - } - - @Override - public Iterable getMarks() { - while (true) { - try { - return new HashSet(markTracks.keySet()); - } catch (ConcurrentModificationException cme) { - logger.info("Caught " + cme + "; trying again."); - } - } - } - - @Override - public Iterable getCourseSidelines() { - return new ArrayList(courseSidelines.values()); - } - - @Override - public long getDelayToLiveInMillis() { - return delayToLiveInMillis; - } - - protected void setDelayToLiveInMillis(long delayToLiveInMillis) { - this.delayToLiveInMillis = delayToLiveInMillis; - } - - @Override - public TrackedRaceStatus getStatus() { - return status; - } - - /** - * Changes to the {@link #status} variable are synchronized on the {@link #statusNotifier} field. - * - * @return - */ - protected Object getStatusNotifier() { - return statusNotifier; - } - - protected void setStatus(TrackedRaceStatus newStatus) { - assert newStatus != null; - final TrackedRaceStatusEnum oldStatus; - synchronized (getStatusNotifier()) { - oldStatus = getStatus().getStatus(); - this.status = newStatus; - getStatusNotifier().notifyAll(); - } - if (newStatus.getStatus() == TrackedRaceStatusEnum.LOADING && oldStatus != TrackedRaceStatusEnum.LOADING) { - suspendAllCachesNotUpdatingWhileLoading(); - } else if (oldStatus == TrackedRaceStatusEnum.LOADING && newStatus.getStatus() != TrackedRaceStatusEnum.LOADING) { - resumeAllCachesNotUpdatingWhileLoading(); - } - - } - - private void suspendAllCachesNotUpdatingWhileLoading() { - crossTrackErrorCache.suspend(); - maneuverCache.suspend(); - } - - private void resumeAllCachesNotUpdatingWhileLoading() { - crossTrackErrorCache.resume(); - maneuverCache.resume(); - } - - /** - * Waits on the current ("old") status object which is notified in {@link #setStatus(TrackedRaceStatus)} when the - * status is changed. The change as well as the check synchronize on the old status object. - */ - @Override - public void waitUntilNotLoading() { - synchronized (getStatusNotifier()) { - while (getStatus().getStatus() == TrackedRaceStatusEnum.LOADING) { - try { - getStatusNotifier().wait(); - } catch (InterruptedException e) { - logger.info("waitUntilNotLoading on tracked race " + this + " interrupted: " + e.getMessage() - + ". Continuing to wait."); - } - } - } - } - - @Override - public void attachRaceLog(final RaceLog raceLog) { - if (raceLog != null) { - this.attachedRaceLogs.put(raceLog.getId(), raceLog); - // Use the new race log, that possibly contains device mappings, to load GPSFix tracks from the DB - loadingFromGPSFixStoreState = LoadingFromStoresState.NOT_STARTED; - // When this tracked race is to be serialized, wait for the loading from stores to complete. - new Thread("Mongo mark and competitor track loader for tracked race " + getRace().getName()) { - @Override - public void run() { - LockUtil.lockForRead(getSerializationLock()); - LockUtil.lockForWrite(getLoadingFromGPSFixStoreLock()); - synchronized (TrackedRaceImpl.this) { - loadingFromGPSFixStoreState = LoadingFromStoresState.RUNNING; // indicates that the serialization - // lock is now safely held - TrackedRaceImpl.this.notifyAll(); - } - try { - logger.info("Started loading competitor tracks for " + getRace().getName()); - for (Competitor competitor : race.getCompetitors()) { - try { - gpsFixStore.loadCompetitorTrack( - (DynamicGPSFixTrack) tracks.get(competitor), raceLog, - competitor); - } catch (TransformationException | NoCorrespondingServiceRegisteredException e) { - logger.log(Level.WARNING, "Could not load track for " + competitor); - } - } - logger.info("Finished loading competitor tracks for " + getRace().getName()); - logger.info("Started loading mark tracks for " + getRace().getName()); - for (Mark mark : getMarks()) { - try { - gpsFixStore.loadMarkTrack((DynamicGPSFixTrack) markTracks.get(mark), raceLog, - mark); - } catch (TransformationException | NoCorrespondingServiceRegisteredException e) { - logger.log(Level.WARNING, "Could not load track for " + mark); - } - } - logger.info("Finished loading mark tracks for " + getRace().getName()); - - } finally { - synchronized (TrackedRaceImpl.this) { - loadingFromGPSFixStoreState = LoadingFromStoresState.FINISHED; - TrackedRaceImpl.this.notifyAll(); - } - synchronized (loadingFromGPSFixStoreState) { - loadingFromGPSFixStoreState.notifyAll(); - } - LockUtil.unlockAfterWrite(getLoadingFromGPSFixStoreLock()); - LockUtil.unlockAfterRead(getSerializationLock()); - } - } - }.start(); - } else { - logger.severe("Got a request to attach race log for an empty race log!"); - } - } - - @Override - public void detachRaceLog(Serializable identifier) { - this.attachedRaceLogs.remove(identifier); - } - - @Override - public void detachAllRaceLogs() { - this.attachedRaceLogs.clear(); - } - - @Override - public RaceLog getRaceLog(Serializable identifier) { - return attachedRaceLogs.get(identifier); - } - - @Override - public Distance getDistanceToStartLine(Competitor competitor, long millisecondsBeforeRaceStart) { - final Distance result; - if (getStartOfRace() == null) { - result = null; - } else { - TimePoint beforeStart = new MillisecondsTimePoint(getStartOfRace().asMillis() - millisecondsBeforeRaceStart); - result = getDistanceToStartLine(competitor, beforeStart); - } - return result; - } - - @Override - public Distance getDistanceToStartLine(Competitor competitor, TimePoint timePoint) { - Waypoint startWaypoint = getRace().getCourse().getFirstWaypoint(); - final Distance result; - if (startWaypoint == null) { - result = null; - } else { - Position competitorPosition = getTrack(competitor).getEstimatedPosition(timePoint, /* extrapolate */false); - if (competitorPosition == null) { - result = null; - } else { - Iterable marks = startWaypoint.getControlPoint().getMarks(); - Iterator marksIterator = marks.iterator(); - Mark first = marksIterator.next(); - Position firstPosition = getOrCreateTrack(first).getEstimatedPosition(timePoint, /* extrapolate */false); - if (firstPosition == null) { - result = null; - } else { - if (marksIterator.hasNext()) { - // it's a line / gate - Mark second = marksIterator.next(); - Position secondPosition = getOrCreateTrack(second).getEstimatedPosition(timePoint, /* extrapolate */ - false); - final Bearing bearingGreatCircle = firstPosition.getBearingGreatCircle(secondPosition); - if (bearingGreatCircle == null) { - result = null; - } else { - Position competitorProjectedOntoStartLine = competitorPosition.projectToLineThrough( - firstPosition, bearingGreatCircle); - result = competitorPosition.getDistance(competitorProjectedOntoStartLine); - } - } else { - result = competitorPosition.getDistance(firstPosition); - } - } - } - } - return result; - } - - @Override - public Speed getSpeed(Competitor competitor, long millisecondsBeforeRaceStart) { - if (getStartOfRace() == null) { - return null; - } - - TimePoint beforeStart = new MillisecondsTimePoint(getStartOfRace().asMillis() - millisecondsBeforeRaceStart); - return getTrack(competitor).getEstimatedSpeed(beforeStart); - } - - @Override - public Distance getDistanceFromStarboardSideOfStartLineWhenPassingStart(Competitor competitor) { - final Distance result; - TrackedLegOfCompetitor firstTrackedLegOfCompetitor = getTrackedLeg(competitor, getRace().getCourse().getFirstLeg()); - TimePoint competitorStartTime = firstTrackedLegOfCompetitor.getStartTime(); - if (competitorStartTime != null) { - Position competitorPositionWhenPassingStart = getTrack(competitor).getEstimatedPosition( - competitorStartTime, /* extrapolate */false); - final Position starboardMarkPosition = getStarboardMarkOfStartlinePosition(competitorStartTime); - if (competitorPositionWhenPassingStart != null && starboardMarkPosition != null) { - result = starboardMarkPosition == null ? null : competitorPositionWhenPassingStart.getDistance(starboardMarkPosition); - } else { - result = null; - } - } else { - result = null; - } - return result; - } - - @Override - public Distance getDistanceFromStarboardSideOfStartLine(Competitor competitor, TimePoint timePoint) { - final Distance result; - if (timePoint != null) { - Position competitorPositionWhenPassingStart = getTrack(competitor).getEstimatedPosition( - timePoint, /* extrapolate */false); - final Position starboardMarkPosition = getStarboardMarkOfStartlinePosition(timePoint); - if (competitorPositionWhenPassingStart != null && starboardMarkPosition != null) { - result = starboardMarkPosition == null ? null : competitorPositionWhenPassingStart.getDistance(starboardMarkPosition); - } else { - result = null; - } - } else { - result = null; - } - return result; - } - - /** - * Based on the bearing from the start waypoint to the next mark, identifies which of the two marks of the start - * line is on starboard. If the start waypoint has only one mark, that mark is returned. If the start line has two - * marks but the course has no other waypoint, - * null is returned. If the course has no waypoints at all, null is returned.

- * - * The method has protected visibility largely for testing purposes. - */ - protected Mark getStarboardMarkOfStartlineOrSingleStartMark(TimePoint at) { - LineDetails startLine = getStartLine(at); - final Mark result; - if (startLine != null) { - result = startLine.getStarboardMarkWhileApproachingLine(); - } else { - Waypoint startWaypoint = getRace().getCourse().getFirstWaypoint(); - if (startWaypoint != null && startWaypoint.getMarks().iterator().hasNext()) { - result = startWaypoint.getMarks().iterator().next(); - } else { - result = null; - } - } - return result; - } - - /** - * Based on the bearing from the start waypoint to the - * next mark, identifies which of the two marks of the start line is on starboard and returns its position. If the start waypoint has only - * one mark, that mark is returned. If the start line has two marks but the course has no other waypoint, - * null is returned. If the course has no waypoints at all, null is returned. - */ - private Position getStarboardMarkOfStartlinePosition(TimePoint at) { - Mark starboardMark = getStarboardMarkOfStartlineOrSingleStartMark(at); - if (starboardMark != null) { - return getOrCreateTrack(starboardMark).getEstimatedPosition(at, /*extrapolate*/ false); - } - return null; - } - - protected NamedReentrantReadWriteLock getLoadingFromWindStoreLock() { - return loadingFromWindStoreLock; - } - - protected NamedReentrantReadWriteLock getLoadingFromGPSFixStoreLock() { - return loadingFromGPSFixStoreLock; - } - - /** - * If the waypoint is not a line, or no position can be determined for one of its marks at timePoint, - * null is returned. If no wind information is available but required to compute the advantage, null values - * are returned in those fields that depend on wind data. If the waypoint is null - * or is the only waypoint, null is returned because no reasonable statement can be - * made about the direction from which the line is to be passed. - */ - private LineDetails getLineLengthAndAdvantage(TimePoint timePoint, Waypoint waypoint) { - List markPositions = new ArrayList(); - int numberOfMarks = 0; - boolean allMarksHavePositions = true; - LineDetails result = null; - if (waypoint != null) { - for (Mark lineMark : waypoint.getMarks()) { - numberOfMarks++; - final Position estimatedMarkPosition = getOrCreateTrack(lineMark).getEstimatedPosition(timePoint, /* extrapolate */ - false); - if (estimatedMarkPosition != null) { - markPositions.add(estimatedMarkPosition); - } else { - allMarksHavePositions = false; - } - } - final List legs = getRace().getCourse().getLegs(); - // need at least one leg to make sense of a line - if (!legs.isEmpty()) { - try { - if (allMarksHavePositions && numberOfMarks == 2) { - final int indexOfWaypoint = getRace().getCourse().getIndexOfWaypoint(waypoint); - final boolean isStartLine = indexOfWaypoint == 0; - final TrackedLeg legDeterminingDirection = getTrackedLeg(legs.get(isStartLine ? 0 - : indexOfWaypoint - 1)); - Distance crossTrackErrorOfMark0OnLineFromMark1ToNextWaypoint = markPositions.get(0) - .crossTrackError(markPositions.get(1), legDeterminingDirection.getLegBearing(timePoint)); - final Mark starboardMarkWhileApproachingLine; - final Mark portMarkWhileApproachingLine; - final Position starboardMarkPositionWhileApproachingLine; - final Position portMarkPositionWhileApproachingLine; - if (crossTrackErrorOfMark0OnLineFromMark1ToNextWaypoint.getMeters() < 0) { - portMarkWhileApproachingLine = Util.get(waypoint.getMarks(), 0); - starboardMarkWhileApproachingLine = Util.get(waypoint.getMarks(), 1); - } else { - portMarkWhileApproachingLine = Util.get(waypoint.getMarks(), 1); - starboardMarkWhileApproachingLine = Util.get(waypoint.getMarks(), 0); - } - portMarkPositionWhileApproachingLine = getOrCreateTrack(portMarkWhileApproachingLine) - .getEstimatedPosition(timePoint, /* extrapolate */false); - starboardMarkPositionWhileApproachingLine = getOrCreateTrack(starboardMarkWhileApproachingLine) - .getEstimatedPosition(timePoint, /* extrapolate */false); - final Bearing differenceToCombinedWind; - final NauticalSide advantageousSideWhileApproachingLine; - final Distance distanceAdvantage; - Wind combinedWind = getWind(markPositions.get(0), timePoint); - if (combinedWind != null) { - differenceToCombinedWind = portMarkPositionWhileApproachingLine.getBearingGreatCircle( - starboardMarkPositionWhileApproachingLine).getDifferenceTo(combinedWind.getFrom()); - Distance windwardDistanceFromFirstToSecondMark; - windwardDistanceFromFirstToSecondMark = legDeterminingDirection.getWindwardDistance( - markPositions.get(0), markPositions.get(1), timePoint); - final Position worseMarkPosition; - final Position betterMarkPosition; - if ((isStartLine && windwardDistanceFromFirstToSecondMark.getMeters() > 0) - || (!isStartLine && windwardDistanceFromFirstToSecondMark.getMeters() < 0)) { - // first mark is worse than second mark - worseMarkPosition = markPositions.get(0); - betterMarkPosition = markPositions.get(1); - } else { - // second mark is worse than first mark - worseMarkPosition = markPositions.get(1); - betterMarkPosition = markPositions.get(0); - } - if (windwardDistanceFromFirstToSecondMark.getMeters() >= 0) { - distanceAdvantage = windwardDistanceFromFirstToSecondMark; - } else { - distanceAdvantage = new CentralAngleDistance( - -windwardDistanceFromFirstToSecondMark.getCentralAngleRad()); - } - if (betterMarkPosition.crossTrackError(worseMarkPosition, - legDeterminingDirection.getLegBearing(timePoint)).getCentralAngleRad() > 0) { - advantageousSideWhileApproachingLine = NauticalSide.STARBOARD; - } else { - advantageousSideWhileApproachingLine = NauticalSide.PORT; - } - } else { - differenceToCombinedWind = null; - advantageousSideWhileApproachingLine = null; - distanceAdvantage = null; - } - result = new LineDetailsImpl(timePoint, waypoint, - portMarkPositionWhileApproachingLine - .getDistance(starboardMarkPositionWhileApproachingLine), - differenceToCombinedWind, advantageousSideWhileApproachingLine, distanceAdvantage, - portMarkWhileApproachingLine, starboardMarkWhileApproachingLine); - } - } catch (NoWindException e) { - // result remains null; - } - } - } - return result; - } - - - @Override - public LineDetails getStartLine(TimePoint at) { - return getLineLengthAndAdvantage(at, getRace().getCourse().getFirstWaypoint()); - } - - @Override - public LineDetails getFinishLine(TimePoint at) { - return getLineLengthAndAdvantage(at, getRace().getCourse().getLastWaypoint()); - } - - @Override - public SpeedWithConfidence getAverageWindSpeedWithConfidence(long resolutionInMillis) { - SpeedWithConfidence result = null; - if (getEndOfRace() != null) { - TimePoint fromTimePoint = getStartOfRace(); - TimePoint toTimePoint = getEndOfRace(); - - List windSourcesToDeliver = new ArrayList(); - WindSourceImpl windSource = new WindSourceImpl(WindSourceType.COMBINED); - windSourcesToDeliver.add(windSource); - - double sumWindSpeed = 0.0; - double sumWindSpeedConfidence = 0.0; - int speedCounter = 0; - - int numberOfFixes = (int) ((toTimePoint.asMillis() - fromTimePoint.asMillis()) / resolutionInMillis); - WindTrack windTrack = getOrCreateWindTrack(windSource); - TimePoint timePoint = fromTimePoint; - for (int i = 0; i < numberOfFixes && toTimePoint != null && timePoint.compareTo(toTimePoint) < 0; i++) { - WindWithConfidence> averagedWindWithConfidence = windTrack - .getAveragedWindWithConfidence(null, timePoint); - if (averagedWindWithConfidence != null) { - double windSpeedinKnots = averagedWindWithConfidence.getObject().getKnots(); - double confidence = averagedWindWithConfidence.getConfidence(); - - sumWindSpeed += windSpeedinKnots; - sumWindSpeedConfidence += confidence; - - speedCounter++; - } - timePoint = new MillisecondsTimePoint(timePoint.asMillis() + resolutionInMillis); - } - if (speedCounter > 0) { - Speed averageWindSpeed = new KnotSpeedImpl(sumWindSpeed / speedCounter); - double averageWindSpeedConfidence = sumWindSpeedConfidence / speedCounter; - result = new SpeedWithConfidenceImpl(averageWindSpeed, averageWindSpeedConfidence, toTimePoint); - } - } - return result; - } - - @Override - public Distance getCourseLength() { - List legs = getRace().getCourse().getLegs(); - Distance raceDistance = new NauticalMileDistance(0); - for (Leg leg : legs) { - Waypoint from = leg.getFrom(); - Iterable markPassings = getMarkPassingsInOrder(from); - Iterator markPassingsIterator = markPassings.iterator(); - if (!markPassingsIterator.hasNext()) { - return null; - } - MarkPassing firstPassing = markPassingsIterator.next(); - TimePoint timePointOfFirstPassing = firstPassing.getTimePoint(); - Waypoint to = leg.getTo(); - Position fromPos = getApproximatePosition(from, timePointOfFirstPassing); - Position toPos = getApproximatePosition(to, timePointOfFirstPassing); - Distance legDistance = fromPos.getDistance(toPos); - raceDistance = raceDistance.add(legDistance); - } - return raceDistance; - } - - @Override - public Speed getSpeedWhenCrossingStartLine(Competitor competitor) { - NavigableSet competitorMarkPassings = getMarkPassings(competitor); - Speed competitorSpeedWhenPassingStart = null; - lockForRead(competitorMarkPassings); - try { - if (!competitorMarkPassings.isEmpty()) { - TimePoint competitorStartTime = competitorMarkPassings.first().getTimePoint(); - competitorSpeedWhenPassingStart = getTrack(competitor).getEstimatedSpeed( - competitorStartTime); - } - } finally { - unlockAfterRead(competitorMarkPassings); - } - return competitorSpeedWhenPassingStart; - } - - @Override - public GPSFixStore getGPSFixStore() { - return gpsFixStore; - } -} +package com.sap.sailing.domain.tracking.impl; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.ConcurrentModificationException; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.NavigableSet; +import java.util.Set; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.sap.sailing.domain.base.BearingWithConfidence; +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.Course; +import com.sap.sailing.domain.base.CourseListener; +import com.sap.sailing.domain.base.Leg; +import com.sap.sailing.domain.base.Mark; +import com.sap.sailing.domain.base.RaceDefinition; +import com.sap.sailing.domain.base.Sideline; +import com.sap.sailing.domain.base.SpeedWithBearingWithConfidence; +import com.sap.sailing.domain.base.SpeedWithConfidence; +import com.sap.sailing.domain.base.Timed; +import com.sap.sailing.domain.base.Waypoint; +import com.sap.sailing.domain.base.impl.BearingWithConfidenceImpl; +import com.sap.sailing.domain.base.impl.DouglasPeucker; +import com.sap.sailing.domain.base.impl.SpeedWithConfidenceImpl; +import com.sap.sailing.domain.common.Bearing; +import com.sap.sailing.domain.common.BearingChangeAnalyzer; +import com.sap.sailing.domain.common.CourseChange; +import com.sap.sailing.domain.common.Distance; +import com.sap.sailing.domain.common.LegType; +import com.sap.sailing.domain.common.ManeuverType; +import com.sap.sailing.domain.common.NauticalSide; +import com.sap.sailing.domain.common.NoWindError; +import com.sap.sailing.domain.common.NoWindException; +import com.sap.sailing.domain.common.Position; +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.domain.common.RegattaNameAndRaceName; +import com.sap.sailing.domain.common.Speed; +import com.sap.sailing.domain.common.SpeedWithBearing; +import com.sap.sailing.domain.common.Tack; +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.common.TimingConstants; +import com.sap.sailing.domain.common.TrackedRaceStatusEnum; +import com.sap.sailing.domain.common.WindSource; +import com.sap.sailing.domain.common.WindSourceType; +import com.sap.sailing.domain.common.impl.CentralAngleDistance; +import com.sap.sailing.domain.common.impl.KnotSpeedImpl; +import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; +import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; +import com.sap.sailing.domain.common.impl.NauticalMileDistance; +import com.sap.sailing.domain.common.impl.WindSourceImpl; +import com.sap.sailing.domain.common.racelog.tracking.NoCorrespondingServiceRegisteredException; +import com.sap.sailing.domain.common.racelog.tracking.TransformationException; +import com.sap.sailing.domain.confidence.ConfidenceBasedWindAverager; +import com.sap.sailing.domain.confidence.ConfidenceFactory; +import com.sap.sailing.domain.confidence.HasConfidence; +import com.sap.sailing.domain.confidence.Weigher; +import com.sap.sailing.domain.confidence.impl.HyperbolicTimeDifferenceWeigher; +import com.sap.sailing.domain.confidence.impl.PositionAndTimePointWeigher; +import com.sap.sailing.domain.racelog.RaceLog; +import com.sap.sailing.domain.racelog.analyzing.impl.StartTimeFinder; +import com.sap.sailing.domain.racelog.tracking.GPSFixStore; +import com.sap.sailing.domain.tracking.DynamicGPSFixTrack; +import com.sap.sailing.domain.tracking.GPSFix; +import com.sap.sailing.domain.tracking.GPSFixMoving; +import com.sap.sailing.domain.tracking.GPSFixTrack; +import com.sap.sailing.domain.tracking.GPSTrackListener; +import com.sap.sailing.domain.tracking.LineDetails; +import com.sap.sailing.domain.tracking.Maneuver; +import com.sap.sailing.domain.tracking.MarkPassing; +import com.sap.sailing.domain.tracking.Track; +import com.sap.sailing.domain.tracking.TrackedLeg; +import com.sap.sailing.domain.tracking.TrackedLegOfCompetitor; +import com.sap.sailing.domain.tracking.TrackedRaceStatus; +import com.sap.sailing.domain.tracking.TrackedRaceWithWindEssentials; +import com.sap.sailing.domain.tracking.TrackedRegatta; +import com.sap.sailing.domain.tracking.Wind; +import com.sap.sailing.domain.tracking.WindStore; +import com.sap.sailing.domain.tracking.WindTrack; +import com.sap.sailing.domain.tracking.WindWithConfidence; +import com.sap.sailing.util.SmartFutureCache; +import com.sap.sailing.util.SmartFutureCache.AbstractCacheUpdater; +import com.sap.sailing.util.SmartFutureCache.EmptyUpdateInterval; +import com.sap.sailing.util.impl.ArrayListNavigableSet; +import com.sap.sailing.util.impl.LockUtil; +import com.sap.sailing.util.impl.NamedReentrantReadWriteLock; +import com.sap.sse.common.Util; + +public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials implements CourseListener { + private static final long serialVersionUID = -4825546964220003507L; + + private static final Logger logger = Logger.getLogger(TrackedRaceImpl.class.getName()); + + // TODO make this variable + private static final long DELAY_FOR_CACHE_CLEARING_IN_MILLISECONDS = 7500; + + public static final long TIME_BEFORE_START_TO_TRACK_WIND_MILLIS = 4 * 60 * 1000l; // let wind start four minutes before race + + private TrackedRaceStatus status; + + private final Object statusNotifier; + + /** + * By default, all wind sources are used, none are excluded. However, e.g., for performance reasons, particular wind + * sources such as the track-based estimation wind source, may be excluded by adding them to this set. + */ + private final Set windSourcesToExclude; + + /** + * Keeps the oldest timestamp that is fed into this tracked race, either from a boat fix, a mark fix, a race + * start/finish or a course definition. + */ + private TimePoint timePointOfOldestEvent; + + /** + * The start of tracking time as announced by the tracking infrastructure. + */ + private TimePoint startOfTrackingReceived; + + /** + * The end of tracking time as announced by the tracking infrastructure. + */ + private TimePoint endOfTrackingReceived; + + /** + * Race start time as announced by the tracking infrastructure + */ + private TimePoint startTimeReceived; + + /** + * The calculated race start time + */ + private TimePoint startTime; + + /** + * The calculated race end time + */ + private TimePoint endTime; + + /** + * The first and last passing times of all course waypoints + */ + private transient List>> markPassingsTimes; + + /** + * The latest time point contained by any of the events received and processed + */ + private TimePoint timePointOfNewestEvent; + + /** + * Time stamp that the event received last from the underlying push service carried on it + */ + private TimePoint timePointOfLastEvent; + + private long updateCount; + + private transient Map> competitorRankings; + + /** + * The locks managed here correspond with the {@link #competitorRankings} structure. When + * {@link #getCompetitorsFromBestToWorst(TimePoint)} starts to compute rankings, it locks the write lock for the + * time point. Readers use the read lock. Checking / entering a lock into this map uses synchronized on + * the map itself. + */ + private transient Map competitorRankingsLocks; + + /** + * legs appear in the order in which they appear in the race's course + */ + private final LinkedHashMap trackedLegs; + + private final Map> tracks; + + private final Map> markPassingsForCompetitor; + + /** + * The mark passing sets used as values are ordered by time stamp. + */ + private final Map> markPassingsForWaypoint; + + /** + * Values are the from and to time points between which the maneuvers have been previously + * computed. Clients wanting to know maneuvers for the competitor outside of this time interval need to (re-)compute + * them. + */ + private transient SmartFutureCache>, EmptyUpdateInterval> maneuverCache; + + private transient Map> directionFromStartToNextMarkCache; + + private final ConcurrentHashMap> markTracks; + + private final Map courseSidelines; + + protected long millisecondsOverWhichToAverageSpeed; + + private final Map startToNextMarkCacheInvalidationListeners; + + private transient Timer cacheInvalidationTimer; + private transient Object cacheInvalidationTimerLock; + + protected transient HashMap attachedRaceLogs; + + /** + * The time delay to the current point in time in milliseconds. + */ + private long delayToLiveInMillis; + + private enum LoadingFromStoresState { NOT_STARTED, RUNNING, FINISHED }; + + /** + * The constructor loads wind fixes from the {@link #windStore} asynchronously. + * When completed all threads currently waiting on this object are notified. + */ + private LoadingFromStoresState loadingFromWindStoreState = LoadingFromStoresState.NOT_STARTED; + + /** + * @see #loadingFromWindStoreState but for GPSFixStore + */ + private LoadingFromStoresState loadingFromGPSFixStoreState = LoadingFromStoresState.NOT_STARTED; + private Object loadingFromGPSFixStoreStateMonitor = ""; // String, not Object, to keep it serializable + + private transient CrossTrackErrorCache crossTrackErrorCache; + + private final GPSFixStore gpsFixStore; + + /** + * Wind and loading is started in a background thread during object construction. If a client needs to + * ensure that wind loading either has terminated or has not yet begun, it can obtain the read lock of + * this lock. The wind loading procedure will obtain the write lock before it starts loading wind fixes. + */ + private final NamedReentrantReadWriteLock loadingFromWindStoreLock; + + /** + * @see #loadingFromWindStoreLock but for GPSFixStore + */ + private final NamedReentrantReadWriteLock loadingFromGPSFixStoreLock; + + private final Map, NamedReentrantReadWriteLock> locksForMarkPassings; + + public TrackedRaceImpl(final TrackedRegatta trackedRegatta, RaceDefinition race, final Iterable sidelines, final WindStore windStore, final GPSFixStore gpsFixStore, + long delayToLiveInMillis, final long millisecondsOverWhichToAverageWind, + long millisecondsOverWhichToAverageSpeed, long delayForWindEstimationCacheInvalidation) { + super(race, trackedRegatta, windStore, millisecondsOverWhichToAverageWind); + locksForMarkPassings = new IdentityHashMap<>(); + attachedRaceLogs = new HashMap<>(); + this.status = new TrackedRaceStatusImpl(TrackedRaceStatusEnum.PREPARED, 0.0); + this.statusNotifier = new Object[0]; + this.loadingFromWindStoreLock = new NamedReentrantReadWriteLock("Loading from wind store lock for tracked race " + + race.getName(), /* fair */ false); + this.loadingFromGPSFixStoreLock = new NamedReentrantReadWriteLock("Loading from GPSFix store lock for tracked race " + + race.getName(), /* fair */ false); + this.cacheInvalidationTimerLock = new Object(); + this.updateCount = 0; + this.windSourcesToExclude = new HashSet(); + this.directionFromStartToNextMarkCache = new HashMap>(); + this.millisecondsOverWhichToAverageSpeed = millisecondsOverWhichToAverageSpeed; + this.delayToLiveInMillis = delayToLiveInMillis; + this.startToNextMarkCacheInvalidationListeners = new ConcurrentHashMap(); + this.maneuverCache = createManeuverCache(); + this.markTracks = new ConcurrentHashMap>(); + this.crossTrackErrorCache = new CrossTrackErrorCache(this); + this.gpsFixStore = gpsFixStore; + int i = 0; + for (Waypoint waypoint : race.getCourse().getWaypoints()) { + for (Mark mark : waypoint.getMarks()) { + getOrCreateTrack(mark); + if (i < 2) { + // add cache invalidation listeners for first and second waypoint's marks for + // directionFromStartToNextMarkCache + addStartToNextMarkCacheInvalidationListener(mark); + } + } + i++; + } + courseSidelines = new LinkedHashMap(); + for (Sideline sideline : sidelines) { + courseSidelines.put(sideline.getName(), sideline); + for (Mark mark : sideline.getMarks()) { + getOrCreateTrack(mark); + } + } + + trackedLegs = new LinkedHashMap(); + race.getCourse().lockForRead(); + try { + for (Leg leg : race.getCourse().getLegs()) { + trackedLegs.put(leg, createTrackedLeg(leg)); + } + getRace().getCourse().addCourseListener(this); + } finally { + race.getCourse().unlockAfterRead(); + } + markPassingsForCompetitor = new HashMap>(); + tracks = new HashMap>(); + for (Competitor competitor : race.getCompetitors()) { + markPassingsForCompetitor.put(competitor, new ConcurrentSkipListSet( + MarkPassingByTimeComparator.INSTANCE)); + tracks.put(competitor, new DynamicGPSFixMovingTrackImpl(competitor, + millisecondsOverWhichToAverageSpeed)); + } + markPassingsForWaypoint = new ConcurrentHashMap>(); + for (Waypoint waypoint : race.getCourse().getWaypoints()) { + markPassingsForWaypoint.put(waypoint, new ConcurrentSkipListSet( + MarkPassingByTimeComparator.INSTANCE)); + } + markPassingsTimes = new ArrayList>>(); + loadingFromWindStoreState = LoadingFromStoresState.NOT_STARTED; + // When this tracked race is to be serialized, wait for the loading from stores to complete. + new Thread("Mongo wind loader for tracked race " + getRace().getName()) { + @Override + public void run() { + LockUtil.lockForRead(getSerializationLock()); + LockUtil.lockForWrite(getLoadingFromWindStoreLock()); + synchronized (TrackedRaceImpl.this) { + loadingFromWindStoreState = LoadingFromStoresState.RUNNING; // indicates that the serialization lock is now safely held + TrackedRaceImpl.this.notifyAll(); + } + try { + logger.info("Started loading wind tracks for " + getRace().getName()); + final Map loadedWindTracks = windStore.loadWindTracks( + trackedRegatta.getRegatta().getName(), TrackedRaceImpl.this, millisecondsOverWhichToAverageWind); + windTracks.putAll(loadedWindTracks); + updateEventTimePoints(loadedWindTracks.values()); + logger.info("Finished loading wind tracks for " + getRace().getName() + "! Found " + windTracks.size() + " wind tracks for this race!"); + + } finally { + synchronized (TrackedRaceImpl.this) { + loadingFromWindStoreState = LoadingFromStoresState.FINISHED; + TrackedRaceImpl.this.notifyAll(); + } + synchronized (loadingFromWindStoreState) { + loadingFromWindStoreState.notifyAll(); + } + LockUtil.unlockAfterWrite(getLoadingFromWindStoreLock()); + LockUtil.unlockAfterRead(getSerializationLock()); + } + } + }.start(); + // by default, a tracked race offers one course-based wind estimation and one track-based wind estimation track; + // other wind tracks may be added as fixes are received for them and as they are loaded from the persistent + // store + WindSource courseBasedWindSource = new WindSourceImpl(WindSourceType.COURSE_BASED); + windTracks.put(courseBasedWindSource, + getOrCreateWindTrack(courseBasedWindSource, delayForWindEstimationCacheInvalidation)); + WindSource trackBasedWindSource = new WindSourceImpl(WindSourceType.TRACK_BASED_ESTIMATION); + windTracks.put(trackBasedWindSource, + getOrCreateWindTrack(trackBasedWindSource, delayForWindEstimationCacheInvalidation)); + competitorRankings = new HashMap>(); + competitorRankingsLocks = new HashMap(); + // now wait until wind loading has at least started; then we know that the serialization lock is safely held by the loader + try { + waitUntilLoadingFromWindStoreComplete(); + } catch (InterruptedException e) { + logger.log(Level.SEVERE, "Waiting for loading from stores to finish was interrupted", e); + } + } + + /** + * Assuming that the tracks were loaded from the persistent store, this method updates + * the time stamps that frame the data held by this tracked race. See {@link #timePointOfLastEvent}, {@link #timePointOfNewestEvent} + * and {@link #timePointOfOldestEvent}. + */ + private void updateEventTimePoints(Iterable> tracks) { + for (Track track : tracks) { + track.lockForRead(); + + try { + for (Timed fix : track.getRawFixes()) { + updated(fix.getTimePoint()); + } + } finally { + track.unlockAfterRead(); + } + } + } + + /** + * Object serialization obtains a read lock for the course so that in cannot change while serializing this object. + */ + private void writeObject(ObjectOutputStream s) throws IOException { + LockUtil.lockForWrite(getSerializationLock()); + try { + s.defaultWriteObject(); + } finally { + LockUtil.unlockAfterWrite(getSerializationLock()); + } + } + + /** + * Deserialization has to be maintained in lock-step with {@link #writeObject(ObjectOutputStream) serialization}. + * When de-serializing, a possibly remote {@link #windStore} is ignored because it is transient. Instead, an + * {@link EmptyWindStore} is used for the de-serialized instance. + */ + private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { + ois.defaultReadObject(); + attachedRaceLogs = new HashMap<>(); + markPassingsTimes = new ArrayList>>(); + cacheInvalidationTimerLock = new Object(); + windStore = EmptyWindStore.INSTANCE; + competitorRankings = new HashMap>(); + competitorRankingsLocks = new HashMap(); + directionFromStartToNextMarkCache = new HashMap>(); + crossTrackErrorCache = new CrossTrackErrorCache(this); + maneuverCache = createManeuverCache(); + logger.info("Deserialized race " + getRace().getName()); + } + + @Override + public synchronized void waitUntilLoadingFromWindStoreComplete() throws InterruptedException { + while (loadingFromWindStoreState != LoadingFromStoresState.FINISHED) { + wait(); + } + } + + @Override + public synchronized void waitUntilLoadingFromGPSFixStoreComplete() throws InterruptedException { + synchronized (loadingFromGPSFixStoreStateMonitor) { + while (loadingFromGPSFixStoreState != LoadingFromStoresState.FINISHED) { + loadingFromGPSFixStoreStateMonitor.wait(); + } + } + } + + private SmartFutureCache>, EmptyUpdateInterval> createManeuverCache() { + return new SmartFutureCache>, EmptyUpdateInterval>( + new AbstractCacheUpdater>, EmptyUpdateInterval>() { + @Override + public com.sap.sse.common.Util.Triple> computeCacheUpdate(Competitor competitor, + EmptyUpdateInterval updateInterval) throws NoWindException { + return computeManeuvers(competitor); + } + }, /* nameForLocks */"Maneuver cache for race " + getRace().getName()); + } + + /** + * Precondition: race has already been set, e.g., in constructor before this method is called + */ + abstract protected TrackedLeg createTrackedLeg(Leg leg); + + public RegattaAndRaceIdentifier getRaceIdentifier() { + return new RegattaNameAndRaceName(getTrackedRegatta().getRegatta().getName(), getRace().getName()); + } + + @Override + public NavigableSet getMarkPassings(Competitor competitor) { + return markPassingsForCompetitor.get(competitor); + } + + protected NavigableSet getMarkPassingsInOrderAsNavigableSet(Waypoint waypoint) { + return markPassingsForWaypoint.get(waypoint); + } + + @Override + public WindStore getWindStore() { + return windStore; + } + + @Override + public Iterable getMarkPassingsInOrder(Waypoint waypoint) { + return getMarkPassingsInOrderAsNavigableSet(waypoint); + } + + protected NavigableSet getOrCreateMarkPassingsInOrderAsNavigableSet(Waypoint waypoint) { + NavigableSet result = getMarkPassingsInOrderAsNavigableSet(waypoint); + if (result == null) { + result = createMarkPassingsCollectionForWaypoint(waypoint); + } + return result; + } + + protected NavigableSet createMarkPassingsCollectionForWaypoint(Waypoint waypoint) { + final ConcurrentSkipListSet result = new ConcurrentSkipListSet( + MarkPassingByTimeComparator.INSTANCE); + LockUtil.lockForRead(getSerializationLock()); + try { + markPassingsForWaypoint.put(waypoint, result); + } finally { + LockUtil.unlockAfterRead(getSerializationLock()); + } + return result; + } + + @Override + public TimePoint getStartOfTracking() { + return startOfTrackingReceived; + } + + @Override + public TimePoint getEndOfTracking() { + return endOfTrackingReceived; + } + + public void invalidateStartTime() { + startTime = null; + } + + public void invalidateEndTime() { + endTime = null; + } + + protected void invalidateMarkPassingTimes() { + synchronized (markPassingsTimes) { + markPassingsTimes.clear(); + } + } + + /** + * Calculates the start time of the race from various sources. The highest precedence take the {@link #attachedRaceLogs race logs}, + * followed by the field {@link #startTimeReceived} which can explicitly be set using {@link #setStartTimeReceived(TimePoint)}. + * If that does not provide any start time either, a start time is attempted to be inferred from the time points + * of the start mark passing events. + */ + @Override + public TimePoint getStartOfRace() { + if (startTime == null) { + for (RaceLog raceLog : attachedRaceLogs.values()) { + startTime = new StartTimeFinder(raceLog).analyze(); + if (startTime != null) { + break; + } + } + if (startTime == null) { + startTime = getStartTimeReceived(); + // If not null, check if the first mark passing for the start line is too much after the + // startTimeReceived; if so, return an adjusted, later start time. + // If no official start time was received, try to estimate the start time using the mark passings for + // the start line. + final Waypoint firstWaypoint = getRace().getCourse().getFirstWaypoint(); + if (firstWaypoint != null) { + if (startTimeReceived != null) { + TimePoint timeOfFirstMarkPassing = getFirstPassingTime(firstWaypoint); + if (timeOfFirstMarkPassing != null) { + long startTimeReceived2timeOfFirstMarkPassingFirstMark = timeOfFirstMarkPassing.asMillis() + - startTimeReceived.asMillis(); + if (startTimeReceived2timeOfFirstMarkPassingFirstMark > MAX_TIME_BETWEEN_START_AND_FIRST_MARK_PASSING_IN_MILLISECONDS) { + startTime = new MillisecondsTimePoint(timeOfFirstMarkPassing.asMillis() + - MAX_TIME_BETWEEN_START_AND_FIRST_MARK_PASSING_IN_MILLISECONDS); + } else { + startTime = startTimeReceived; + } + } + } else { + final NavigableSet markPassingsForFirstWaypointInOrder = getMarkPassingsInOrderAsNavigableSet(firstWaypoint); + if (markPassingsForFirstWaypointInOrder != null) { + startTime = calculateStartOfRaceFromMarkPassings(markPassingsForFirstWaypointInOrder, + getRace().getCompetitors()); + } + } + } + } + } + return startTime; + } + + /** + * Calculates the end time of the race from the mark passings of the last course waypoint + */ + @Override + public TimePoint getEndOfRace() { + if (endTime == null) { + endTime = getLastPassingOfFinishLine(); + } + return endTime; + } + + private TimePoint getLastPassingOfFinishLine() { + TimePoint passingTime = null; + final Waypoint lastWaypoint = getRace().getCourse().getLastWaypoint(); + if (lastWaypoint != null) { + Iterable markPassingsInOrder = getMarkPassingsInOrder(lastWaypoint); + if (markPassingsInOrder != null) { + lockForRead(markPassingsInOrder); + try { + for (MarkPassing passingFinishLine : markPassingsInOrder) { + passingTime = passingFinishLine.getTimePoint(); + } + } finally { + unlockAfterRead(markPassingsInOrder); + } + } + } + return passingTime; + } + + private TimePoint getFirstPassingTime(Waypoint waypoint) { + NavigableSet markPassingsInOrder = getMarkPassingsInOrderAsNavigableSet(waypoint); + MarkPassing firstMarkPassing = null; + if (markPassingsInOrder != null) { + lockForRead(markPassingsInOrder); + try { + if (!markPassingsInOrder.isEmpty()) { + firstMarkPassing = markPassingsInOrder.first(); + } + } finally { + unlockAfterRead(markPassingsInOrder); + } + } + TimePoint timeOfFirstMarkPassing = null; + if (firstMarkPassing != null) { + timeOfFirstMarkPassing = firstMarkPassing.getTimePoint(); + } + return timeOfFirstMarkPassing; + } + + private TimePoint calculateStartOfRaceFromMarkPassings(NavigableSet markPassings, + Iterable competitors) { + TimePoint startOfRace = null; + // Find the first mark passing within the largest cluster crossing the line within one minute. + final long ONE_MINUTE_IN_MILLIS = 60 * 1000; + lockForRead(markPassings); + try { + if (markPassings != null) { + int largestStartGroupWithinOneMinuteSize = 0; + MarkPassing startOfLargestGroupSoFar = null; + int candiateGroupSize = 0; + MarkPassing candidateForStartOfLargestGroupSoFar = null; + Iterator iterator = markPassings.iterator(); + // sweep over all start mark passings and for each element find the number of competitors that passed + // the start up to one minute later; + // pick the start mark passing of the competitor leading the largest such group + while (iterator.hasNext()) { + MarkPassing currentMarkPassing = iterator.next(); + if (candidateForStartOfLargestGroupSoFar == null) { + // first start mark passing + candidateForStartOfLargestGroupSoFar = currentMarkPassing; + candiateGroupSize = 1; + startOfLargestGroupSoFar = currentMarkPassing; + largestStartGroupWithinOneMinuteSize = 1; + } else { + if (currentMarkPassing.getTimePoint().asMillis() + - candidateForStartOfLargestGroupSoFar.getTimePoint().asMillis() <= ONE_MINUTE_IN_MILLIS) { + // currentMarkPassing is within one minute of candidateForStartOfLargestGroupSoFar; extend + // candidate group... + candiateGroupSize++; + if (candiateGroupSize > largestStartGroupWithinOneMinuteSize) { + // ...and remember as best fit if greater than largest group so far + startOfLargestGroupSoFar = candidateForStartOfLargestGroupSoFar; + largestStartGroupWithinOneMinuteSize = candiateGroupSize; + } + } else { + // currentMarkPassing is more than a minute after candidateForStartOfLargestGroupSoFar; + // advance + // candidateForStartOfLargestGroupSoFar and reduce group size counter, until + // candidateForStartOfLargestGroupSoFar + // is again within the one-minute interval; may catch up all the way to currentMarkPassing + // if that was + // more than a minute after its predecessor + while (currentMarkPassing.getTimePoint().asMillis() + - candidateForStartOfLargestGroupSoFar.getTimePoint().asMillis() > ONE_MINUTE_IN_MILLIS) { + candidateForStartOfLargestGroupSoFar = markPassings + .higher(candidateForStartOfLargestGroupSoFar); + candiateGroupSize--; + } + } + } + } + startOfRace = startOfLargestGroupSoFar == null ? null : startOfLargestGroupSoFar.getTimePoint(); + } + } finally { + unlockAfterRead(markPassings); + } + return startOfRace; + } + + @Override + public boolean hasStarted(TimePoint at) { + return getStartOfRace() != null && getStartOfRace().compareTo(at) <= 0; + } + + @Override + public boolean isLive(TimePoint at) { + final Date startOfLivePeriod; + final Date endOfLivePeriod; + if (!hasGPSData() || !hasWindData()) { + startOfLivePeriod = null; + endOfLivePeriod = null; + } else { + if (getStartOfRace() == null) { + startOfLivePeriod = getStartOfTracking().asDate(); + } else { + startOfLivePeriod = new Date(getStartOfRace().asMillis() - TimingConstants.PRE_START_PHASE_DURATION_IN_MILLIS); + } + if (getEndOfRace() == null) { + if (getTimePointOfNewestEvent() != null) { + endOfLivePeriod = new Date(getTimePointOfNewestEvent().asMillis() + + TimingConstants.IS_LIVE_GRACE_PERIOD_IN_MILLIS); + } else { + endOfLivePeriod = null; + } + } else { + endOfLivePeriod = new Date(getEndOfRace().asMillis() + TimingConstants.IS_LIVE_GRACE_PERIOD_IN_MILLIS); + } + } + + // if an empty timepoint is given then take the start of the race + if (at == null) { + at = new MillisecondsTimePoint(startOfLivePeriod.getTime()+1); + } + + // whenLastTrackedRaceWasLive is null if there is no tracked race for fleet, or the tracked race hasn't started yet at the server time + // when this DTO was assembled, or there were no GPS or wind data + final boolean result = + startOfLivePeriod != null && + endOfLivePeriod != null && + startOfLivePeriod.getTime() <= at.asMillis() && + at.asMillis() <= endOfLivePeriod.getTime(); + return result; + } + + @Override + public RaceDefinition getRace() { + return race; + } + + @Override + public Iterable getTrackedLegs() { + // ensure that no course modification is carried out while copying the tracked legs + getRace().getCourse().lockForRead(); + try { + return new ArrayList(trackedLegs.values()); + } finally { + getRace().getCourse().unlockAfterRead(); + } + } + + @Override + public Iterable>> getMarkPassingsTimes() { + getRace().getCourse().lockForRead(); // ensure the list of waypoints doesn't change while we're updating the + // markPassingTimes structure + try { + synchronized (markPassingsTimes) { + if (markPassingsTimes.isEmpty()) { + // Remark: sometimes it can happen that a mark passing with a wrong time stamp breaks the right time + // order of the waypoint times + Date previousLegPassingTime = null; + for (Waypoint waypoint : getRace().getCourse().getWaypoints()) { + TimePoint firstPassingTime = null; + TimePoint lastPassingTime = null; + NavigableSet markPassings = getMarkPassingsInOrderAsNavigableSet(waypoint); + if (markPassings != null && !markPassings.isEmpty()) { + // ensure the leg times are in the right time order; there may perhaps be left-overs for + // marks to be reached later that + // claim it has been passed in the past which may have been an accidental tracker + // read-out; + // the results of getMarkPassingsInOrder(to) has by definition an ascending time-point + // ordering + lockForRead(markPassings); + try { + for (MarkPassing currentMarkPassing : markPassings) { + Date currentPassingDate = currentMarkPassing.getTimePoint().asDate(); + if (previousLegPassingTime == null + || currentPassingDate.after(previousLegPassingTime)) { + firstPassingTime = currentMarkPassing.getTimePoint(); + previousLegPassingTime = currentPassingDate; + break; + } + } + } finally { + unlockAfterRead(markPassings); + } + } + com.sap.sse.common.Util.Pair timesPair = new com.sap.sse.common.Util.Pair(firstPassingTime, + lastPassingTime); + markPassingsTimes.add(new com.sap.sse.common.Util.Pair>(waypoint, timesPair)); + } + } + return markPassingsTimes; + } + } finally { + getRace().getCourse().unlockAfterRead(); + } + } + + @Override + public Distance getDistanceTraveled(Competitor competitor, TimePoint timePoint) { + final Distance result; + NavigableSet markPassings = getMarkPassings(competitor); + try { + lockForRead(markPassings); + if (markPassings.isEmpty()) { + result = null; + } else { + TimePoint end = timePoint; + if (markPassings.last().getWaypoint() == getRace().getCourse().getLastWaypoint() + && timePoint.compareTo(markPassings.last().getTimePoint()) > 0) { + // competitor has finished race; use time point of crossing the finish line + end = markPassings.last().getTimePoint(); + } else if (markPassings.last().getWaypoint() != getRace().getCourse().getLastWaypoint() + && ((getEndOfTracking() != null && timePoint.after(getEndOfTracking())) + || getStatus().getStatus() == TrackedRaceStatusEnum.FINISHED)) { + // If the race is no longer tracking and hence no more data can be expected, and the competitor + // hasn't finished the race, no valid distance traveled can be determined + // for the competitor in this race. + end = null; + } + if (end == null) { + result = null; + } else { + result = getTrack(competitor).getDistanceTraveled(markPassings.first().getTimePoint(), end); + } + } + return result; + } finally { + unlockAfterRead(markPassings); + } + } + + @Override + public GPSFixTrack getTrack(Competitor competitor) { + return tracks.get(competitor); + } + + @Override + public TrackedLeg getTrackedLegFinishingAt(Waypoint endOfLeg) { + getRace().getCourse().lockForRead(); + try { + int indexOfWaypoint = getRace().getCourse().getIndexOfWaypoint(endOfLeg); + if (indexOfWaypoint == -1) { + throw new IllegalArgumentException("Waypoint " + endOfLeg + " not found in " + getRace().getCourse()); + } else if (indexOfWaypoint == 0) { + throw new IllegalArgumentException("Waypoint " + endOfLeg + " isn't start of any leg in " + + getRace().getCourse()); + } + return trackedLegs.get(race.getCourse().getLegs().get(indexOfWaypoint - 1)); + } finally { + getRace().getCourse().unlockAfterRead(); + } + } + + @Override + public TrackedLeg getTrackedLegStartingAt(Waypoint startOfLeg) { + getRace().getCourse().lockForRead(); + try { + int indexOfWaypoint = getRace().getCourse().getIndexOfWaypoint(startOfLeg); + if (indexOfWaypoint == -1) { + throw new IllegalArgumentException("Waypoint " + startOfLeg + " not found in " + getRace().getCourse()); + } else if (indexOfWaypoint == Util.size(getRace().getCourse().getWaypoints()) - 1) { + throw new IllegalArgumentException("Waypoint " + startOfLeg + " isn't start of any leg in " + + getRace().getCourse()); + } + return trackedLegs.get(race.getCourse().getLegs().get(indexOfWaypoint)); + } finally { + getRace().getCourse().unlockAfterRead(); + } + } + + @Override + public TrackedLegOfCompetitor getTrackedLeg(Competitor competitor, TimePoint at) { + NavigableSet roundings = getMarkPassings(competitor); + lockForRead(roundings); + try { + NavigableSet localRoundings = new ArrayListNavigableSet<>(roundings.size(), + new TimedComparator()); + localRoundings.addAll(roundings); + } finally { + unlockAfterRead(roundings); + } + TrackedLegOfCompetitor result = null; + if (roundings != null) { + TrackedLeg trackedLeg; + // obtain last waypoint before obtaining mark passings monitor because obtaining the last waypoint + // obtains the read lock for the course + final Waypoint lastWaypoint = getRace().getCourse().getLastWaypoint(); + MarkPassing lastBeforeOrAt = roundings.floor(new DummyMarkPassingWithTimePointOnly(at)); + // already finished the race? + if (lastBeforeOrAt != null) { + // and not at or after last mark passing + if (lastWaypoint != lastBeforeOrAt.getWaypoint()) { + trackedLeg = getTrackedLegStartingAt(lastBeforeOrAt.getWaypoint()); + } else { + // exactly *at* last mark passing? + if (!roundings.isEmpty() && at.equals(roundings.last().getTimePoint())) { + // exactly at finish line; return last leg + trackedLeg = getTrackedLegFinishingAt(lastBeforeOrAt.getWaypoint()); + } else { + // no, then we're after the last mark passing + trackedLeg = null; + } + } + } else { + // before beginning of race + trackedLeg = null; + } + if (trackedLeg != null) { + result = trackedLeg.getTrackedLeg(competitor); + } + } + return result; + } + + public TrackedLeg getTrackedLeg(Leg leg) { + getRace().getCourse().lockForRead(); + try { + return trackedLegs.get(leg); + } finally { + getRace().getCourse().unlockAfterRead(); + } + } + + @Override + public TrackedLegOfCompetitor getTrackedLeg(Competitor competitor, Leg leg) { + final TrackedLeg trackedLeg = getTrackedLeg(leg); + return trackedLeg == null ? null : trackedLeg.getTrackedLeg(competitor); + } + + @Override + public long getUpdateCount() { + return updateCount; + } + + @Override + public int getRankDifference(Competitor competitor, Leg leg, TimePoint timePoint) { + int previousRank; + if (leg == getRace().getCourse().getFirstLeg()) { + // first leg; report rank difference from 0 + previousRank = 0; + } else { + TrackedLeg previousLeg = getTrackedLegFinishingAt(leg.getFrom()); + previousRank = previousLeg.getTrackedLeg(competitor).getRank(timePoint); + } + int currentRank = getTrackedLeg(competitor, leg).getRank(timePoint); + return currentRank - previousRank; + } + + @Override + public int getRank(Competitor competitor) throws NoWindException { + return getRank(competitor, MillisecondsTimePoint.now()); + } + + @Override + public Competitor getOverallLeader(TimePoint timePoint) throws NoWindException { + try { + Competitor result = null; + List ranks = getCompetitorsFromBestToWorst(timePoint); + if (ranks != null && !ranks.isEmpty()) { + result = ranks.iterator().next(); + } + return result; + } catch (NoWindError e) { + throw e.getCause(); + } + } + + @Override + public int getRank(Competitor competitor, TimePoint timePoint) throws NoWindException { + try { + int result; + if (getMarkPassings(competitor).isEmpty()) { + result = 0; + } else { + result = getCompetitorsFromBestToWorst(timePoint).indexOf(competitor) + 1; + } + return result; + } catch (NoWindError e) { + throw e.getCause(); + } + } + + @Override + public List getCompetitorsFromBestToWorst(TimePoint timePoint) throws NoWindException { + NamedReentrantReadWriteLock readWriteLock; + synchronized (competitorRankingsLocks) { + readWriteLock = competitorRankingsLocks.get(timePoint); + if (readWriteLock == null) { + readWriteLock = new NamedReentrantReadWriteLock("competitor rankings for race " + getRace().getName() + + " for time point " + timePoint, /* fair */false); + competitorRankingsLocks.put(timePoint, readWriteLock); + } + } + List rankedCompetitors; + final boolean lockForWrite; + synchronized (competitorRankings) { + rankedCompetitors = competitorRankings.get(timePoint); + if (rankedCompetitors == null) { + lockForWrite = true; + } else { + lockForWrite = false; + } + } + if (lockForWrite) { + LockUtil.lockForWrite(readWriteLock); + } else { + LockUtil.lockForRead(readWriteLock); + } + try { + if (rankedCompetitors == null) { + rankedCompetitors = competitorRankings.get(timePoint); // try again; maybe a writer released the write + // lock after updating the cache + if (rankedCompetitors == null) { + RaceRankComparator comparator = new RaceRankComparator(this, timePoint); + rankedCompetitors = new ArrayList(); + for (Competitor c : getRace().getCompetitors()) { + rankedCompetitors.add(c); + } + Collections.sort(rankedCompetitors, comparator); + synchronized (competitorRankings) { + competitorRankings.put(timePoint, rankedCompetitors); + } + } + } + return rankedCompetitors; + } finally { + if (lockForWrite) { + LockUtil.unlockAfterWrite(readWriteLock); + } else { + LockUtil.unlockAfterRead(readWriteLock); + } + } + } + + @Override + public Distance getAverageAbsoluteCrossTrackError(Competitor competitor, TimePoint timePoint, boolean waitForLatestAnalysis) + throws NoWindException { + NavigableSet markPassings = getMarkPassings(competitor); + TimePoint from = null; + lockForRead(markPassings); + try { + if (markPassings != null && !markPassings.isEmpty()) { + from = markPassings.iterator().next().getTimePoint(); + } + } finally { + unlockAfterRead(markPassings); + } + Distance result; + if (from != null) { + result = getAverageAbsoluteCrossTrackError(competitor, from, timePoint, /* upwindOnly */true, waitForLatestAnalysis); + } else { + result = null; + } + return result; + } + + @Override + public Distance getAverageSignedCrossTrackError(Competitor competitor, TimePoint timePoint, boolean waitForLatestAnalysis) + throws NoWindException { + NavigableSet markPassings = getMarkPassings(competitor); + TimePoint from = null; + lockForRead(markPassings); + try { + if (markPassings != null && !markPassings.isEmpty()) { + from = markPassings.iterator().next().getTimePoint(); + } + } finally { + unlockAfterRead(markPassings); + } + Distance result; + if (from != null) { + result = getAverageSignedCrossTrackError(competitor, from, timePoint, /* upwindOnly */true, waitForLatestAnalysis); + } else { + result = null; + } + return result; + } + + @Override + public Distance getAverageAbsoluteCrossTrackError(Competitor competitor, TimePoint from, TimePoint to, boolean upwindOnly, + boolean waitForLatestAnalysis) throws NoWindException { + Distance result; + result = crossTrackErrorCache + .getAverageAbsoluteCrossTrackError(competitor, from, to, upwindOnly, waitForLatestAnalysis); + return result; + } + + @Override + public Distance getAverageSignedCrossTrackError(Competitor competitor, TimePoint from, TimePoint to, boolean upwindOnly, + boolean waitForLatestAnalysis) throws NoWindException { + Distance result; + result = crossTrackErrorCache + .getAverageSignedCrossTrackError(competitor, from, to, upwindOnly, waitForLatestAnalysis); + return result; + } + + @Override + public TrackedLegOfCompetitor getCurrentLeg(Competitor competitor, TimePoint timePoint) { + NavigableSet competitorMarkPassings = markPassingsForCompetitor.get(competitor); + DummyMarkPassingWithTimePointOnly markPassingTimePoint = new DummyMarkPassingWithTimePointOnly(timePoint); + TrackedLegOfCompetitor result = null; + if (!competitorMarkPassings.isEmpty()) { + MarkPassing lastMarkPassingAtOfBeforeTimePoint = competitorMarkPassings.floor(markPassingTimePoint); + if (lastMarkPassingAtOfBeforeTimePoint != null) { + Waypoint waypointPassedLastAtOrBeforeTimePoint = lastMarkPassingAtOfBeforeTimePoint.getWaypoint(); + // don't return a leg if competitor has already finished last leg and therefore the race + if (waypointPassedLastAtOrBeforeTimePoint != getRace().getCourse().getLastWaypoint()) { + result = getTrackedLegStartingAt(waypointPassedLastAtOrBeforeTimePoint).getTrackedLeg(competitor); + } + } + } + return result; + } + + @Override + public TrackedLeg getCurrentLeg(TimePoint timePoint) { + Waypoint lastWaypointPassed = null; + int indexOfLastWaypointPassed = -1; + for (Map.Entry> entry : markPassingsForWaypoint.entrySet()) { + if (!entry.getValue().isEmpty()) { + MarkPassing first = entry.getValue().first(); + // Did the mark passing happen at or before the requested time point? + if (first.getTimePoint().compareTo(timePoint) <= 0) { + int indexOfWaypoint = getRace().getCourse().getIndexOfWaypoint(entry.getKey()); + if (indexOfWaypoint > indexOfLastWaypointPassed) { + indexOfLastWaypointPassed = indexOfWaypoint; + lastWaypointPassed = entry.getKey(); + } + } + } + } + TrackedLeg result = null; + if (lastWaypointPassed != null && lastWaypointPassed != getRace().getCourse().getLastWaypoint()) { + result = getTrackedLegStartingAt(lastWaypointPassed); + } + return result; + } + + @Override + public MarkPassing getMarkPassing(Competitor competitor, Waypoint waypoint) { + final NavigableSet markPassings = getMarkPassings(competitor); + if (markPassings != null) { + lockForRead(markPassings); + try { + for (MarkPassing markPassing : markPassings) { + if (markPassing.getWaypoint() == waypoint) { + return markPassing; + } + } + } finally { + unlockAfterRead(markPassings); + } + } + return null; + } + + /** + * This method was a synchronization bottleneck when it was using a regular HashMap for {@link #markTracks}. It is + * frequently used, and the most frequent case is that the get call on {@link #markTracks} succeeds + * with a non-null result. To improve performance for this case, {@link #markTracks} now is a + * {@link ConcurrentHashMap} that can be read while writes are going on without locking or synchronization. Only if + * the get call does not provide a result, the entire procedure is repeated, this time with + * synchronization to avoid duplicate track creation for the same mark. + */ + @Override + public GPSFixTrack getOrCreateTrack(Mark mark) { + GPSFixTrack result = markTracks.get(mark); + if (result == null) { + // try again, this time with more expensive synchronization + synchronized (markTracks) { + LockUtil.lockForRead(getSerializationLock()); + try { + result = markTracks.get(mark); + if (result == null) { + result = createMarkTrack(mark); + markTracks.put(mark, result); + } + } finally { + LockUtil.unlockAfterRead(getSerializationLock()); + } + } + } + return result; + } + + protected DynamicGPSFixTrackImpl createMarkTrack(Mark mark) { + return new DynamicGPSFixTrackImpl(mark, millisecondsOverWhichToAverageSpeed); + } + + @Override + public Position getApproximatePosition(Waypoint waypoint, TimePoint timePoint) { + Position result = null; + for (Mark mark : waypoint.getMarks()) { + Position nextPos = getOrCreateTrack(mark).getEstimatedPosition(timePoint, /* extrapolate */false); + if (result == null) { + result = nextPos; + } else if (nextPos != null) { + result = result.translateGreatCircle(result.getBearingGreatCircle(nextPos), result.getDistance(nextPos) + .scale(0.5)); + } + } + return result; + } + + @Override + public boolean hasWindData() { + boolean result = false; + Course course = getRace().getCourse(); + Waypoint firstWaypoint = course.getFirstWaypoint(); + TimePoint timepoint = startTime != null ? startTime : startOfTrackingReceived; + if (firstWaypoint != null && timepoint != null) { + Position position = getApproximatePosition(firstWaypoint, timepoint); + if (position != null) { + Wind wind = getWind(position, timepoint); + if (wind != null) { + result = true; + } + } + } + return result; + } + + @Override + public boolean hasGPSData() { + boolean result = false; + if (!tracks.values().isEmpty()) { + for (GPSFixTrack gpsTrack : tracks.values()) { + if (gpsTrack.getFirstRawFix() != null) { + result = true; + break; + } + } + } + return result; + } + + @Override + public Wind getWind(Position p, TimePoint at) { + return getWind(p, at, getWindSourcesToExclude()); + } + + @Override + public Wind getWind(Position p, TimePoint at, Iterable windSourcesToExclude) { + final WindWithConfidence> windWithConfidence = getWindWithConfidence(p, at, + windSourcesToExclude); + return windWithConfidence == null ? null : windWithConfidence.getObject(); + } + + @Override + public WindWithConfidence> getWindWithConfidence(Position p, TimePoint at) { + return getWindWithConfidence(p, at, getWindSourcesToExclude()); + } + + @Override + public Iterable getWindSourcesToExclude() { + synchronized (windSourcesToExclude) { + return Collections.unmodifiableCollection(windSourcesToExclude); + } + } + + @Override + public void setWindSourcesToExclude(Iterable windSourcesToExclude) { + Set old = new HashSet<>(this.windSourcesToExclude); + synchronized (this.windSourcesToExclude) { + LockUtil.lockForRead(getSerializationLock()); + try { + this.windSourcesToExclude.clear(); + for (WindSource windSourceToExclude : windSourcesToExclude) { + this.windSourcesToExclude.add(windSourceToExclude); + } + } finally { + LockUtil.unlockAfterRead(getSerializationLock()); + } + } + if (!old.equals(this.windSourcesToExclude)) { + clearAllCachesExceptManeuvers(); + triggerManeuverCacheRecalculationForAllCompetitors(); + } + } + + @Override + public WindWithConfidence> getWindWithConfidence(Position p, TimePoint at, + Iterable windSourcesToExclude) { + boolean canUseSpeedOfAtLeastOneWindSource = false; + Weigher> timeWeigherThatPretendsToAlsoWeighPositions = new PositionAndTimePointWeigher( + /* halfConfidenceAfterMilliseconds */10000l); + ConfidenceBasedWindAverager> averager = ConfidenceFactory.INSTANCE + .createWindAverager(timeWeigherThatPretendsToAlsoWeighPositions); + List>> windFixesWithConfidences = new ArrayList>>(); + for (WindSource windSource : getWindSources()) { + // TODO consider parallelizing and consider caching + if (!Util.contains(windSourcesToExclude, windSource)) { + WindTrack track = getOrCreateWindTrack(windSource); + WindWithConfidence> windWithConfidence = track.getAveragedWindWithConfidence(p, at); + if (windWithConfidence != null) { + windFixesWithConfidences.add(windWithConfidence); + canUseSpeedOfAtLeastOneWindSource = canUseSpeedOfAtLeastOneWindSource + || windSource.getType().useSpeed(); + } + } + } + HasConfidence> average = averager.getAverage( + windFixesWithConfidences, new com.sap.sse.common.Util.Pair(p, at)); + WindWithConfidence> result = average == null ? null + : new WindWithConfidenceImpl>(average.getObject(), average.getConfidence(), + new com.sap.sse.common.Util.Pair(p, at), canUseSpeedOfAtLeastOneWindSource); + return result; + } + + @Override + public Wind getDirectionFromStartToNextMark(final TimePoint at) { + Future future; + FutureTask newFuture = null; + synchronized (directionFromStartToNextMarkCache) { + future = directionFromStartToNextMarkCache.get(at); + if (future == null) { + newFuture = new FutureTask(new Callable() { + @Override + public Wind call() { + Wind result; + Leg firstLeg = getRace().getCourse().getFirstLeg(); + if (firstLeg != null) { + Position firstLegEnd = getApproximatePosition(firstLeg.getTo(), at); + Position firstLegStart = getApproximatePosition(firstLeg.getFrom(), at); + if (firstLegStart != null && firstLegEnd != null) { + result = new WindImpl(firstLegStart, at, new KnotSpeedWithBearingImpl(1.0, + firstLegEnd.getBearingGreatCircle(firstLegStart))); + } else { + result = null; + } + } else { + result = null; + } + return result; + } + }); + directionFromStartToNextMarkCache.put(at, newFuture); + } + } + if (newFuture != null) { + newFuture.run(); + future = newFuture; + } + try { + return future.get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + + @Override + public TimePoint getTimePointOfOldestEvent() { + return timePointOfOldestEvent; + } + + @Override + public TimePoint getTimePointOfNewestEvent() { + return timePointOfNewestEvent; + } + + @Override + public TimePoint getTimePointOfLastEvent() { + return timePointOfLastEvent; + } + + /** + * @param timeOfEvent + * may be null meaning to only unblock waiters but not update any time points + */ + protected void updated(TimePoint timeOfEvent) { + updateCount++; + clearAllCachesExceptManeuvers(); + if (timeOfEvent != null) { + if (timePointOfNewestEvent == null || timePointOfNewestEvent.compareTo(timeOfEvent) < 0) { + timePointOfNewestEvent = timeOfEvent; + } + if (timePointOfOldestEvent == null || timePointOfOldestEvent.compareTo(timeOfEvent) > 0) { + timePointOfOldestEvent = timeOfEvent; + } + timePointOfLastEvent = timeOfEvent; + } + synchronized (this) { + notifyAll(); + } + } + + protected void setStartTimeReceived(TimePoint start) { + if ((start == null) != (startTimeReceived == null) || (start != null && !start.equals(startTimeReceived))) { + this.startTimeReceived = start; + invalidateStartTime(); + invalidateMarkPassingTimes(); + } + } + + @Override + public TimePoint getStartTimeReceived() { + return startTimeReceived; + } + + protected void setStartOfTrackingReceived(TimePoint startOfTracking) { + this.startOfTrackingReceived = startOfTracking; + } + + protected void setEndOfTrackingReceived(TimePoint endOfTracking) { + this.endOfTrackingReceived = endOfTracking; + } + + /** + * Schedules the clearing of the caches. If a cache clearing is already scheduled, this is a no-op. + */ + private void clearAllCachesExceptManeuvers() { + synchronized (cacheInvalidationTimerLock) { + if (cacheInvalidationTimer == null) { + cacheInvalidationTimer = new Timer("Cache invalidation timer for TrackedRaceImpl " + + getRace().getName()); + cacheInvalidationTimer.schedule(new TimerTask() { + @Override + public void run() { + synchronized (cacheInvalidationTimerLock) { + cacheInvalidationTimer.cancel(); + cacheInvalidationTimer = null; + } + synchronized (competitorRankings) { + competitorRankings.clear(); + } + synchronized (competitorRankingsLocks) { + competitorRankingsLocks.clear(); + } + } + }, DELAY_FOR_CACHE_CLEARING_IN_MILLISECONDS); + } + } + } + + @Override + public synchronized void waitForNextUpdate(int sinceUpdate) throws InterruptedException { + while (updateCount <= sinceUpdate) { + wait(); // ...until updated(...) notifies us + } + } + + @Override + public void waypointAdded(int zeroBasedIndex, Waypoint waypointThatGotAdded) { + // expecting to hold the course's write lock + invalidateMarkPassingTimes(); + LockUtil.lockForRead(getSerializationLock()); + try { + // assuming that getRace().getCourse()'s write lock is held by the current thread + updateStartToNextMarkCacheInvalidationCacheListenersAfterWaypointAdded(zeroBasedIndex, waypointThatGotAdded); + getOrCreateMarkPassingsInOrderAsNavigableSet(waypointThatGotAdded); + for (Mark mark : waypointThatGotAdded.getMarks()) { + getOrCreateTrack(mark); + } + // a waypoint got added; this means that a leg got added as well; but we shouldn't claim we know where + // in the leg list of the course the leg was added; that's an implementation secret of CourseImpl. So try: + LinkedHashMap reorderedTrackedLegs = new LinkedHashMap(); + List newLegs = getRace().getCourse().getLegs(); + for (Leg leg : newLegs) { + TrackedLeg trackedLeg = trackedLegs.get(leg); + if (trackedLeg != null) { + reorderedTrackedLegs.put(leg, trackedLeg); + } else { + reorderedTrackedLegs.put(leg, createTrackedLeg(leg)); + } + } + // now ensure that the iteration order is in sync with the leg iteration order + trackedLegs.clear(); + for (Map.Entry entry : reorderedTrackedLegs.entrySet()) { + trackedLegs.put(entry.getKey(), entry.getValue()); + entry.getValue().waypointsMayHaveChanges(); + } + updated(/* time point */null); // no maneuver cache invalidation required because we don't yet have mark + // passings for new waypoint + } finally { + LockUtil.unlockAfterRead(getSerializationLock()); + } + } + + private void updateStartToNextMarkCacheInvalidationCacheListenersAfterWaypointAdded(int zeroBasedIndex, + Waypoint waypointThatGotAdded) { + if (zeroBasedIndex < 2) { + // the observing listener on any previous mark will be GCed; we need to ensure + // that the cache is recomputed + clearDirectionFromStartToNextMarkCache(); + Iterator waypointsIter = getRace().getCourse().getWaypoints().iterator(); + waypointsIter.next(); // skip first + if (waypointsIter.hasNext()) { + waypointsIter.next(); // skip second + if (waypointsIter.hasNext()) { + Waypoint oldSecond = waypointsIter.next(); + stopAndRemoveStartToNextMarkCacheInvalidationListener(oldSecond); + } + } + } + addStartToNextMarkCacheInvalidationListener(waypointThatGotAdded); + } + + private void clearDirectionFromStartToNextMarkCache() { + synchronized (directionFromStartToNextMarkCache) { + directionFromStartToNextMarkCache.clear(); + } + } + + private void addStartToNextMarkCacheInvalidationListener(Waypoint waypoint) { + for (Mark mark : waypoint.getMarks()) { + addStartToNextMarkCacheInvalidationListener(mark); + } + } + + private void addStartToNextMarkCacheInvalidationListener(Mark mark) { + GPSFixTrack track = getOrCreateTrack(mark); + StartToNextMarkCacheInvalidationListener listener = new StartToNextMarkCacheInvalidationListener(track); + LockUtil.lockForRead(getSerializationLock()); + try { + startToNextMarkCacheInvalidationListeners.put(mark, listener); + } finally { + LockUtil.unlockAfterRead(getSerializationLock()); + } + track.addListener(listener); + } + + private void stopAndRemoveStartToNextMarkCacheInvalidationListener(Waypoint waypoint) { + for (Mark mark : waypoint.getMarks()) { + stopAndRemoveStartToNextMarkCacheInvalidationListener(mark); + } + } + + private void stopAndRemoveStartToNextMarkCacheInvalidationListener(Mark mark) { + StartToNextMarkCacheInvalidationListener listener = startToNextMarkCacheInvalidationListeners.get(mark); + if (listener != null) { + listener.stopListening(); + LockUtil.lockForRead(getSerializationLock()); + try { + startToNextMarkCacheInvalidationListeners.remove(mark); + } finally { + LockUtil.unlockAfterRead(getSerializationLock()); + } + } + } + + @Override + public void waypointRemoved(int zeroBasedIndex, Waypoint waypointThatGotRemoved) { + invalidateMarkPassingTimes(); + LockUtil.lockForRead(getSerializationLock()); + try { + // assuming that getRace().getCourse()'s write lock is held by the current thread + updateStartToNextMarkCacheInvalidationCacheListenersAfterWaypointRemoved(zeroBasedIndex, + waypointThatGotRemoved); + Leg toRemove = null; + Leg last = null; + int i = 0; + for (Map.Entry e : trackedLegs.entrySet()) { + last = e.getKey(); + if (i == zeroBasedIndex) { + toRemove = e.getKey(); + break; + } + i++; + } + if (toRemove == null && !trackedLegs.isEmpty()) { + // last waypoint removed + toRemove = last; + } + if (toRemove != null) { + logger.info("Removing tracked leg at zero-based index " + zeroBasedIndex + " from tracked race " + + getRace().getName()); + LinkedHashMap newTrackedLegs = new LinkedHashMap<>(); + for (Map.Entry trackedLegsEntry : trackedLegs.entrySet()) { + if (trackedLegsEntry.getKey() == toRemove) { + break; + } else { + newTrackedLegs.put(trackedLegsEntry.getKey(), trackedLegsEntry.getValue()); + } + } + trackedLegs.clear(); + trackedLegs.putAll(newTrackedLegs); + List newLegs = getRace().getCourse().getLegs(); + for (int j = zeroBasedIndex; j < newLegs.size(); j++) { + trackedLegs.put(newLegs.get(j), createTrackedLeg(newLegs.get(j))); + } + updated(/* time point */null); + } + // remove all corresponding markpassings if a waypoint has been removed + NavigableSet markPassingsRemoved; + markPassingsRemoved = markPassingsForWaypoint.remove(waypointThatGotRemoved); + for (NavigableSet markPassingsForOneCompetitor : markPassingsForCompetitor.values()) { + if (!markPassingsForOneCompetitor.isEmpty()) { + final Competitor competitor = markPassingsForOneCompetitor.iterator().next().getCompetitor(); + LockUtil.lockForWrite(getMarkPassingsLock(markPassingsForOneCompetitor)); + try { + markPassingsForOneCompetitor.removeAll(markPassingsRemoved); + } finally { + LockUtil.unlockAfterWrite(getMarkPassingsLock(markPassingsForOneCompetitor)); + } + triggerManeuverCacheRecalculation(competitor); + } + } + } finally { + LockUtil.unlockAfterRead(getSerializationLock()); + } + } + + protected NamedReentrantReadWriteLock getMarkPassingsLock(Iterable markPassings) { + synchronized (locksForMarkPassings) { + NamedReentrantReadWriteLock lock = locksForMarkPassings.get(markPassings); + if (lock == null) { + lock = new NamedReentrantReadWriteLock("mark passings lock for tracked race " + getRace().getName(), /* fair */ + false); + locksForMarkPassings.put(markPassings, lock); + } + return lock; + } + } + + private void updateStartToNextMarkCacheInvalidationCacheListenersAfterWaypointRemoved(int zeroBasedIndex, + Waypoint waypointThatGotRemoved) { + if (zeroBasedIndex < 2) { + // the observing listener on any previous mark will be GCed; we need to ensure + // that the cache is recomputed + clearDirectionFromStartToNextMarkCache(); + stopAndRemoveStartToNextMarkCacheInvalidationListener(waypointThatGotRemoved); + Iterator waypointsIter = getRace().getCourse().getWaypoints().iterator(); + if (waypointsIter.hasNext()) { // catches the case of a course being empty + waypointsIter.next(); // skip first + if (waypointsIter.hasNext()) { + waypointsIter.next(); // skip second + if (waypointsIter.hasNext()) { + Waypoint newSecond = waypointsIter.next(); + addStartToNextMarkCacheInvalidationListener(newSecond); + } + } + } + } + } + + @Override + public TrackedRegatta getTrackedRegatta() { + return trackedRegatta; + } + + @Override + public Wind getEstimatedWindDirection(Position position, TimePoint timePoint) { + WindWithConfidence estimatedWindWithConfidence = getEstimatedWindDirectionWithConfidence(position, + timePoint); + return estimatedWindWithConfidence == null ? null : estimatedWindWithConfidence.getObject(); + } + + @Override + public WindWithConfidence getEstimatedWindDirectionWithConfidence(Position position, TimePoint timePoint) { + DummyMarkPassingWithTimePointOnly dummyMarkPassingForNow = new DummyMarkPassingWithTimePointOnly(timePoint); + Weigher weigher = ConfidenceFactory.INSTANCE.createExponentialTimeDifferenceWeigher( + // use a minimum confidence to avoid the bearing to flip to 270deg in case all is zero + getMillisecondsOverWhichToAverageSpeed(), /* minimum confidence */0.0000000001); + Map> bearings = clusterBearingsByLegType(timePoint, position, + dummyMarkPassingForNow, weigher); + // use the minimum confidence of the four "quadrants" as the result's confidence + BearingWithConfidenceImpl reversedUpwindAverage = null; + int upwindNumberOfRelevantBoats = 0; + double confidence = 0; + int numberOfBoatsRelevantForEstimate = 0; + BearingWithConfidence resultBearing = null; + if (bearings != null) { + BearingWithConfidenceCluster[] bearingClustersUpwind = bearings.get(LegType.UPWIND).splitInTwo( + getMinimumAngleBetweenDifferentTacksUpwind(), timePoint); + if (!bearingClustersUpwind[0].isEmpty() && !bearingClustersUpwind[1].isEmpty()) { + BearingWithConfidence average0 = bearingClustersUpwind[0].getAverage(timePoint); + BearingWithConfidence average1 = bearingClustersUpwind[1].getAverage(timePoint); + upwindNumberOfRelevantBoats = Math + .min(bearingClustersUpwind[0].size(), bearingClustersUpwind[1].size()); + confidence = Math.min(average0.getConfidence(), average1.getConfidence()) + * getRace().getBoatClass().getUpwindWindEstimationConfidence(upwindNumberOfRelevantBoats); + reversedUpwindAverage = new BearingWithConfidenceImpl(average0.getObject() + .middle(average1.getObject()).reverse(), confidence, timePoint); + } + BearingWithConfidenceImpl downwindAverage = null; + int downwindNumberOfRelevantBoats = 0; + BearingWithConfidenceCluster[] bearingClustersDownwind = bearings.get(LegType.DOWNWIND) + .splitInTwo(getMinimumAngleBetweenDifferentTacksDownwind(), timePoint); + if (!bearingClustersDownwind[0].isEmpty() && !bearingClustersDownwind[1].isEmpty()) { + BearingWithConfidence average0 = bearingClustersDownwind[0].getAverage(timePoint); + BearingWithConfidence average1 = bearingClustersDownwind[1].getAverage(timePoint); + downwindNumberOfRelevantBoats = Math.min(bearingClustersDownwind[0].size(), + bearingClustersDownwind[1].size()); + confidence = Math.min(average0.getConfidence(), average1.getConfidence()) + * getRace().getBoatClass().getDownwindWindEstimationConfidence(downwindNumberOfRelevantBoats); + downwindAverage = new BearingWithConfidenceImpl(average0.getObject().middle( + average1.getObject()), confidence, timePoint); + } + numberOfBoatsRelevantForEstimate = upwindNumberOfRelevantBoats + downwindNumberOfRelevantBoats; + BearingWithConfidenceCluster resultCluster = new BearingWithConfidenceCluster(weigher); + assert upwindNumberOfRelevantBoats == 0 || reversedUpwindAverage != null; + if (upwindNumberOfRelevantBoats > 0) { + resultCluster.add(reversedUpwindAverage); + } + assert downwindNumberOfRelevantBoats == 0 || downwindAverage != null; + if (downwindNumberOfRelevantBoats > 0) { + resultCluster.add(downwindAverage); + } + resultBearing = resultCluster.getAverage(timePoint); + } + return resultBearing == null ? null : new WindWithConfidenceImpl(new WindImpl(null, timePoint, + new KnotSpeedWithBearingImpl( + /* speedInKnots */numberOfBoatsRelevantForEstimate, resultBearing.getObject())), + resultBearing.getConfidence(), resultBearing.getRelativeTo(), /* useSpeed */false); + } + + // TODO confidences need to be computed not only based on timePoint but also on position: boats far away don't + // contribute as confidently as boats close by + private Map> clusterBearingsByLegType(TimePoint timePoint, + Position position, DummyMarkPassingWithTimePointOnly dummyMarkPassingForNow, Weigher weigher) { + Weigher weigherForMarkPassingProximity = new HyperbolicTimeDifferenceWeigher( + getMillisecondsOverWhichToAverageSpeed() * 5); + Map> bearings = new HashMap>(); + for (LegType legType : LegType.values()) { + bearings.put(legType, new BearingWithConfidenceCluster(weigher)); + } + Map legTypesCache = new HashMap(); + getRace().getCourse().lockForRead(); // ensure the course doesn't change, particularly lose the leg we're + // interested in, while we're running + try { + for (Competitor competitor : getRace().getCompetitors()) { + TrackedLegOfCompetitor leg; + try { + leg = getTrackedLeg(competitor, timePoint); + } catch (IllegalArgumentException iae) { + logger.warning("Caught " + iae + " during wind estimation; ignoring seemingly broken leg"); + logger.log(Level.SEVERE, "clusterBearingsByLegType", iae); + // supposedly, we got a "Waypoint X isn't start of any leg in Y" exception; leg not found + leg = null; + } + // if bearings was set to null this indicates there was an exception; no need for further calculations, + // return null + if (bearings != null && leg != null) { + TrackedLeg trackedLeg = getTrackedLeg(leg.getLeg()); + LegType legType; + try { + legType = legTypesCache.get(trackedLeg); + if (legType == null) { + legType = trackedLeg.getLegType(timePoint); + legTypesCache.put(trackedLeg, legType); + } + if (legType != LegType.REACHING) { + GPSFixTrack track = getTrack(competitor); + if (!track.hasDirectionChange(timePoint, + /* be even more conservative than maneuver detection to really try to get "straight line" behavior */ + getManeuverDegreeAngleThreshold()/2.)) { + SpeedWithBearingWithConfidence estimatedSpeedWithConfidence = track + .getEstimatedSpeed(timePoint, weigher); + if (estimatedSpeedWithConfidence != null + && estimatedSpeedWithConfidence.getObject() != null && + // Mark passings may be missing or far off. This can lead to boats apparently + // going "backwards" regarding the leg's direction; ignore those + isNavigatingForward(estimatedSpeedWithConfidence.getObject().getBearing(), + trackedLeg, timePoint)) { + // additionally to generally excluding maneuvers, reduce confidence around mark + // passings: + NavigableSet markPassings = getMarkPassings(competitor); + double markPassingProximityConfidenceReduction = 1.0; + lockForRead(markPassings); + try { + NavigableSet prevMarkPassing = markPassings.headSet( + dummyMarkPassingForNow, /* inclusive */true); + NavigableSet nextMarkPassing = markPassings.tailSet( + dummyMarkPassingForNow, /* inclusive */true); + if (prevMarkPassing != null && !prevMarkPassing.isEmpty()) { + markPassingProximityConfidenceReduction *= Math.max(0.0, + 1.0 - weigherForMarkPassingProximity.getConfidence(prevMarkPassing + .last().getTimePoint(), timePoint)); + } + if (nextMarkPassing != null && !nextMarkPassing.isEmpty()) { + markPassingProximityConfidenceReduction *= Math.max(0.0, + 1.0 - weigherForMarkPassingProximity.getConfidence(nextMarkPassing + .first().getTimePoint(), timePoint)); + } + } finally { + unlockAfterRead(markPassings); + } + BearingWithConfidence bearing = new BearingWithConfidenceImpl( + estimatedSpeedWithConfidence.getObject() == null ? null + : estimatedSpeedWithConfidence.getObject().getBearing(), + markPassingProximityConfidenceReduction + * estimatedSpeedWithConfidence.getConfidence(), + estimatedSpeedWithConfidence.getRelativeTo()); + BearingWithConfidenceCluster bearingClusterForLegType = bearings + .get(legType); + bearingClusterForLegType.add(bearing); + } + } + } + } catch (NoWindException e) { + logger.fine("Unable to determine leg type for race " + getRace().getName() + + " while trying to estimate wind (Background: I've got a NoWindException)"); + bearings = null; + } + } + } + } finally { + getRace().getCourse().unlockAfterRead(); + } + return bearings; + } + + /** + * Checks if the bearing generally moves in the direction that the trackedLeg has at time + * point at. + */ + private boolean isNavigatingForward(Bearing bearing, TrackedLeg trackedLeg, TimePoint at) { + Bearing legBearing = trackedLeg.getLegBearing(at); + return Math.abs(bearing.getDifferenceTo(legBearing).getDegrees()) < 90; + } + + /** + * This is probably best explained by example. If the wind bearing is from port to starboard, the situation looks + * like this: + * + *

+     *                                 ^
+     *                 Wind            | Boat
+     *               ----------->      |
+     *                                 |
+     * 
+     * 
+ * + * In this case, the boat gets the wind from port, so the result has to be {@link Tack#PORT}. The angle between the + * boat's heading (which we can only approximate by the boat's course over ground) and the wind bearing in this case + * is 90 degrees. wind.{@link Bearing#getDifferenceTo(Bearing) getDifferenceTo}(boat) in this case will + * return a bearing representing -90 degrees. + *

+ * + * If the wind is blowing the other way, the angle returned by {@link Bearing#getDifferenceTo(Bearing)} will + * correspond to +90 degrees. In other words, a negative angle means starboard tack, a positive angle represents + * port tack. + *

+ * + * For the unlikely case of 0 degrees difference, {@link Tack#STARBOARD} will result. + * + * @return null in case the boat's bearing cannot be determined for timePoint + * @throws NoWindException + */ + @Override + public Tack getTack(Competitor competitor, TimePoint timePoint) throws NoWindException { + final SpeedWithBearing estimatedSpeed = getTrack(competitor).getEstimatedSpeed(timePoint); + Tack result = null; + if (estimatedSpeed != null) { + result = getTack(getTrack(competitor).getEstimatedPosition(timePoint, /* extrapolate */false), timePoint, + estimatedSpeed.getBearing()); + } + return result; + } + + /** + * Based on the wind direction at timePoint and at position where, compares the + * boatBearing to the wind's bearing at that time and place and determined the tack. + * + * @throws NoWindException + * in case the wind cannot be determined because without a wind direction, the tack cannot be determined + * either + */ + private Tack getTack(Position where, TimePoint timePoint, Bearing boatBearing) throws NoWindException { + final Wind wind = getWind(where, timePoint); + Tack result; + if (wind == null) { + throw new NoWindException("Can't determine wind direction in position " + where + " at " + timePoint + + ", therefore cannot determine tack"); + } + Bearing windBearing = wind.getBearing(); + Bearing difference = windBearing.getDifferenceTo(boatBearing); + result = difference.getDegrees() <= 0 ? Tack.PORT : Tack.STARBOARD; + return result; + } + + @Override + public String toString() { + return "TrackedRace for " + getRace(); + } + + @Override + public List approximate(Competitor competitor, Distance maxDistance, TimePoint from, TimePoint to) { + DouglasPeucker douglasPeucker = new DouglasPeucker( + getTrack(competitor)); + return douglasPeucker.approximate(maxDistance, from, to); + } + + protected void triggerManeuverCacheRecalculationForAllCompetitors() { + final List shuffledCompetitors = new ArrayList<>(); + for (Competitor competitor : (getRace().getCompetitors())) { + shuffledCompetitors.add(competitor); + } + Collections.shuffle(shuffledCompetitors); + for (Competitor competitor : shuffledCompetitors) { + triggerManeuverCacheRecalculation(competitor); + } + } + + protected void triggerManeuverCacheRecalculation(final Competitor competitor) { + maneuverCache.triggerUpdate(competitor, /* updateInterval */null); + } + + private com.sap.sse.common.Util.Triple> computeManeuvers(Competitor competitor) throws NoWindException { + logger.finest("computeManeuvers(" + competitor.getName() + ") called in tracked race " + this); + long startedAt = System.currentTimeMillis(); + // compute the maneuvers for competitor + com.sap.sse.common.Util.Triple> result = null; + NavigableSet markPassings = getMarkPassings(competitor); + boolean markPassingsNotEmpty; + TimePoint extendedFrom = null; + MarkPassing crossedFinishLine = null; + // getLastWaypoint() will wait for a read lock on the course; do this outside the synchronized block to avoid + // deadlocks + final Waypoint lastWaypoint = getRace().getCourse().getLastWaypoint(); + lockForRead(markPassings); + try { + markPassingsNotEmpty = markPassings != null && !markPassings.isEmpty(); + if (markPassingsNotEmpty) { + extendedFrom = markPassings.iterator().next().getTimePoint(); + crossedFinishLine = getMarkPassing(competitor, lastWaypoint); + } + } finally { + unlockAfterRead(markPassings); + } + if (markPassingsNotEmpty) { + TimePoint extendedTo; + if (crossedFinishLine != null) { + extendedTo = crossedFinishLine.getTimePoint(); + } else { + final GPSFixMoving lastRawFix = getTrack(competitor).getLastRawFix(); + if (lastRawFix != null) { + extendedTo = lastRawFix.getTimePoint(); + } else { + extendedTo = null; + } + } + if (extendedTo != null) { + try { + List extendedResultForCache = detectManeuvers(competitor, extendedFrom, extendedTo, /* ignoreMarkPassings */ false); + result = new com.sap.sse.common.Util.Triple>(extendedFrom, extendedTo, + extendedResultForCache); + } catch (NoWindException ex) { + // Catching the NoWindException here without letting it propagate thru other handlers. + // This is mainly to avoid having logs flooded with stack traces. It is safe to catch + // it here because we can assume that this exception does not hide any severe problem + // other than that there is no wind. Because maneuvers are mostly computed using a + // future cache and need wind (like getTack) they will often fail before the wind has been + // loaded from database. We can safely return null here because we can be sure that + // cache will be updated when new fixes are fed into the stream therefore leading to a + // recomputation. + logger.fine("NoWindException during computation of maneuvers for " + competitor.getName()); + } + } // else competitor has no fixes to consider; remove any maneuver cache entry + } // else competitor hasn't started yet; remove any maneuver cache entry + logger.finest("computeManeuvers(" + competitor.getName() + ") called in tracked race " + this + " took " + + (System.currentTimeMillis() - startedAt) + "ms"); + return result; + } + + /** + * @param ignoreMarkPassings + * When true, no {@link ManeuverType#MARK_PASSING} maneuvers will be identified, and the + * fact that a mark passing would split up what else may be a penalty circle is ignored. This is helpful + * for recursive calls, e.g., after identifying a tack and a jibe around a mark passing and trying to + * identify for the time before and after the mark passing which maneuvers exist on which side of the + * passing. + * @return a valid but possibly empty list + * @see #detectManeuvers(Competitor, List, boolean, TimePoint, TimePoint) + */ + private List detectManeuvers(Competitor competitor, TimePoint from, TimePoint to, boolean ignoreMarkPassings) throws NoWindException { + return detectManeuvers( + competitor, approximate(competitor, getRace().getBoatClass().getMaximumDistanceForCourseApproximation(), + from, to), ignoreMarkPassings, from, to); + } + + /** + * Tries to detect maneuvers on the competitor's track based on a number of approximating fixes. The + * fixes contain bearing information, but this is not the bearing leading to the next approximation fix but the + * bearing the boat had at the time of the approximating fix which is taken from the original track. + *

+ * + * The time period assumed for a maneuver duration is taken from the + * {@link BoatClass#getApproximateManeuverDurationInMilliseconds() boat class}. If no maneuver is detected, an empty + * list is returned. Maneuvers can only be expected to be detected if at least three fixes are provided in + * approximatedFixesToAnalyze. For the inner approximating fixes (all except the first and the last + * approximating fix), their course changes according to the approximated path (and not the underlying actual + * tracked fixes) are computed. Subsequent course changes to the same direction are then grouped. Those in closer + * timely distance than {@link #getApproximateManeuverDurationInMilliseconds()} (including single course changes + * that have no surrounding other course changes to group) are grouped into one {@link Maneuver}. + * + * @param ignoreMarkPassings + * When true, no {@link ManeuverType#MARK_PASSING} maneuvers will be identified, and the + * fact that a mark passing would split up what else may be a penalty circle is ignored. This is helpful + * for recursive calls, e.g., after identifying a tack and a jibe around a mark passing and trying to + * identify for the time before and after the mark passing which maneuvers exist on which side of the + * passing. + * @param earliestManeuverStart + * maneuver start will not be before this time point; if a maneuver is found whose time point is at or + * after this time point, no matter how close it is, its start regarding speed and course into the + * maneuver and the leg before the maneuver is not taken from an earlier time point, even if half the + * maneuver duration before the maneuver time point were before this time point. + * @param latestManeuverEnd + * maneuver end will not be after this time point; if a maneuver is found whose time point is at or + * before this time point, no matter how close it is, its end regarding speed and course out of the + * maneuver and the leg after the maneuver is not taken from a later time point, even if half the + * maneuver duration after the maneuver time point were after this time point. + * + * @return an empty list if no maneuver is detected for competitor between from and + * to, or else the list of maneuvers detected. + */ + private List detectManeuvers(Competitor competitor, List approximatingFixesToAnalyze, + boolean ignoreMarkPassings, TimePoint earliestManeuverStart, TimePoint latestManeuverEnd) + throws NoWindException { + List result = new ArrayList(); + if (approximatingFixesToAnalyze.size() > 2) { + List> courseChangeSequenceInSameDirection = new ArrayList>(); + Iterator approximationPointsIter = approximatingFixesToAnalyze.iterator(); + GPSFixMoving previous = approximationPointsIter.next(); + GPSFixMoving current = approximationPointsIter.next(); + // the bearings in these variables are between approximation points + SpeedWithBearing speedWithBearingOnApproximationFromPreviousToCurrent = previous + .getSpeedAndBearingRequiredToReach(current); + SpeedWithBearing speedWithBearingOnApproximationAtBeginningOfUnidirectionalCourseChanges = speedWithBearingOnApproximationFromPreviousToCurrent; + SpeedWithBearing speedWithBearingOnApproximationFromCurrentToNext; // will certainly be assigned because + // iter's collection's size > 2 + do { + GPSFixMoving next = approximationPointsIter.next(); + speedWithBearingOnApproximationFromCurrentToNext = current.getSpeedAndBearingRequiredToReach(next); + // compute course change on "approximation track" + CourseChange courseChange = speedWithBearingOnApproximationFromPreviousToCurrent + .getCourseChangeRequiredToReach(speedWithBearingOnApproximationFromCurrentToNext); + com.sap.sse.common.Util.Pair courseChangeAtFix = new com.sap.sse.common.Util.Pair(current, + courseChange); + if (!courseChangeSequenceInSameDirection.isEmpty() + && Math.signum(courseChangeSequenceInSameDirection.get(0).getB().getCourseChangeInDegrees()) != Math + .signum(courseChange.getCourseChangeInDegrees())) { + // course change in different direction; cluster the course changes in same direction so far, then + // start new list + List maneuvers = groupChangesInSameDirectionIntoManeuvers(competitor, + speedWithBearingOnApproximationAtBeginningOfUnidirectionalCourseChanges, + courseChangeSequenceInSameDirection, ignoreMarkPassings, earliestManeuverStart, latestManeuverEnd); + result.addAll(maneuvers); + courseChangeSequenceInSameDirection.clear(); + speedWithBearingOnApproximationAtBeginningOfUnidirectionalCourseChanges = speedWithBearingOnApproximationFromPreviousToCurrent; + } + courseChangeSequenceInSameDirection.add(courseChangeAtFix); + previous = current; + current = next; + speedWithBearingOnApproximationFromPreviousToCurrent = speedWithBearingOnApproximationFromCurrentToNext; + } while (approximationPointsIter.hasNext()); + if (!courseChangeSequenceInSameDirection.isEmpty()) { + result.addAll(groupChangesInSameDirectionIntoManeuvers(competitor, + speedWithBearingOnApproximationAtBeginningOfUnidirectionalCourseChanges, + courseChangeSequenceInSameDirection, ignoreMarkPassings, earliestManeuverStart, latestManeuverEnd)); + } + } + return result; + } + + /** + * Fetches results from {@link #maneuverCache}. The cache is updated asynchronously after relevant updates have been + * received (see {@link #triggerManeuverCacheRecalculation(Competitor)} and + * {@link #triggerManeuverCacheRecalculationForAllCompetitors()}). Callers can choose whether to wait for any + * ongoing updates by using the waitForLatest parameter. From the cache the interval requested is then + * {@link #extractInterval(TimePoint, TimePoint, List) extracted}. + * + * @param waitForLatest + * if true, any currently ongoing maneuver recalculation for competitor is + * waited for before returning the result; otherwise, whatever is in the {@link #maneuverCache} for + * competitor, reduced to the interval requested, will be returned. + */ + @Override + public List getManeuvers(Competitor competitor, TimePoint from, TimePoint to, boolean waitForLatest) + throws NoWindException { + com.sap.sse.common.Util.Triple> allManeuvers = maneuverCache.get(competitor, waitForLatest); + List result; + if (allManeuvers == null) { + result = Collections.emptyList(); + } else { + result = extractInterval(from, to, allManeuvers.getC()); + } + return result; + } + + private List extractInterval(TimePoint from, TimePoint to, List listOfTimed) { + List result; + result = new LinkedList(); + for (T timed : listOfTimed) { + if (timed.getTimePoint().compareTo(from) >= 0 && timed.getTimePoint().compareTo(to) <= 0) { + result.add(timed); + } + } + return result; + } + + /** + * Groups the {@link CourseChange} sequence into groups where the times of the fixes at which the course changes + * took place are no further apart than {@link #getApproximateManeuverDurationInMilliseconds()} milliseconds or + * where the distances of those course changes are less than two hull lengths apart. For those, a single + * {@link Maneuver} object is created and added to the resulting list. The maneuver sums up the direction changes of + * the individual {@link CourseChange} objects. This can result in direction changes of more than 180 degrees in one + * direction which may, e.g., represent a penalty circle or a mark rounding maneuver. As the maneuver's time point, + * the average time point of the course changes that went into the maneuver construction is used. + *

+ * @param speedWithBearingOnApproximationAtBeginning + * the speed/bearing before the first approximating fix passed in + * courseChangeSequenceInSameDirection + * @param courseChangeSequenceInSameDirection + * all expected to have equal {@link CourseChange#to()} values + * @param ignoreMarkPassings + * When true, no {@link ManeuverType#MARK_PASSING} maneuvers will be identified, and the + * fact that a mark passing would split up what else may be a penalty circle is ignored. This is helpful + * for recursive calls, e.g., after identifying a tack and a jibe around a mark passing and trying to + * identify for the time before and after the mark passing which maneuvers exist on which side of the + * passing. + * @param earliestManeuverStart + * maneuver start will not be before this time point; if a maneuver is found whose time point is at or + * after this time point, no matter how close it is, its start regarding speed and course into the + * maneuver and the leg before the maneuver is not taken from an earlier time point, even if half the + * maneuver duration before the maneuver time point were before this time point. + * @param latestManeuverEnd + * maneuver end will not be after this time point; if a maneuver is found whose time point is at or + * before this time point, no matter how close it is, its end regarding speed and course out of the + * maneuver and the leg after the maneuver is not taken from a later time point, even if half the + * maneuver duration after the maneuver time point were after this time point. + * + * @return a non-null list + */ + private List groupChangesInSameDirectionIntoManeuvers(Competitor competitor, + SpeedWithBearing speedWithBearingOnApproximationAtBeginning, + List> courseChangeSequenceInSameDirection, boolean ignoreMarkPassings, + TimePoint earliestManeuverStart, TimePoint latestManeuverEnd) throws NoWindException { + List result = new ArrayList(); + List> group = new ArrayList>(); + if (!courseChangeSequenceInSameDirection.isEmpty()) { + Distance threeHullLengths = competitor.getBoat().getBoatClass().getHullLength().scale(3); + SpeedWithBearing beforeGroupOnApproximation = speedWithBearingOnApproximationAtBeginning; // speed/bearing + // before group + SpeedWithBearing beforeCurrentCourseChangeOnApproximation = beforeGroupOnApproximation; // speed/bearing + // before current + // course change + Iterator> iter = courseChangeSequenceInSameDirection.iterator(); + double totalCourseChangeInDegrees = 0.0; + long totalMilliseconds = 0l; + SpeedWithBearing afterCurrentCourseChange = null; // sure to be set because iter's collection is not empty + // and the first use requires group not to be empty which can only happen after the first group.add + do { + com.sap.sse.common.Util.Pair currentFixAndCourseChange = iter.next(); + if (!group.isEmpty() + // TODO use different maneuver times for upwind / reaching / downwind / cross-leg (mark passing) + // group contains complete maneuver if the next fix is too late or too far away to belong to the + // same maneuver + // FIXME penalty circles slow down the boat so much that time limit may get exceeded although + // distance limit is matched + && currentFixAndCourseChange.getA().getTimePoint().asMillis() + - group.get(group.size() - 1).getA().getTimePoint().asMillis() > getApproximateManeuverDurationInMilliseconds() + && currentFixAndCourseChange.getA().getPosition() + .getDistance(group.get(group.size() - 1).getA().getPosition()) + .compareTo(threeHullLengths) > 0) { + // if next is more then approximate maneuver duration later or further apart than two hull lengths, + // turn the current group into a maneuver and add to result + Util.addAll(createManeuverFromGroupOfCourseChanges(competitor, beforeGroupOnApproximation, + group, afterCurrentCourseChange, totalCourseChangeInDegrees, totalMilliseconds, earliestManeuverStart, latestManeuverEnd), result); + group.clear(); + totalCourseChangeInDegrees = 0.0; + totalMilliseconds = 0l; + beforeGroupOnApproximation = beforeCurrentCourseChangeOnApproximation; + } + afterCurrentCourseChange = beforeCurrentCourseChangeOnApproximation + .applyCourseChange(currentFixAndCourseChange.getB()); + totalMilliseconds += currentFixAndCourseChange.getA().getTimePoint().asMillis(); + totalCourseChangeInDegrees += currentFixAndCourseChange.getB().getCourseChangeInDegrees(); + group.add(currentFixAndCourseChange); + beforeCurrentCourseChangeOnApproximation = afterCurrentCourseChange; // speed/bearing after course + // change + } while (iter.hasNext()); + if (!group.isEmpty()) { + Util.addAll(createManeuverFromGroupOfCourseChanges(competitor, beforeGroupOnApproximation, group, + afterCurrentCourseChange, totalCourseChangeInDegrees, totalMilliseconds, earliestManeuverStart, latestManeuverEnd), result); + } + } + return result; + } + + private Iterable createManeuverFromGroupOfCourseChanges(Competitor competitor, + SpeedWithBearing speedWithBearingOnApproximationAtBeginning, List> group, + SpeedWithBearing speedWithBearingOnApproximationAtEnd, double totalCourseChangeInDegrees, + long totalMilliseconds, TimePoint earliestManeuverStart, TimePoint latestManeuverEnd) throws NoWindException { + List result = new ArrayList<>(); + TimePoint timePointBeforeManeuver = Collections.max(Arrays.asList(new MillisecondsTimePoint(group.get(0).getA().getTimePoint() + .asMillis() - getApproximateManeuverDurationInMilliseconds() / 2), earliestManeuverStart)); + TimePoint timePointAfterManeuver = Collections.min(Arrays.asList(new MillisecondsTimePoint(group.get(group.size() - 1).getA() + .getTimePoint().asMillis() + getApproximateManeuverDurationInMilliseconds() / 2), latestManeuverEnd)); + TimePoint maneuverTimePoint = computeManeuverTimepoint(competitor, timePointBeforeManeuver, timePointAfterManeuver); + final GPSFixTrack competitorTrack = getTrack(competitor); + Position maneuverPosition = competitorTrack.getEstimatedPosition(maneuverTimePoint, /* extrapolate */false); + Tack tackAfterManeuver = getTack(maneuverPosition, timePointAfterManeuver, + speedWithBearingOnApproximationAtEnd.getBearing()); + ManeuverType maneuverType; + Distance maneuverLoss = null; + // the TrackedLegOfCompetitor variables may be null, e.g., in case the time points are before or after the race + TrackedLegOfCompetitor legBeforeManeuver = getTrackedLeg(competitor, timePointBeforeManeuver); + TrackedLegOfCompetitor legAfterManeuver = getTrackedLeg(competitor, timePointAfterManeuver); + Waypoint waypointPassed = null; // set for MARK_PASSING maneuvers only + NauticalSide sideToWhichWaypointWasPassed = null; // set for MARK_PASSING maneuvers only + final NauticalSide courseChangedTo = totalCourseChangeInDegrees < 0 ? NauticalSide.PORT : NauticalSide.STARBOARD; + // check for mask passing first; a tacking / jibe-setting mark rounding thus takes precedence over being + // detected as a penalty circle + final TimePoint markPassingTimePoint; + if (legBeforeManeuver != legAfterManeuver + // a maneuver at the start line is not to be considered a MARK_PASSING maneuver; show a tack as a tack + && legAfterManeuver != null + && legAfterManeuver.getLeg().getFrom() != getRace().getCourse().getFirstWaypoint()) { + waypointPassed = legAfterManeuver.getLeg().getFrom(); + MarkPassing markPassing = getMarkPassing(competitor, waypointPassed); + markPassingTimePoint = markPassing != null ? markPassing.getTimePoint() : maneuverTimePoint; + Position markPassingPosition = markPassing != null ? competitorTrack.getEstimatedPosition(markPassingTimePoint, /* extrapolate */false) : maneuverPosition; + sideToWhichWaypointWasPassed = courseChangedTo; + // produce an additional mark passing maneuver; continue to analyze to catch jibe sets and kiwi drops + result.add(new MarkPassingManeuverImpl(ManeuverType.MARK_PASSING, tackAfterManeuver, markPassingPosition, + markPassingTimePoint, speedWithBearingOnApproximationAtBeginning, + speedWithBearingOnApproximationAtEnd, totalCourseChangeInDegrees, maneuverLoss, waypointPassed, + sideToWhichWaypointWasPassed)); + } else { + markPassingTimePoint = null; + } + final Wind wind = getWind(maneuverPosition, maneuverTimePoint); + final SpeedWithBearing estimatedSpeedBeforeManeuver = competitorTrack.getEstimatedSpeed(timePointBeforeManeuver); + final SpeedWithBearing estimatedSpeedAfterManeuver = competitorTrack.getEstimatedSpeed(timePointAfterManeuver); + if (wind != null && estimatedSpeedBeforeManeuver != null && estimatedSpeedAfterManeuver != null) { + BearingChangeAnalyzer bearingChangeAnalyzer = BearingChangeAnalyzer.INSTANCE; + final Bearing courseBeforeManeuver = estimatedSpeedBeforeManeuver.getBearing(); + final Bearing courseAfterManeuver = estimatedSpeedAfterManeuver.getBearing(); + boolean jibed = bearingChangeAnalyzer.didPass(courseBeforeManeuver, totalCourseChangeInDegrees, + courseAfterManeuver, wind.getBearing()); + boolean tacked = bearingChangeAnalyzer.didPass(courseBeforeManeuver, totalCourseChangeInDegrees, + courseAfterManeuver, wind.getFrom()); + if (markPassingTimePoint != null && (tacked || jibed)) { + // In case of a mark passing we need to split the maneuver analysis into the phase before and after + // the mark passing. First of all, this is important to identify the correct maneuver time point for + // each tack and jibe, second it is essential to call a penalty which is only the case if the tack and + // the jibe are on the same side of the mark passing; otherwise this may have been a jibe set or a + // kiwi drop. + // Therefore, we recursively detect the maneuvers for the segment before and the segment after the + // mark passing and add the results to our result. + result.addAll(detectManeuvers(competitor, timePointBeforeManeuver, markPassingTimePoint.minus(1), /* ignoreMarkPassings */ true)); + result.addAll(detectManeuvers(competitor, markPassingTimePoint.plus(1), timePointAfterManeuver, /* ignoreMarkPassings */ true)); + } else { + // Either there was no mark passing, or the mark passing was not accompanied by a tack or a jibe + if (tacked && jibed && markPassingTimePoint == null) { + maneuverType = ManeuverType.PENALTY_CIRCLE; + if (legBeforeManeuver != null) { + maneuverLoss = legBeforeManeuver.getManeuverLoss(timePointBeforeManeuver, maneuverTimePoint, + timePointAfterManeuver); + } + } else if (tacked) { + maneuverType = ManeuverType.TACK; + if (legBeforeManeuver != null) { + maneuverLoss = legBeforeManeuver.getManeuverLoss(timePointBeforeManeuver, maneuverTimePoint, + timePointAfterManeuver); + } + } else if (jibed) { + maneuverType = ManeuverType.JIBE; + if (legBeforeManeuver != null) { + maneuverLoss = legBeforeManeuver.getManeuverLoss(timePointBeforeManeuver, maneuverTimePoint, + timePointAfterManeuver); + } + } else { + // heading up or bearing away + Bearing windBearing = wind.getBearing(); + Bearing toWindBeforeManeuver = windBearing + .getDifferenceTo(speedWithBearingOnApproximationAtBeginning.getBearing()); + Bearing toWindAfterManeuver = windBearing.getDifferenceTo(speedWithBearingOnApproximationAtEnd + .getBearing()); + maneuverType = Math.abs(toWindBeforeManeuver.getDegrees()) < Math.abs(toWindAfterManeuver + .getDegrees()) ? ManeuverType.HEAD_UP : ManeuverType.BEAR_AWAY; + } + final Maneuver maneuver = new ManeuverImpl(maneuverType, tackAfterManeuver, maneuverPosition, + maneuverTimePoint, speedWithBearingOnApproximationAtBeginning, + speedWithBearingOnApproximationAtEnd, totalCourseChangeInDegrees, maneuverLoss); + result.add(maneuver); + } + } + return result; + } + + /** + * Computes the maneuver time point as the time point along between maneuver start and end where the competitor's + * track has greatest change in course. + */ + private TimePoint computeManeuverTimepoint(Competitor competitor, TimePoint timePointBeforeManeuver, + TimePoint timePointAfterManeuver) { + TimePoint result = timePointBeforeManeuver; + GPSFixTrack track = getTrack(competitor); + GPSFixMoving lastFix = null; + double maxAngleSpeedInDegreesPerSecond = 0; + track.lockForRead(); + try { + for (Iterator i = track.getFixesIterator(timePointBeforeManeuver, /* inclusive */true); i + .hasNext();) { + GPSFixMoving fix = i.next(); + if (fix.getTimePoint().after(timePointAfterManeuver)) { + break; + } + if (lastFix != null) { + final SpeedWithBearing lastEstimatedSpeed = track.getEstimatedSpeed(lastFix.getTimePoint()); + if (lastEstimatedSpeed != null) { + Bearing courseAtLastFix = lastEstimatedSpeed.getBearing(); + final SpeedWithBearing estimatedSpeed = track.getEstimatedSpeed(fix.getTimePoint()); + if (estimatedSpeed != null) { + Bearing courseAtFix = estimatedSpeed.getBearing(); + double angleSpeedInDegreesPerSecond = Math.abs((courseAtFix + .getDifferenceTo(courseAtLastFix).getDegrees()) + / (double) (fix.getTimePoint().asMillis() - lastFix.getTimePoint().asMillis())); + if (angleSpeedInDegreesPerSecond > maxAngleSpeedInDegreesPerSecond) { + maxAngleSpeedInDegreesPerSecond = angleSpeedInDegreesPerSecond; + result = lastFix.getTimePoint(); + } + } + } + } + lastFix = fix; + } + } finally { + track.unlockAfterRead(); + } + return result; + } + + /** + * Fetches the boat class-specific parameter + */ + private double getManeuverDegreeAngleThreshold() { + return getRace().getBoatClass().getManeuverDegreeAngleThreshold(); + } + + private double getMinimumAngleBetweenDifferentTacksDownwind() { + return getRace().getBoatClass().getMinimumAngleBetweenDifferentTacksDownwind(); + } + + private double getMinimumAngleBetweenDifferentTacksUpwind() { + return getRace().getBoatClass().getMinimumAngleBetweenDifferentTacksUpwind(); + } + + private long getApproximateManeuverDurationInMilliseconds() { + return getRace().getBoatClass().getApproximateManeuverDurationInMilliseconds(); + } + + private class StartToNextMarkCacheInvalidationListener implements GPSTrackListener { + private static final long serialVersionUID = 3540278554797445085L; + private final GPSFixTrack listeningTo; + + public StartToNextMarkCacheInvalidationListener(GPSFixTrack listeningTo) { + this.listeningTo = listeningTo; + } + + public void stopListening() { + listeningTo.removeListener(this); + } + + @Override + public void gpsFixReceived(GPSFix fix, Mark mark) { + clearDirectionFromStartToNextMarkCache(); + } + + @Override + public void speedAveragingChanged(long oldMillisecondsOverWhichToAverage, long newMillisecondsOverWhichToAverage) { + } + + @Override + public boolean isTransient() { + return false; + } + } + + @Override + public Distance getWindwardDistanceToOverallLeader(Competitor competitor, TimePoint timePoint) + throws NoWindException { + final TrackedLegOfCompetitor trackedLeg = getTrackedLeg(competitor, timePoint); + return trackedLeg == null ? null : trackedLeg.getWindwardDistanceToOverallLeader(timePoint); + } + + @Override + public Iterable getMarks() { + while (true) { + try { + return new HashSet(markTracks.keySet()); + } catch (ConcurrentModificationException cme) { + logger.info("Caught " + cme + "; trying again."); + } + } + } + + @Override + public Iterable getCourseSidelines() { + return new ArrayList(courseSidelines.values()); + } + + @Override + public long getDelayToLiveInMillis() { + return delayToLiveInMillis; + } + + protected void setDelayToLiveInMillis(long delayToLiveInMillis) { + this.delayToLiveInMillis = delayToLiveInMillis; + } + + @Override + public TrackedRaceStatus getStatus() { + return status; + } + + /** + * Changes to the {@link #status} variable are synchronized on the {@link #statusNotifier} field. + * + * @return + */ + protected Object getStatusNotifier() { + return statusNotifier; + } + + protected void setStatus(TrackedRaceStatus newStatus) { + assert newStatus != null; + final TrackedRaceStatusEnum oldStatus; + synchronized (getStatusNotifier()) { + oldStatus = getStatus().getStatus(); + this.status = newStatus; + getStatusNotifier().notifyAll(); + } + if (newStatus.getStatus() == TrackedRaceStatusEnum.LOADING && oldStatus != TrackedRaceStatusEnum.LOADING) { + suspendAllCachesNotUpdatingWhileLoading(); + } else if (oldStatus == TrackedRaceStatusEnum.LOADING && newStatus.getStatus() != TrackedRaceStatusEnum.LOADING) { + resumeAllCachesNotUpdatingWhileLoading(); + } + + } + + private void suspendAllCachesNotUpdatingWhileLoading() { + crossTrackErrorCache.suspend(); + maneuverCache.suspend(); + } + + private void resumeAllCachesNotUpdatingWhileLoading() { + crossTrackErrorCache.resume(); + maneuverCache.resume(); + } + + /** + * Waits on the current ("old") status object which is notified in {@link #setStatus(TrackedRaceStatus)} when the + * status is changed. The change as well as the check synchronize on the old status object. + */ + @Override + public void waitUntilNotLoading() { + synchronized (getStatusNotifier()) { + while (getStatus().getStatus() == TrackedRaceStatusEnum.LOADING) { + try { + getStatusNotifier().wait(); + } catch (InterruptedException e) { + logger.info("waitUntilNotLoading on tracked race " + this + " interrupted: " + e.getMessage() + + ". Continuing to wait."); + } + } + } + } + + @Override + public void attachRaceLog(final RaceLog raceLog) { + if (raceLog != null) { + this.attachedRaceLogs.put(raceLog.getId(), raceLog); + // Use the new race log, that possibly contains device mappings, to load GPSFix tracks from the DB + loadingFromGPSFixStoreState = LoadingFromStoresState.NOT_STARTED; + // When this tracked race is to be serialized, wait for the loading from stores to complete. + new Thread("Mongo mark and competitor track loader for tracked race " + getRace().getName()) { + @Override + public void run() { + LockUtil.lockForRead(getSerializationLock()); + LockUtil.lockForWrite(getLoadingFromGPSFixStoreLock()); + synchronized (TrackedRaceImpl.this) { + loadingFromGPSFixStoreState = LoadingFromStoresState.RUNNING; // indicates that the serialization + // lock is now safely held + TrackedRaceImpl.this.notifyAll(); + } + try { + logger.info("Started loading competitor tracks for " + getRace().getName()); + for (Competitor competitor : race.getCompetitors()) { + try { + gpsFixStore.loadCompetitorTrack( + (DynamicGPSFixTrack) tracks.get(competitor), raceLog, + competitor); + } catch (TransformationException | NoCorrespondingServiceRegisteredException e) { + logger.log(Level.WARNING, "Could not load track for " + competitor); + } + } + logger.info("Finished loading competitor tracks for " + getRace().getName()); + logger.info("Started loading mark tracks for " + getRace().getName()); + for (Mark mark : getMarks()) { + try { + gpsFixStore.loadMarkTrack((DynamicGPSFixTrack) markTracks.get(mark), raceLog, + mark); + } catch (TransformationException | NoCorrespondingServiceRegisteredException e) { + logger.log(Level.WARNING, "Could not load track for " + mark); + } + } + logger.info("Finished loading mark tracks for " + getRace().getName()); + + } finally { + synchronized (TrackedRaceImpl.this) { + loadingFromGPSFixStoreState = LoadingFromStoresState.FINISHED; + TrackedRaceImpl.this.notifyAll(); + } + synchronized (loadingFromGPSFixStoreStateMonitor) { + loadingFromGPSFixStoreStateMonitor.notifyAll(); + } + LockUtil.unlockAfterWrite(getLoadingFromGPSFixStoreLock()); + LockUtil.unlockAfterRead(getSerializationLock()); + } + } + }.start(); + } else { + logger.severe("Got a request to attach race log for an empty race log!"); + } + } + + @Override + public void detachRaceLog(Serializable identifier) { + this.attachedRaceLogs.remove(identifier); + } + + @Override + public void detachAllRaceLogs() { + this.attachedRaceLogs.clear(); + } + + @Override + public RaceLog getRaceLog(Serializable identifier) { + return attachedRaceLogs.get(identifier); + } + + @Override + public Distance getDistanceToStartLine(Competitor competitor, long millisecondsBeforeRaceStart) { + final Distance result; + if (getStartOfRace() == null) { + result = null; + } else { + TimePoint beforeStart = new MillisecondsTimePoint(getStartOfRace().asMillis() - millisecondsBeforeRaceStart); + result = getDistanceToStartLine(competitor, beforeStart); + } + return result; + } + + @Override + public Distance getDistanceToStartLine(Competitor competitor, TimePoint timePoint) { + Waypoint startWaypoint = getRace().getCourse().getFirstWaypoint(); + final Distance result; + if (startWaypoint == null) { + result = null; + } else { + Position competitorPosition = getTrack(competitor).getEstimatedPosition(timePoint, /* extrapolate */false); + if (competitorPosition == null) { + result = null; + } else { + Iterable marks = startWaypoint.getControlPoint().getMarks(); + Iterator marksIterator = marks.iterator(); + Mark first = marksIterator.next(); + Position firstPosition = getOrCreateTrack(first).getEstimatedPosition(timePoint, /* extrapolate */false); + if (firstPosition == null) { + result = null; + } else { + if (marksIterator.hasNext()) { + // it's a line / gate + Mark second = marksIterator.next(); + Position secondPosition = getOrCreateTrack(second).getEstimatedPosition(timePoint, /* extrapolate */ + false); + final Bearing bearingGreatCircle = firstPosition.getBearingGreatCircle(secondPosition); + if (bearingGreatCircle == null) { + result = null; + } else { + Position competitorProjectedOntoStartLine = competitorPosition.projectToLineThrough( + firstPosition, bearingGreatCircle); + result = competitorPosition.getDistance(competitorProjectedOntoStartLine); + } + } else { + result = competitorPosition.getDistance(firstPosition); + } + } + } + } + return result; + } + + @Override + public Speed getSpeed(Competitor competitor, long millisecondsBeforeRaceStart) { + if (getStartOfRace() == null) { + return null; + } + + TimePoint beforeStart = new MillisecondsTimePoint(getStartOfRace().asMillis() - millisecondsBeforeRaceStart); + return getTrack(competitor).getEstimatedSpeed(beforeStart); + } + + @Override + public Distance getDistanceFromStarboardSideOfStartLineWhenPassingStart(Competitor competitor) { + final Distance result; + TrackedLegOfCompetitor firstTrackedLegOfCompetitor = getTrackedLeg(competitor, getRace().getCourse().getFirstLeg()); + TimePoint competitorStartTime = firstTrackedLegOfCompetitor.getStartTime(); + if (competitorStartTime != null) { + Position competitorPositionWhenPassingStart = getTrack(competitor).getEstimatedPosition( + competitorStartTime, /* extrapolate */false); + final Position starboardMarkPosition = getStarboardMarkOfStartlinePosition(competitorStartTime); + if (competitorPositionWhenPassingStart != null && starboardMarkPosition != null) { + result = starboardMarkPosition == null ? null : competitorPositionWhenPassingStart.getDistance(starboardMarkPosition); + } else { + result = null; + } + } else { + result = null; + } + return result; + } + + @Override + public Distance getDistanceFromStarboardSideOfStartLine(Competitor competitor, TimePoint timePoint) { + final Distance result; + if (timePoint != null) { + Position competitorPositionWhenPassingStart = getTrack(competitor).getEstimatedPosition( + timePoint, /* extrapolate */false); + final Position starboardMarkPosition = getStarboardMarkOfStartlinePosition(timePoint); + if (competitorPositionWhenPassingStart != null && starboardMarkPosition != null) { + result = starboardMarkPosition == null ? null : competitorPositionWhenPassingStart.getDistance(starboardMarkPosition); + } else { + result = null; + } + } else { + result = null; + } + return result; + } + + /** + * Based on the bearing from the start waypoint to the next mark, identifies which of the two marks of the start + * line is on starboard. If the start waypoint has only one mark, that mark is returned. If the start line has two + * marks but the course has no other waypoint, + * null is returned. If the course has no waypoints at all, null is returned.

+ * + * The method has protected visibility largely for testing purposes. + */ + protected Mark getStarboardMarkOfStartlineOrSingleStartMark(TimePoint at) { + LineDetails startLine = getStartLine(at); + final Mark result; + if (startLine != null) { + result = startLine.getStarboardMarkWhileApproachingLine(); + } else { + Waypoint startWaypoint = getRace().getCourse().getFirstWaypoint(); + if (startWaypoint != null && startWaypoint.getMarks().iterator().hasNext()) { + result = startWaypoint.getMarks().iterator().next(); + } else { + result = null; + } + } + return result; + } + + /** + * Based on the bearing from the start waypoint to the + * next mark, identifies which of the two marks of the start line is on starboard and returns its position. If the start waypoint has only + * one mark, that mark is returned. If the start line has two marks but the course has no other waypoint, + * null is returned. If the course has no waypoints at all, null is returned. + */ + private Position getStarboardMarkOfStartlinePosition(TimePoint at) { + Mark starboardMark = getStarboardMarkOfStartlineOrSingleStartMark(at); + if (starboardMark != null) { + return getOrCreateTrack(starboardMark).getEstimatedPosition(at, /*extrapolate*/ false); + } + return null; + } + + protected NamedReentrantReadWriteLock getLoadingFromWindStoreLock() { + return loadingFromWindStoreLock; + } + + protected NamedReentrantReadWriteLock getLoadingFromGPSFixStoreLock() { + return loadingFromGPSFixStoreLock; + } + + /** + * If the waypoint is not a line, or no position can be determined for one of its marks at timePoint, + * null is returned. If no wind information is available but required to compute the advantage, null values + * are returned in those fields that depend on wind data. If the waypoint is null + * or is the only waypoint, null is returned because no reasonable statement can be + * made about the direction from which the line is to be passed. + */ + private LineDetails getLineLengthAndAdvantage(TimePoint timePoint, Waypoint waypoint) { + List markPositions = new ArrayList(); + int numberOfMarks = 0; + boolean allMarksHavePositions = true; + LineDetails result = null; + if (waypoint != null) { + for (Mark lineMark : waypoint.getMarks()) { + numberOfMarks++; + final Position estimatedMarkPosition = getOrCreateTrack(lineMark).getEstimatedPosition(timePoint, /* extrapolate */ + false); + if (estimatedMarkPosition != null) { + markPositions.add(estimatedMarkPosition); + } else { + allMarksHavePositions = false; + } + } + final List legs = getRace().getCourse().getLegs(); + // need at least one leg to make sense of a line + if (!legs.isEmpty()) { + try { + if (allMarksHavePositions && numberOfMarks == 2) { + final int indexOfWaypoint = getRace().getCourse().getIndexOfWaypoint(waypoint); + final boolean isStartLine = indexOfWaypoint == 0; + final TrackedLeg legDeterminingDirection = getTrackedLeg(legs.get(isStartLine ? 0 + : indexOfWaypoint - 1)); + Distance crossTrackErrorOfMark0OnLineFromMark1ToNextWaypoint = markPositions.get(0) + .crossTrackError(markPositions.get(1), legDeterminingDirection.getLegBearing(timePoint)); + final Mark starboardMarkWhileApproachingLine; + final Mark portMarkWhileApproachingLine; + final Position starboardMarkPositionWhileApproachingLine; + final Position portMarkPositionWhileApproachingLine; + if (crossTrackErrorOfMark0OnLineFromMark1ToNextWaypoint.getMeters() < 0) { + portMarkWhileApproachingLine = Util.get(waypoint.getMarks(), 0); + starboardMarkWhileApproachingLine = Util.get(waypoint.getMarks(), 1); + } else { + portMarkWhileApproachingLine = Util.get(waypoint.getMarks(), 1); + starboardMarkWhileApproachingLine = Util.get(waypoint.getMarks(), 0); + } + portMarkPositionWhileApproachingLine = getOrCreateTrack(portMarkWhileApproachingLine) + .getEstimatedPosition(timePoint, /* extrapolate */false); + starboardMarkPositionWhileApproachingLine = getOrCreateTrack(starboardMarkWhileApproachingLine) + .getEstimatedPosition(timePoint, /* extrapolate */false); + final Bearing differenceToCombinedWind; + final NauticalSide advantageousSideWhileApproachingLine; + final Distance distanceAdvantage; + Wind combinedWind = getWind(markPositions.get(0), timePoint); + if (combinedWind != null) { + differenceToCombinedWind = portMarkPositionWhileApproachingLine.getBearingGreatCircle( + starboardMarkPositionWhileApproachingLine).getDifferenceTo(combinedWind.getFrom()); + Distance windwardDistanceFromFirstToSecondMark; + windwardDistanceFromFirstToSecondMark = legDeterminingDirection.getWindwardDistance( + markPositions.get(0), markPositions.get(1), timePoint); + final Position worseMarkPosition; + final Position betterMarkPosition; + if ((isStartLine && windwardDistanceFromFirstToSecondMark.getMeters() > 0) + || (!isStartLine && windwardDistanceFromFirstToSecondMark.getMeters() < 0)) { + // first mark is worse than second mark + worseMarkPosition = markPositions.get(0); + betterMarkPosition = markPositions.get(1); + } else { + // second mark is worse than first mark + worseMarkPosition = markPositions.get(1); + betterMarkPosition = markPositions.get(0); + } + if (windwardDistanceFromFirstToSecondMark.getMeters() >= 0) { + distanceAdvantage = windwardDistanceFromFirstToSecondMark; + } else { + distanceAdvantage = new CentralAngleDistance( + -windwardDistanceFromFirstToSecondMark.getCentralAngleRad()); + } + if (betterMarkPosition.crossTrackError(worseMarkPosition, + legDeterminingDirection.getLegBearing(timePoint)).getCentralAngleRad() > 0) { + advantageousSideWhileApproachingLine = NauticalSide.STARBOARD; + } else { + advantageousSideWhileApproachingLine = NauticalSide.PORT; + } + } else { + differenceToCombinedWind = null; + advantageousSideWhileApproachingLine = null; + distanceAdvantage = null; + } + result = new LineDetailsImpl(timePoint, waypoint, + portMarkPositionWhileApproachingLine + .getDistance(starboardMarkPositionWhileApproachingLine), + differenceToCombinedWind, advantageousSideWhileApproachingLine, distanceAdvantage, + portMarkWhileApproachingLine, starboardMarkWhileApproachingLine); + } + } catch (NoWindException e) { + // result remains null; + } + } + } + return result; + } + + + @Override + public LineDetails getStartLine(TimePoint at) { + return getLineLengthAndAdvantage(at, getRace().getCourse().getFirstWaypoint()); + } + + @Override + public LineDetails getFinishLine(TimePoint at) { + return getLineLengthAndAdvantage(at, getRace().getCourse().getLastWaypoint()); + } + + @Override + public SpeedWithConfidence getAverageWindSpeedWithConfidence(long resolutionInMillis) { + SpeedWithConfidence result = null; + if (getEndOfRace() != null) { + TimePoint fromTimePoint = getStartOfRace(); + TimePoint toTimePoint = getEndOfRace(); + + List windSourcesToDeliver = new ArrayList(); + WindSourceImpl windSource = new WindSourceImpl(WindSourceType.COMBINED); + windSourcesToDeliver.add(windSource); + + double sumWindSpeed = 0.0; + double sumWindSpeedConfidence = 0.0; + int speedCounter = 0; + + int numberOfFixes = (int) ((toTimePoint.asMillis() - fromTimePoint.asMillis()) / resolutionInMillis); + WindTrack windTrack = getOrCreateWindTrack(windSource); + TimePoint timePoint = fromTimePoint; + for (int i = 0; i < numberOfFixes && toTimePoint != null && timePoint.compareTo(toTimePoint) < 0; i++) { + WindWithConfidence> averagedWindWithConfidence = windTrack + .getAveragedWindWithConfidence(null, timePoint); + if (averagedWindWithConfidence != null) { + double windSpeedinKnots = averagedWindWithConfidence.getObject().getKnots(); + double confidence = averagedWindWithConfidence.getConfidence(); + + sumWindSpeed += windSpeedinKnots; + sumWindSpeedConfidence += confidence; + + speedCounter++; + } + timePoint = new MillisecondsTimePoint(timePoint.asMillis() + resolutionInMillis); + } + if (speedCounter > 0) { + Speed averageWindSpeed = new KnotSpeedImpl(sumWindSpeed / speedCounter); + double averageWindSpeedConfidence = sumWindSpeedConfidence / speedCounter; + result = new SpeedWithConfidenceImpl(averageWindSpeed, averageWindSpeedConfidence, toTimePoint); + } + } + return result; + } + + @Override + public Distance getCourseLength() { + List legs = getRace().getCourse().getLegs(); + Distance raceDistance = new NauticalMileDistance(0); + for (Leg leg : legs) { + Waypoint from = leg.getFrom(); + Iterable markPassings = getMarkPassingsInOrder(from); + Iterator markPassingsIterator = markPassings.iterator(); + if (!markPassingsIterator.hasNext()) { + return null; + } + MarkPassing firstPassing = markPassingsIterator.next(); + TimePoint timePointOfFirstPassing = firstPassing.getTimePoint(); + Waypoint to = leg.getTo(); + Position fromPos = getApproximatePosition(from, timePointOfFirstPassing); + Position toPos = getApproximatePosition(to, timePointOfFirstPassing); + Distance legDistance = fromPos.getDistance(toPos); + raceDistance = raceDistance.add(legDistance); + } + return raceDistance; + } + + @Override + public Speed getSpeedWhenCrossingStartLine(Competitor competitor) { + NavigableSet competitorMarkPassings = getMarkPassings(competitor); + Speed competitorSpeedWhenPassingStart = null; + lockForRead(competitorMarkPassings); + try { + if (!competitorMarkPassings.isEmpty()) { + TimePoint competitorStartTime = competitorMarkPassings.first().getTimePoint(); + competitorSpeedWhenPassingStart = getTrack(competitor).getEstimatedSpeed( + competitorStartTime); + } + } finally { + unlockAfterRead(competitorMarkPassings); + } + return competitorSpeedWhenPassingStart; + } + + @Override + public GPSFixStore getGPSFixStore() { + return gpsFixStore; + } +} From 8f879b56fc4407a56131dc28395597860329c5ef Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Wed, 4 Jun 2014 21:52:58 +0200 Subject: [PATCH 10/10] use large buffer max size again for GPS import; some formats such as KML22 read until the end of the stream, hoping they find something towards the end... --- .../impl/BaseRouteConverterGPSFixImporterImpl.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/java/com.sap.sailing.server.trackfiles/src/com/sap/sailing/server/trackfiles/impl/BaseRouteConverterGPSFixImporterImpl.java b/java/com.sap.sailing.server.trackfiles/src/com/sap/sailing/server/trackfiles/impl/BaseRouteConverterGPSFixImporterImpl.java index 07e4c5875ae..36e914f2735 100644 --- a/java/com.sap.sailing.server.trackfiles/src/com/sap/sailing/server/trackfiles/impl/BaseRouteConverterGPSFixImporterImpl.java +++ b/java/com.sap.sailing.server.trackfiles/src/com/sap/sailing/server/trackfiles/impl/BaseRouteConverterGPSFixImporterImpl.java @@ -62,8 +62,12 @@ public abstract class BaseRouteConverterGPSFixImporterImpl extends BaseGPSFixImp Method m = NavigationFormatParser.class.getDeclaredMethod("read", InputStream.class, Integer.TYPE, CompactCalendar.class, List.class); m.setAccessible(true); - ParserResult result = parser.read(inputStream, /* read buffer size; 1MB max should be sufficient to - determine that RouteConverter cannot parse the format */ 1024 * 1024, null, supportedReadFormats); + ParserResult result = parser.read(inputStream, /* read buffer size; 1GB max... the problem is that some + * formats continue to read to the end even if they don't + * happen to find anything in the stream because they assume + * that something parsable may be coming late in the + * stream... + */ 1024 * 1024 * 1024, null, supportedReadFormats); if (result == null) { throw new FormatNotSupportedException(); }