diff --git a/configuration/addSecret b/configuration/addSecret new file mode 100755 index 00000000000..22f682f59be --- /dev/null +++ b/configuration/addSecret @@ -0,0 +1,22 @@ +#!/bin/bash + +# Use like this: +# +# echo "MY_SECRET_ENV_VAR=\\\"some secret that may even contain blanks or = or &\\\"" | ./addSecret +# +# Copy stdin to SECRETS_TO_ADD +SECRETS_TO_ADD=$( cat ) +SERVERS_PATH=/home/sailing/servers +SECRETS_PATH=/root/secrets +SECRETS_IN_APP_PATH=configuration/secrets +FIND_ANALYTICS_SERVERS_SCRIPT="$( dirname ${0} )/findSailingAnalyticsServers" +for IP in $( ${FIND_ANALYTICS_SERVERS_SCRIPT} ); do + echo "Handling server with IP ${IP} ..." + ssh root@$IP 'echo "Appending '${SECRETS_TO_ADD}' to '${SECRETS_PATH}'" +echo "'${SECRETS_TO_ADD}'" >>'${SECRETS_PATH}' +echo "Server directories:" +for i in $( ls '${SERVERS_PATH}' ); do + echo "Appending to '${SERVERS_PATH}'/${i}/'${SECRETS_IN_APP_PATH}'" + echo "'${SECRETS_TO_ADD}'" >>'${SERVERS_PATH}'/${i}/'${SECRETS_IN_APP_PATH}' +done' +done diff --git a/configuration/findSailingAnalyticsServers b/configuration/findSailingAnalyticsServers new file mode 100755 index 00000000000..e99a61b361a --- /dev/null +++ b/configuration/findSailingAnalyticsServers @@ -0,0 +1,2 @@ +#!/bin/bash +aws ec2 describe-instances --filters Name=tag-key,Values=sailing-analytics-server | jq -r '.Reservations[].Instances[].NetworkInterfaces[].PrivateIpAddresses[].Association.PublicIp' diff --git a/java/com.sap.sailing.datamining.shared/src/com/sap/sailing/datamining/shared/TackTypeSegmentsDataMiningSettings.java b/java/com.sap.sailing.datamining.shared/src/com/sap/sailing/datamining/shared/TackTypeSegmentsDataMiningSettings.java new file mode 100644 index 00000000000..69c0a5814bc --- /dev/null +++ b/java/com.sap.sailing.datamining.shared/src/com/sap/sailing/datamining/shared/TackTypeSegmentsDataMiningSettings.java @@ -0,0 +1,30 @@ +package com.sap.sailing.datamining.shared; + +import com.sap.sse.common.Duration; +import com.sap.sse.common.settings.SerializableSettings; + +public class TackTypeSegmentsDataMiningSettings extends SerializableSettings { + private static final long serialVersionUID = 3239182650750480678L; + private final Duration minimumTackTypeSegmentDuration; + private final Duration minimumDurationBetweenAdjacentTackTypeSegments; + + public TackTypeSegmentsDataMiningSettings(Duration minimumTackTypeSegmentDuration, Duration minimumDurationBetweenAdjacentTackTypeSegments) { + super(); + this.minimumTackTypeSegmentDuration = minimumTackTypeSegmentDuration; + this.minimumDurationBetweenAdjacentTackTypeSegments = minimumDurationBetweenAdjacentTackTypeSegments; + } + + public Duration getMinimumTackTypeSegmentDuration() { + return minimumTackTypeSegmentDuration; + } + public Duration getMinimumDurationBetweenAdjacentTackTypeSegments() { + return minimumDurationBetweenAdjacentTackTypeSegments; + } + + public static TackTypeSegmentsDataMiningSettings createDefaultSettings() { + return new TackTypeSegmentsDataMiningSettings( + /* minimumTackTypeSegmentDuration */ null, + /* minimumDurationBetweenAdjacentTackTypeSegments */ null); + } + +} diff --git a/java/com.sap.sailing.datamining.test/META-INF/MANIFEST.MF b/java/com.sap.sailing.datamining.test/META-INF/MANIFEST.MF index 79a5b1270ef..c65814ee2ac 100644 --- a/java/com.sap.sailing.datamining.test/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.datamining.test/META-INF/MANIFEST.MF @@ -12,5 +12,8 @@ Import-Package: junit.framework;version="4.8.2", Require-Bundle: org.mockito.mockito-core;bundle-version="4.8.1", org.hamcrest;bundle-version="2.2.0", com.sap.sse.common, - org.objenesis;bundle-version="2.1.0" + org.objenesis;bundle-version="2.1.0", + net.bytebuddy.byte-buddy;bundle-version="1.12.18", + net.bytebuddy.byte-buddy-agent;bundle-version="1.12.18", + com.sap.sailing.domain.test Automatic-Module-Name: com.sap.sailing.datamining.test diff --git a/java/com.sap.sailing.datamining.test/src/com/sap/sailing/datamining/impl/components/TestSegmentsTackType.java b/java/com.sap.sailing.datamining.test/src/com/sap/sailing/datamining/impl/components/TestSegmentsTackType.java new file mode 100644 index 00000000000..539b22374ad --- /dev/null +++ b/java/com.sap.sailing.datamining.test/src/com/sap/sailing/datamining/impl/components/TestSegmentsTackType.java @@ -0,0 +1,194 @@ +package com.sap.sailing.datamining.impl.components; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import org.junit.Before; +import org.junit.Test; + +import com.sap.sailing.datamining.data.HasLeaderboardContext; +import com.sap.sailing.datamining.data.HasRaceOfCompetitorContext; +import com.sap.sailing.datamining.data.HasTackTypeSegmentContext; +import com.sap.sailing.datamining.data.HasTrackedRaceContext; +import com.sap.sailing.datamining.impl.data.LeaderboardWithContext; +import com.sap.sailing.datamining.impl.data.RaceOfCompetitorWithContext; +import com.sap.sailing.datamining.impl.data.TrackedRaceWithContext; +import com.sap.sailing.datamining.shared.TackTypeSegmentsDataMiningSettings; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.CompetitorWithBoat; +import com.sap.sailing.domain.base.Waypoint; +import com.sap.sailing.domain.base.impl.CourseAreaImpl; +import com.sap.sailing.domain.common.Position; +import com.sap.sailing.domain.common.impl.DegreePosition; +import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; +import com.sap.sailing.domain.common.tracking.GPSFixMoving; +import com.sap.sailing.domain.common.tracking.impl.GPSFixMovingImpl; +import com.sap.sailing.domain.leaderboard.Leaderboard; +import com.sap.sailing.domain.leaderboard.impl.FlexibleLeaderboardImpl; +import com.sap.sailing.domain.leaderboard.impl.LowPoint; +import com.sap.sailing.domain.leaderboard.impl.ThresholdBasedResultDiscardingRuleImpl; +import com.sap.sailing.domain.ranking.OneDesignRankingMetric; +import com.sap.sailing.domain.test.StoredTrackBasedTest; +import com.sap.sailing.domain.tracking.DynamicGPSFixTrack; +import com.sap.sailing.domain.tracking.MarkPassing; +import com.sap.sailing.domain.tracking.impl.DynamicTrackedRaceImpl; +import com.sap.sailing.domain.tracking.impl.MarkPassingImpl; +import com.sap.sse.common.Distance; +import com.sap.sse.common.Distance.NullDistance; +import com.sap.sse.common.Duration; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.Util; +import com.sap.sse.common.impl.DegreeBearingImpl; +import com.sap.sse.common.impl.MillisecondsTimePoint; + +public class TestSegmentsTackType extends StoredTrackBasedTest { + + private DynamicTrackedRaceImpl trackedRace; + private CompetitorWithBoat competitorA; + private HasRaceOfCompetitorContext raceOfCompContext; + private TackTypeSegmentRetrievalProcessor resultTTSegmentsRetrieval; + + @Before + public void setup() { + competitorA = createCompetitorWithBoat("A"); + trackedRace = createTestTrackedRace("TestRegatta", "TestRace", "F18", createCompetitorAndBoatsMap(competitorA), + MillisecondsTimePoint.now(), /* useMarkPassingCalculator */ true, null, + OneDesignRankingMetric::new); + final Leaderboard leaderboard = new FlexibleLeaderboardImpl("Test", + new ThresholdBasedResultDiscardingRuleImpl(new int[0]), new LowPoint(), + new CourseAreaImpl("Here", UUID.randomUUID(), /* centerPosition */ null, /* radius */ null)); + final HasLeaderboardContext leaderboardContext = new LeaderboardWithContext(leaderboard, null); + final HasTrackedRaceContext trackedRaceContext = new TrackedRaceWithContext(leaderboardContext, + trackedRace.getTrackedRegatta().getRegatta(), null, null, trackedRace); + raceOfCompContext = new RaceOfCompetitorWithContext(trackedRaceContext, competitorA, TackTypeSegmentsDataMiningSettings.createDefaultSettings()); + resultTTSegmentsRetrieval = new TackTypeSegmentRetrievalProcessor(null, Collections.emptySet(), TackTypeSegmentsDataMiningSettings.createDefaultSettings(), 0, null); + } + + private Iterable retrieveData() { + return resultTTSegmentsRetrieval.retrieveData(raceOfCompContext); + } + + @Test + public void testingSegmentsAreNotNull() { + // set up GPS fixes for competitor, as well as mark passings: + DynamicGPSFixTrack competitorATrack = trackedRace.getTrack(competitorA); + final KnotSpeedWithBearingImpl sogCog = new KnotSpeedWithBearingImpl(10, new DegreeBearingImpl(45)); + TimePoint timePoint = trackedRace.getStartOfTracking().plus(10); + GPSFixMovingImpl currentGPS = new GPSFixMovingImpl(new DegreePosition(54.4680424, 10.234451), timePoint, sogCog); + final Duration timeBetweenFixes = Duration.ofMillis(490); + for (int i=0; i<20; i++) { + competitorATrack.addGPSFix(currentGPS); + final Position currentPosition = sogCog.travelTo(currentGPS.getPosition(), timeBetweenFixes); + timePoint = timePoint.plus(timeBetweenFixes); + currentGPS = new GPSFixMovingImpl(currentPosition, timePoint, sogCog); + } + TimePoint markPassingTimePoint = trackedRace.getStartOfTracking().plus(20); + final List markPassingsForCompetitor = new ArrayList<>(); + final Duration legDuration = Duration.ofSeconds(60); + for (Waypoint waypoint : trackedRace.getRace().getCourse().getWaypoints()) { + markPassingsForCompetitor.add(new MarkPassingImpl(markPassingTimePoint, waypoint, competitorA)); + markPassingTimePoint = markPassingTimePoint.plus(legDuration); + } + trackedRace.updateMarkPassings(competitorA, markPassingsForCompetitor); + assertNotNull(trackedRace.getEndOfRace()); + // now run the actual test: + final Iterable allTTSegments = retrieveData(); + Distance sumDistance = new NullDistance(); + for (HasTackTypeSegmentContext oneTTSegment : allTTSegments) { + if (oneTTSegment != null) { + sumDistance = sumDistance.add(oneTTSegment.getDistance()); + } + } + assertTrue(sumDistance.compareTo(Distance.NULL) > 0); + } + + @Test + public void testingMissingMarkPassing() { + // set up GPS fixes for competitor, but no mark passings: + DynamicGPSFixTrack competitorATrack = trackedRace.getTrack(competitorA); + final KnotSpeedWithBearingImpl sogCog = new KnotSpeedWithBearingImpl(10, new DegreeBearingImpl(45)); + TimePoint timePoint = trackedRace.getStartOfTracking().plus(10); + GPSFixMovingImpl currentGPS = new GPSFixMovingImpl(new DegreePosition(54.4680424, 10.234451), timePoint, sogCog); + final Duration timeBetweenFixes = Duration.ofMillis(490); + for (int i=0; i<20; i++) { + competitorATrack.addGPSFix(currentGPS); + final Position currentPosition = sogCog.travelTo(currentGPS.getPosition(), timeBetweenFixes); + timePoint = timePoint.plus(timeBetweenFixes); + currentGPS = new GPSFixMovingImpl(currentPosition, timePoint, sogCog); + } + // now run the actual test: + final Iterable allTTSegments = retrieveData(); + assertTrue(Util.isEmpty(allTTSegments)); + } + + @Test + public void testingFixExactlyOnMarkPassingAndRaceOpenEnded() { + final List expectedSegmentStarts = new ArrayList<>(); + // set up GPS fixes for competitor, as well as mark passings: + DynamicGPSFixTrack competitorATrack = trackedRace.getTrack(competitorA); + // start on port tack: + final KnotSpeedWithBearingImpl sogCogPortTackUpwind = new KnotSpeedWithBearingImpl(10, new DegreeBearingImpl(45)); + TimePoint timePoint = trackedRace.getStartOfTracking().plus(10); + final Position middleOfStartLine = trackedRace.getApproximatePosition(trackedRace.getRace().getCourse().getFirstWaypoint(), timePoint); + final Position topMarkPosition = trackedRace.getApproximatePosition(Util.get(trackedRace.getRace().getCourse().getWaypoints(), 1), timePoint); + // start straight "under" the top mark + GPSFixMovingImpl currentGPS = new GPSFixMovingImpl(new DegreePosition(middleOfStartLine.getLatDeg(), topMarkPosition.getLngDeg()), timePoint, sogCogPortTackUpwind); + expectedSegmentStarts.add(timePoint); + final Duration timeBetweenFixes = Duration.ofMillis(490); + for (int i=0; i<20; i++) { + competitorATrack.addGPSFix(currentGPS); + final Position currentPosition = sogCogPortTackUpwind.travelTo(currentGPS.getPosition(), timeBetweenFixes); + timePoint = timePoint.plus(timeBetweenFixes); + currentGPS = new GPSFixMovingImpl(currentPosition, timePoint, sogCogPortTackUpwind); + } + // now tack onto starboard tack: + final KnotSpeedWithBearingImpl sogCogStarboardTackUpwind = new KnotSpeedWithBearingImpl(10, new DegreeBearingImpl(315)); + expectedSegmentStarts.add(timePoint); + for (int i=0; i<20; i++) { + competitorATrack.addGPSFix(currentGPS); + final Position currentPosition = sogCogStarboardTackUpwind.travelTo(currentGPS.getPosition(), timeBetweenFixes); + timePoint = timePoint.plus(timeBetweenFixes); + currentGPS = new GPSFixMovingImpl(currentPosition, timePoint, sogCogStarboardTackUpwind); + } + final List markPassingsForCompetitor = new ArrayList<>(); + final TimePoint startMarkPassingTimePoint = trackedRace.getStartOfTracking().plus(20); + markPassingsForCompetitor.add(new MarkPassingImpl(startMarkPassingTimePoint, trackedRace.getRace().getCourse().getFirstWaypoint(), competitorA)); + final TimePoint windwardMarkPassingTimePoint = timePoint; // exactly the time point of the last fix + markPassingsForCompetitor.add(new MarkPassingImpl(windwardMarkPassingTimePoint, Util.get(trackedRace.getRace().getCourse().getWaypoints(), 1), competitorA)); + trackedRace.updateMarkPassings(competitorA, markPassingsForCompetitor); + // continue sailing downwind: + // start on starboard tack: + final KnotSpeedWithBearingImpl sogCogStarboardTackDownwind = new KnotSpeedWithBearingImpl(10, new DegreeBearingImpl(225)); + expectedSegmentStarts.add(timePoint); + for (int i=0; i<20; i++) { + competitorATrack.addGPSFix(currentGPS); + final Position currentPosition = sogCogStarboardTackDownwind.travelTo(currentGPS.getPosition(), timeBetweenFixes); + timePoint = timePoint.plus(timeBetweenFixes); + currentGPS = new GPSFixMovingImpl(currentPosition, timePoint, sogCogPortTackUpwind); + } + // now gybe onto port tack: + final KnotSpeedWithBearingImpl sogCogPortTackDownwind = new KnotSpeedWithBearingImpl(10, new DegreeBearingImpl(135)); + expectedSegmentStarts.add(timePoint); + for (int i=0; i<20; i++) { + competitorATrack.addGPSFix(currentGPS); + final Position currentPosition = sogCogPortTackDownwind.travelTo(currentGPS.getPosition(), timeBetweenFixes); + timePoint = timePoint.plus(timeBetweenFixes); + currentGPS = new GPSFixMovingImpl(currentPosition, timePoint, sogCogPortTackDownwind); + } + // now run the actual test: + final Iterable allTTSegments = retrieveData(); + assertEquals(4, Util.size(allTTSegments)); + // assert that all segments are of similar distance; they are not exactly equal + // because due to smoothing of COG/SOG, tack type transitions are not exactly where COG goes from 45 to, say, 315 + final Distance distanceFirstSegment = allTTSegments.iterator().next().getDistance(); + for (final HasTackTypeSegmentContext ttSegment : allTTSegments) { + assertEquals(distanceFirstSegment.getMeters(), ttSegment.getDistance().getMeters(), distanceFirstSegment.scale(0.2).getMeters() /* allow for 20% tolerance */); + } + } +} \ No newline at end of file diff --git a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages.properties b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages.properties index 49582dda867..b528640b671 100644 --- a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages.properties +++ b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages.properties @@ -259,9 +259,16 @@ LengthOfTheLeg=Length of the Leg LegSailingDomainRetrieverChain=Leg TackType=Long / short tack getTackTypeofRace=Long / short tack of the race +tackTypeSegmentsRetrieverChainDefinition=Long/Short tack segments +TackTypeSegments=Long/Short segments +TackTypeSegmentName=Long/Short tack segments name +TackTypeDuration=Long/Short tack duration +TackTypeDistance=Long/Short tack distance InRace=In race InTrackingInterval=In tracking interval NumberOfCompetitors=Number of Competitors CompetitorInLeaderboard=Competitor in Leaderboard CompetitorSailingDomainRetrieverChain=Competitors in Leaderboards -SmoothedSpeed=Smoothed Speed \ No newline at end of file +SmoothedSpeed=Smoothed Speed +RatioDistanceLongVsShortTack=Ratio distance long tack / short tack +RatioDurationLongVsShortTack=Ratio duration long tack / short tack \ No newline at end of file diff --git a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_cs.properties b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_cs.properties index f5c4552f4dd..e1324c00f35 100644 --- a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_cs.properties +++ b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_cs.properties @@ -259,3 +259,9 @@ LengthOfTheLeg=Délka úseku LegSailingDomainRetrieverChain=Úsek TackType=Dlouhý/krátký obrat getTackTypeofRace=Dlouhý/krátký obrat rozjížďky +InRace=In race +InTrackingInterval=In tracking interval +NumberOfCompetitors=Number of Competitors +CompetitorInLeaderboard=Competitor in Leaderboard +CompetitorSailingDomainRetrieverChain=Competitors in Leaderboards +SmoothedSpeed=Smoothed Speed diff --git a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_da.properties b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_da.properties index 8ff2b5fc4bf..c3deff723bf 100644 --- a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_da.properties +++ b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_da.properties @@ -259,3 +259,9 @@ LengthOfTheLeg=Længde på benet LegSailingDomainRetrieverChain=Ben TackType=Lang/kort stagvende getTackTypeofRace=Lang/kort stagvende for kapsejladsen +InRace=In race +InTrackingInterval=In tracking interval +NumberOfCompetitors=Number of Competitors +CompetitorInLeaderboard=Competitor in Leaderboard +CompetitorSailingDomainRetrieverChain=Competitors in Leaderboards +SmoothedSpeed=Smoothed Speed diff --git a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_de.properties b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_de.properties index 964a815816b..21c5104347e 100755 --- a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_de.properties +++ b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_de.properties @@ -267,4 +267,6 @@ InTrackingInterval=Im Tracking-Zeitraum der Wettfahrt NumberOfCompetitors=Anzahl Teilnehmer CompetitorInLeaderboard=Teilnehmer aus Rangliste CompetitorSailingDomainRetrieverChain=Teilnehmer in Ranglisten -SmoothedSpeed=Geglättete Geschwindigkeit \ No newline at end of file +SmoothedSpeed=Geglättete Geschwindigkeit +RatioDistanceLongVsShortTack=Verhältnis gesegelte Distanz auf Streck- zu Holebug +RatioDurationLongVsShortTack=Verhältnis gesegelte Dauer auf Streck- zu Holebug \ No newline at end of file diff --git a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_es.properties b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_es.properties index 48f8cd2891d..34a317982ea 100644 --- a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_es.properties +++ b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_es.properties @@ -259,3 +259,9 @@ LengthOfTheLeg=Longitud del tramo LegSailingDomainRetrieverChain=Tramo TackType=Bordo largo/corto getTackTypeofRace=Bordo largo/corto de la prueba +InRace=In race +InTrackingInterval=In tracking interval +NumberOfCompetitors=Number of Competitors +CompetitorInLeaderboard=Competitor in Leaderboard +CompetitorSailingDomainRetrieverChain=Competitors in Leaderboards +SmoothedSpeed=Smoothed Speed diff --git a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_fr.properties b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_fr.properties index aee71191623..692ecba948b 100644 --- a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_fr.properties +++ b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_fr.properties @@ -259,3 +259,9 @@ LengthOfTheLeg=Longueur de la portion de parcours LegSailingDomainRetrieverChain=Portion de parcours TackType=Virement long/court getTackTypeofRace=Virement long/court de la course +InRace=In race +InTrackingInterval=In tracking interval +NumberOfCompetitors=Number of Competitors +CompetitorInLeaderboard=Competitor in Leaderboard +CompetitorSailingDomainRetrieverChain=Competitors in Leaderboards +SmoothedSpeed=Smoothed Speed diff --git a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_it.properties b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_it.properties index eeac0ff0805..bb613c2998d 100644 --- a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_it.properties +++ b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_it.properties @@ -259,3 +259,9 @@ LengthOfTheLeg=Lunghezza della tratta LegSailingDomainRetrieverChain=Tratta TackType=Bordo lungo/corto getTackTypeofRace=Bordo lungo/corto della gara +InRace=In race +InTrackingInterval=In tracking interval +NumberOfCompetitors=Number of Competitors +CompetitorInLeaderboard=Competitor in Leaderboard +CompetitorSailingDomainRetrieverChain=Competitors in Leaderboards +SmoothedSpeed=Smoothed Speed diff --git a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_ja.properties b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_ja.properties index 11ab9830c55..f611bb67b9b 100644 --- a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_ja.properties +++ b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_ja.properties @@ -259,3 +259,9 @@ LengthOfTheLeg=レグの長さ LegSailingDomainRetrieverChain=レグ TackType=ロング/ショートタック getTackTypeofRace=レースのロング/ショートタック +InRace=In race +InTrackingInterval=In tracking interval +NumberOfCompetitors=Number of Competitors +CompetitorInLeaderboard=Competitor in Leaderboard +CompetitorSailingDomainRetrieverChain=Competitors in Leaderboards +SmoothedSpeed=Smoothed Speed diff --git a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_pt.properties b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_pt.properties index ecb9e722c6c..13fc0af072a 100644 --- a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_pt.properties +++ b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_pt.properties @@ -259,3 +259,9 @@ LengthOfTheLeg=Comprimento da perna LegSailingDomainRetrieverChain=Perna TackType=Cambada longa/curta getTackTypeofRace=Cambada longa/curta da corrida +InRace=In race +InTrackingInterval=In tracking interval +NumberOfCompetitors=Number of Competitors +CompetitorInLeaderboard=Competitor in Leaderboard +CompetitorSailingDomainRetrieverChain=Competitors in Leaderboards +SmoothedSpeed=Smoothed Speed diff --git a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_ru.properties b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_ru.properties index 070e4092230..8537e80498c 100644 --- a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_ru.properties +++ b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_ru.properties @@ -259,3 +259,9 @@ LengthOfTheLeg=Длина отрезка LegSailingDomainRetrieverChain=Отрезок TackType=Длинный /короткий галс getTackTypeofRace=Длинный /короткий галс гонки +InRace=In race +InTrackingInterval=In tracking interval +NumberOfCompetitors=Number of Competitors +CompetitorInLeaderboard=Competitor in Leaderboard +CompetitorSailingDomainRetrieverChain=Competitors in Leaderboards +SmoothedSpeed=Smoothed Speed diff --git a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_sl.properties b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_sl.properties index 6817e9d85d3..9db3341d888 100644 --- a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_sl.properties +++ b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_sl.properties @@ -259,3 +259,9 @@ LengthOfTheLeg=Dolžina stranice LegSailingDomainRetrieverChain=Stranica TackType=Dolgo/kratko prečenje getTackTypeofRace=Dolgo/kratko prečenje plova +InRace=In race +InTrackingInterval=In tracking interval +NumberOfCompetitors=Number of Competitors +CompetitorInLeaderboard=Competitor in Leaderboard +CompetitorSailingDomainRetrieverChain=Competitors in Leaderboards +SmoothedSpeed=Smoothed Speed diff --git a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_zh.properties b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_zh.properties index b4b8c107414..cfcc1b441db 100644 --- a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_zh.properties +++ b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_zh.properties @@ -259,3 +259,9 @@ LengthOfTheLeg=航段长度 LegSailingDomainRetrieverChain=航段 TackType=长程/短程迎风转向 getTackTypeofRace=比赛轮次的长程/短程迎风转向 +InRace=In race +InTrackingInterval=In tracking interval +NumberOfCompetitors=Number of Competitors +CompetitorInLeaderboard=Competitor in Leaderboard +CompetitorSailingDomainRetrieverChain=Competitors in Leaderboards +SmoothedSpeed=Smoothed Speed diff --git a/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/Activator.java b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/Activator.java index 99db8615352..359adc2da18 100644 --- a/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/Activator.java +++ b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/Activator.java @@ -22,6 +22,7 @@ import com.sap.sailing.datamining.data.HasManeuverSpeedDetailsContext; import com.sap.sailing.datamining.data.HasMarkPassingContext; import com.sap.sailing.datamining.data.HasRaceOfCompetitorContext; import com.sap.sailing.datamining.data.HasRaceResultOfCompetitorContext; +import com.sap.sailing.datamining.data.HasTackTypeSegmentContext; import com.sap.sailing.datamining.data.HasTrackedLegContext; import com.sap.sailing.datamining.data.HasTrackedLegOfCompetitorContext; import com.sap.sailing.datamining.data.HasTrackedRaceContext; @@ -112,6 +113,7 @@ public class Activator extends AbstractDataMiningActivatorWithPredefinedQueries internalClasses.add(HasBravoFixContext.class); internalClasses.add(HasBravoFixTrackContext.class); internalClasses.add(HasFoilingSegmentContext.class); + internalClasses.add(HasTackTypeSegmentContext.class); internalClasses.add(HasManeuverContext.class); internalClasses.add(HasManeuverSpeedDetailsContext.class); internalClasses.add(HasCompleteManeuverCurveWithEstimationDataContext.class); diff --git a/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/SailingDataRetrievalChainDefinitions.java b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/SailingDataRetrievalChainDefinitions.java index 645d7277e4d..5260485974e 100644 --- a/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/SailingDataRetrievalChainDefinitions.java +++ b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/SailingDataRetrievalChainDefinitions.java @@ -17,6 +17,7 @@ import com.sap.sailing.datamining.data.HasManeuverSpeedDetailsContext; import com.sap.sailing.datamining.data.HasMarkPassingContext; import com.sap.sailing.datamining.data.HasRaceOfCompetitorContext; import com.sap.sailing.datamining.data.HasRaceResultOfCompetitorContext; +import com.sap.sailing.datamining.data.HasTackTypeSegmentContext; import com.sap.sailing.datamining.data.HasTrackedLegContext; import com.sap.sailing.datamining.data.HasTrackedLegOfCompetitorContext; import com.sap.sailing.datamining.data.HasTrackedRaceContext; @@ -36,6 +37,7 @@ import com.sap.sailing.datamining.impl.components.ManeuverRetrievalProcessor; import com.sap.sailing.datamining.impl.components.ManeuverSpeedDetailsRetrievalProcessor; import com.sap.sailing.datamining.impl.components.MarkPassingRetrievalProcessor; import com.sap.sailing.datamining.impl.components.RaceOfCompetitorRetrievalProcessor; +import com.sap.sailing.datamining.impl.components.TackTypeSegmentRetrievalProcessor; import com.sap.sailing.datamining.impl.components.TrackedLegOfCompetitorRetrievalProcessor; import com.sap.sailing.datamining.impl.components.TrackedLegRetrievalProcessor; import com.sap.sailing.datamining.impl.components.TrackedRaceRetrievalProcessor; @@ -46,6 +48,7 @@ import com.sap.sailing.datamining.shared.ManeuverSettings; import com.sap.sailing.datamining.shared.ManeuverSettingsImpl; import com.sap.sailing.datamining.shared.ManeuverSpeedDetailsSettings; import com.sap.sailing.datamining.shared.ManeuverSpeedDetailsSettingsImpl; +import com.sap.sailing.datamining.shared.TackTypeSegmentsDataMiningSettings; import com.sap.sailing.server.interfaces.RacingEventService; import com.sap.sse.datamining.components.DataRetrieverChainDefinition; import com.sap.sse.datamining.impl.components.SimpleDataRetrieverChainDefinition; @@ -86,7 +89,9 @@ public class SailingDataRetrievalChainDefinitions { // final DataRetrieverChainDefinition raceOfCompetitorRetrieverChainDefinition = new SimpleDataRetrieverChainDefinition<>( trackedRaceRetrieverChainDefinition, HasRaceOfCompetitorContext.class, "RaceOfCompetitorSailingDomainRetrieverChain"); - raceOfCompetitorRetrieverChainDefinition.endWith(TrackedRaceRetrievalProcessor.class, RaceOfCompetitorRetrievalProcessor.class, HasRaceOfCompetitorContext.class, "Competitor"); + // use TackTypeSegmentsDataMiningSettings here to support the per-leg statistics about tack type segments + raceOfCompetitorRetrieverChainDefinition.endWith(TrackedRaceRetrievalProcessor.class, RaceOfCompetitorRetrievalProcessor.class, HasRaceOfCompetitorContext.class, + TackTypeSegmentsDataMiningSettings.class, TackTypeSegmentsDataMiningSettings.createDefaultSettings(), "Competitor"); dataRetrieverChainDefinitions.add(raceOfCompetitorRetrieverChainDefinition); // final DataRetrieverChainDefinition competitorDayRetrieverChainDefinition = new SimpleDataRetrieverChainDefinition<>( @@ -106,6 +111,12 @@ public class SailingDataRetrievalChainDefinitions { HasFoilingSegmentContext.class, FoilingSegmentsDataMiningSettings.class, FoilingSegmentsDataMiningSettings.createDefaultSettings(), "FoilingSegments"); dataRetrieverChainDefinitions.add(foilingSegmentsRetrieverChainDefinition); // + final DataRetrieverChainDefinition tackTypeSegmentsRetrieverChainDefinition = new SimpleDataRetrieverChainDefinition<>( + raceOfCompetitorRetrieverChainDefinition, HasTackTypeSegmentContext.class, "tackTypeSegmentsRetrieverChainDefinition"); + tackTypeSegmentsRetrieverChainDefinition.endWith(RaceOfCompetitorRetrievalProcessor.class, TackTypeSegmentRetrievalProcessor.class, + HasTackTypeSegmentContext.class, TackTypeSegmentsDataMiningSettings.class, TackTypeSegmentsDataMiningSettings.createDefaultSettings(), "TackTypeSegments"); + dataRetrieverChainDefinitions.add(tackTypeSegmentsRetrieverChainDefinition); + // final DataRetrieverChainDefinition legRetrieverChainDefinition = new SimpleDataRetrieverChainDefinition<>( trackedRaceRetrieverChainDefinition, HasTrackedLegContext.class, "LegSailingDomainRetrieverChain"); legRetrieverChainDefinition.endWith(TrackedRaceRetrievalProcessor.class, TrackedLegRetrievalProcessor.class, @@ -114,8 +125,9 @@ public class SailingDataRetrievalChainDefinitions { // final DataRetrieverChainDefinition legOfCompetitorRetrieverChainDefinition = new SimpleDataRetrieverChainDefinition<>( legRetrieverChainDefinition, HasTrackedLegOfCompetitorContext.class, "LegOfCompetitorSailingDomainRetrieverChain"); + // use TackTypeSegmentsDataMiningSettings here to support the per-leg statistics about tack type segments legOfCompetitorRetrieverChainDefinition.endWith(TrackedLegRetrievalProcessor.class, TrackedLegOfCompetitorRetrievalProcessor.class, - HasTrackedLegOfCompetitorContext.class, "LegOfCompetitor"); + HasTrackedLegOfCompetitorContext.class, TackTypeSegmentsDataMiningSettings.class, TackTypeSegmentsDataMiningSettings.createDefaultSettings(), "LegOfCompetitor"); dataRetrieverChainDefinitions.add(legOfCompetitorRetrieverChainDefinition); // final DataRetrieverChainDefinition gpsFixRetrieverChainDefinition = new SimpleDataRetrieverChainDefinition<>( diff --git a/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/data/HasGPSFixTrackContext.java b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/data/HasGPSFixTrackContext.java new file mode 100644 index 00000000000..a8af3be6bdb --- /dev/null +++ b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/data/HasGPSFixTrackContext.java @@ -0,0 +1,13 @@ +package com.sap.sailing.datamining.data; + +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.common.tracking.GPSFixMoving; +import com.sap.sailing.domain.tracking.GPSFixTrack; +import com.sap.sse.datamining.annotations.Connector; + +public interface HasGPSFixTrackContext { + @Connector(scanForStatistics=false) + HasRaceOfCompetitorContext getRaceOfCompetitorContext(); + + GPSFixTrack getGPSFixTrack(); +} diff --git a/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/data/HasRaceOfCompetitorContext.java b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/data/HasRaceOfCompetitorContext.java index 1f10ae1317c..16ce00d07f4 100755 --- a/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/data/HasRaceOfCompetitorContext.java +++ b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/data/HasRaceOfCompetitorContext.java @@ -172,4 +172,10 @@ public interface HasRaceOfCompetitorContext { @Statistic(messageKey="AverageRaceWindSpeed") Speed getAverageRaceWindSpeed(); + + @Statistic(messageKey="RatioDurationLongVsShortTack", resultDecimals=2) + public double getRatioDurationLongVsShortTack(); + + @Statistic(messageKey="RatioDistanceLongVsShortTack", resultDecimals=2) + public double getRatioDistanceLongVsShortTack(); } diff --git a/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/data/HasTackTypeSegmentContext.java b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/data/HasTackTypeSegmentContext.java new file mode 100644 index 00000000000..493423d41c5 --- /dev/null +++ b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/data/HasTackTypeSegmentContext.java @@ -0,0 +1,38 @@ +package com.sap.sailing.datamining.data; + +import com.sap.sailing.domain.common.LegType; +import com.sap.sailing.domain.common.NoWindException; +import com.sap.sailing.domain.common.TackType; +import com.sap.sse.common.Distance; +import com.sap.sse.common.Duration; +import com.sap.sse.common.TimePoint; +import com.sap.sse.datamining.annotations.Connector; +import com.sap.sse.datamining.annotations.Dimension; +import com.sap.sse.datamining.annotations.Statistic; + +public interface HasTackTypeSegmentContext { + @Connector(scanForStatistics=false) + HasGPSFixTrackContext getGPSFixTrackContext(); + + TimePoint getStartOfTackTypeSegment(); + + TimePoint getEndOfTackTypeSegment(); + + @Dimension(messageKey="TackType") + TackType getTackType(); + + @Dimension(messageKey="LegType") + LegType getLegType() throws NoWindException; + + @Dimension(messageKey="LegNumber") + int getLegNumber(); + + @Dimension(messageKey="TackTypeSegmentName") + String getName(); + + @Statistic(messageKey="TackTypeDuration") + Duration getDuration(); + + @Statistic(messageKey="TackTypeDistance") + Distance getDistance(); +} diff --git a/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/data/HasTrackedLegOfCompetitorContext.java b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/data/HasTrackedLegOfCompetitorContext.java index 338135349e2..5a8172ec775 100644 --- a/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/data/HasTrackedLegOfCompetitorContext.java +++ b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/data/HasTrackedLegOfCompetitorContext.java @@ -109,4 +109,10 @@ public interface HasTrackedLegOfCompetitorContext extends HasWindOnTrackedLeg { @Statistic(messageKey="VMG", resultDecimals=2) public Speed getVelocityMadeGood(); + + @Statistic(messageKey="RatioDurationLongVsShortTack", resultDecimals=2) + public double getRatioDurationLongVsShortTack(); + + @Statistic(messageKey="RatioDistanceLongVsShortTack", resultDecimals=2) + public double getRatioDistanceLongVsShortTack(); } \ No newline at end of file diff --git a/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/components/FoilingSegmentRetrievalProcessor.java b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/components/FoilingSegmentRetrievalProcessor.java index b1299beed75..34d29f5705d 100755 --- a/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/components/FoilingSegmentRetrievalProcessor.java +++ b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/components/FoilingSegmentRetrievalProcessor.java @@ -51,7 +51,7 @@ public class FoilingSegmentRetrievalProcessor extends AbstractRetrievalProcessor TimePoint startOfSegment = null; bravoFixTrack.lockForRead(); try { - for (final BravoFix bravoFix : bravoFixTrack.getFixes(startOfRace, /* fromInclusive */ true, end, /* toInclusive */ false)) { + for (final BravoFix bravoFix : bravoFixTrack.getFixes(startOfRace, /* fromInclusive */ true, end, /* toInclusive */ false)) { //geht ganzes Rennen durch if (isAborted()) { break; } @@ -61,10 +61,10 @@ public class FoilingSegmentRetrievalProcessor extends AbstractRetrievalProcessor element.getTrackedRaceContext().getTrackedRace().getTrack(element.getCompetitor()).getEstimatedSpeed(bravoFix.getTimePoint())) <= 0)) || (settings.getMaximumSpeedNotFoiling() != null && settings.getMaximumSpeedNotFoiling().compareTo( element.getTrackedRaceContext().getTrackedRace().getTrack(element.getCompetitor()).getEstimatedSpeed(bravoFix.getTimePoint())) <= 0); - if (currentFixIsFoiling != isFoiling) { - if (currentFixIsFoiling) { + if (currentFixIsFoiling != isFoiling) { //checken ob boot zum loop davor mit voherigem fix immernoch am fliegen ist, wenn nicht dann muss segment gemacht werden, wenn ja dann wird es zum segment ergänzt + if (currentFixIsFoiling) { //wenn ich vorher nicht am fliegen war und jetzt schon start von segment startOfSegment = bravoFix.getTimePoint(); - } else { + } else { // wenn ich vorher am fliegen war und jetzt nicht mehr, segment muss gebildet werden!!!!! if (settings.getMinimumFoilingSegmentDuration() == null || startOfSegment.until(last).compareTo(settings.getMinimumFoilingSegmentDuration()) >= 0) { addOrMergeFoilingSegment(element, foilingSegments, bravoFixTrack, startOfSegment, @@ -72,7 +72,7 @@ public class FoilingSegmentRetrievalProcessor extends AbstractRetrievalProcessor } startOfSegment = null; } - isFoiling = currentFixIsFoiling; + isFoiling = currentFixIsFoiling; //aktualisiert das es vorher nicht gleich war also entweder fliegen zuende oder gerade beginnt } last = bravoFix.getTimePoint(); } diff --git a/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/components/RaceOfCompetitorRetrievalProcessor.java b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/components/RaceOfCompetitorRetrievalProcessor.java index d5117666895..2c30a6cdd4c 100644 --- a/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/components/RaceOfCompetitorRetrievalProcessor.java +++ b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/components/RaceOfCompetitorRetrievalProcessor.java @@ -7,17 +7,26 @@ import java.util.concurrent.ExecutorService; import com.sap.sailing.datamining.data.HasRaceOfCompetitorContext; import com.sap.sailing.datamining.data.HasTrackedRaceContext; import com.sap.sailing.datamining.impl.data.RaceOfCompetitorWithContext; +import com.sap.sailing.datamining.shared.TackTypeSegmentsDataMiningSettings; import com.sap.sailing.domain.base.Competitor; import com.sap.sse.datamining.components.Processor; import com.sap.sse.datamining.impl.components.AbstractRetrievalProcessor; public class RaceOfCompetitorRetrievalProcessor extends AbstractRetrievalProcessor { + /** + * Settings will be used to control the retrieval of tack type segments for the + * corresponding statistics such as distance/duration relation between long and + * short tack for the race of the competitor + */ + private final TackTypeSegmentsDataMiningSettings settings; public RaceOfCompetitorRetrievalProcessor(ExecutorService executor, - Collection> resultReceivers, int retrievalLevel, - String retrievedDataTypeMessageKey) { - super(HasTrackedRaceContext.class, HasRaceOfCompetitorContext.class, executor, resultReceivers, retrievalLevel, + Collection> resultReceivers, + TackTypeSegmentsDataMiningSettings settings, int retrievalLevel, String retrievedDataTypeMessageKey) { + super(HasTrackedRaceContext.class, + HasRaceOfCompetitorContext.class, executor, resultReceivers, retrievalLevel, retrievedDataTypeMessageKey); + this.settings = settings; } @Override @@ -28,7 +37,7 @@ public class RaceOfCompetitorRetrievalProcessor extends AbstractRetrievalProcess if (isAborted()) { break; } - HasRaceOfCompetitorContext raceOfCompetitorWithContext = new RaceOfCompetitorWithContext(element, competitor); + HasRaceOfCompetitorContext raceOfCompetitorWithContext = new RaceOfCompetitorWithContext(element, competitor, settings); raceOfCompetitorsWithContext.add(raceOfCompetitorWithContext); } } diff --git a/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/components/TackTypeSegmentRetrievalProcessor.java b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/components/TackTypeSegmentRetrievalProcessor.java new file mode 100644 index 00000000000..6de0eebb04f --- /dev/null +++ b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/components/TackTypeSegmentRetrievalProcessor.java @@ -0,0 +1,141 @@ +package com.sap.sailing.datamining.impl.components; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.ExecutorService; + +import com.sap.sailing.datamining.data.HasRaceOfCompetitorContext; +import com.sap.sailing.datamining.data.HasTackTypeSegmentContext; +import com.sap.sailing.datamining.impl.data.GPSFixTrackWithContext; +import com.sap.sailing.datamining.impl.data.TackTypeSegmentWithContext; +import com.sap.sailing.datamining.shared.TackTypeSegmentsDataMiningSettings; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.Waypoint; +import com.sap.sailing.domain.common.NoWindException; +import com.sap.sailing.domain.common.TackType; +import com.sap.sailing.domain.common.tracking.GPSFix; +import com.sap.sailing.domain.common.tracking.GPSFixMoving; +import com.sap.sailing.domain.tracking.GPSFixTrack; +import com.sap.sailing.domain.tracking.MarkPassing; +import com.sap.sailing.domain.tracking.TrackedLegOfCompetitor; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sse.common.TimePoint; +import com.sap.sse.datamining.components.Processor; +import com.sap.sse.datamining.impl.components.AbstractRetrievalProcessor; + +public class TackTypeSegmentRetrievalProcessor extends AbstractRetrievalProcessor { + private final TackTypeSegmentsDataMiningSettings settings; + + public TackTypeSegmentRetrievalProcessor(ExecutorService executor, + Collection> resultReceivers, + TackTypeSegmentsDataMiningSettings settings, int retrievalLevel, String retrievedDataTypeMessageKey) { + super(HasRaceOfCompetitorContext.class, HasTackTypeSegmentContext.class, executor, resultReceivers, + retrievalLevel, retrievedDataTypeMessageKey); + this.settings = settings; + } + + @Override + public Iterable retrieveData(HasRaceOfCompetitorContext element) { + List tackTypeSegments = new ArrayList<>(); + final TrackedRace trackedRace = element.getTrackedRaceContext().getTrackedRace(); + final TimePoint startOfRace = element.getTrackedRaceContext().getTrackedRace().getStartOfRace(); + final Competitor competitor = element.getCompetitor(); + if (startOfRace != null) { + final GPSFixTrack gpsFixTrack = trackedRace.getTrack(element.getCompetitor()); + if (gpsFixTrack != null) { + final Iterator markPassingIterator = trackedRace.getMarkPassings(competitor).iterator(); + if (markPassingIterator.hasNext()) { // only search for tack type segments if the competitor has started the race + MarkPassing legStartMarkPassing = markPassingIterator.next(); + TrackedLegOfCompetitor trackedLegOfCompetitor = trackedRace.getTrackedLegStartingAt(legStartMarkPassing.getWaypoint()).getTrackedLeg(competitor); + MarkPassing nextMarkPassing = markPassingIterator.hasNext() ? markPassingIterator.next() : null; + final Waypoint finish = trackedRace.getRace().getCourse().getLastWaypoint(); + TackType currentTackType = null; + boolean segmentEmpty = true; + TackType nextTackType = null; + TimePoint startOfCurrentSegment = legStartMarkPassing.getTimePoint(); + GPSFix gpsFix = null; + gpsFixTrack.lockForRead(); + try { + // run through all of the competitor's fixes, starting at the first mark passing and finishing when there are + // no more fixes left or the finish mark passing has been reached + for (final Iterator i=gpsFixTrack.getFixesIterator(startOfCurrentSegment, /* fromInclusive */ true); + i.hasNext() && legStartMarkPassing.getWaypoint() != finish; ) { + if (isAborted()) { + break; + } + gpsFix = i.next(); + while (nextMarkPassing != null && !gpsFix.getTimePoint().before(nextMarkPassing.getTimePoint())) { + // reached leg's end; complete the current segment if not empty and move through mark passings + // iterator until having found the leg the gpsFix is in, while moving the trackedLegOfCompetitor along + legStartMarkPassing = nextMarkPassing; + if (!segmentEmpty) { + addOrMergeTackTypeSegment(element, tackTypeSegments, gpsFixTrack, startOfCurrentSegment, + nextMarkPassing.getTimePoint(), currentTackType); + segmentEmpty = gpsFix.getTimePoint().equals(nextMarkPassing.getTimePoint()); // if the fix is exactly at leg start + startOfCurrentSegment = nextMarkPassing.getTimePoint(); + } + // if nextMarkPassing is last MarkPassing there will be no leg after, trackedLeg is null + trackedLegOfCompetitor = nextMarkPassing.getWaypoint()==finish ? null : trackedRace.getTrackedLegStartingAt(nextMarkPassing.getWaypoint()).getTrackedLeg(competitor); + nextMarkPassing = markPassingIterator.hasNext() ? markPassingIterator.next() : null; + } + try { + // no null case like line 87, because for loop checks that mark passing at leg start is not finish mark passing + nextTackType = trackedLegOfCompetitor == null ? null : trackedLegOfCompetitor.getTackType(gpsFix.getTimePoint()); + } catch (NoWindException e) { + nextTackType = null; + } + // invariant: nextMarkPassing is either null or after gpsFix's time and representing the passing of the mark at the end + // of the leg gpsFix is in; trackedLegOfCompetitor corresponds to the leg gpsFix is in + if (!segmentEmpty && nextTackType != currentTackType) { + addOrMergeTackTypeSegment(element, tackTypeSegments, gpsFixTrack, startOfCurrentSegment, + gpsFix.getTimePoint() /* don't include the last interval ending at the non-TackType fix */, currentTackType); + segmentEmpty = true; // because gpsFix is now exactly at the beginning of the new segment + startOfCurrentSegment = gpsFix.getTimePoint(); + } else { + segmentEmpty = false; // gpsFix is in the current segment + } + currentTackType = nextTackType; + } + } finally { + gpsFixTrack.unlockAfterRead(); + } + if (!segmentEmpty) { + addOrMergeTackTypeSegment(element, tackTypeSegments, gpsFixTrack, startOfCurrentSegment, gpsFix.getTimePoint(), currentTackType); + // no need to update segmentEmpty / startOfCurrentSegment / lastTackType because now we're done + } + } + } + } + return tackTypeSegments; + } + + private void addOrMergeTackTypeSegment(HasRaceOfCompetitorContext element, + List tackTypeSegments, final GPSFixTrack gpsFixTrack, + TimePoint startOfSegment, TimePoint endOfSegment, TackType tackType) { + if (settings.getMinimumTackTypeSegmentDuration() == null || startOfSegment.until(endOfSegment) + .compareTo(settings.getMinimumTackTypeSegmentDuration()) >= 0) { + if (tackTypeSegments.isEmpty() || settings.getMinimumDurationBetweenAdjacentTackTypeSegments() == null) { + tackTypeSegments.add(createTackTypeSegment(startOfSegment, endOfSegment, element, gpsFixTrack, tackType)); + } else { + // we wouldn't want to merge segments with different tack types; different from foiling segments where you *would* want to join to closely-adjacent foiled segments + final HasTackTypeSegmentContext previousSegment = tackTypeSegments.get(tackTypeSegments.size()-1); + final TimePoint previousEnd = previousSegment.getEndOfTackTypeSegment(); + if (previousSegment.getTackType() == tackType && previousEnd.until(startOfSegment).compareTo(settings.getMinimumDurationBetweenAdjacentTackTypeSegments()) < 0) { + // merge: + tackTypeSegments.set(tackTypeSegments.size()-1, createTackTypeSegment(previousSegment.getStartOfTackTypeSegment(), endOfSegment, element, gpsFixTrack, tackType)); + } else { + // add; duration between the segments is large enough or tack type is different + tackTypeSegments.add(createTackTypeSegment(startOfSegment, endOfSegment, element, gpsFixTrack, tackType)); + } + } + } + } + + private HasTackTypeSegmentContext createTackTypeSegment(TimePoint startOfSegment, TimePoint endOfSegment, + HasRaceOfCompetitorContext raceOfCompetitorContext, GPSFixTrack gpsFixTrack, TackType tackType) { + return new TackTypeSegmentWithContext(new GPSFixTrackWithContext(raceOfCompetitorContext, gpsFixTrack), startOfSegment, endOfSegment, tackType); + } + +} \ No newline at end of file diff --git a/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/components/TrackedLegOfCompetitorRetrievalProcessor.java b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/components/TrackedLegOfCompetitorRetrievalProcessor.java index edd5443c2d6..f476c721576 100644 --- a/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/components/TrackedLegOfCompetitorRetrievalProcessor.java +++ b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/components/TrackedLegOfCompetitorRetrievalProcessor.java @@ -7,17 +7,25 @@ import java.util.concurrent.ExecutorService; import com.sap.sailing.datamining.data.HasTrackedLegContext; import com.sap.sailing.datamining.data.HasTrackedLegOfCompetitorContext; import com.sap.sailing.datamining.impl.data.TrackedLegOfCompetitorWithContext; +import com.sap.sailing.datamining.shared.TackTypeSegmentsDataMiningSettings; import com.sap.sailing.domain.base.Competitor; import com.sap.sse.datamining.components.Processor; import com.sap.sse.datamining.impl.components.AbstractRetrievalProcessor; public class TrackedLegOfCompetitorRetrievalProcessor extends AbstractRetrievalProcessor { + /** + * Settings will be used to control the retrieval of tack type segments for the + * corresponding statistics such as distance/duration relation between long and + * short tack for the leg of the competitor + */ + private final TackTypeSegmentsDataMiningSettings settings; public TrackedLegOfCompetitorRetrievalProcessor(ExecutorService executor, - Collection> resultReceivers, int retrievalLevel, - String retrievedDataTypeMessageKey) { + Collection> resultReceivers, + TackTypeSegmentsDataMiningSettings settings, int retrievalLevel, String retrievedDataTypeMessageKey) { super(HasTrackedLegContext.class, HasTrackedLegOfCompetitorContext.class, executor, resultReceivers, retrievalLevel, retrievedDataTypeMessageKey); + this.settings = settings; } @Override @@ -27,7 +35,7 @@ public class TrackedLegOfCompetitorRetrievalProcessor extends AbstractRetrievalP if (isAborted()) { break; } - HasTrackedLegOfCompetitorContext trackedLegOfCompetitorWithContext = new TrackedLegOfCompetitorWithContext(element, element.getTrackedLeg().getTrackedLeg(competitor)); + HasTrackedLegOfCompetitorContext trackedLegOfCompetitorWithContext = new TrackedLegOfCompetitorWithContext(element, element.getTrackedLeg().getTrackedLeg(competitor), settings); trackedLegOfCompetitorsWithContext.add(trackedLegOfCompetitorWithContext); } return trackedLegOfCompetitorsWithContext; diff --git a/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/components/TrackedRaceRetrievalProcessor.java b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/components/TrackedRaceRetrievalProcessor.java index b0bcb99afd7..93061f6ad2e 100644 --- a/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/components/TrackedRaceRetrievalProcessor.java +++ b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/components/TrackedRaceRetrievalProcessor.java @@ -17,9 +17,10 @@ import com.sap.sse.datamining.impl.components.AbstractRetrievalProcessor; public class TrackedRaceRetrievalProcessor extends AbstractRetrievalProcessor { public TrackedRaceRetrievalProcessor(ExecutorService executor, - Collection> resultReceivers, int retrievalLevel, - String retrievedDataTypeMessageKey) { - super(HasLeaderboardContext.class, HasTrackedRaceContext.class, executor, resultReceivers, retrievalLevel, + Collection> resultReceivers, + int retrievalLevel, String retrievedDataTypeMessageKey) { + super(HasLeaderboardContext.class, + HasTrackedRaceContext.class, executor, resultReceivers, retrievalLevel, retrievedDataTypeMessageKey); } diff --git a/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/GPSFixTrackWithContext.java b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/GPSFixTrackWithContext.java new file mode 100644 index 00000000000..dcbe5a4f2f9 --- /dev/null +++ b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/GPSFixTrackWithContext.java @@ -0,0 +1,29 @@ +package com.sap.sailing.datamining.impl.data; + +import com.sap.sailing.datamining.data.HasGPSFixTrackContext; +import com.sap.sailing.datamining.data.HasRaceOfCompetitorContext; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.common.tracking.GPSFixMoving; +import com.sap.sailing.domain.tracking.GPSFixTrack; + +public class GPSFixTrackWithContext implements HasGPSFixTrackContext { + + private final HasRaceOfCompetitorContext raceOfCompetitorContext; + + private final GPSFixTrack gpsFixTrack; + + public GPSFixTrackWithContext(HasRaceOfCompetitorContext raceOfCompetitorContext, GPSFixTrack gpsFixTrack) { + this.raceOfCompetitorContext = raceOfCompetitorContext; + this.gpsFixTrack = gpsFixTrack; + } + + @Override + public HasRaceOfCompetitorContext getRaceOfCompetitorContext() { + return raceOfCompetitorContext; + } + + @Override + public GPSFixTrack getGPSFixTrack() { + return gpsFixTrack; + } +} diff --git a/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/RaceOfCompetitorWithContext.java b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/RaceOfCompetitorWithContext.java index b4c65ef153a..aa98a41f022 100755 --- a/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/RaceOfCompetitorWithContext.java +++ b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/RaceOfCompetitorWithContext.java @@ -11,7 +11,10 @@ import java.util.function.BiFunction; import com.sap.sailing.datamining.Activator; import com.sap.sailing.datamining.SailingClusterGroups; import com.sap.sailing.datamining.data.HasRaceOfCompetitorContext; +import com.sap.sailing.datamining.data.HasTackTypeSegmentContext; import com.sap.sailing.datamining.data.HasTrackedRaceContext; +import com.sap.sailing.datamining.impl.components.TackTypeSegmentRetrievalProcessor; +import com.sap.sailing.datamining.shared.TackTypeSegmentsDataMiningSettings; import com.sap.sailing.domain.base.Boat; import com.sap.sailing.domain.base.Competitor; import com.sap.sailing.domain.base.Course; @@ -53,10 +56,12 @@ public class RaceOfCompetitorWithContext implements HasRaceOfCompetitorContext { private final HasTrackedRaceContext trackedRaceContext; private final Competitor competitor; + private final TackTypeSegmentsDataMiningSettings settings; - public RaceOfCompetitorWithContext(HasTrackedRaceContext trackedRaceContext, Competitor competitor) { + public RaceOfCompetitorWithContext(HasTrackedRaceContext trackedRaceContext, Competitor competitor, TackTypeSegmentsDataMiningSettings settings) { this.trackedRaceContext = trackedRaceContext; this.competitor = competitor; + this.settings = settings; } @Override @@ -662,4 +667,51 @@ public class RaceOfCompetitorWithContext implements HasRaceOfCompetitorContext { Integer rank = getTrackedRace().getRank(getCompetitor(), timePoint); return rank == 0 ? null : rank; } + + @Override + public double getRatioDurationLongVsShortTack() { + final TackTypeRatioCollector resultProcessor = new TackTypeRatioCollector(Duration.NULL) { + @Override + protected Duration add(Duration a, Duration b) { + return a.plus(b); + } + + @Override + protected double divide(Duration a, Duration b) { + return a.divide(b); + } + + @Override + protected Duration getAddable(HasTackTypeSegmentContext element) { + return element.getDuration(); + } + }; + final TackTypeSegmentRetrievalProcessor tackTypeSegmentRetriever = new TackTypeSegmentRetrievalProcessor( + /* executor */ null, + Collections.emptySet(), settings, 0, "TackTypeSegments"); + return Util.stream(tackTypeSegmentRetriever.retrieveData(this)).collect(resultProcessor); + } + + @Override + public double getRatioDistanceLongVsShortTack() { + final TackTypeRatioCollector resultProcessor = new TackTypeRatioCollector(Distance.NULL) { + @Override + protected Distance add(Distance a, Distance b) { + return a.add(b); + } + + @Override + protected double divide(Distance a, Distance b) { + return a.divide(b); + } + + @Override + protected Distance getAddable(HasTackTypeSegmentContext element) { + return element.getDistance(); + } + }; + final TackTypeSegmentRetrievalProcessor tackTypeSegmentRetriever = new TackTypeSegmentRetrievalProcessor(/* executor */ null, + Collections.emptySet(), settings, 0, "TackTypeSegments"); + return Util.stream(tackTypeSegmentRetriever.retrieveData(this)).collect(resultProcessor); + } } \ No newline at end of file diff --git a/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/TackTypeRatioCollector.java b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/TackTypeRatioCollector.java new file mode 100644 index 00000000000..317ca19da72 --- /dev/null +++ b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/TackTypeRatioCollector.java @@ -0,0 +1,78 @@ +package com.sap.sailing.datamining.impl.data; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collector; + +import com.sap.sailing.datamining.data.HasTackTypeSegmentContext; +import com.sap.sailing.domain.common.TackType; + +public abstract class TackTypeRatioCollector implements Collector, Double> { + + private final ADDABLE nullValue; + + public TackTypeRatioCollector(ADDABLE nullValue) { + this.nullValue = nullValue; + } + + @Override + public Supplier> supplier() { + return ()->{ + final Map result = new HashMap<>(); + for (final TackType tt : TackType.values()) { + result.put(tt, nullValue); + } + return result; + }; + } + + @Override + public BiConsumer, HasTackTypeSegmentContext> accumulator() { + return (sumPerTackType, element)->{ + final TackType tt = element.getTackType(); + synchronized (this) { + sumPerTackType.put(tt, add(sumPerTackType.get(tt), getAddable(element))); + } + }; + } + + + @Override + public BinaryOperator> combiner() { + return (r1, r2) -> { + for (final Entry e : r1.entrySet()) { + r2.put(e.getKey(), add(e.getValue(), r2.get(e.getKey()))); + } + return r2; + }; + } + + + + @Override + public Function, Double> finisher() { + return sumPerTackType->{ + final ADDABLE shortTackSum = sumPerTackType.get(TackType.SHORTTACK); + return shortTackSum.equals(nullValue) ? null : divide(sumPerTackType.get(TackType.LONGTACK), shortTackSum); + }; + } + + + @Override + public Set characteristics() { + return Collections.emptySet(); + } + + protected abstract ADDABLE add(ADDABLE a, ADDABLE b); + + protected abstract double divide(ADDABLE a, ADDABLE b); + + protected abstract ADDABLE getAddable(HasTackTypeSegmentContext element); +} diff --git a/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/TackTypeSegmentWithContext.java b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/TackTypeSegmentWithContext.java new file mode 100644 index 00000000000..357c17b9791 --- /dev/null +++ b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/TackTypeSegmentWithContext.java @@ -0,0 +1,106 @@ +package com.sap.sailing.datamining.impl.data; + +import java.text.SimpleDateFormat; + +import com.sap.sailing.datamining.data.HasGPSFixTrackContext; +import com.sap.sailing.datamining.data.HasTackTypeSegmentContext; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.common.LegType; +import com.sap.sailing.domain.common.NoWindException; +import com.sap.sailing.domain.common.TackType; +import com.sap.sailing.domain.common.tracking.GPSFixMoving; +import com.sap.sailing.domain.tracking.GPSFixTrack; +import com.sap.sailing.domain.tracking.TrackedLeg; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sse.common.Distance; +import com.sap.sse.common.Duration; +import com.sap.sse.common.TimePoint; + +public class TackTypeSegmentWithContext implements HasTackTypeSegmentContext { + private final HasGPSFixTrackContext gpsFixTrackContext; + private final TimePoint startOfTackTypeSegment; + private final TimePoint endOfTackTypeSegment; // TODO is this exclusive or inclusive? Suggestion: it should be exclusive, as usual in most Java interval specifications; please comment accordingly + private final TackType tackType; + private static final SimpleDateFormat TIMEPOINT_FORMATTER = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); + + public TackTypeSegmentWithContext(GPSFixTrackWithContext gpsFixTrackWithContext, TimePoint startOfTackTypeSegment, + TimePoint endOfTackTypeSegment, TackType tackType) { + super(); + this.gpsFixTrackContext = gpsFixTrackWithContext; + this.startOfTackTypeSegment = startOfTackTypeSegment; + this.endOfTackTypeSegment = endOfTackTypeSegment; + this.tackType = tackType; + } + + @Override + public String getName() { + return gpsFixTrackContext.getRaceOfCompetitorContext().getCompetitor().getName() + "@" + + TIMEPOINT_FORMATTER.format(startOfTackTypeSegment.asDate()); + } + + @Override + public HasGPSFixTrackContext getGPSFixTrackContext() { + return gpsFixTrackContext; + } + + @Override + public TimePoint getStartOfTackTypeSegment() { + return startOfTackTypeSegment; + } + + @Override + public TimePoint getEndOfTackTypeSegment() { + return endOfTackTypeSegment; + } + + @Override + public Duration getDuration() { + return getStartOfTackTypeSegment().until(getEndOfTackTypeSegment()); + } + + @Override + public Distance getDistance() { + return getGpsFixTrack(). + getDistanceTraveled(getStartOfTackTypeSegment(), getEndOfTackTypeSegment()); + } + + private GPSFixTrack getGpsFixTrack() { + return getTrackedRace().getTrack(getCompetitor()); + } + + private Competitor getCompetitor() { + return getGPSFixTrackContext().getRaceOfCompetitorContext().getCompetitor(); + } + + private TrackedRace getTrackedRace() { + return getGPSFixTrackContext().getRaceOfCompetitorContext().getTrackedRaceContext().getTrackedRace(); + } + + @Override + public TackType getTackType() { + return tackType; + } + + @Override + public LegType getLegType() throws NoWindException { + return getTrackedLeg().getLegType( + getStartOfTackTypeSegment().plus(getStartOfTackTypeSegment().until(getEndOfTackTypeSegment()).divide(2))); + } + + @Override + public int getLegNumber() { + final TrackedLeg trackedLeg = getTrackedLeg(); + return trackedLeg == null ? 0 : getTrackedRace().getRace().getCourse().getIndexOfWaypoint(trackedLeg.getLeg().getTo()); + } + + private TrackedLeg getTrackedLeg() { + return getTrackedRace().getTrackedLeg(getCompetitor(), getStartOfTackTypeSegment()).getTrackedLeg(); + } + + @Override + public String toString() { + return "TackTypeSegmentWithContext [gpsFixTrackContext=" + gpsFixTrackContext + ", startOfTackTypeSegment=" + + startOfTackTypeSegment + ", endOfTackTypeSegment=" + endOfTackTypeSegment + ", tackType=" + tackType + + "]"; + } +} diff --git a/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/TrackedLegOfCompetitorWithContext.java b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/TrackedLegOfCompetitorWithContext.java index 1d2b62c5b4f..a891e0e4d93 100644 --- a/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/TrackedLegOfCompetitorWithContext.java +++ b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/TrackedLegOfCompetitorWithContext.java @@ -10,8 +10,11 @@ import java.util.function.Supplier; import com.sap.sailing.datamining.Activator; import com.sap.sailing.datamining.SailingClusterGroups; +import com.sap.sailing.datamining.data.HasTackTypeSegmentContext; import com.sap.sailing.datamining.data.HasTrackedLegContext; import com.sap.sailing.datamining.data.HasTrackedLegOfCompetitorContext; +import com.sap.sailing.datamining.impl.components.TackTypeSegmentRetrievalProcessor; +import com.sap.sailing.datamining.shared.TackTypeSegmentsDataMiningSettings; import com.sap.sailing.domain.base.Boat; import com.sap.sailing.domain.base.Competitor; import com.sap.sailing.domain.base.Leg; @@ -44,6 +47,7 @@ public class TrackedLegOfCompetitorWithContext implements HasTrackedLegOfCompeti private static final long serialVersionUID = 5944904146286262768L; private final HasTrackedLegContext trackedLegContext; + private final TackTypeSegmentsDataMiningSettings settings; private final TrackedLegOfCompetitor trackedLegOfCompetitor; private final Competitor competitor; @@ -54,10 +58,11 @@ public class TrackedLegOfCompetitorWithContext implements HasTrackedLegOfCompeti private boolean isRankAtFinishInitialized; private Wind wind; - public TrackedLegOfCompetitorWithContext(HasTrackedLegContext trackedLegContext, TrackedLegOfCompetitor trackedLegOfCompetitor) { + public TrackedLegOfCompetitorWithContext(HasTrackedLegContext trackedLegContext, TrackedLegOfCompetitor trackedLegOfCompetitor, TackTypeSegmentsDataMiningSettings settings) { this.trackedLegContext = trackedLegContext; this.trackedLegOfCompetitor = trackedLegOfCompetitor; this.competitor = trackedLegOfCompetitor.getCompetitor(); + this.settings = settings; } @Override @@ -516,4 +521,56 @@ public class TrackedLegOfCompetitorWithContext implements HasTrackedLegOfCompeti final TimePoint finishTime = getTrackedLegOfCompetitor().getFinishTime(); return getTrackedLegOfCompetitor().getAverageVelocityMadeGood(finishTime == null ? TimePoint.now() : finishTime); } + + @Override + public double getRatioDurationLongVsShortTack() { + final TackTypeRatioCollector resultProcessor = new TackTypeRatioCollector(Duration.NULL) { + @Override + protected Duration add(Duration a, Duration b) { + return a.plus(b); + } + + @Override + protected double divide(Duration a, Duration b) { + return a.divide(b); + } + + @Override + protected Duration getAddable(HasTackTypeSegmentContext element) { + return element.getDuration(); + } + }; + final TackTypeSegmentRetrievalProcessor tackTypeSegmentRetriever = new TackTypeSegmentRetrievalProcessor( + /* executor */ null, + Collections.emptySet(), settings, 0, "TackTypeSegments"); + return Util.stream(tackTypeSegmentRetriever.retrieveData( + new RaceOfCompetitorWithContext(getTrackedLegContext().getTrackedRaceContext(), competitor, settings))) + .filter(tt->tt.getLegNumber() == getTrackedLegContext().getLegNumber()).collect(resultProcessor); + } + + @Override + public double getRatioDistanceLongVsShortTack() { + final TackTypeRatioCollector resultProcessor = new TackTypeRatioCollector(Distance.NULL) { + @Override + protected Distance add(Distance a, Distance b) { + return a.add(b); + } + + @Override + protected double divide(Distance a, Distance b) { + return a.divide(b); + } + + @Override + protected Distance getAddable(HasTackTypeSegmentContext element) { + return element.getDistance(); + } + }; + final TackTypeSegmentRetrievalProcessor tackTypeSegmentRetriever = new TackTypeSegmentRetrievalProcessor( + /* executor */ null, + Collections.emptySet(), settings, 0, "TackTypeSegments"); + return Util.stream(tackTypeSegmentRetriever.retrieveData( + new RaceOfCompetitorWithContext(getTrackedLegContext().getTrackedRaceContext(), competitor, settings))) + .filter(tt->tt.getLegNumber() == getTrackedLegContext().getLegNumber()).collect(resultProcessor); + } } diff --git a/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/TrackedLegOfCompetitorWithSpecificTimePointWithContext.java b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/TrackedLegOfCompetitorWithSpecificTimePointWithContext.java index 56dadffb87c..7d069f665e6 100644 --- a/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/TrackedLegOfCompetitorWithSpecificTimePointWithContext.java +++ b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/TrackedLegOfCompetitorWithSpecificTimePointWithContext.java @@ -3,6 +3,7 @@ package com.sap.sailing.datamining.impl.data; import java.util.function.BiFunction; import com.sap.sailing.datamining.data.HasTrackedLegContext; +import com.sap.sailing.datamining.shared.TackTypeSegmentsDataMiningSettings; import com.sap.sailing.domain.tracking.TrackedLegOfCompetitor; import com.sap.sse.common.TimePoint; @@ -26,7 +27,7 @@ public class TrackedLegOfCompetitorWithSpecificTimePointWithContext extends Trac public TrackedLegOfCompetitorWithSpecificTimePointWithContext(HasTrackedLegContext trackedLegContext, TrackedLegOfCompetitor trackedLegOfCompetitor, TimePoint timePoint) { - super(trackedLegContext, trackedLegOfCompetitor); + super(trackedLegContext, trackedLegOfCompetitor, TackTypeSegmentsDataMiningSettings.createDefaultSettings()); this.timePoint = timePoint; } diff --git a/java/com.sap.sailing.declination/NOAAImporter (No Proxy).launch b/java/com.sap.sailing.declination/NOAAImporter (No Proxy).launch index def42c795f5..76f18a68b7b 100644 --- a/java/com.sap.sailing.declination/NOAAImporter (No Proxy).launch +++ b/java/com.sap.sailing.declination/NOAAImporter (No Proxy).launch @@ -1,5 +1,6 @@ + @@ -8,6 +9,6 @@ - + diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/security/SecuredDomainType.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/security/SecuredDomainType.java index 89d09fd92d6..f812bbdb880 100644 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/security/SecuredDomainType.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/security/SecuredDomainType.java @@ -66,7 +66,8 @@ public class SecuredDomainType extends HasPermissionsImpl { EXPORT, SIMULATOR, VIEWSTREAMLETS, - VIEWANALYSISCHARTS; + VIEWANALYSISCHARTS, + COLORED_TAILS; private static final Action[] ALL_ACTIONS = DefaultActions.plus(TrackedRaceActions.values()); diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/subscription/PremiumRole.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/subscription/PremiumRole.java index 4dd8d50653c..06f27ea1aa5 100644 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/subscription/PremiumRole.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/subscription/PremiumRole.java @@ -28,6 +28,8 @@ public class PremiumRole extends RolePrototype { .withActions(SecuredSecurityTypes.UserActions.BE_PREMIUM).build(), WildcardPermission.builder().withTypes(SecuredDomainType.TRACKED_RACE) .withActions(SecuredDomainType.TrackedRaceActions.VIEWANALYSISCHARTS).build(), + WildcardPermission.builder().withTypes(SecuredDomainType.TRACKED_RACE) + .withActions(SecuredDomainType.TrackedRaceActions.COLORED_TAILS).build(), WildcardPermission.builder().withTypes(SecuredDomainType.LEADERBOARD) .withActions(SecuredDomainType.LeaderboardActions.PREMIUM_LEADERBOARD_INFORMATION).build()); } diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/tracking/BravoFix.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/tracking/BravoFix.java index 51f54938759..2112d7ff276 100644 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/tracking/BravoFix.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/tracking/BravoFix.java @@ -46,4 +46,5 @@ public interface BravoFix extends SensorFix { @Statistic(messageKey = "heel", resultDecimals = 1) Bearing getHeel(); + } diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoRaceLogStoreImpl.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoRaceLogStoreImpl.java index 65873f7aa24..f3cc9cd5dbb 100644 --- a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoRaceLogStoreImpl.java +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoRaceLogStoreImpl.java @@ -41,7 +41,7 @@ public class MongoRaceLogStoreImpl implements RaceLogStore { } private void addListener(RaceLogIdentifier identifier, final RaceLog raceLog) { - MongoRaceLogStoreVisitor listener = new MongoRaceLogStoreVisitor(identifier, mongoObjectFactory); + final MongoRaceLogStoreVisitor listener = new MongoRaceLogStoreVisitor(identifier, mongoObjectFactory); listeners.put(raceLog, listener); raceLog.addListener(listener); } @@ -55,7 +55,7 @@ public class MongoRaceLogStoreImpl implements RaceLogStore { @Override public void removeListenersAddedByStoreFrom(RaceLog raceLog) { - RaceLogEventVisitor visitor = listeners.get(raceLog); + final RaceLogEventVisitor visitor = listeners.get(raceLog); if (visitor != null) { raceLog.removeListener(visitor); } diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/common/test/MultiTimeRangeTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/common/test/MultiTimeRangeTest.java index 68292eaa98f..f0b60f7bff5 100644 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/common/test/MultiTimeRangeTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/common/test/MultiTimeRangeTest.java @@ -13,6 +13,7 @@ import org.junit.Test; import com.sap.sse.common.MultiTimeRange; import com.sap.sse.common.TimeRange; import com.sap.sse.common.Util; +import com.sap.sse.common.impl.MillisecondsTimePoint; import com.sap.sse.common.impl.MultiTimeRangeImpl; public class MultiTimeRangeTest { @@ -104,4 +105,36 @@ public class MultiTimeRangeTest { assertEquals(createMulti(100, 120, 350, 400), createMulti(100, 200, 300, 400).subtract(createMulti(120, 350))); assertEquals(createMulti(100, 120, 350, 360, 370, 400), createMulti(100, 200, 300, 400).subtract(createMulti(120, 350, 360, 370))); } + + @Test + public void testOrderingWithOf() { + final MillisecondsTimePoint _10 = new MillisecondsTimePoint(10); + final MillisecondsTimePoint _20 = new MillisecondsTimePoint(20); + final MillisecondsTimePoint _100 = new MillisecondsTimePoint(100); + final MillisecondsTimePoint _200 = new MillisecondsTimePoint(200); + final MultiTimeRange mtr = MultiTimeRange.of(TimeRange.create(_100, _200), TimeRange.create(_10, _20)); + assertEquals(TimeRange.create(_10, _20), Util.get(mtr, 0)); + assertEquals(TimeRange.create(_100, _200), Util.get(mtr, 1)); + } + + @Test + public void testOrderingWithUnion() { + final MillisecondsTimePoint _10 = new MillisecondsTimePoint(10); + final MillisecondsTimePoint _20 = new MillisecondsTimePoint(20); + final MillisecondsTimePoint _100 = new MillisecondsTimePoint(100); + final MillisecondsTimePoint _200 = new MillisecondsTimePoint(200); + final MultiTimeRange mtr = MultiTimeRange.of(TimeRange.create(_100, _200)).union(TimeRange.create(_10, _20)); + assertEquals(TimeRange.create(_10, _20), Util.get(mtr, 0)); + assertEquals(TimeRange.create(_100, _200), Util.get(mtr, 1)); + } + + @Test + public void testEmptyTimeRangeRemoval() { + final MillisecondsTimePoint _10 = new MillisecondsTimePoint(10); + final MillisecondsTimePoint _100 = new MillisecondsTimePoint(100); + final MillisecondsTimePoint _200 = new MillisecondsTimePoint(200); + final MultiTimeRange mtr = MultiTimeRange.of(TimeRange.create(_100, _200)).union(TimeRange.create(_10, _10)); + assertEquals(1, Util.size(mtr)); + assertEquals(TimeRange.create(_100, _200), Util.get(mtr, 0)); + } } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/RaceColumn.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/RaceColumn.java index 949ea89a201..213dc45e6d9 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/RaceColumn.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/RaceColumn.java @@ -40,6 +40,13 @@ import com.sap.sse.common.Util.Pair; * */ public interface RaceColumn extends Named { + /** + * Sets the information object used to access the race column's race logs (see + * {@link #setRaceLogInformation(RaceLogStore, RegattaLikeIdentifier)}) and (re-)loads the contents of all fleets' + * race logs. + */ + void setRaceLogInformationAndLoad(RaceLogStore raceLogStore, RegattaLikeIdentifier regattaLikeParent); + /** * Sets the information object used to access the race column's race logs. */ @@ -49,7 +56,6 @@ public interface RaceColumn extends Named { * Gets the race column's race log associated to the passed fleet. Note that the result may be null * particularly for columns in a {@link MetaLeaderboard}. * - * @param fleet * @return the race log or null in case this column belongs to a {@link MetaLeaderboard} */ RaceLog getRaceLog(Fleet fleet); diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/AbstractRaceColumn.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/AbstractRaceColumn.java index de22e2e634d..a549406cb16 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/AbstractRaceColumn.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/AbstractRaceColumn.java @@ -74,14 +74,19 @@ public abstract class AbstractRaceColumn extends SimpleAbstractRaceColumn implem } @Override - public synchronized void setRaceLogInformation(RaceLogStore raceLogStore, RegattaLikeIdentifier regattaLikeParent) { - this.raceLogStore = raceLogStore; - this.regattaLikeParent = regattaLikeParent; + public synchronized void setRaceLogInformationAndLoad(RaceLogStore raceLogStore, RegattaLikeIdentifier regattaLikeParent) { + setRaceLogInformation(raceLogStore, regattaLikeParent); for (final Fleet fleet : getFleets()) { reloadRaceLog(fleet); } } + @Override + public synchronized void setRaceLogInformation(RaceLogStore raceLogStore, RegattaLikeIdentifier regattaLikeParent) { + this.raceLogStore = raceLogStore; + this.regattaLikeParent = regattaLikeParent; + } + @Override public RaceLog getRaceLog(Fleet fleet) { return raceLogs.get(fleet); @@ -191,6 +196,7 @@ public abstract class AbstractRaceColumn extends SimpleAbstractRaceColumn implem @Override public void reloadRaceLog(Fleet fleet) { + // FIXME bug3286: newOrLoadedRaceLog will have MongoRaceLogStoreListener attached; raceLogAvailable, result of de-serialization, will not; merging newOrLoadedRaceLog into raceLogAvailable will leave resulting log without persistence RaceLogIdentifier identifier = getRaceLogIdentifier(fleet); RaceLog newOrLoadedRaceLog = raceLogStore.getRaceLog(identifier, /* ignoreCache */true); RaceLog raceLogAvailable = raceLogs.get(fleet); diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/RegattaImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/RegattaImpl.java index 5aaf2d321ff..0448d1af352 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/RegattaImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/RegattaImpl.java @@ -284,7 +284,7 @@ public class RegattaImpl extends NamedImpl implements Regatta, RaceColumnListene } this.series = seriesList; for (Series s : series) { - linkToRegattaAndConnectRaceLogsAndAddListeners(s); + linkToRegattaAndConnectRaceLogsAndAddListeners(s, /* load race logs */ true); } this.persistent = persistent; this.scoringScheme = scoringScheme; @@ -323,14 +323,19 @@ public class RegattaImpl extends NamedImpl implements Regatta, RaceColumnListene return rankingMetricConstructor == null ? OneDesignRankingMetric::new : rankingMetricConstructor; } - private void registerRaceLogsOnRaceColumns(Series series) { + private void registerRaceLogsOnRaceColumns(Series series, boolean loadRaceLogs) { for (RaceColumn raceColumn : series.getRaceColumns()) { - setRaceLogInformationOnRaceColumn(raceColumn); + setRaceLogInformationOnRaceColumn(raceColumn, loadRaceLogs); } } - private void setRaceLogInformationOnRaceColumn(RaceColumn raceColumn) { - raceColumn.setRaceLogInformation(raceLogStore, new RegattaAsRegattaLikeIdentifier(this)); + private void setRaceLogInformationOnRaceColumn(RaceColumn raceColumn, boolean loadRaceLogs) { + final RegattaLikeIdentifier regattaLikeIdentifier = new RegattaAsRegattaLikeIdentifier(this); + if (loadRaceLogs) { + raceColumn.setRaceLogInformationAndLoad(raceLogStore, regattaLikeIdentifier); + } else { + raceColumn.setRaceLogInformation(raceLogStore, regattaLikeIdentifier); + } } @Override @@ -375,17 +380,14 @@ public class RegattaImpl extends NamedImpl implements Regatta, RaceColumnListene /** * {@link RaceColumnListeners} may not be de-serialized (yet) when the regatta is de-serialized. To avoid - * re-registering empty objects most probably leading to null pointer exception one needs to initialize all + * re-registering empty objects most probably leading to a {link NullPointerException} one needs to initialize all * listeners after all objects have been read. */ public void initializeSeriesAfterDeserialize() { - for (Series series : getSeries()) { - linkToRegattaAndConnectRaceLogsAndAddListeners(series); - if (series.getRaceColumns() != null) { - for (RaceColumnInSeries column : series.getRaceColumns()) { - column.setRaceLogInformation(raceLogStore, new RegattaAsRegattaLikeIdentifier(this)); - } - } else { + for (final Series series : getSeries()) { + // the following also transitively invokes setRaceLogInformation(raceLogStore, getRegattaLikeIdentifier()) on all race columns + linkToRegattaAndConnectRaceLogsAndAddListeners(series, /* load race logs */ false); + if (series.getRaceColumns() == null) { logger.warning("Race Columns were null during deserialization. This should not happen."); } } @@ -594,7 +596,7 @@ public class RegattaImpl extends NamedImpl implements Regatta, RaceColumnListene @Override public void raceColumnAddedToContainer(RaceColumn raceColumn) { - setRaceLogInformationOnRaceColumn(raceColumn); + setRaceLogInformationOnRaceColumn(raceColumn, /* loadRaceLogs */ true); raceColumnListeners.notifyListenersAboutRaceColumnAddedToContainer(raceColumn); } @@ -790,7 +792,7 @@ public class RegattaImpl extends NamedImpl implements Regatta, RaceColumnListene public void addSeries(Series seriesToAdd) { Series existingSeries = getSeriesByName(seriesToAdd.getName()); if (existingSeries == null) { - linkToRegattaAndConnectRaceLogsAndAddListeners(seriesToAdd); + linkToRegattaAndConnectRaceLogsAndAddListeners(seriesToAdd, /* load race logs */ true); synchronized (this.series) { ArrayList newSeriesList = new ArrayList(); for (Series seriesObject : this.series) { @@ -802,10 +804,10 @@ public class RegattaImpl extends NamedImpl implements Regatta, RaceColumnListene } } - private void linkToRegattaAndConnectRaceLogsAndAddListeners(Series seriesToAdd) { + private void linkToRegattaAndConnectRaceLogsAndAddListeners(Series seriesToAdd, boolean loadRaceLogs) { seriesToAdd.setRegatta(this); seriesToAdd.addRaceColumnListener(this); - registerRaceLogsOnRaceColumns(seriesToAdd); + registerRaceLogsOnRaceColumns(seriesToAdd, loadRaceLogs); } @Override diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/FlexibleLeaderboardImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/FlexibleLeaderboardImpl.java index d3c821a76ed..c2316ebbc49 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/FlexibleLeaderboardImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/FlexibleLeaderboardImpl.java @@ -178,7 +178,7 @@ public class FlexibleLeaderboardImpl extends AbstractLeaderboardImpl implements column = createRaceColumn(name, medalRace); column.addRaceColumnListener(this); races.add(column); - column.setRaceLogInformation(raceLogStore, new FlexibleLeaderboardAsRegattaLikeIdentifier(this)); + column.setRaceLogInformationAndLoad(raceLogStore, new FlexibleLeaderboardAsRegattaLikeIdentifier(this)); column.setRegattaLikeHelper(regattaLikeHelper); getRaceColumnListeners().notifyListenersAboutRaceColumnAddedToContainer(column); } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/MetaLeaderboardColumn.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/MetaLeaderboardColumn.java index f1d3565a229..01dff173088 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/MetaLeaderboardColumn.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/MetaLeaderboardColumn.java @@ -231,6 +231,10 @@ public class MetaLeaderboardColumn extends SimpleAbstractRaceColumn implements R public void setMasterDataExportOngoingThreadFlag(boolean flagValue) { } + @Override + public void setRaceLogInformationAndLoad(RaceLogStore raceLogStore, RegattaLikeIdentifier regattaLikeParent) { + } + @Override public void setRaceLogInformation(RaceLogStore raceLogStore, RegattaLikeIdentifier regattaLikeParent) { } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/GPSFixTrack.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/GPSFixTrack.java index 59a02fe5d2f..3846ffab2e1 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/GPSFixTrack.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/GPSFixTrack.java @@ -197,5 +197,4 @@ public interface GPSFixTrack extends MappedTra * {@link #getFixes()} if and only if this method returns {@code true}. */ boolean isValid(FixType e); - } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedLegOfCompetitor.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedLegOfCompetitor.java index 546efa43ba4..551e15f226d 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedLegOfCompetitor.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedLegOfCompetitor.java @@ -10,8 +10,8 @@ import com.sap.sailing.domain.common.NoWindException; import com.sap.sailing.domain.common.SpeedWithBearing; import com.sap.sailing.domain.common.TackType; import com.sap.sailing.domain.common.tracking.GPSFixMoving; +import com.sap.sailing.domain.leaderboard.caching.LeaderboardDTOCalculationReuseCache; import com.sap.sailing.domain.ranking.RankingMetric.RankingInfo; -import com.sap.sailing.domain.tracking.impl.NoCachingWindLegTypeAndLegBearingCache; import com.sap.sse.common.Bearing; import com.sap.sse.common.Distance; import com.sap.sse.common.Duration; @@ -359,7 +359,7 @@ public interface TrackedLegOfCompetitor extends Serializable { * for this single call. Good, e.g., for test cases. */ default TackType getTackType(TimePoint timePoint) throws NoWindException { - return getTackType(timePoint, new NoCachingWindLegTypeAndLegBearingCache()); + return getTackType(timePoint, new LeaderboardDTOCalculationReuseCache(timePoint)); } Double getExpeditionAWA(TimePoint at); diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DynamicGPSFixMovingTrackImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DynamicGPSFixMovingTrackImpl.java index 5eec9f82271..486966c7ae8 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DynamicGPSFixMovingTrackImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DynamicGPSFixMovingTrackImpl.java @@ -275,5 +275,4 @@ public class DynamicGPSFixMovingTrackImpl extends GPSFixTrackImpl - - - - - - - - - - - - - + + + + + + + + + + + + + + + diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/google/gwt/user/client/rpc/core/com/sap/sailing/datamining/shared/TackTypeSegmentsDataMiningSettings_CustomFieldSerializer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/google/gwt/user/client/rpc/core/com/sap/sailing/datamining/shared/TackTypeSegmentsDataMiningSettings_CustomFieldSerializer.java new file mode 100644 index 00000000000..4f65052807b --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/google/gwt/user/client/rpc/core/com/sap/sailing/datamining/shared/TackTypeSegmentsDataMiningSettings_CustomFieldSerializer.java @@ -0,0 +1,48 @@ +package com.google.gwt.user.client.rpc.core.com.sap.sailing.datamining.shared; + +import com.google.gwt.user.client.rpc.CustomFieldSerializer; +import com.google.gwt.user.client.rpc.SerializationException; +import com.google.gwt.user.client.rpc.SerializationStreamReader; +import com.google.gwt.user.client.rpc.SerializationStreamWriter; +import com.sap.sailing.datamining.shared.TackTypeSegmentsDataMiningSettings; +import com.sap.sse.common.Duration; + +public final class TackTypeSegmentsDataMiningSettings_CustomFieldSerializer extends CustomFieldSerializer { + @Override + public boolean hasCustomInstantiateInstance() { + return true; + } + + @Override + public TackTypeSegmentsDataMiningSettings instantiateInstance(SerializationStreamReader streamReader) throws SerializationException { + return instantiate(streamReader); + } + + public static TackTypeSegmentsDataMiningSettings instantiate(SerializationStreamReader streamReader) throws SerializationException { + final Duration minimumTackTypeSegmentDuration = (Duration) streamReader.readObject(); + final Duration minimumDurationBetweenAdjacentTackTypeSegments = (Duration) streamReader.readObject(); + return new TackTypeSegmentsDataMiningSettings(minimumTackTypeSegmentDuration, minimumDurationBetweenAdjacentTackTypeSegments); + } + + @Override + public void deserializeInstance(SerializationStreamReader streamReader, TackTypeSegmentsDataMiningSettings instance) + throws SerializationException { + deserialize(streamReader, instance); + } + + public static void deserialize(SerializationStreamReader streamReader, TackTypeSegmentsDataMiningSettings instance) { + // handled by instantiate + } + + @Override + public void serializeInstance(SerializationStreamWriter streamWriter, TackTypeSegmentsDataMiningSettings instance) + throws SerializationException { + serialize(streamWriter, instance); + } + + public static void serialize(SerializationStreamWriter streamWriter, TackTypeSegmentsDataMiningSettings instance) + throws SerializationException { + streamWriter.writeObject(instance.getMinimumTackTypeSegmentDuration()); + streamWriter.writeObject(instance.getMinimumDurationBetweenAdjacentTackTypeSegments()); + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/premium/SailingPremiumListBox.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/premium/SailingPremiumListBox.java new file mode 100644 index 00000000000..8f15ac27589 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/premium/SailingPremiumListBox.java @@ -0,0 +1,35 @@ +package com.sap.sailing.gwt.common.client.premium; + +import static com.sap.sailing.gwt.common.client.premium.SailingPremiumIconRessource.INSTANCE; + +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.ui.Image; +import com.sap.sailing.gwt.ui.client.EntryPointLinkFactory; +import com.sap.sse.security.shared.HasPermissions.Action; +import com.sap.sse.security.shared.dto.SecuredDTO; +import com.sap.sse.security.ui.client.premium.PaywallResolver; +import com.sap.sse.security.ui.client.premium.PremiumListBox; + +public class SailingPremiumListBox extends PremiumListBox { + + public SailingPremiumListBox(final String label, final String emptyValue, final Action action, final PaywallResolver paywallResolver, SecuredDTO contextDTO) { + super(label, emptyValue, action, paywallResolver, contextDTO); + } + + @Override + protected Image createPremiumIcon() { + return new Image(); + } + + @Override + protected void onUserPermissionUpdate(final boolean isPermitted) { + super.onUserPermissionUpdate(isPermitted); + image.setUrl((isPermitted ? INSTANCE.premiumIconPermitted() : INSTANCE.premiumIcon()).getSafeUri()); + } + + @Override + protected void onSubscribeDialogConfirmation(final Iterable unlockingPlans) { + Window.open(EntryPointLinkFactory.createSubscriptionPageLink(unlockingPlans), "_blank", ""); + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/DesktopEntryPoint.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/DesktopEntryPoint.java index f8067933031..6c77b8c11d4 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/DesktopEntryPoint.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/DesktopEntryPoint.java @@ -30,10 +30,8 @@ public class DesktopEntryPoint extends AbstractMvpEntryPoint() { @Override public void onSuccess(Boolean result) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/whatsnew/resources/SailingAnalyticsNotes.html b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/whatsnew/resources/SailingAnalyticsNotes.html index c9e0e3c4aca..a3cd7c26084 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/whatsnew/resources/SailingAnalyticsNotes.html +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/whatsnew/resources/SailingAnalyticsNotes.html @@ -12,6 +12,11 @@
  • Added two dimensions on wind fix datamining ("is in race" and "is in tracking time range").
  • Added a simple data mining retriever chain allowing for competitor counting.
  • Added a statistic for smoothed speed on GPS fixes in data mining.
  • +
  • Issues with updating the boat tails on the map have been resolved.
  • +
  • Several performance improvements for tail updates on the map have been implemented.
  • +
  • Colored tails now require a premium subscription.
  • +
  • Added data mining fact type for "tack type segment" (long/short tack) which + comes with the statistics distance and duration.
  • November 2023
      diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/actions/AbstractGetMapRelatedDataAction.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/actions/AbstractGetMapRelatedDataAction.java index 71a07f9ab45..2a782551a15 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/actions/AbstractGetMapRelatedDataAction.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/actions/AbstractGetMapRelatedDataAction.java @@ -2,27 +2,55 @@ package com.sap.sailing.gwt.ui.actions; import java.util.Date; import java.util.Map; +import java.util.UUID; +import com.sap.sailing.domain.common.DetailType; import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sailing.domain.common.dto.CompetitorDTO; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; -import com.sap.sse.gwt.client.async.AsyncAction; -public abstract class AbstractGetMapRelatedDataAction implements AsyncAction { +/** + * A remote data request that is specific to a race in the scope of a leaderboard and a leaderboard group, and for a set + * of competitors each specifies a time range in which to obtain position data, optionally annotated with detail values + * of a specific {@link DetailType}. + *

      + * + * Requests of this type are asynchronous, meaning in particular that the responses can arrive in an order different + * from the request order. Even new requests may be assembled and sent before the responses to all outstanding requests + * have been received. Sometimes a new request may supersede a request with an outstanding response, in which case + * the callback to the request with the outstanding response may have to be informed that it must not apply the + * result for one or more competitors. For example, if a request for position data is made with a specific {@link DetailType}, + * and then, before the response for that request has been received, another request, this time for a different + * {@link DetailType}, is constructed and sent because the user has switched to a different {@link DetailType} in the UI, + * then the callback of the request with the outstanding response must be told to toss its position data because it + * would be annotated with values for the wrong {@link DetailType} now. + * + * @author Axel Uhl (d043530) + * + * @param + */ +public abstract class AbstractGetMapRelatedDataAction { private final SailingServiceAsync sailingService; private final RegattaAndRaceIdentifier raceIdentifier; - private final Map from; - private final Map to; + private final Map from; + private final Map to; private final boolean extrapolate; - - public AbstractGetMapRelatedDataAction(SailingServiceAsync sailingService, - RegattaAndRaceIdentifier raceIdentifier, Map from, - Map to, boolean extrapolate) { + private final DetailType detailType; + private final String leaderboardName; + private final String leaderboardGroupName; + private final UUID leaderboardGroupId; + + public AbstractGetMapRelatedDataAction(SailingServiceAsync sailingService, RegattaAndRaceIdentifier raceIdentifier, + Map from, Map to, boolean extrapolate, DetailType detailType, + String leaderboardName, String leaderboardGroupName, UUID leaderboardGroupId) { this.sailingService = sailingService; this.raceIdentifier = raceIdentifier; this.from = from; this.to = to; this.extrapolate = extrapolate; + this.detailType = detailType; + this.leaderboardName = leaderboardName; + this.leaderboardGroupName = leaderboardGroupName; + this.leaderboardGroupId = leaderboardGroupId; } protected SailingServiceAsync getSailingService() { @@ -33,15 +61,31 @@ public abstract class AbstractGetMapRelatedDataAction implements AsyncAction< return raceIdentifier; } - protected Map getFrom() { + protected Map getFromByCompetitorIdAsString() { return from; } - protected Map getTo() { + protected Map getToByCompetitorIdAsString() { return to; } protected boolean isExtrapolate() { return extrapolate; } + + protected DetailType getDetailType() { + return detailType; + } + + protected String getLeaderboardName() { + return leaderboardName; + } + + protected String getLeaderboardGroupName() { + return leaderboardGroupName; + } + + protected UUID getLeaderboardGroupId() { + return leaderboardGroupId; + } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/actions/GetBoatPositionsAction.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/actions/GetBoatPositionsAction.java index 4e5ae659dad..07de059041c 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/actions/GetBoatPositionsAction.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/actions/GetBoatPositionsAction.java @@ -8,7 +8,6 @@ import java.util.UUID; import com.google.gwt.user.client.rpc.AsyncCallback; import com.sap.sailing.domain.common.DetailType; import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sailing.domain.common.dto.CompetitorDTO; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.shared.CompactBoatPositionsDTO; import com.sap.sse.common.TimePoint; @@ -17,26 +16,15 @@ import com.sap.sse.common.Util.Pair; import com.sap.sse.common.impl.TimeRangeImpl; import com.sap.sse.gwt.client.async.TimeRangeAsyncAction; -public class GetBoatPositionsAction implements TimeRangeAsyncAction> { - private final SailingServiceAsync sailingService; - private final RegattaAndRaceIdentifier raceIdentifier; - private final Map from; - private final Map to; - private final boolean extrapolate; - private final DetailType detailType; +public class GetBoatPositionsAction extends AbstractGetMapRelatedDataAction implements TimeRangeAsyncAction> { private final String leaderboardName; private final String leaderboardGroupName; private final UUID leaderboardGroupId; public GetBoatPositionsAction(SailingServiceAsync sailingService, RegattaAndRaceIdentifier raceIdentifier, - Map from, Map to, boolean extrapolate, DetailType detailType, + Map from, Map to, boolean extrapolate, DetailType detailType, String leaderboardName, String leaderboardGroupName, UUID leaderboardGroupId) { - this.sailingService = sailingService; - this.raceIdentifier = raceIdentifier; - this.from = from; - this.to = to; - this.extrapolate = extrapolate; - this.detailType = detailType; + super(sailingService, raceIdentifier, from, to, extrapolate, detailType, leaderboardName, leaderboardGroupName, leaderboardGroupId); this.leaderboardName = leaderboardName; this.leaderboardGroupName = leaderboardGroupName; this.leaderboardGroupId = leaderboardGroupId; @@ -52,18 +40,18 @@ public class GetBoatPositionsAction implements TimeRangeAsyncAction, TimeRange> getTimeRanges() { - final Map, TimeRange> timeRangeByCompetitorId = new HashMap<>(from.size()); - for (final Map.Entry entry : from.entrySet()) { + final Map, TimeRange> timeRangeByCompetitorId = new HashMap<>(getFromByCompetitorIdAsString().size()); + for (final Map.Entry entry : getFromByCompetitorIdAsString().entrySet()) { final Date fromDate = entry.getValue(); - final Date toDate = to.get(entry.getKey()); + final Date toDate = getToByCompetitorIdAsString().get(entry.getKey()); if (fromDate != null && toDate != null) { - timeRangeByCompetitorId.put(new Pair<>(entry.getKey().getIdAsString(), detailType), + timeRangeByCompetitorId.put(new Pair<>(entry.getKey(), getDetailType()), new TimeRangeImpl(TimePoint.of(fromDate), TimePoint.of(toDate), /* toIsInclusive */ true)); } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/actions/GetRaceMapDataAction.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/actions/GetRaceMapDataAction.java index 0911234797a..d09c5ecbde7 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/actions/GetRaceMapDataAction.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/actions/GetRaceMapDataAction.java @@ -1,63 +1,69 @@ package com.sap.sailing.gwt.ui.actions; import java.util.Date; -import java.util.HashMap; import java.util.Map; import java.util.UUID; +import com.google.gwt.core.client.GWT; import com.google.gwt.user.client.rpc.AsyncCallback; import com.sap.sailing.domain.common.DetailType; import com.sap.sailing.domain.common.LegIdentifier; import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.domain.common.dto.CompetitorDTO; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.shared.CompactBoatPositionsDTO; import com.sap.sailing.gwt.ui.shared.CompactRaceMapDataDTO; +import com.sap.sailing.gwt.ui.shared.GPSFixDTOWithSpeedWindTackAndLegTypeIterable; import com.sap.sailing.gwt.ui.shared.RaceMapDataDTO; +import com.sap.sse.common.Util.Pair; +import com.sap.sse.gwt.client.async.AsyncAction; +import com.sap.sse.gwt.client.async.AsyncActionsExecutor; +import com.sap.sse.gwt.client.async.TimeRangeActionsExecutor; -public class GetRaceMapDataAction extends AbstractGetMapRelatedDataAction { +/** + * When it gets {@ink #dropped(AsyncActionsExecutor) dropped}, at least the + * {@link SailingServiceAsync#getBoatPositions(RegattaAndRaceIdentifier, Map, Map, boolean, DetailType, String, String, UUID, AsyncCallback) + * getBoatPositions} call must be executed, then through the {@link TimeRangeActionsExecutor}, with the callback + * provided to the constructor. + * + * @author Axel Uhl (d043530) + * + */ +public class GetRaceMapDataAction extends AbstractGetMapRelatedDataAction implements AsyncAction { private final Map competitorsByIdAsString; private final Date date; private final LegIdentifier simulationLegIdentifier; private final byte[] md5OfIdsAsStringOfCompetitorParticipatingInRaceInAlphanumericOrderOfTheirID; private Date timeForEstimation; private boolean targetEstimationRequired; - private final DetailType detailType; - private final String leaderboardName; - private final String leaderboardGroupName; - private final UUID leaderboardGroupId; + private final TimeRangeActionsExecutor> timeRangeActionsExecutor; + private final GetBoatPositionsCallback getBoatPositionsCallback; - public GetRaceMapDataAction(SailingServiceAsync sailingService, Map competitorsByIdAsString, - RegattaAndRaceIdentifier raceIdentifier, Date date, Map from, - Map to, boolean extrapolate, LegIdentifier simulationLegIdentifier, + public GetRaceMapDataAction(SailingServiceAsync sailingService, + TimeRangeActionsExecutor> timeRangeActionsExecutor, + Map competitorsByIdAsString, RegattaAndRaceIdentifier raceIdentifier, Date date, Map from, + Map to, boolean extrapolate, LegIdentifier simulationLegIdentifier, byte[] md5OfIdsAsStringOfCompetitorParticipatingInRaceInAlphanumericOrderOfTheirID, Date timeForEstimation, boolean targetEstimationRequired, DetailType detailType, String leaderboardName, - String leaderboardGroupName, UUID leaderboardGroupId) { - super(sailingService, raceIdentifier, from, to, extrapolate); + String leaderboardGroupName, UUID leaderboardGroupId, GetBoatPositionsCallback getBoatPositionsCallback) { + super(sailingService, raceIdentifier, from, to, extrapolate, detailType, leaderboardName, leaderboardGroupName, leaderboardGroupId); + this.timeRangeActionsExecutor = timeRangeActionsExecutor; this.competitorsByIdAsString = competitorsByIdAsString; this.timeForEstimation = timeForEstimation; this.targetEstimationRequired = targetEstimationRequired; this.date = date; this.simulationLegIdentifier = simulationLegIdentifier; this.md5OfIdsAsStringOfCompetitorParticipatingInRaceInAlphanumericOrderOfTheirID = md5OfIdsAsStringOfCompetitorParticipatingInRaceInAlphanumericOrderOfTheirID; - this.detailType = detailType; - this.leaderboardName = leaderboardName; - this.leaderboardGroupName = leaderboardGroupName; - this.leaderboardGroupId = leaderboardGroupId; + this.getBoatPositionsCallback = getBoatPositionsCallback; } @Override public void execute(final AsyncCallback callback) { - Map fromByCompetitorIdAsString = new HashMap(); - for (Map.Entry fromEntry : getFrom().entrySet()) { - fromByCompetitorIdAsString.put(fromEntry.getKey().getIdAsString(), fromEntry.getValue()); - } - Map toByCompetitorIdAsString = new HashMap(); - for (Map.Entry toEntry : getTo().entrySet()) { - toByCompetitorIdAsString.put(toEntry.getKey().getIdAsString(), toEntry.getValue()); - } + Map fromByCompetitorIdAsString = getFromByCompetitorIdAsString(); + Map toByCompetitorIdAsString = getToByCompetitorIdAsString(); getSailingService().getRaceMapData(getRaceIdentifier(), date, fromByCompetitorIdAsString, toByCompetitorIdAsString, isExtrapolate(), simulationLegIdentifier, - md5OfIdsAsStringOfCompetitorParticipatingInRaceInAlphanumericOrderOfTheirID, timeForEstimation, targetEstimationRequired, detailType, - leaderboardName, leaderboardGroupName, leaderboardGroupId, + md5OfIdsAsStringOfCompetitorParticipatingInRaceInAlphanumericOrderOfTheirID, timeForEstimation, targetEstimationRequired, getDetailType(), + getLeaderboardName(), getLeaderboardGroupName(), getLeaderboardGroupId(), new AsyncCallback() { @Override public void onFailure(Throwable caught) { @@ -70,4 +76,19 @@ public class GetRaceMapDataAction extends AbstractGetMapRelatedDataAction getSelectedFilteredCompetitors() { - Set result = new HashSet<>(selectedCompetitors.values()); - Util.retainAll(getFilteredCompetitors(), result); - return result; + return Util.filter(getFilteredCompetitors(), c->selectedCompetitors.containsKey(c.getIdAsString())); } @Override @@ -137,18 +135,13 @@ public class CompetitorSelectionModel implements CompetitorSelectionProvider { @Override public Iterable getFilteredCompetitors() { - Set currentFilteredList = new LinkedHashSet<>(allCompetitors); - if (competitorsFilterSet != null) { - for (Filter filter : competitorsFilterSet.getFilters()) { - for (Iterator i=currentFilteredList.iterator(); i.hasNext(); ) { - CompetitorDTO competitorDTO = i.next(); - if (!filter.matches(competitorDTO)) { - i.remove(); - } - } - } + final Iterable result; + if (competitorsFilterSet == null || competitorsFilterSet.getFilters().isEmpty()) { + result = allCompetitors; + } else { + result = Util.filter(allCompetitors, competitorDTO -> competitorsFilterSet.getFilters().stream().allMatch(filter->filter.matches(competitorDTO))); } - return currentFilteredList; + return result; } public void setSelected(CompetitorDTO competitor, boolean selected, CompetitorSelectionChangeListener... listenersNotToNotify) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java index 453817256a3..855771ed8a2 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java @@ -594,4 +594,6 @@ public interface SailingService extends RemoteService, RemoteReplicationService YellowBrickConfigurationWithSecurityDTO configuration) throws Exception; List getCourseAreaForEventOfLeaderboard(String leaderboardName); + + String getGoogleMapsLoaderAuthenticationParams(); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java index 608407e0dbe..ef1dc94a4e3 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java @@ -682,4 +682,6 @@ public interface SailingServiceAsync extends RemoteReplicationServiceAsync { * from the {@link Event}s obtains their {@link CourseArea}s. */ void getCourseAreaForEventOfLeaderboard(String leaderboardName, AsyncCallback> callback); + + void getGoogleMapsLoaderAuthenticationParams(AsyncCallback callback); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SimulatorService.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SimulatorService.java index e9699dbf3a3..263ab737d0d 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SimulatorService.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SimulatorService.java @@ -60,4 +60,6 @@ public interface SimulatorService extends RemoteService { List getRacesNames(); List getCompetitorsNames(int selectedRaceIndex); + + String getGoogleMapsLoaderAuthenticationParams(); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SimulatorServiceAsync.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SimulatorServiceAsync.java index bda3dd91f5f..51771113f26 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SimulatorServiceAsync.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SimulatorServiceAsync.java @@ -47,4 +47,6 @@ public interface SimulatorServiceAsync { AsyncCallback callback); void getCompetitorsNames(int selectedRaceIndex, AsyncCallback> asyncCallback); + + void getGoogleMapsLoaderAuthenticationParams(AsyncCallback asyncCallback); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java index 31ed970e6cf..89be23aa724 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java @@ -2444,4 +2444,10 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages, String tackTypeUnit(); String tackTypeTooltip(); String tackType(); + String tackTypeSegments(); + String errorMinimumDurationBetweenAdjacentTackTypeSegmentsMustNotBeNegative(); + String errorMinimumTackTypeSegmentDurationMustNotBeNegative(); + String minimumDurationBetweenAdjacentTackTypeSegmentsInSeconds(); + String minimumTackTypeSegmentsDurationInSeconds(); + String errorNoAuthenticationParamsForGoogleMapsFound(String message); } \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties index e88d5d2d5c9..b64205cb23d 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties @@ -2480,4 +2480,10 @@ incrementalScoreCorrectionInPoints=Incremental score correction (points) errorObtainingCourseAreasForLeaderboard=Error obtaining course areas for leaderboard {0}: {1} tackType=Long / short tack tackTypeTooltip=For upwind: If the difference between COG and next waypoint direction is smaller than the one between COG and wind direction it is long tack (1.0); if it is smaller short tack (-1.0). For downwind: Similar to upwind but instead of "wind direction", one use the opposite direction. So where the wind is blowing to. For reaching: Similar to upwind but instead of comparing to "COG and wind direction", one use 10°. -tackTypeUnit=L=1.0, S=-1.0, Unknown=0.0 \ No newline at end of file +tackTypeUnit=L=1.0, S=-1.0, Unknown=0.0 +tackTypeSegments=Tack type segments +errorMinimumDurationBetweenAdjacentTackTypeSegmentsMustNotBeNegative=Minimum duration between adjacent tack type segments must not be negative +errorMinimumTackTypeSegmentDurationMustNotBeNegative=Minimum duration of a tack type segment must not be negative +minimumDurationBetweenAdjacentTackTypeSegmentsInSeconds=Minimum duration between adjacent tack type segments (s) +minimumTackTypeSegmentsDurationInSeconds=Minimum duration of tack type segments (s) +errorNoAuthenticationParamsForGoogleMapsFound=Error: No authentication parameters for the Google Maps API were found: {0} \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties index 64bc886603e..ff8419da5fc 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties @@ -2476,4 +2476,10 @@ incrementalScoreCorrectionInPoints=Inkrementelle Punktestrafe errorObtainingCourseAreasForLeaderboard=Fehler beim Abfragen der Bahnkreise zur Veranstaltung der Rangliste {0}: {1} tackType=Streck-/Holebug tackTypeTooltip=Für Amwind: Wenn der Unterschied zwischen KüG und Peilung zur nächsten Bahnmarke kleiner ist als KüG und Windrichtung, handelt es sich um Streckbug (1.0); wenn es kleiner ist Holebug (-1.0). Für Vorwind: Ähnlich wie zum Amwind, aber statt "Windrichtung" nutzt man die gegenteilige Richtung. Also die, wo der Wind hin weht. Für Halbwind: Ähnlich wie zur Kreuz, aber statt "KüG und Windrichtung" nutzt man 10°. -tackTypeUnit=S=1.0, H=-1.0, Unbekannt=0.0 \ No newline at end of file +tackTypeUnit=S=1.0, H=-1.0, Unbekannt=0.0 +tackTypeSegments=Streck-/Holebug-Segmente +errorMinimumDurationBetweenAdjacentTackTypeSegmentsMustNotBeNegative=Minimale Dauer zwischen zwei Streck-/Holebug-Segmenten darf nicht negativ sein +errorMinimumTackTypeSegmentDurationMustNotBeNegative=Minimale Dauer eines Streck-/Holebug-Segments darf nicht negativ sein +minimumDurationBetweenAdjacentTackTypeSegmentsInSeconds=Minimaler zeitlicher Abstand zwischen benachbarten Streck-/Holebug-Segmenten (s) +minimumTackTypeSegmentsDurationInSeconds=Mindestdauer eines Streck-/Holebug-Segments (s) +errorNoAuthenticationParamsForGoogleMapsFound=Fehler: Es wurden keine Authentifizierungs-Parameter für die Google Maps API gefunden: {0} \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/Colorline.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/Colorline.java index 0f60d29832d..f5dcdc8f40b 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/Colorline.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/Colorline.java @@ -16,8 +16,15 @@ import com.google.gwt.maps.client.mvc.MVCArray; import com.google.gwt.maps.client.overlays.Polyline; /** + * One or more {@link Polyline}s connected to form a longer polyline, with the possibility to change + * styles from segment to segment. Two different modes currently exist: {@link ColorlineMode#MONOCHROMATIC} + * and {@link ColorlineMode#POLYCHROMATIC}. In monochromatic mode the entire line is represented by a single + * {@link Polyline} as no style changes are required. In polychromatic mode, each connection between two + * dots is a separate {@link Polyline}, and the options for each segment---particularly their color--- + * is decided by a {@link ColorlineColorProvider} embedded in the {@link ColorlineOptions} used for this + * color line. * - * @author Tim Hessenmller (D062243) + * @author Tim Hessenm�ller (D062243) */ public class Colorline { private ColorlineOptions options; @@ -43,12 +50,11 @@ public class Colorline { public Colorline(ColorlineColorProvider colorProvider) { this(new ColorlineOptions(colorProvider)); } + public Colorline(ColorlineOptions options) { this.options = options; polylines = new ArrayList<>(); - pathChangeListeners = new HashSet<>(); - clickMapHandlers = new HashSet<>(); mouseOverMapHandlers = new HashSet<>(); mouseDownMapHandlers = new HashSet<>(); @@ -56,13 +62,11 @@ public class Colorline { mouseOutMapHandlers = new HashSet<>(); } - /*public ColorlineOptions getOptions() { - return options; - }*/ public void setOptions(final ColorlineOptions options) { if (this.options.getColorMode() != options.getColorMode()) { // If colorMode changed the path needs to change from one polyline to multiple polylines and vice versa - MVCArray path = MVCArray.newInstance(getPath().toArray(new LatLng[0])); + final List pathOfLatLngs = getPath(); + MVCArray path = MVCArray.newInstance(pathOfLatLngs.toArray(new LatLng[pathOfLatLngs.size()])); this.options = options; setPath(path); } else { @@ -70,18 +74,23 @@ public class Colorline { } // Since colorMode and/or options have changed it's best to update the colors of all polylines for (int i = 0; i < polylines.size(); i++) { - String color = options.getColorProvider().getColor(i); - polylines.get(i).setOptions(options.newPolylineOptionsInstance(color)); + updatePolylineColor(i); } } + private void updatePolylineColor(int fixIndexInTail) { + String color = options.getColorProvider().getColor(fixIndexInTail); + polylines.get(fixIndexInTail).setOptions(options.newPolylineOptionsInstance(color)); + } + /** * Creates an ordered {@link List} of all {@link LatLng} vertices in this {@code Colorline}. + * * @return ordered {@link List} of {@link LatLng}. */ public List getPath() { final int cap = getLength(); - List path = new ArrayList<>(cap); + final List path = new ArrayList<>(cap); if (cap > 0) { // In POLYCHROMATIC mode it is possible to have a single invisible vertex if the total path length is 1 path.add(polylines.get(0).getPath().get(0)); @@ -122,25 +131,30 @@ public class Colorline { /** * Inserts a vertex at a specified position in the path. - * @param index {@code int} indicating the insertion position. - * @param position {@link LatLng} the vertex to insert. - * @throws IllegalArgumentException if {@code position} is {@code null}. - * @throws IndexOutOfBoundsException if {@code index} is not in bounds of path. + * + * @param fixIndexInTail + * {@code int} indicating the insertion position. + * @param position + * {@link LatLng} the vertex to insert. + * @throws IllegalArgumentException + * if {@code position} is {@code null}. + * @throws IndexOutOfBoundsException + * if {@code index} is not in bounds of path. */ - public void insertAt(int index, LatLng position) throws IllegalArgumentException, IndexOutOfBoundsException { + public void insertAt(int fixIndexInTail, LatLng position) throws IllegalArgumentException, IndexOutOfBoundsException { if (position == null) throw new IllegalArgumentException("Cannot insert value: null"); for (Colorline line : pathChangeListeners) { - line.insertAt(index, position); + line.insertAt(fixIndexInTail, position); } switch (options.getColorMode()) { case MONOCHROMATIC: if (polylines.isEmpty()) { polylines.add(createPolyline(MVCArray.newInstance(), 0)); } - polylines.get(0).getPath().insertAt(index, position); + polylines.get(0).getPath().insertAt(fixIndexInTail, position); break; case POLYCHROMATIC: - if (index == 0) { + if (fixIndexInTail == 0) { // Prepend a new Polyline if (polylines.isEmpty() || polylines.get(0).getPath().getLength() == 2) { // There either is no Polyline or the existing Polyline at index 0 is completed @@ -150,43 +164,45 @@ public class Colorline { if (!polylines.isEmpty()) { // If we can connect the new Polyline to an existing one do so path.push(polylines.get(0).getPath().get(0)); } - polylines.add(0, createPolyline(path, index)); + polylines.add(0, createPolyline(path, fixIndexInTail)); } else { // The Polyline at index 0 is incomplete // Complete it - polylines.get(0).getPath().insertAt(0, position); + polylines.get(0).getPath().insertAt(0, position); // FIXME bug5921: what does this do to the polyline's color? Shouldn't the color always be determined by the first of the two points in the segment? } - } else if (index == getLength()) { - if (index == 1 && polylines.get(0).getPath().getLength() == 1) { + } else if (fixIndexInTail == getLength()) { + if (fixIndexInTail == 1 && polylines.get(0).getPath().getLength() == 1) { // Finish first polyline polylines.get(0).getPath().push(position); } else { // Append a new Polyline MVCArray path = MVCArray.newInstance(); - path.push(polylines.get(index - 2).getPath().get(1)); + path.push(polylines.get(fixIndexInTail - 2).getPath().get(1)); path.push(position); - polylines.add(index - 1, createPolyline(path, index)); + polylines.add(fixIndexInTail - 1, createPolyline(path, fixIndexInTail)); } } else { // Split an existing Polyline into two - LatLng end = polylines.get(index - 1).getPath().get(1); - polylines.get(index - 1).getPath().setAt(1, position); + LatLng end = polylines.get(fixIndexInTail - 1).getPath().get(1); + polylines.get(fixIndexInTail - 1).getPath().setAt(1, position); MVCArray path = MVCArray.newInstance(); path.push(position); path.push(end); - polylines.add(index, createPolyline(path, index)); + polylines.add(fixIndexInTail, createPolyline(path, fixIndexInTail)); } break; } } /** - * Removes a vertex at a specified position from the displayed path. - * If the removed vertex was not at one of the ends the two adjacent vertices will now - * directly connect to each other. - * @param index {@code int} indication the vertex to be removed from path. + * Removes a vertex at a specified position from the displayed path. If the removed vertex was not at one of the + * ends the two adjacent vertices will now directly connect to each other. + * + * @param index + * {@code int} indication the vertex to be removed from path. * @return {@link LatLng} vertex that was removed. - * @throws IndexOutOfBoundsException if {@code index} is not in bounds of path. + * @throws IndexOutOfBoundsException + * if {@code index} is not in bounds of path. */ public LatLng removeAt(int index) throws IndexOutOfBoundsException { if (index < 0 || index >= getLength()) { @@ -235,9 +251,13 @@ public class Colorline { /** * Sets a specified vertex. - * @param index {@code int} vertex to set. - * @param position {@link LatLng} to set vertex to. - * @throws IndexOutOfBoundsException if {@code index} is not in bounds of path. + * + * @param index + * {@code int} vertex to set. + * @param position + * {@link LatLng} to set vertex to. + * @throws IndexOutOfBoundsException + * if {@code index} is not in bounds of path. */ public void setAt(int index, LatLng position) throws IndexOutOfBoundsException { for (Colorline line : pathChangeListeners) { @@ -255,6 +275,7 @@ public class Colorline { } else { // Set a vertex somewhere in the middle which affects 2 polylines polylines.get(index - 1).getPath().setAt(1, position); polylines.get(index).getPath().setAt(0, position); + updatePolylineColor(index); } break; } @@ -317,8 +338,8 @@ public class Colorline { return -1; } - private Polyline createPolyline(MVCArray path, int colorIndex) { - Polyline line = options.newPolylineInstance(colorIndex); + private Polyline createPolyline(MVCArray path, int fixIndexInTail) { + final Polyline line = options.newPolylineInstance(fixIndexInTail); line.setPath(path); if (map != null) { line.setMap(map); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/ColorlineColorProvider.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/ColorlineColorProvider.java index f616474701f..19e1d252c9c 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/ColorlineColorProvider.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/ColorlineColorProvider.java @@ -2,5 +2,9 @@ package com.sap.sailing.gwt.ui.client.shared.racemap; @FunctionalInterface public interface ColorlineColorProvider { - public String getColor(int index); + /** + * @param fixIndexInTail + * zero-based index into the visible tail; 0 means the first visible fix + */ + public String getColor(int fixIndexInTail); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/ColorlineOptions.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/ColorlineOptions.java index 13ee8f0d00e..6bea1a7138f 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/ColorlineOptions.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/ColorlineOptions.java @@ -46,12 +46,13 @@ public class ColorlineOptions { } - public Polyline newPolylineInstance(int index) { + public Polyline newPolylineInstance(int fixIndexInTail) { if (colorProvider == null) { throw new IllegalStateException("A ColorProvider must be set prior to creating new Polylines."); } - return newPolylineInstance(colorProvider.getColor(index)); + return newPolylineInstance(colorProvider.getColor(fixIndexInTail)); } + public Polyline newPolylineInstance(String strokeColor) { Polyline line = Polyline.newInstance(newPolylineOptionsInstance(strokeColor)); line.setEditable(editable); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/CompetitorInfoOverlays.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/CompetitorInfoOverlays.java index 81d4be9c70a..6c926582e9b 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/CompetitorInfoOverlays.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/CompetitorInfoOverlays.java @@ -187,7 +187,7 @@ public class CompetitorInfoOverlays implements QuickFlagDataListener { @Override public void speedInKnotsChanged(CompetitorDTO competitorDTO, Double quickSpeedInKnots) { - String competitorId = competitorDTO.getIdAsString(); + final String competitorId = competitorDTO.getIdAsString(); speedsInKnots.put(competitorId, quickSpeedInKnots); final CompetitorInfoOverlay competitorInfoOverlay = competitorInfoOverlays.get(competitorId); if (competitorInfoOverlay != null) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/DefaultQuickFlagDataProvider.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/DefaultQuickFlagDataProvider.java index df7c9f3d976..5e965869253 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/DefaultQuickFlagDataProvider.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/DefaultQuickFlagDataProvider.java @@ -10,14 +10,14 @@ import com.sap.sailing.gwt.ui.shared.QuickRankDTO; import com.sap.sse.common.Util; public class DefaultQuickFlagDataProvider extends AbstractQuickFlagDataProvider { - private Map currentQuickRanksFromServer = Collections.emptyMap(); - private Map currentQuickSpeedsInKnotsFromServer = Collections.emptyMap(); + private Map currentQuickRanksFromServerByCompetitorIdAsString = Collections.emptyMap(); + private Map currentQuickSpeedsInKnotsFromServerByCompetitorIdAsString = Collections.emptyMap(); @Override public void quickRanksReceivedFromServer(Map receivedQuickRanksFromServer) { - final Map oldQuickRanksFromServer = this.currentQuickRanksFromServer; - this.currentQuickRanksFromServer = Util.nullToEmptyMap(receivedQuickRanksFromServer); - for (final Entry e : currentQuickRanksFromServer.entrySet()) { + final Map oldQuickRanksFromServer = this.currentQuickRanksFromServerByCompetitorIdAsString; + this.currentQuickRanksFromServerByCompetitorIdAsString = Util.nullToEmptyMap(receivedQuickRanksFromServer); + for (final Entry e : currentQuickRanksFromServerByCompetitorIdAsString.entrySet()) { final QuickRankDTO oldQuickRank = oldQuickRanksFromServer.get(e.getKey()); if (Util.equalsWithNull(oldQuickRank, e.getValue())) { notifyListenersRankChanged(e.getKey(), oldQuickRank, e.getValue()); @@ -27,17 +27,17 @@ public class DefaultQuickFlagDataProvider extends AbstractQuickFlagDataProvider @Override public Map getQuickRanks() { - return currentQuickRanksFromServer; + return currentQuickRanksFromServerByCompetitorIdAsString; } @Override - public void quickSpeedsInKnotsReceivedFromServer(Map quickSpeedsFromServerInKnots) { - final Map oldQuickSpeedsFromServerInKnots = this.currentQuickSpeedsInKnotsFromServer; - this.currentQuickSpeedsInKnotsFromServer = Util.nullToEmptyMap(quickSpeedsFromServerInKnots); - for (final Entry e : currentQuickSpeedsInKnotsFromServer.entrySet()) { + public void quickSpeedsInKnotsReceivedFromServer(Map quickSpeedsFromServerInKnotsByCompetitorIdAsString, Map competitorsByIdAsString) { + final Map oldQuickSpeedsFromServerInKnots = this.currentQuickSpeedsInKnotsFromServerByCompetitorIdAsString; + this.currentQuickSpeedsInKnotsFromServerByCompetitorIdAsString = Util.nullToEmptyMap(quickSpeedsFromServerInKnotsByCompetitorIdAsString); + for (final Entry e : currentQuickSpeedsInKnotsFromServerByCompetitorIdAsString.entrySet()) { final Double oldQuickSpeedInKnots = oldQuickSpeedsFromServerInKnots.get(e.getKey()); if (Util.equalsWithNull(oldQuickSpeedInKnots, e.getValue())) { - notifyListenersSpeedInKnotsChanged(e.getKey(), e.getValue()); + notifyListenersSpeedInKnotsChanged(competitorsByIdAsString.get(e.getKey()), e.getValue()); } } @@ -45,7 +45,6 @@ public class DefaultQuickFlagDataProvider extends AbstractQuickFlagDataProvider @Override public Double getQuickSpeedsInKnots(CompetitorDTO competitor) { - return currentQuickSpeedsInKnotsFromServer.get(competitor); + return currentQuickSpeedsInKnotsFromServerByCompetitorIdAsString.get(competitor.getIdAsString()); } - } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/FixesAndTails.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/FixesAndTails.java index 2a8a61e2076..65f43766466 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/FixesAndTails.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/FixesAndTails.java @@ -5,108 +5,346 @@ import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; -import java.util.Iterator; +import java.util.HashSet; import java.util.List; import java.util.ListIterator; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; +import java.util.function.Function; -import com.google.gwt.maps.client.base.LatLng; +import com.google.gwt.core.client.GWT; import com.google.gwt.maps.client.mvc.MVCArray; import com.google.gwt.maps.client.overlays.Polyline; import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.rpc.AsyncCallback; +import com.sap.sailing.domain.common.DetailType; import com.sap.sailing.domain.common.dto.CompetitorDTO; import com.sap.sailing.domain.common.dto.CompetitorWithBoatDTO; +import com.sap.sailing.gwt.ui.actions.GetBoatPositionsAction; +import com.sap.sailing.gwt.ui.actions.GetRaceMapDataAction; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.shared.GPSFixDTOWithSpeedWindTackAndLegType; import com.sap.sailing.gwt.ui.shared.GPSFixDTOWithSpeedWindTackAndLegTypeIterable; import com.sap.sse.common.ColorMapper; +import com.sap.sse.common.Duration; +import com.sap.sse.common.MultiTimeRange; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.TimeRange; import com.sap.sse.common.Util; -import com.sap.sse.common.Util.Triple; +import com.sap.sse.common.Util.Pair; import com.sap.sse.common.ValueRangeFlexibleBoundaries; -import com.sap.sse.common.impl.MillisecondsTimePoint; -import com.sap.sse.common.impl.TimeRangeImpl; -import com.sap.sse.common.util.Trigger; -import com.sap.sse.common.util.Triggerable; -import com.sap.sse.gwt.client.TriggerableTimer; +import com.sap.sse.gwt.client.async.TimeRangeActionsExecutor; /** - * Manages the cache of {@link GPSFixDTOWithSpeedWindTackAndLegType}s for the competitors and the polylines encoding the tails that visualize the - * course the boats took. The tails are based on the GPS fix data. This class offers methods to update the fixes and - * the tails, making sure that the data is always managed consistently. In particular, it keeps an eye on - * {@link GPSFixDTOWithSpeedWindTackAndLegType#extrapolated extrapolated fixes}. Those are just a guess where a boat may have been and will need - * to be removed once actual data for that time is available. + * Manages the cache of {@link GPSFixDTOWithSpeedWindTackAndLegType}s for the competitors and the {@link Colorline}s + * representing sub-sets of those fixes, encoding the tails that visualize the course the boats took. The fixes cached + * for each competitor are intended to represent a contiguous segment of the competitor's track managed by the server. A + * tail may show a sub-segment of the fixes cached for a competitor, for example, because new fixes are added to the end + * of a competitors fixes cache, extending the contiguous cached segment beyond the length visualized by the tail. This + * way, moving back and forth in time while updating the tail may sometimes be possible even without having to load more + * data from the server. The last fix of a segment of cached fixes may be an + * {@link GPSFixDTOWithSpeedWindTackAndLegType#extrapolated extrapolated} one. When a later (extrapolated or real) fix + * is to be cached for that competitor, the earlier extrapolated fix is removed from the cache and, if currently + * visualized on the tail, also from the tail. + *

      + * + * The "contiguous condition" may be violated temporarily due to requests for data being executed asynchronously, with + * results being processed out of order, and with compound requests getting dropped and only their position data request + * being re-sent later. + *

      + * + * If a competitor's tail is to be shown for a time range for which data is missing from the cache, and the time range + * does not have an overlap with the cached time range, in order to maintain only contiguous track segments in the + * cache, the previously cached fixes will be dropped from the cache, and a new contiguous segment will be started as + * the response to the new request is processed. "In-flight" requests for data that would extend the previous track + * segment will have their callbacks informed so that when processing the response they don't try to merge their fixes + * into the cache anymore, basically dropping the fixes received to avoid cache inconsistencies. + *

      + * + * Fixes can optionally be annotated with detail values that can be used to color the tail based on the respective + * value. For example, the detail value may represent the boat's speed over ground (SOG) in knots, or its velocity made + * good (VMG) in knots. The semantics of the detail value is described by a {@link DetailType}. Currently, the detail + * values are always obtained together with the basic GPS fix data such as position and time point. As the user selects + * a different detail type, the detail values of those competitors who have their tail colored based on the detail + * values need to be updated to reflect the new detail type; this requires a re-load at least of the track segment + * currently visualized for that competitor, dropping the data cached for that competitor so far. (See also bug 5925 + * which considers separating the detail values from the fixes.) + *

      + * + * When showing colored tails, the color range is determined by monitoring minimum and maximum detail values across all + * competitors for which colored tails are shown. The color range is adjusted in case the value range changes beyond a + * certain threshold (see {@link ValueRangeFlexibleBoundaries}). To achieve this, adding fixes to a colored tail must + * check the new fixes for extreme values; when removing a fix from a colored tail that had a minimal / maximal detail + * value, that tail must be searched again for a new minimal / maximal value. + *

      + * + * A client of an instance of this class (typically a {@link RaceMap}) interacts in three possible ways: + *

        + *
      1. Preparing the requests that update the fixes cache: Based on the tail length and a "current time" (usually + * defined by the {@link com.sap.sse.gwt.client.player.Timer Timer}'s + * {@link com.sap.sse.gwt.client.player.Timer#getTime() time slider}), one or two requests including callback handlers + * are returned: one for a quick request for only a short piece of the track, and optionally another one for a longer + * piece of the track, where the remote call is expected to take a bit longer than we would like to wait with showing + * the current boat position. The requests know the time ranges to request for which competitor, and they know whether + * when processing their response they first need to {@link #clearTails() clear} the cache before entering new, + * non-overlapping positions. The two requests are linked to each other and share the knowledge about whether the first + * of them to process its response needs to clear the cache. The resulting time ranges already received or requested per + * competitor are remembered and are used to trim subsequent request. Should later requests require a clearing of the + * cache (such as after changing the detail type or when requesting a disconnected time range), requests still "in + * flight" will have their callbacks informed so that they will discard the positions they will receive.
      2. + *
      3. Processing the responses to those requests: The callbacks returned with the one or two requests from the previous + * step check whether their results are still valid; if so, they use + * {@link #updateFixes(Map, Map, long, boolean, DetailType)} to install the position fixes received in the cache and + * either clear (in case the response started a new contiguous track segment) or incrementally update an existing + * tail.
      4. + *
      5. Request initial creation or incremental update of a competitor's tail: This is used to adjust the time range + * for which the tail visualizes the cached fixes. Tail creation and update is largely independent of requesting and + * updating cached fixes; instead, tail updates use the cached fixes currently available. Fixes being updated into + * the cache upon receiving responses from the server will update the visible tails if they fall into the desired + * visible time range.
      6. + *
      + * + * This class offers methods to update the fixes and the tails, making sure that the data is always managed + * consistently. In particular, it keeps an eye on {@link GPSFixDTOWithSpeedWindTackAndLegType#extrapolated extrapolated + * fixes}. Those are just a guess where a boat may have been and will need to be removed once actual data for that time + * is available. * * @author Axel Uhl (d043530) * */ public class FixesAndTails { + private static final Duration MAX_DURATION_FOR_QUICK_REQUESTS = Duration.ONE_SECOND.times(10l); + /** * Fixes of each competitors tail. If a list is contained for a competitor, the list contains a timely "contiguous" * list of fixes for the competitor. This means the server has no more data for the time interval covered, unless * the last fix was {@link GPSFixDTOWithSpeedWindTackAndLegType#extrapolated obtained by extrapolation}. *

      * - * If the fixes for a competitor contain an {@link GPSFixDTOWithSpeedWindTackAndLegType#extrapolated extrapolated} fix, that fix is always - * guaranteed to be the last element of the list when outside the execution of a method on this class. This in - * particular means that when more fixes are added, and there is now one fix later than the extrapolated fix, the - * extrapolated fix will be removed, re-establishing the invariant of an extrapolated fix always being the last - * in the list. + * If the fixes for a competitor contain an {@link GPSFixDTOWithSpeedWindTackAndLegType#extrapolated extrapolated} + * fix, that fix is always guaranteed to be the last element of the list when outside the execution of a method on + * this class. This in particular means that when more fixes are added, and there is now one fix later than the + * extrapolated fix, the extrapolated fix will be removed, re-establishing the invariant of an extrapolated fix + * always being the last in the list. */ - private final Map> fixes; + private final Map> fixesByCompetitorIdsAsStrings; /** * Tails of competitors currently displayed as overlays on the map. A tail may have an {@link MVCArray#getLength() - * empty} {@link Polyline#getPath()}. In this case, {@link #firstShownFix} and {@link #lastShownFix} will hold + * empty} {@link Polyline#getPath()}. In this case, {@link #firstShownFixByCompetitorIdsAsStrings} and {@link #lastShownFixByCompetitorIdsAsStrings} will hold * -1 for that competitor key. */ - private final Map> tails; + private final Map tailsByCompetitorIdsAsStrings; /** - * Key set is equal to that of {@link #tails} and tells what the index in {@link #fixes} of the first fix shown - * in {@link #tails} is. If a key is contained in this map, it is also contained in {@link #lastShownFix} and vice - * versa. If a tail is present but has an empty path, this map contains -1 for that competitor. + * Key set is equal to that of {@link #tailsByCompetitorIdsAsStrings} and tells what the index in {@link #fixesByCompetitorIdsAsStrings} of the first fix shown + * in {@link #tailsByCompetitorIdsAsStrings} is. If a key is contained in this map, it is also contained in {@link #lastShownFixByCompetitorIdsAsStrings} and vice + * versa. If a tail is present but has an empty path, this map does not contain an entry for that competitor. */ - private final Map> firstShownFix; + private final Map firstShownFixByCompetitorIdsAsStrings; /** - * Key set is equal to that of {@link #tails} and tells what the index in {@link #fixes} of the last fix shown in - * {@link #tails} is. If a key is contained in this map, it is also contained in {@link #firstShownFix} and vice - * versa. If a tail is present but has an empty path, this map contains -1 for that competitor. + * Key set is equal to that of {@link #tailsByCompetitorIdsAsStrings} and tells what the index in {@link #fixesByCompetitorIdsAsStrings} of the last fix shown in + * {@link #tailsByCompetitorIdsAsStrings} is. If a key is contained in this map, it is also contained in {@link #firstShownFixByCompetitorIdsAsStrings} and vice + * versa. If a tail is present but has an empty path, this map does not contain an entry for that competitor. */ - private final Map> lastShownFix; + private final Map lastShownFixByCompetitorIdsAsStrings; + + /** + * The position fixes in {@link #fixesByCompetitorIdsAsStrings} are filled by processing the responses to asynchronous requests. This map + * keeps track of the beginning and end of the contiguous time range for which position data has been requested, per + * competitor. This may cover time ranges requested but not yet responded to, represented by the + * {@link #inFlightRequests}. The earliest requested time point for a competitor may also be earlier than the + * earliest fix eventually received and cached, for example, because the start of the requested time range does not + * coincide exactly with the time stamp of a fix. Likewise, the latest requested time point may be later than the + * latest fix eventually received and cached. + *

      + * + * These time ranges shall be used to trim new position requests (see + * {@link #computeFromAndTo(Date, Iterable, long, long, boolean)}), and to decide + * whether a new request will contiguously extend the time range or will have to start a new, disconnected time + * range, clearing the cache contents for that competitor. When trimming, if no request is currently + * {@link #inFlightRequests in flight}, instead of the last time point requested, the time + * point of the last fix received shall be used for trimming because we may hope to receive + * fixes delivered late, e.g. in a live situation with too short a delay. + */ + private final Map timeRangesRequestedByCompetitorIdAsString; /** - * Stores the index to the smallest found detailValue in {@link #fixes} for a given competitor. + * Stores the index to the smallest found {@link GPSFixDTOWithSpeedWindTackAndLegType#detailValue detailValue} in + * {@link #fixesByCompetitorIdsAsStrings} for a given competitor. If a competitor ID is not part of the map's key + * set, nothing is known about any minimum detail values on the competitor's visible + * {@link #tailsByCompetitorIdsAsStrings tail}. */ - private final Map minDetailValueFix; + private final Map minDetailValueFixByCompetitorIdsAsStrings; + /** - * Stores the index to the largest found detailValue in {@link #fixes} for a given competitor. + * Stores the index to the largest found {@link GPSFixDTOWithSpeedWindTackAndLegType#detailValue detailValue} in + * {@link #fixesByCompetitorIdsAsStrings} for a given competitor. If a competitor ID is not part of the map's key + * set, nothing is known about any maximum detail values on the competitor's visible + * {@link #tailsByCompetitorIdsAsStrings tail}. */ - private final Map maxDetailValueFix; + private final Map maxDetailValueFixByCompetitorIdsAsStrings; + /** - * Stores at what index the last search on the fixes of a given competitor stopped. - */ - private final Map lastSearchedFix; - - /** - * Keeps track of detailValues (stored in {@link GPSFixDTOWithSpeedWindTackAndLegType} in {@link #fixes}) + * Keeps track of detailValues (stored in {@link GPSFixDTOWithSpeedWindTackAndLegType} in {@link #fixesByCompetitorIdsAsStrings}) * boundaries so that a {@link ColorMapper} can be used. */ private ValueRangeFlexibleBoundaries detailValueBoundaries; private final CoordinateSystem coordinateSystem; + + /** + * Tells the type of value stored in the fixes' {@link GPSFixDTOWithSpeedWindTackAndLegType#detailValue} field for + * the competitor used as the key in this map. If {@code null} or no mapping exists for the competitor, the detail + * values shall be ignored and may be of inconsistent types. If not {@code null}, all fixes stored in {@link #fixesByCompetitorIdsAsStrings} + * are guaranteed to have their detail value of this type. + */ + private final Map detailTypesRequestedByCompetitorIdsAsStrings; + + /** + * Requests returned from {@link #computeFromAndTo(Date, Iterable, long, long, boolean)} for execution as quick or slow + * requests that have not yet started {@link PositionRequest#processResponse(Map) processing their response}. + */ + private final Set inFlightRequests; + + /** + * The result of {@link FixesAndTails#computeFromAndTo(Date, Iterable, long, long, boolean)}; two such requests may result + * from the ask to obtain positions for a certain time range per competitor from the server: one for quick + * execution, and another one for a potentially long-running request. The two requests may be "entangled" regarding + * their need to clear the fixes cache of a competitor when the first of the two has received its result. As soon as + * the first of the two has cleared fixes from the cache, the second one to receive its response will refrain from + * doing so. + *

      + * + * A request may be marked as invalid regarding zero or more competitors. This happens when a request is + * made for a new disconnected track segment or a different detail type, where the response will clear the cache for + * one or more competitors. In such a case, all "in-flight" requests at the time will be informed to drop their + * position fixes for those competitors. + * + * @author Axel Uhl (d043530) + * + */ + class PositionRequest { + /** + * The same set for two entangled requests; when a request processes its response and + * finds here that a competitor's cache must first be cleared before entering new position fixes, + * that competitor is removed from this set, so an entangled request that receives its response + * later will not clear the cache again. + */ + private final Set mustClearCacheForTheseCompetitorIdsAsString; + + private final Set ignoreFixesForTheseCompetitorIdsAsString; + + private final Map timeRangesByCompetitorIdAsString; + + private final long transitionTimeInMillis; + + private final DetailType detailTypeForFixes; + + PositionRequest(Map timeRanges, Set mustClearCacheForTheseCompetitorIdsAsString, DetailType detailTypeForFixes, long transitionTimeInMillis) { + ignoreFixesForTheseCompetitorIdsAsString = new HashSet<>(); + this.mustClearCacheForTheseCompetitorIdsAsString = mustClearCacheForTheseCompetitorIdsAsString; + this.timeRangesByCompetitorIdAsString = Collections.unmodifiableMap(timeRanges); + this.detailTypeForFixes = detailTypeForFixes; + this.transitionTimeInMillis = transitionTimeInMillis; + } + + /** + * Creates a position request that is entangled with the {@code entangledWith} request. It uses the + * same {@link DetailType} and {@code transitionTimeInMillis} but has its own time ranges. The first of the + * two requests---this new one or the {@code entagledWith} request---that starts processing its response + * makes sure to clear the cache for the competitors for which this was requested; the other request + * is told to not clear those caches anymore. + */ + PositionRequest(Map timeRanges, PositionRequest entangleWith) { + mustClearCacheForTheseCompetitorIdsAsString = entangleWith.mustClearCacheForTheseCompetitorIdsAsString; + ignoreFixesForTheseCompetitorIdsAsString = new HashSet<>(); + this.timeRangesByCompetitorIdAsString = Collections.unmodifiableMap(timeRanges); + this.detailTypeForFixes = entangleWith.detailTypeForFixes; + this.transitionTimeInMillis = entangleWith.transitionTimeInMillis; + } + + boolean isMustClearCacheForCompetitor(CompetitorDTO competitor) { + return mustClearCacheForTheseCompetitorIdsAsString.contains(competitor.getIdAsString()); + } + + Map getFromByCompetitorIdAsString() { + return getFromOrToByCompetitorIdAsString(TimeRange::from); + } + + Map getToByCompetitorIdAsString() { + return getFromOrToByCompetitorIdAsString(TimeRange::to); + } + + private Map getFromOrToByCompetitorIdAsString(Function dateFetcher) { + final Map result = new HashMap<>(); + for (final Entry e : timeRangesByCompetitorIdAsString.entrySet()) { + if (!ignoreFixesForTheseCompetitorIdsAsString.contains(e.getKey())) { + result.put(e.getKey(), dateFetcher.apply(e.getValue()).asDate()); + } + } + return result; + } + + /** + * Informs this request that when it receives position fixes for {@code competitor} it shall not + * store them into the cache. The reason is probably that a newer request has been formed that will + * have to clear that competitor's cache when processing its response, e.g., because the time range + * requested is disconnected from the track segment cached for that competitor so far. + */ + void ignoreFixesFor(CompetitorDTO competitor) { + ignoreFixesForTheseCompetitorIdsAsString.add(competitor.getIdAsString()); + } + + void processResponse(Map boatPositions) { + if (!inFlightRequests.remove(this)) { + GWT.log("WARNING: processing response for a request that does not seem to have been sent or that has already been processed: "+this); + } + for (final Entry e : boatPositions.entrySet()) { + if (!ignoreFixesForTheseCompetitorIdsAsString.contains(e.getKey().getIdAsString())) { + final boolean mustClearCache = mustClearCacheForTheseCompetitorIdsAsString.remove(e.getKey().getIdAsString()); + updateFixes(e.getKey(), e.getValue(), mustClearCache, transitionTimeInMillis, detailTypeForFixes); + } + } + } + + @Override + public String toString() { + return "PositionRequest [mustClearCacheForTheseCompetitors=" + mustClearCacheForTheseCompetitorIdsAsString + + ", ignoreFixesForTheseCompetitors=" + ignoreFixesForTheseCompetitorIdsAsString + ", timeRanges=" + + timeRangesByCompetitorIdAsString + ", transitionTimeInMillis=" + transitionTimeInMillis + ", detailTypeForFixes=" + + detailTypeForFixes + "]"; + } + + public TimePoint getToTimepoint(CompetitorDTO competitor) { + final TimePoint result; + if (ignoreFixesForTheseCompetitorIdsAsString.contains(competitor.getIdAsString())) { + result = null; + } else { + final TimeRange competitorTimeRange = timeRangesByCompetitorIdAsString.get(competitor.getIdAsString()); + if (competitorTimeRange == null) { + result = null; + } else { + result = competitorTimeRange.to(); + } + } + return result; + } + } public FixesAndTails(CoordinateSystem coordinateSystem) { this.coordinateSystem = coordinateSystem; - fixes = new HashMap<>(); - tails = new HashMap<>(); - firstShownFix = new HashMap<>(); - lastShownFix = new HashMap<>(); - minDetailValueFix = new HashMap<>(); - maxDetailValueFix = new HashMap<>(); - lastSearchedFix = new HashMap<>(); + detailTypesRequestedByCompetitorIdsAsStrings = new HashMap<>(); + fixesByCompetitorIdsAsStrings = new HashMap<>(); + tailsByCompetitorIdsAsStrings = new HashMap<>(); + firstShownFixByCompetitorIdsAsStrings = new HashMap<>(); + lastShownFixByCompetitorIdsAsStrings = new HashMap<>(); + minDetailValueFixByCompetitorIdsAsStrings = new HashMap<>(); + maxDetailValueFixByCompetitorIdsAsStrings = new HashMap<>(); + inFlightRequests = new HashSet<>(); + timeRangesRequestedByCompetitorIdAsString = new HashMap<>(); } /** @@ -115,33 +353,27 @@ public class FixesAndTails { * competitor. The list returned is unmodifiable for the caller. */ public List getFixes(CompetitorDTO competitor) { - final List competitorFixes = fixes.get(competitor); + final List competitorFixes = fixesByCompetitorIdsAsStrings.get(competitor.getIdAsString()); return competitorFixes == null ? null : Collections.unmodifiableList(competitorFixes); } - /** - * triggers any {@link Triggerable} {@link Trigger#register(Triggerable) registered} with the {@code competitor}'s - * tail - */ public Colorline getTail(CompetitorDTO competitor) { - final Trigger trigger = tails.get(competitor); - return trigger == null ? null : trigger.get(); + return tailsByCompetitorIdsAsStrings.get(competitor.getIdAsString()); } public Integer getFirstShownFix(CompetitorDTO competitor) { - final Trigger firstShownFixForCompetitor = firstShownFix.get(competitor); - return firstShownFixForCompetitor==null?null:firstShownFixForCompetitor.get(); + return firstShownFixByCompetitorIdsAsStrings.get(competitor.getIdAsString()); } /** * The set of all competitors for which this object maintains tails. The collection is unmodifiable for the caller. */ - public Set getCompetitorsWithTails() { - return Collections.unmodifiableSet(tails.keySet()); + public Set getCompetitorIdsAsStringWithTails() { + return Collections.unmodifiableSet(tailsByCompetitorIdsAsStrings.keySet()); } public boolean hasFixesFor(CompetitorDTO competitor) { - return fixes.containsKey(competitor); + return fixesByCompetitorIdsAsStrings.containsKey(competitor.getIdAsString()); } /** @@ -166,71 +398,41 @@ public class FixesAndTails { /** * Creates a polyline for the competitor represented by competitorDTO, taking the fixes from - * {@link #fixes fixes.get(competitorDTO)} and using the fixes starting at time point from (inclusive) - * up to the last fix with time point before to. The polyline is returned. Updates are applied to - * {@link #lastShownFix}, {@link #firstShownFix} and {@link #tails}.

      + * {@link #fixesByCompetitorIdsAsStrings fixes.get(competitorDTO)} and using the fixes starting at time point from (inclusive) + * up to the last fix with time point before to. The polyline is added to the map and returned. Updates + * are applied to {@link #lastShownFixByCompetitorIdsAsStrings}, {@link #firstShownFixByCompetitorIdsAsStrings} and {@link #tailsByCompetitorIdsAsStrings}. + *

      + * + * The {@link #fixesByCompetitorIdsAsStrings} map must hold an entry for {@code competitorDTO}, but the fixes it holds for that competitor + * do not need to cover or even touch the time range described by {@code from} and {@code to}. As a result, the + * color-line returned may be empty or contain fewer fixes than desired. Later calls to + * {@link #updateFixes(Map, Map, long, boolean, DetailType)} may then extend the tail accordingly. * * Precondition: tails.containsKey(competitorDTO) == false + * + * @param detailTypeToShow + * the detail type the caller expects the fixes of {@code competitorDTO} to contain */ - protected Colorline createTailAndUpdateIndices(final CompetitorDTO competitorDTO, Date from, Date to, TailFactory tailFactory) { - List points = new ArrayList(); - List fixesForCompetitor = getFixes(competitorDTO); - int indexOfFirst = -1; - int indexOfLast = -1; - int i = 0; - // TODO consider binary search to find beginning of interesting segment faster - for (Iterator fixIter = fixesForCompetitor.iterator(); fixIter.hasNext() && indexOfLast == -1;) { - GPSFixDTOWithSpeedWindTackAndLegType fix = fixIter.next(); - if (!fix.timepoint.before(to)) { - indexOfLast = i-1; - } else { - final LatLng point; - if (indexOfFirst == -1) { - if (!fix.timepoint.before(from)) { - indexOfFirst = i; - point = coordinateSystem.toLatLng(fix.position); - } else { - point = null; - } - } else { - point = coordinateSystem.toLatLng(fix.position); - } - if (point != null) { - points.add(point); - } - } - i++; + protected Colorline createTailAndUpdateIndices(final CompetitorDTO competitorDTO, Date from, Date to, TailFactory tailFactory, DetailType detailTypeToShow) { + if (detailTypeToShow != null && detailTypesRequestedByCompetitorIdsAsStrings.get(competitorDTO.getIdAsString()) != detailTypeToShow) { + GWT.log("WARNING: Detail type mismatch in createTailAndUpdateIndices: have "+detailTypesRequestedByCompetitorIdsAsStrings.get(competitorDTO.getIdAsString())+" but caller expected "+detailTypeToShow); } - if (indexOfLast == -1) { - indexOfLast = i - 1; - } - if (indexOfFirst != -1 && indexOfLast != -1) { - firstShownFix.put(competitorDTO, new Trigger<>(indexOfFirst)); - lastShownFix.put(competitorDTO, new Trigger<>(indexOfLast)); - } - final Colorline result = tailFactory.createTail(competitorDTO, points); - tails.put(competitorDTO, new Trigger<>(result)); + final Colorline result = tailFactory.createTail(competitorDTO, Collections.emptyList()); + tailsByCompetitorIdsAsStrings.put(competitorDTO.getIdAsString(), result); + fillEmptyTail(competitorDTO, from, to, detailTypeToShow != null); return result; } /** - * Adds the fixes received in fixesForCompetitors to {@link #fixes} and ensures they are still + * Adds the fixes received in fixesToAddForCompetitor to {@link #fixesByCompetitorIdsAsStrings} and ensures they are still * contiguous for each competitor. If overlapsWithKnownFixes indicates that the fixes received in * result overlap with those already known, the fixes are merged into the list of already known fixes * for the competitor. Otherwise, the fixes received in result replace those known so far for the - * respective competitor. The {@link #tails} affected by these fixes are updated accordingly when modifications fall - * inside the interval shown by the tail, as defined by {@link #firstShownFix} and {@link #lastShownFix}. The tails + * respective competitor. The {@link #tailsByCompetitorIdsAsStrings} affected by these fixes are updated accordingly when modifications fall + * inside the interval shown by the tail, as defined by {@link #firstShownFixByCompetitorIdsAsStrings} and {@link #lastShownFixByCompetitorIdsAsStrings}. The tails * are, however, not trimmed according to the specification for the tail length. This has to happen elsewhere (see * also {@link #updateTail}). * - * A {@link Triggerable} is added to the {@link #tails}, {@link #firstShownFix} and {@link #lastShownFix} entries - * for each competitor for which the tail must be cleared because no overlap existed. This {@link Triggerable} will - * be executed as soon as the tail of the tail's first/last index in the {@link #fixes} collection is - * {@link Trigger#get() accessed}. Until these triggerables are run this data structure may be in an inconsistent - * state. In particular, the tails and the {@link #firstShownFix} and {@link #lastShownFix} maps could point to - * non-existing items in the {@link #fixes} collection. - * - * * @param fixesForCompetitors * For each list the invariant must hold that an {@link GPSFixDTOWithSpeedWindTackAndLegType#extrapolated * extrapolated} fix must be the last one in the list @@ -238,87 +440,56 @@ public class FixesAndTails { * if for a competitor whose fixes are provided in fixesForCompetitors this holds * false, any fixes previously stored for that competitor are removed, and the tail is * deleted from the map (see {@link #removeTail(CompetitorWithBoatDTO)}); the new fixes are then added to the - * {@link #fixes} map, and a new tail will have to be constructed as needed (does not happen here). If + * {@link #fixesByCompetitorIdsAsStrings} map, and a new tail will have to be constructed as needed (does not happen here). If * this map holds true, {@link #mergeFixes(CompetitorWithBoatDTO, List, long)} is used to merge the - * new fixes from fixesForCompetitors into the {@link #fixes} collection, and the tail is + * new fixes from fixesForCompetitors into the {@link #fixesByCompetitorIdsAsStrings} collection, and the tail is * left unchanged. NOTE: When a non-overlapping set of fixes is updated (false), this * map's record for the competitor is UPDATED to true after the tail deletion and - * {@link #fixes} replacement has taken place. This helps in cases where this update is only one of two + * {@link #fixesByCompetitorIdsAsStrings} replacement has taken place. This helps in cases where this update is only one of two * into which an original request was split (one quick update of the tail's head and another one for the * longer tail itself), such that the second request that uses the same map will be considered * having an overlap now, not leading to a replacement of the previous update originating from the same * request. + * @param detailTypeForFixes used to update {@link #detailTypesRequestedByCompetitorIdsAsStrings} * */ - protected void updateFixes(Map fixesForCompetitors, - Map overlapsWithKnownFixes, TailFactory tailFactory, //TODO Not being used - long timeForPositionTransitionMillis, boolean detailTypeChanged) { - if (detailTypeChanged) { - resetDetailValueSearch(); - } - for (final Map.Entry e : fixesForCompetitors.entrySet()) { - if (e.getValue() != null && !e.getValue().isEmpty()) { - final CompetitorDTO competitor = e.getKey(); - List fixesForCompetitor = fixes.get(competitor); - if (fixesForCompetitor == null) { - fixesForCompetitor = new ArrayList<>(); - fixes.put(competitor, fixesForCompetitor); - } - if (!overlapsWithKnownFixes.get(competitor)) { - // clearing and then re-populating establishes the invariant that an extrapolated fix must be the last - fixesForCompetitor.clear(); - // to re-establish the invariants for tails, firstShownFix and lastShownFix, we now need to remove - // all points from the competitor's polyline and clear the entries in firstShownFix and lastShownFix - final Triggerable triggerable = new Triggerable(()->clearTail(competitor)); - registerTriggerable(competitor, triggerable); - Util.addAll(e.getValue(), fixesForCompetitor); - overlapsWithKnownFixes.put(competitor, true); // In case this was only one part of a split request, the next request *does* have an overlap - minDetailValueFix.remove(competitor); - maxDetailValueFix.remove(competitor); - } else { - mergeFixes(competitor, e.getValue(), timeForPositionTransitionMillis); - } + private void updateFixes(final CompetitorDTO competitor, + final GPSFixDTOWithSpeedWindTackAndLegTypeIterable fixesToAddForCompetitor, final boolean mustClearCache, + long timeForPositionTransitionMillis, DetailType detailTypeForFixes) { + final List fixesForCompetitor = fixesByCompetitorIdsAsStrings.computeIfAbsent(competitor.getIdAsString(), + c->{ + final List f = new ArrayList<>(); + fixesByCompetitorIdsAsStrings.put(c, f); + return f; + }); + if (mustClearCache) { + // clearing and then re-populating establishes the invariant that an extrapolated fix must be the last + fixesForCompetitor.clear(); + detailTypesRequestedByCompetitorIdsAsStrings.put(competitor.getIdAsString(), detailTypeForFixes); + // to re-establish the invariants for tails, firstShownFix and lastShownFix, we now need to remove + // all points from the competitor's polyline and clear the entries in firstShownFix and lastShownFix + clearTail(competitor); + Util.addAll(fixesToAddForCompetitor, fixesForCompetitor); + } else { + if (detailTypeForFixes != null && detailTypesRequestedByCompetitorIdsAsStrings.get(competitor.getIdAsString()) != detailTypeForFixes) { + GWT.log("WARNING: Inconsistent detail types when merging fixed for competitor "+competitor+ + ". Got fixes with "+detailTypesRequestedByCompetitorIdsAsStrings.get(competitor.getIdAsString())+" so far but now received fixes with "+detailTypeForFixes); } + mergeFixes(competitor, fixesToAddForCompetitor, timeForPositionTransitionMillis); } } /** - * A {@link Triggerable} that is usually a {@link TriggerableTimer} and is or will be scheduled for delayed execution - * will be registered with the {@link #tails}, {@link #firstShownFix} and {@link #lastShownFix} structures such that - * when any of them is accessed for the {@code competitor} passed as parameter then the {@code triggerable} will be - * triggered to ensure consistency and establish all invariants. This way, invariants may temporarily be left unmet - * which gives the UI a better chance of delaying particularly the tail updates. However, in case of read access the - * invariants all must be established, hence the trigger pattern. - */ - private void registerTriggerable(final CompetitorDTO competitor, final Triggerable triggerable) { - final Trigger tailTrigger = tails.get(competitor); - if (tailTrigger != null) { - tailTrigger.register(triggerable); - } - registerIndexTrigger(triggerable, firstShownFix, competitor); - registerIndexTrigger(triggerable, lastShownFix, competitor); - } - - private void registerIndexTrigger(Triggerable triggerable, Map> indexMap, CompetitorDTO competitor) { - Trigger firstShownFixTrigger = indexMap.get(competitor); - if (firstShownFixTrigger == null) { - firstShownFixTrigger = new Trigger<>(-1); - indexMap.put(competitor, firstShownFixTrigger); - } - firstShownFixTrigger.register(triggerable); - } - - /** - * While updating the {@link #fixes} for competitorDTO, the invariants for {@link #tails} and - * {@link #firstShownFix} and {@link #lastShownFix} are maintained: each time a fix is inserted and we have a tail - * in {@link #tails} for competitorDTO, the {@link #firstShownFix} record for + * While updating the {@link #fixesByCompetitorIdsAsStrings} for competitorDTO, the invariants for {@link #tailsByCompetitorIdsAsStrings} and + * {@link #firstShownFixByCompetitorIdsAsStrings} and {@link #lastShownFixByCompetitorIdsAsStrings} are maintained: each time a fix is inserted and we have a tail + * in {@link #tailsByCompetitorIdsAsStrings} for competitorDTO, the {@link #firstShownFixByCompetitorIdsAsStrings} record for * competitorDTO is incremented if it is greater than the insertion index, and the - * {@link #lastShownFix} records for competitorDTO is incremented if is is greater than or equal to the + * {@link #lastShownFixByCompetitorIdsAsStrings} records for competitorDTO is incremented if is is greater than or equal to the * insertion index. This means, in particular, that when a fix is inserted exactly at the index that points to the * first fix shown so far, the fix inserted will become the new first fix shown. When inserting a fix exactly at - * index {@link #lastShownFix}, the fix that so far was the last one shown remains the last one shown because in - * this case, {@link #lastShownFix} will be incremented by one. If {@link #firstShownFix} <= - * insertindex <= {@link #lastShownFix}, meaning that the fix is in the range of fixes shown in the + * index {@link #lastShownFixByCompetitorIdsAsStrings}, the fix that so far was the last one shown remains the last one shown because in + * this case, {@link #lastShownFixByCompetitorIdsAsStrings} will be incremented by one. If {@link #firstShownFixByCompetitorIdsAsStrings} <= + * insertindex <= {@link #lastShownFixByCompetitorIdsAsStrings}, meaning that the fix is in the range of fixes shown in the * competitor's tail, the tail is adjusted by inserting the corresponding fix. *

      * @@ -333,15 +504,12 @@ public class FixesAndTails { * the list */ private void mergeFixes(CompetitorDTO competitorDTO, GPSFixDTOWithSpeedWindTackAndLegTypeIterable mergeThis, final long timeForPositionTransitionMillis) { - List intoThis = fixes.get(competitorDTO); - final Trigger firstShownFixForCompetitor = firstShownFix.get(competitorDTO); - int indexOfFirstShownFix = (firstShownFixForCompetitor == null || firstShownFixForCompetitor.get() == null) ? -1 - : firstShownFixForCompetitor.get(); - final Trigger lastShownFixForCompetitor = lastShownFix.get(competitorDTO); - int indexOfLastShownFix = (lastShownFixForCompetitor == null || lastShownFixForCompetitor.get() == null) ? -1 : lastShownFixForCompetitor.get(); + final List intoThis = fixesByCompetitorIdsAsStrings.get(competitorDTO.getIdAsString()); + final Integer firstShownFixForCompetitor = firstShownFixByCompetitorIdsAsStrings.get(competitorDTO.getIdAsString()); + int indexOfFirstShownFix = firstShownFixForCompetitor == null ? -1 : firstShownFixForCompetitor; + final Integer lastShownFixForCompetitor = lastShownFixByCompetitorIdsAsStrings.get(competitorDTO.getIdAsString()); + int indexOfLastShownFix = lastShownFixForCompetitor == null ? -2 : lastShownFixForCompetitor; final Colorline tail = getTail(competitorDTO); - int intoThisIndex = 0; - int earliestMergeIndex = -1; final Comparator fixByTimePointComparator = new Comparator() { @Override public int compare(GPSFixDTOWithSpeedWindTackAndLegType o1, GPSFixDTOWithSpeedWindTackAndLegType o2) { @@ -349,55 +517,61 @@ public class FixesAndTails { } }; for (GPSFixDTOWithSpeedWindTackAndLegType mergeThisFix : mergeThis) { - intoThisIndex = Collections.binarySearch(intoThis, mergeThisFix, fixByTimePointComparator); + int intoThisIndex = Collections.binarySearch(intoThis, mergeThisFix, fixByTimePointComparator); if (intoThisIndex < 0) { intoThisIndex = -intoThisIndex-1; } - if (earliestMergeIndex == -1 || intoThisIndex < earliestMergeIndex) { - earliestMergeIndex = intoThisIndex; - } if (intoThisIndex < intoThis.size() && intoThis.get(intoThisIndex).timepoint.equals(mergeThisFix.timepoint)) { // exactly same time point; replace with fix from mergeThis unless the new fix is extrapolated and there is a later fix in intoThis; // in the (unlikely) case the existing non-extrapolated fix is replaced by an extrapolated one, the indices of the shown fixes // need according adjustments if (!mergeThisFix.extrapolated || intoThis.size() == intoThisIndex+1) { - intoThis.set(intoThisIndex, mergeThisFix); - if (tail != null && intoThisIndex >= indexOfFirstShownFix && intoThisIndex <= indexOfLastShownFix) { + final Double oldDetailValue = intoThis.set(intoThisIndex, mergeThisFix).detailValue; + if (tail != null && intoThisIndex >= indexOfFirstShownFix && intoThisIndex <= indexOfLastShownFix) { // false if first/last shown index is -1 + adjustMinMaxForReplaced(competitorDTO, intoThisIndex, oldDetailValue, intoThis); tail.setAt(intoThisIndex - indexOfFirstShownFix, coordinateSystem.toLatLng(mergeThisFix.position)); + // if the fix removed had a min/max detailValue then min/maxDetailValueFixByCompetitorIdsAsString will be reset for competitor below } } else { // extrapolated fix would be added one or more positions before the last fix in intoThis; instead, // remove the fix at the respective index with the same time point and adjust indices: intoThis.remove(intoThisIndex); if (tail != null && intoThisIndex >= indexOfFirstShownFix && intoThisIndex <= indexOfLastShownFix) { - final int finalIntoThisIndex = intoThisIndex; - final int finalIndexOfFirstShownFix = indexOfFirstShownFix; - final Triggerable triggerable = new Triggerable(()->tail.removeAt(finalIntoThisIndex - finalIndexOfFirstShownFix)); - registerTriggerable(competitorDTO, triggerable); - Timer timer = new TriggerableTimer(triggerable); - runDelayedOrImmediately(timer, (int) (timeForPositionTransitionMillis==-1?-1:timeForPositionTransitionMillis/2)); + adjustMinMaxForRemoved(competitorDTO, intoThisIndex); + tail.removeAt(intoThisIndex - indexOfFirstShownFix); + // if the fix removed had a min/max detailValue then min/maxDetailValueFixByCompetitorIdsAsString will be reset for competitor below } - if (intoThisIndex < indexOfFirstShownFix) { - indexOfFirstShownFix--; - } - if (intoThisIndex <= indexOfLastShownFix) { - indexOfLastShownFix--; - } - // Make sure that minDetailValueFix and maxDetailValueFix still track the correct fixes - if (minDetailValueFix.containsKey(competitorDTO)) { - if (intoThisIndex < minDetailValueFix.get(competitorDTO)) { - minDetailValueFix.put(competitorDTO, minDetailValueFix.get(competitorDTO) - 1); - } else if (intoThisIndex == minDetailValueFix.get(competitorDTO)) { - // The fix with the highest value was removed so re-search - minDetailValueFix.remove(competitorDTO); + // Make sure that minDetailValueFix and maxDetailValueFix still track the correct fixes; do this AFTER calling adjustMinMaxForRemoved, see comment there + final Integer minIndex = minDetailValueFixByCompetitorIdsAsStrings.get(competitorDTO.getIdAsString()); + final Integer maxIndex = maxDetailValueFixByCompetitorIdsAsStrings.get(competitorDTO.getIdAsString()); + if (minIndex != null) { + if (intoThisIndex < minIndex) { + minDetailValueFixByCompetitorIdsAsStrings.put(competitorDTO.getIdAsString(), minIndex - 1); + } else if (intoThisIndex == minIndex) { + // The fix with the least value was removed so re-search + minDetailValueFixByCompetitorIdsAsStrings.remove(competitorDTO.getIdAsString()); } } - if (maxDetailValueFix.containsKey(competitorDTO)) { - if (intoThisIndex < maxDetailValueFix.get(competitorDTO)) { - maxDetailValueFix.put(competitorDTO, maxDetailValueFix.get(competitorDTO) - 1); - } else if (intoThisIndex == maxDetailValueFix.get(competitorDTO)) { - // The fix with the highest value was removed so re-search - maxDetailValueFix.remove(competitorDTO); + if (maxIndex != null) { + if (intoThisIndex < maxIndex) { + maxDetailValueFixByCompetitorIdsAsStrings.put(competitorDTO.getIdAsString(), maxIndex - 1); + } else if (intoThisIndex == maxIndex) { + // The fix with the greatest value was removed so re-search + maxDetailValueFixByCompetitorIdsAsStrings.remove(competitorDTO.getIdAsString()); + } + } + { + boolean indicesChanged = false; + if (intoThisIndex < indexOfFirstShownFix) { + indexOfFirstShownFix--; + indicesChanged = true; + } + if (intoThisIndex <= indexOfLastShownFix) { + indexOfLastShownFix--; + indicesChanged = true; + } + if (indicesChanged) { + updateTailBoundaries(competitorDTO, indexOfFirstShownFix, indexOfLastShownFix); } } intoThisIndex--; @@ -407,153 +581,394 @@ public class FixesAndTails { // being the only fix) if (!mergeThisFix.extrapolated || intoThisIndex == intoThis.size()) { intoThis.add(intoThisIndex, mergeThisFix); + // this has to happen *before* adjustMinMaxForInserted is called! Else, min/max point to the fix just inserted + { + final Integer minIndex = minDetailValueFixByCompetitorIdsAsStrings.get(competitorDTO.getIdAsString()); + final Integer maxIndex = maxDetailValueFixByCompetitorIdsAsStrings.get(competitorDTO.getIdAsString()); + if (minIndex != null && intoThisIndex <= minIndex) { + minDetailValueFixByCompetitorIdsAsStrings.put(competitorDTO.getIdAsString(), minIndex + 1); + } + if (maxIndex != null && intoThisIndex <= maxIndex) { + maxDetailValueFixByCompetitorIdsAsStrings.put(competitorDTO.getIdAsString(), maxIndex + 1); + } + } if (tail != null && intoThisIndex >= indexOfFirstShownFix && intoThisIndex <= indexOfLastShownFix) { // fix inserted at a position currently visualized by tail + adjustMinMaxForInserted(competitorDTO, intoThisIndex, intoThis); tail.insertAt(intoThisIndex - indexOfFirstShownFix, coordinateSystem.toLatLng(mergeThisFix.position)); } - if (intoThisIndex < indexOfFirstShownFix) { - indexOfFirstShownFix++; - } - if (intoThisIndex <= indexOfLastShownFix) { - indexOfLastShownFix++; - } - if (minDetailValueFix.containsKey(competitorDTO) && intoThisIndex <= minDetailValueFix.get(competitorDTO)) { - minDetailValueFix.put(competitorDTO, minDetailValueFix.get(competitorDTO) + 1); - } - if (maxDetailValueFix.containsKey(competitorDTO) && intoThisIndex <= maxDetailValueFix.get(competitorDTO)) { - maxDetailValueFix.put(competitorDTO, maxDetailValueFix.get(competitorDTO) + 1); + { + boolean indicesChanged = false; + if (intoThisIndex < indexOfFirstShownFix) { + indexOfFirstShownFix++; + indicesChanged = true; + } + if (intoThisIndex <= indexOfLastShownFix) { + indexOfLastShownFix++; + indicesChanged = true; + } + if (indicesChanged) { + updateTailBoundaries(competitorDTO, indexOfFirstShownFix, indexOfLastShownFix); + } } // If there is a fix prior to the one added and that prior fix was obtained by extrapolation, remove it now because // extrapolated fixes can only be the last in the list if (intoThisIndex > 0 && intoThis.get(intoThisIndex-1).extrapolated) { intoThis.remove(intoThisIndex-1); if (tail != null && intoThisIndex-1 >= indexOfFirstShownFix && intoThisIndex-1 <= indexOfLastShownFix) { - final int finalIntoThisIndex = intoThisIndex; - final int finalIndexOfFirstShownFix = indexOfFirstShownFix; - Triggerable triggerable = new Triggerable(new Runnable() { - @Override - public void run() { - tail.removeAt(finalIntoThisIndex-1 - finalIndexOfFirstShownFix); - } - }); - registerTriggerable(competitorDTO, triggerable); - Timer timer = new TriggerableTimer(triggerable); - runDelayedOrImmediately(timer, (int) (timeForPositionTransitionMillis==-1?-1:timeForPositionTransitionMillis/2)); + adjustMinMaxForRemoved(competitorDTO, intoThisIndex-1); + tail.removeAt(intoThisIndex-1 - indexOfFirstShownFix); } - if (intoThisIndex-1 < indexOfFirstShownFix) { - indexOfFirstShownFix--; + // min/max index adjustment needs to happen *after* calling adjustMinMaxForRemoved; see comment there + final Integer minIndex = minDetailValueFixByCompetitorIdsAsStrings.get(competitorDTO.getIdAsString()); + final Integer maxIndex = maxDetailValueFixByCompetitorIdsAsStrings.get(competitorDTO.getIdAsString()); + if (minIndex != null && intoThisIndex - 1 <= minIndex) { + minDetailValueFixByCompetitorIdsAsStrings.put(competitorDTO.getIdAsString(), minIndex - 1); } - if (intoThisIndex-1 <= indexOfLastShownFix) { - indexOfLastShownFix--; + if (maxIndex != null && intoThisIndex - 1 <= maxIndex) { + maxDetailValueFixByCompetitorIdsAsStrings.put(competitorDTO.getIdAsString(), maxIndex - 1); } - if (minDetailValueFix.containsKey(competitorDTO) && intoThisIndex - 1 <= minDetailValueFix.get(competitorDTO)) { - minDetailValueFix.put(competitorDTO, minDetailValueFix.get(competitorDTO) - 1); + { + boolean indicesChanged = false; + if (intoThisIndex-1 < indexOfFirstShownFix) { + indexOfFirstShownFix--; + indicesChanged = true; + } + if (intoThisIndex-1 <= indexOfLastShownFix) { + indexOfLastShownFix--; + indicesChanged = true; + } + if (indicesChanged) { + updateTailBoundaries(competitorDTO, indexOfFirstShownFix, indexOfLastShownFix); + } } - if (maxDetailValueFix.containsKey(competitorDTO) && intoThisIndex - 1 <= maxDetailValueFix.get(competitorDTO)) { - maxDetailValueFix.put(competitorDTO, maxDetailValueFix.get(competitorDTO) - 1); - } - intoThisIndex--; } - } else { - intoThisIndex--; // to compensate for the following ++ } } - intoThisIndex++; } - // invariant: for one CompetitorDTO, either both of firstShownFix and lastShownFix have an entry for that key, - // or both don't - if (indexOfFirstShownFix != -1) { - firstShownFix.put(competitorDTO, new Trigger<>(indexOfFirstShownFix)); + } + + /** + * The fix with index {@code insertedFixIndex} in {@link #fixesByCompetitorIdsAsStrings} is being inserted into the + * visible tail of competitor {@code competitorDTO}. If {@link #minDetailValueFixByCompetitorIdsAsStrings} / + * {@link #maxDetailValueFixByCompetitorIdsAsStrings} have a value for the {@code competitorDTO} key, the + * {@link GPSFixDTOWithSpeedWindTackAndLegType#detailValue detailValue} of that fix is compared to that of the new + * fix being added, and if the new fix has a new extreme value, the respective map is updated to hold + * {@code fixIndex} for the {@code competitorDTO} key. + *

      + * + * Call this after making the necessary adjustments to {@link #minDetailValueFixByCompetitorIdsAsStrings} / + * {@link #maxDetailValueFixByCompetitorIdsAsStrings} because this method assumes to find the fix with min/max detail + * value at the position specified in those maps. + */ + private void adjustMinMaxForInserted(final CompetitorDTO competitorDTO, final int insertedFixIndex, final List competitorFixes) { + final GPSFixDTOWithSpeedWindTackAndLegType insertedFix = competitorFixes.get(insertedFixIndex); + { + final Integer minIndex = minDetailValueFixByCompetitorIdsAsStrings.get(competitorDTO.getIdAsString()); + final GPSFixDTOWithSpeedWindTackAndLegType minFix; + if (minIndex != null + && (minFix = competitorFixes.get(minIndex)).detailValue != null + && insertedFix.detailValue != null + && insertedFix.detailValue < minFix.detailValue) { + minDetailValueFixByCompetitorIdsAsStrings.put(competitorDTO.getIdAsString(), insertedFixIndex); + } } - if (indexOfLastShownFix != -1) { - lastShownFix.put(competitorDTO, new Trigger<>(indexOfLastShownFix)); + { + final Integer maxIndex = maxDetailValueFixByCompetitorIdsAsStrings.get(competitorDTO.getIdAsString()); + final GPSFixDTOWithSpeedWindTackAndLegType maxFix; + if (maxIndex != null + && (maxFix = competitorFixes.get(maxIndex)).detailValue != null + && insertedFix.detailValue != null + && insertedFix.detailValue > maxFix.detailValue) { + maxDetailValueFixByCompetitorIdsAsStrings.put(competitorDTO.getIdAsString(), insertedFixIndex); + } } - if (earliestMergeIndex != -1) { - lastSearchedFix.merge(competitorDTO, earliestMergeIndex, Math::min); + } + + /** + * The fix with index {@code removedFixIndex} in {@link #fixesByCompetitorIdsAsStrings} is being removed from the + * visible tail of competitor {@code competitorDTO}. If {@link #minDetailValueFixByCompetitorIdsAsStrings} / + * {@link #maxDetailValueFixByCompetitorIdsAsStrings} have {@code removedFixIndex} as the value for the + * {@code competitorDTO} key, the {@link GPSFixDTOWithSpeedWindTackAndLegType#detailValue detailValue} of that fix + * was an extreme value. The mapping for {@code competitorDTO} is therefore then removed from the respective map so + * that later, in {@link #updateDetailValueBoundaries(Iterable)}, a new search for the extreme value on the visible + * tail needs to be carried out. + *

      + * + * Call before making the adjustments to + * {@link #minDetailValueFixByCompetitorIdsAsStrings}/{@link #maxDetailValueFixByCompetitorIdsAsStrings}. + */ + private void adjustMinMaxForRemoved(CompetitorDTO competitorDTO, int removedFixIndex) { + { + final Integer minIndex = minDetailValueFixByCompetitorIdsAsStrings.get(competitorDTO.getIdAsString()); + if (minIndex != null && minIndex.intValue() == removedFixIndex) { + minDetailValueFixByCompetitorIdsAsStrings.remove(competitorDTO.getIdAsString()); + } + } + { + final Integer maxIndex = maxDetailValueFixByCompetitorIdsAsStrings.get(competitorDTO.getIdAsString()); + if (maxIndex != null && maxIndex.intValue() == removedFixIndex) { + maxDetailValueFixByCompetitorIdsAsStrings.remove(competitorDTO.getIdAsString()); + } + } + } + + /** + * The fix with index {@code replacedFixIndex} in {@link #fixesByCompetitorIdsAsStrings} replaces a fix in the + * visible tail of competitor {@code competitorDTO}. If {@link #minDetailValueFixByCompetitorIdsAsStrings} / + * {@link #maxDetailValueFixByCompetitorIdsAsStrings} have {@link replacedFixIndex} as the value for the + * {@code competitorDTO} key, the {@link GPSFixDTOWithSpeedWindTackAndLegType#detailValue oldDetailValue} of that + * fix was an extreme value. Two cases then have to be distinguished: + *

        + *
      • The new detail value is at least as extreme as the {@code oldDetailValue}: in this case no change is + * required because the respective extreme (min/max) is still at the same index.
      • + *
      • The new detail value is not as extreme as the {@code oldDetailValue}: now we cannot know whether + * the new value still would be extreme compared to the detail values of all other fixes on the visible + * tail, so we need to clear the mapping for {@code competitorDTO} from the respective map, forcing a + * new search the next time {@link #updateDetailValueBoundaries(Iterable)} is invoked.
      • + *
      + * + * If the replacement is not at the index of a previous extreme value we can still compare the new detail + * value to what currently is considered the extreme value, and if the replacing fix has a new extreme value, + * the index in the respective map is updated to {@code replacedFixIndex}. + */ + private void adjustMinMaxForReplaced(CompetitorDTO competitorDTO, int replacedFixIndex, Double oldDetailValue, final List competitorFixes) { + final GPSFixDTOWithSpeedWindTackAndLegType newFix = competitorFixes.get(replacedFixIndex); + { + final Integer minIndex = minDetailValueFixByCompetitorIdsAsStrings.get(competitorDTO.getIdAsString()); + if (minIndex != null) { + if (minIndex.intValue() == replacedFixIndex) { + if (newFix.detailValue == null || newFix.detailValue > oldDetailValue) { + minDetailValueFixByCompetitorIdsAsStrings.remove(competitorDTO.getIdAsString()); + } + } else { + final GPSFixDTOWithSpeedWindTackAndLegType minFix = competitorFixes.get(minIndex); + // replacing a fix with a non-minimal detailValue + if (newFix.detailValue != null && minFix.detailValue != null && newFix.detailValue < minFix.detailValue) { + // the replacement fix is a new minimum + minDetailValueFixByCompetitorIdsAsStrings.put(competitorDTO.getIdAsString(), replacedFixIndex); + } + } + } + } + { + final Integer maxIndex = maxDetailValueFixByCompetitorIdsAsStrings.get(competitorDTO.getIdAsString()); + if (maxIndex != null) { + if (maxIndex.intValue() == replacedFixIndex) { + if (newFix.detailValue == null || newFix.detailValue < oldDetailValue) { + maxDetailValueFixByCompetitorIdsAsStrings.remove(competitorDTO.getIdAsString()); + } + } else { + final GPSFixDTOWithSpeedWindTackAndLegType maxFix = competitorFixes.get(maxIndex); + // replacing a fix with a non-maximal detailValue + if (newFix.detailValue != null && maxFix.detailValue != null && newFix.detailValue < maxFix.detailValue) { + // the replacement fix is a new maximum + maxDetailValueFixByCompetitorIdsAsStrings.put(competitorDTO.getIdAsString(), replacedFixIndex); + } + } + } + } + } + + private void updateTailBoundaries(CompetitorDTO competitorDTO, int indexOfFirstShownFix, int indexOfLastShownFix) { + if (indexOfFirstShownFix > indexOfLastShownFix || + (indexOfFirstShownFix < 0 && indexOfLastShownFix < 0)) { + firstShownFixByCompetitorIdsAsStrings.remove(competitorDTO.getIdAsString()); + lastShownFixByCompetitorIdsAsStrings.remove(competitorDTO.getIdAsString()); + } else { + firstShownFixByCompetitorIdsAsStrings.put(competitorDTO.getIdAsString(), indexOfFirstShownFix); + lastShownFixByCompetitorIdsAsStrings.put(competitorDTO.getIdAsString(), indexOfLastShownFix); } } /** * If the tail starts before from, removes leading vertices from tail that are before - * from. This is determined by using the {@link #firstShownFix} index which tells us where in - * {@link #fixes} we find the sequence of fixes currently represented in the tail. + * from. This is determined by using the {@link #firstShownFixByCompetitorIdsAsStrings} index which tells us where in + * {@link #fixesByCompetitorIdsAsStrings} we find the sequence of fixes currently represented in the tail. *

      * - * If the tail starts after from, vertices for those {@link #fixes} for competitorDTO at + * If the tail starts after from, vertices for those {@link #fixesByCompetitorIdsAsStrings} for competitorDTO at * or after time point from and before the time point of the first fix displayed so far in the tail and * before to are prepended to the tail. *

      * * Now to the end of the tail: if the existing tail's end exceeds to, the vertices in excess are - * removed (aided by {@link #lastShownFix}). Otherwise, for the competitor's fixes starting at the tail's end up to + * removed (aided by {@link #lastShownFixByCompetitorIdsAsStrings}). Otherwise, for the competitor's fixes starting at the tail's end up to * to are appended to the tail. *

      * - * When this method returns, {@link #firstShownFix} and {@link #lastShownFix} have been updated accordingly. + * When this method returns, {@link #firstShownFixByCompetitorIdsAsStrings} and {@link #lastShownFixByCompetitorIdsAsStrings} have been updated accordingly. + *

      + * + * Requirements: + *

        + *
      • handle a so far empty tail ({@code tail.getLength() == 0}, and {@link #firstShownFixByCompetitorIdsAsStrings}/{@link #lastShownFixByCompetitorIdsAsStrings} + * not containing key {@code competitorDTO})
      • + *
      • handle moving to a new {@code from/to} time range that does not overlap the current tail's time range
      • + *
      • all fixes from {@link #getFixes(CompetitorDTO) getFixes(competitorDTO)} that are between {@code from/to} + * (inclusive), and only those fixes, are visualized on the tail if tail exists
      • + *
      • {@link #firstShownFixByCompetitorIdsAsStrings}/{@link #lastShownFixByCompetitorIdsAsStrings} afterwards reflect the new tail; in particular, if the tail is empty, + * they both do not contain the key {@code competitorDTO}.
      • + *
      + *

      + * * @param delayForTailChangeInMillis * the time in milliseconds after which to actually draw the tail update, or -1 to perform * the update immediately + * @param selectedDetailType + * for verifying against {@link #detailTypesRequestedByCompetitorIdsAsStrings} */ - protected void updateTail(final CompetitorDTO competitorDTO, final Date from, final Date to, final int delayForTailChangeInMillis) { + protected void updateTail(final CompetitorDTO competitorDTO, final Date from, final Date to, final int delayForTailChangeInMillis, DetailType selectedDetailType) { Timer delayedOrImmediateExecutor = new Timer() { @Override public void run() { + if (selectedDetailType != null && detailTypesRequestedByCompetitorIdsAsStrings.get(competitorDTO.getIdAsString()) != selectedDetailType) { + GWT.log("WARNING: Detail type mismatch in updateTail: have "+detailTypesRequestedByCompetitorIdsAsStrings.get(competitorDTO.getIdAsString())+" but caller expected "+selectedDetailType); + } final Colorline tail = getTail(competitorDTO); if (tail != null) { int vertexCount = tail.getLength(); - final List fixesForCompetitor = getFixes(competitorDTO); - final Trigger firstShownFixForCompetitor = firstShownFix.get(competitorDTO); - int indexOfFirstShownFix = (firstShownFixForCompetitor == null || firstShownFixForCompetitor.get() == null) ? -1 : firstShownFixForCompetitor.get(); - // remove fixes before what is now to be the beginning of the polyline: - while (indexOfFirstShownFix != -1 && vertexCount > 0 - && fixesForCompetitor.get(indexOfFirstShownFix).timepoint.before(from)) { - tail.removeAt(0); - vertexCount--; - indexOfFirstShownFix++; - } - // now the polyline contains no more vertices representing fixes before "from"; - // go back in time starting at indexOfFirstShownFix while the fixes are still at or after "from" - // and insert corresponding vertices into the polyline - while (indexOfFirstShownFix > 0 - && !fixesForCompetitor.get(indexOfFirstShownFix - 1).timepoint.before(from)) { - indexOfFirstShownFix--; - GPSFixDTOWithSpeedWindTackAndLegType fix = fixesForCompetitor.get(indexOfFirstShownFix); - tail.insertAt(0, coordinateSystem.toLatLng(fix.position)); - vertexCount++; - } - // now adjust the polyline's tail: remove excess vertices that are after "to" - final Trigger lastShownFixForCompetitor = lastShownFix.get(competitorDTO); - int indexOfLastShownFix = (lastShownFixForCompetitor == null || lastShownFixForCompetitor.get() == null) ? -1 : lastShownFixForCompetitor.get(); - while (indexOfLastShownFix != -1 && vertexCount > 0 - && fixesForCompetitor.get(indexOfLastShownFix).timepoint.after(to)) { - if (vertexCount-1 == 0 || (indexOfLastShownFix-1 >= 0 && !fixesForCompetitor.get(indexOfLastShownFix-1).timepoint.after(to))) { - // the loop will abort after this iteration + final Integer firstShownFixForCompetitor = firstShownFixByCompetitorIdsAsStrings.get(competitorDTO.getIdAsString()); + final Integer lastShownFixForCompetitor = lastShownFixByCompetitorIdsAsStrings.get(competitorDTO.getIdAsString()); + if (firstShownFixForCompetitor == null) { + // empty tail; do a few consistency checks: + if (lastShownFixForCompetitor != null) { + GWT.log("Inconsistent lastShownFix for competitor "+competitorDTO+"; should have been null but was "+lastShownFixForCompetitor); } - if (tail.getLength() > --vertexCount) { - tail.removeAt(vertexCount); + if (vertexCount != 0) { + throw new IllegalStateException("Inconsistent fistShownFix/lastShownFix for competitor "+competitorDTO+ + "; tail is empty, both should have been null but were "+firstShownFixForCompetitor+ + " and "+lastShownFixForCompetitor); } - indexOfLastShownFix--; - } - // now the polyline contains no more vertices representing fixes after "to"; - // go forward in time starting at indexOfLastShownFix while the fixes are still at or before "to" - // and insert corresponding vertices into the polyline - while (indexOfLastShownFix < fixesForCompetitor.size() - 1 - && !fixesForCompetitor.get(indexOfLastShownFix + 1).timepoint.after(to)) { - indexOfLastShownFix++; - GPSFixDTOWithSpeedWindTackAndLegType fix = fixesForCompetitor.get(indexOfLastShownFix); - tail.insertAt(vertexCount++, coordinateSystem.toLatLng(fix.position)); - if (indexOfFirstShownFix < 0) { // empty tail before? - indexOfFirstShownFix = indexOfLastShownFix; // set to the first vertex inserted into tail + fillEmptyTail(competitorDTO, from, to, selectedDetailType != null); + } else { + if (lastShownFixForCompetitor == null) { + throw new IllegalStateException("Inconsistent lastShownFix for competitor "+competitorDTO+ + "; should have contained the competitor as key because firstShownFix did"); + } + final List fixesForCompetitor = getFixes(competitorDTO); + // we have a non-empty tail, but there may be a gap between old and new; if so, clear tail and start over + if (!TimeRange.create(TimePoint.of(fixesForCompetitor.get(firstShownFixForCompetitor).timepoint), + TimePoint.of(fixesForCompetitor.get(lastShownFixForCompetitor).timepoint)). + touches(TimeRange.create(TimePoint.of(from), TimePoint.of(to)))) { + clearTail(competitorDTO); + fillEmptyTail(competitorDTO, from, to, selectedDetailType != null); + } else { + // the time ranges of the non-empty tail and the desired time range from..to touch; adjust incrementally + int indexOfFirstShownFix = firstShownFixForCompetitor; + int indexOfLastShownFix = lastShownFixForCompetitor; + // remove fixes before what is now to be the beginning of the polyline: + while (vertexCount > 0 && fixesForCompetitor.get(indexOfFirstShownFix).timepoint.before(from)) { + adjustMinMaxForRemoved(competitorDTO, indexOfFirstShownFix); + tail.removeAt(0); + vertexCount--; + indexOfFirstShownFix++; + firstShownFixByCompetitorIdsAsStrings.put(competitorDTO.getIdAsString(), indexOfFirstShownFix); + } + // now the polyline contains no more vertices representing fixes before "from"; + // go back in time starting at indexOfFirstShownFix while the fixes are still at or after "from" + // and insert corresponding vertices into the polyline + while (indexOfFirstShownFix > 0 + && !fixesForCompetitor.get(indexOfFirstShownFix - 1).timepoint.before(from)) { + indexOfFirstShownFix--; + firstShownFixByCompetitorIdsAsStrings.put(competitorDTO.getIdAsString(), indexOfFirstShownFix); + final GPSFixDTOWithSpeedWindTackAndLegType fix = fixesForCompetitor.get(indexOfFirstShownFix); + adjustMinMaxForInserted(competitorDTO, indexOfFirstShownFix, fixesForCompetitor); + tail.insertAt(0, coordinateSystem.toLatLng(fix.position)); + vertexCount++; + } + // now adjust the tail's end: remove excess vertices that are after "to" + while (vertexCount > 0 && fixesForCompetitor.get(indexOfLastShownFix).timepoint.after(to)) { + adjustMinMaxForRemoved(competitorDTO, indexOfLastShownFix); + tail.removeAt(--vertexCount); + indexOfLastShownFix--; + lastShownFixByCompetitorIdsAsStrings.put(competitorDTO.getIdAsString(), indexOfLastShownFix); + } + // now the polyline contains no more vertices representing fixes after "to"; + // go forward in time starting at indexOfLastShownFix while the fixes are still at or before "to" + // and insert corresponding vertices into the polyline + while (indexOfLastShownFix < fixesForCompetitor.size() - 1 + && !fixesForCompetitor.get(indexOfLastShownFix + 1).timepoint.after(to)) { + indexOfLastShownFix++; + lastShownFixByCompetitorIdsAsStrings.put(competitorDTO.getIdAsString(), indexOfLastShownFix); + final GPSFixDTOWithSpeedWindTackAndLegType fix = fixesForCompetitor.get(indexOfLastShownFix); + adjustMinMaxForInserted(competitorDTO, indexOfLastShownFix, fixesForCompetitor); + tail.insertAt(vertexCount++, coordinateSystem.toLatLng(fix.position)); + } } } - firstShownFix.put(competitorDTO, new Trigger<>(indexOfFirstShownFix)); - lastShownFix.put(competitorDTO, new Trigger<>(indexOfLastShownFix)); } } }; runDelayedOrImmediately(delayedOrImmediateExecutor, delayForTailChangeInMillis); } + /** + * Assuming the {@link #getTail(CompetitorDTO) tail of competitorDTO} is empty, fills in all + * fixes between {@code from} and {@code to} (inclusive) and adjusts {@link #firstShownFixByCompetitorIdsAsStrings} + * and {@link #lastShownFixByCompetitorIdsAsStrings} accordingly. In particular, if no fix is inserted, the + * {@code competitorDTO} key is removed from those two maps. + */ + private void fillEmptyTail(CompetitorDTO competitorDTO, Date from, Date to, boolean findMinAndMaxDetailValue) { + int first = -1; + int last = -1; + int minIndex = -1; + int maxIndex = -1; + double max = Double.MIN_VALUE; + double min = Double.MAX_VALUE; + final Colorline tail = getTail(competitorDTO); + int vertexCount = tail.getLength(); + if (vertexCount != 0) { + throw new IllegalStateException("Can call fillEmptyTail only for empty tails; the tail of competitor "+ + competitorDTO+" contains "+vertexCount+" vertices"); + } + final List competitorFixes = getFixes(competitorDTO); + if (competitorFixes != null) { + GPSFixDTOWithSpeedWindTackAndLegType fix; + int i; + for (i=0; i max) { + maxIndex = i; + max = fix.detailValue; + } + } + tail.insertAt(vertexCount++, coordinateSystem.toLatLng(fix.position)); + last = i; + } + } + } + if (last < 0 && first >= 0) { + GWT.log("Inconsistency: last < 0 but first=="+first+" for competitor "+competitorDTO); + } + if (last != -1) { + lastShownFixByCompetitorIdsAsStrings.put(competitorDTO.getIdAsString(), last); + if (findMinAndMaxDetailValue) { + if (minIndex >= 0) { + minDetailValueFixByCompetitorIdsAsStrings.put(competitorDTO.getIdAsString(), minIndex); + } else { + minDetailValueFixByCompetitorIdsAsStrings.remove(competitorDTO.getIdAsString()); + } + if (maxIndex >= 0) { + maxDetailValueFixByCompetitorIdsAsStrings.put(competitorDTO.getIdAsString(), maxIndex); + } else { + maxDetailValueFixByCompetitorIdsAsStrings.remove(competitorDTO.getIdAsString()); + } + } + } else { + tailRemoved(competitorDTO.getIdAsString()); + } + } + private void runDelayedOrImmediately(Timer runThis, final int delayForTailChangeInMillis) { if (delayForTailChangeInMillis == -1) { runThis.run(); @@ -563,93 +978,193 @@ public class FixesAndTails { } /** - * Consistently removes the competitor's tail from {@link #tails} and from the map, and the corresponding position - * data from {@link #firstShownFix} and {@link #lastShownFix}. + * Consistently removes the competitor's tail from {@link #tailsByCompetitorIdsAsStrings} and from the map, and the corresponding position + * data from {@link #firstShownFixByCompetitorIdsAsStrings} and {@link #lastShownFixByCompetitorIdsAsStrings}. */ - protected void removeTail(CompetitorDTO competitor) { - final Trigger removedTail = tails.remove(competitor); + protected void removeTail(String competitorIdAsString) { + final Colorline removedTail = tailsByCompetitorIdsAsStrings.remove(competitorIdAsString); if (removedTail != null) { - removedTail.get().setMap(null); + removedTail.setMap(null); } - firstShownFix.remove(competitor); - lastShownFix.remove(competitor); + tailRemoved(competitorIdAsString); + } + + /** + * Removes the entry for {@code competitor} from {@link #firstShownFixByCompetitorIdsAsStrings}, {@link #lastShownFixByCompetitorIdsAsStrings}, + * {@link #minDetailValueFixByCompetitorIdsAsStrings}, and {@link #maxDetailValueFixByCompetitorIdsAsStrings}; to be called when the competitor's + * tail has been cleared or entirely removed. + */ + private void tailRemoved(String competitorIdAsString) { + firstShownFixByCompetitorIdsAsStrings.remove(competitorIdAsString); + lastShownFixByCompetitorIdsAsStrings.remove(competitorIdAsString); + minDetailValueFixByCompetitorIdsAsStrings.remove(competitorIdAsString); + maxDetailValueFixByCompetitorIdsAsStrings.remove(competitorIdAsString); } /** * Leaves the tail on the map and empties its path {@link Polyline#getPath()}. Correspondingly, the - * {@link #firstShownFix} and {@link #lastShownFix} entries for competitor are set to -1. + * {@link #firstShownFixByCompetitorIdsAsStrings} and {@link #lastShownFixByCompetitorIdsAsStrings} entries for competitor are set to -1. */ private void clearTail(CompetitorDTO competitor) { - final Trigger tail = tails.get(competitor); + final Colorline tail = tailsByCompetitorIdsAsStrings.get(competitor.getIdAsString()); if (tail != null) { - tail.get().clear(); - firstShownFix.put(competitor, new Trigger<>(-1)); - lastShownFix.put(competitor, new Trigger<>(-1)); + tail.clear(); + tailRemoved(competitor.getIdAsString()); } } /** - * From {@link #fixes} as well as the selection of {@link #getCompetitorsToShow competitors to show}, computes the - * from/to times for which to request GPS fixes from the server. No update is performed here to {@link #fixes}. The - * result guarantees that, when used in + * From {@link #earliestTimePointRequested} as well as the selection of {@code competitorsToShow}, computes the + * from/to times for which to request GPS fixes from the server, per competitor. No update is performed here to + * {@link #fixesByCompetitorIdsAsStrings}. The result guarantees that, when used in * {@link SailingServiceAsync#getBoatPositions(String, String, Map, Map, boolean, AsyncCallback)}, for each - * competitor from {@link #competitorsToShow} there are all fixes known by the server for that competitor starting - * at upTo-{@link #tailLengthInMilliSeconds} and ending at upTo (exclusive). + * competitor from {@code competitorsToShow} all fixes known by the server for that competitor starting at + * upTo-{@link #tailLengthInMilliSeconds} and ending at upTo (exclusive) will be loaded + * into the {@link #fixesByCompetitorIdsAsStrings} cache. + *

      * - * @return a triple whose {@link Triple#getA() first} component contains the "from", and whose {@link Triple#getB() - * second} component contains the "to" times for the competitors whose trails / positions to show; the - * {@link Triple#getC() third} component tells whether the existing fixes can remain and be augmented by - * those requested (true) or need to be replaced (false) + * The {@link #earliestTimePointRequested} map is updated, assuming that the requests returned will be sent before + * invoking this method again. The two {@link PositionRequest}s returned are added to the {@link #inFlightRequests} + * set. + * + * @return a pair of {@link PositionRequest} objects; the {@link Pair#getA() first} provides the parameters for a + * "quick" request, assuming that only a few positions (up to {@link #MAX_DURATION_FOR_QUICK_REQUESTS}) need + * to be fetched and where the call can be performed in a compound {@link GetRaceMapDataAction}; the + * {@link Pair#getB() second} is for a "slow" request that needs to fetch longer segments of tracks and that + * can be sent using {@link GetBoatPositionsAction}. None of the pair's components is {@code null}), but the + * {@link PositionRequest#getFromByCompetitorIdAsString()} and {@link PositionRequest#getToByCompetitorIdAsString()} results may be empty. The two + * requests are "entangled", in the sense that if the combined effect of the requests has to be that the + * cache for one or more competitors is to be cleared before updating the new positions, exactly the first + * of the two requests to process its response will carry out this clearing. It is important to reliably + * obtain the positions for both requests, so if the "quick" request should be dropped together with a + * compound {@link GetRaceMapDataAction}, at least the position-related part needs to be run, e.g., using + * a more specific {@link GetBoatPositionsAction} with the {@link TimeRangeActionsExecutor}. */ - protected Util.Triple, Map, Map> computeFromAndTo( - Date upTo, Iterable competitorsToShow, long effectiveTailLengthInMilliseconds, boolean detailTypeChanged) { - Date tailstart = new Date(upTo.getTime() - effectiveTailLengthInMilliseconds); - Map from = new HashMap<>(); - Map to = new HashMap<>(); - Map overlapWithKnownFixes = new HashMap<>(); - for (CompetitorDTO competitor : competitorsToShow) { - final List fixesForCompetitor = getFixes(competitor); - final Date fromDate; - final Date toDate; - final Date timepointOfLastKnownFix = fixesForCompetitor == null ? null : getTimepointOfLastNonExtrapolated(fixesForCompetitor); - final Date timepointOfFirstKnownFix = fixesForCompetitor == null ? null : getTimepointOfFirstNonExtrapolated(fixesForCompetitor); - final boolean overlap = !detailTypeChanged && (timepointOfFirstKnownFix != null && timepointOfLastKnownFix != null && - new TimeRangeImpl(new MillisecondsTimePoint(tailstart), new MillisecondsTimePoint(upTo)).intersects( - new TimeRangeImpl(new MillisecondsTimePoint(timepointOfFirstKnownFix), new MillisecondsTimePoint(timepointOfLastKnownFix)))); - if (fixesForCompetitor != null && timepointOfFirstKnownFix != null - && !tailstart.before(timepointOfFirstKnownFix) && timepointOfLastKnownFix != null - && !tailstart.after(timepointOfLastKnownFix) && !detailTypeChanged) { - // the beginning of what we need is contained in the interval we already have; skip what we already have - fromDate = new Date(timepointOfLastKnownFix.getTime()+1l); // "from" is "inclusive", so add 1ms to also skip the last fix we have + protected Pair computeFromAndTo(Date upTo, + Iterable competitorsToShow, long effectiveTailLengthInMilliseconds, + long transitionTimeInMillis, DetailType detailType) { + final TimePoint upToTimePoint = TimePoint.of(upTo); + final TimePoint tailstart = upToTimePoint.minus(effectiveTailLengthInMilliseconds); + final TimeRange quickTipTimeRange = TimeRange.create(upToTimePoint, upToTimePoint); + final Set mustClearCacheForTheseCompetitorIdsAsString = new HashSet<>(); + final TimeRange timeRangeNeeded = TimeRange.create(tailstart, upToTimePoint); + final Map timeRangesForQuickRequest = new HashMap<>(); + final Map timeRangesForSlowRequest = new HashMap<>(); + for (final CompetitorDTO competitor : competitorsToShow) { + final TimeRange timeRangeNotToRequestAgain = getTimeRangeNotToRequestAgain(competitor); + // The cache must be cleared upon result processing if the detail type has changed to a different, non-null one, + // or the timeRangeNeeded does not touch/overlap the timeRangeAlreadyRequested + if (detailType != null && detailType != detailTypesRequestedByCompetitorIdsAsStrings.get(competitor.getIdAsString()) + || (timeRangeNotToRequestAgain != null && !timeRangeNeeded.touches(timeRangeNotToRequestAgain))) { + mustClearCacheForTheseCompetitorIdsAsString.add(competitor.getIdAsString()); + ignoreResultsForCompetitorInPendingRequests(competitor); + timeRangesForQuickRequest.put(competitor.getIdAsString(), quickTipTimeRange); + timeRangesForSlowRequest.put(competitor.getIdAsString(), timeRangeNeeded); + timeRangesRequestedByCompetitorIdAsString.put(competitor.getIdAsString(), timeRangeNeeded); } else { - fromDate = tailstart; - } - if (fixesForCompetitor != null && timepointOfFirstKnownFix != null - && !upTo.before(timepointOfFirstKnownFix) && timepointOfLastKnownFix != null - && !upTo.after(timepointOfLastKnownFix)) { - // the end of what we need is contained in the interval we already have; skip what we already have - toDate = timepointOfFirstKnownFix; - } else { - toDate = upTo; - } - // only request something for the competitor if we're missing information at all - if (!fromDate.after(toDate)) { - from.put(competitor, fromDate); - to.put(competitor, toDate); - overlapWithKnownFixes.put(competitor, overlap); + if (timeRangeNotToRequestAgain == null) { + // we need to ask for the full time range needed because there is no data in the cache yet; cache clearing not necessary + timeRangesForQuickRequest.put(competitor.getIdAsString(), quickTipTimeRange); + timeRangesForSlowRequest.put(competitor.getIdAsString(), timeRangeNeeded); + timeRangesRequestedByCompetitorIdAsString.put(competitor.getIdAsString(), timeRangeNeeded); + } else { + final MultiTimeRange timeRangesToRequest = timeRangeNeeded.subtract(timeRangeNotToRequestAgain); + if (Util.size(timeRangesToRequest) > 1) { + GWT.log("Request for "+competitor+" exceeds cached region at start and end; requesting the full tail"); + // We would need to ask for data on both ends of the cached contiguous track segment; for now we + // have no support for querying multiple segments for the same competitor in one round trip, and we + // don't want to make another round trip for this infrequent case (probably occurs only when tail length + // is extended in play/live mode beyond cached fixes). We will instead ask for the full segment and merge its + // fixes + timeRangesForQuickRequest.put(competitor.getIdAsString(), quickTipTimeRange); + timeRangesForSlowRequest.put(competitor.getIdAsString(), timeRangeNeeded); + timeRangesRequestedByCompetitorIdAsString.put(competitor.getIdAsString(), timeRangeNeeded); + } else { + if (!Util.isEmpty(timeRangesToRequest)) { + final TimeRange timeRangeToRequest = timeRangesToRequest.iterator().next(); + // The single time range that is missing could be before or after the cached fixes. + // Anything before the cached fixes shall be loaded with the slow request: + if (timeRangeToRequest.startsBefore(timeRangeNotToRequestAgain)) { + timeRangesForSlowRequest.put(competitor.getIdAsString(), timeRangeToRequest); + } else { + // A segment to load after the cached fixes may need to be split into two: + // a quick segment with maximum length MAX_DURATION_FOR_QUICK_REQUESTS, and + // a slow segment with the remaining part of the time range to request. + if (timeRangeToRequest.getDuration().compareTo(MAX_DURATION_FOR_QUICK_REQUESTS) <= 0) { + timeRangesForQuickRequest.put(competitor.getIdAsString(), timeRangeToRequest); + } else { + timeRangesForQuickRequest.put(competitor.getIdAsString(), TimeRange.create(timeRangeToRequest.to().minus(MAX_DURATION_FOR_QUICK_REQUESTS), timeRangeToRequest.to())); + timeRangesForSlowRequest.put(competitor.getIdAsString(), TimeRange.create(timeRangeToRequest.from(), timeRangeToRequest.to().minus(MAX_DURATION_FOR_QUICK_REQUESTS))); + } + } + timeRangesRequestedByCompetitorIdAsString.put(competitor.getIdAsString(), timeRangeToRequest.extend(timeRangesRequestedByCompetitorIdAsString.get(competitor.getIdAsString()))); + } + } + } } + detailTypesRequestedByCompetitorIdsAsStrings.put(competitor.getIdAsString(), detailType); } - return new Util.Triple, Map, Map>(from, to, - overlapWithKnownFixes); + final PositionRequest quick = new PositionRequest(timeRangesForQuickRequest, mustClearCacheForTheseCompetitorIdsAsString, detailType, transitionTimeInMillis); + final PositionRequest slow = new PositionRequest(timeRangesForSlowRequest, quick); // entangle with quick request + inFlightRequests.add(quick); + inFlightRequests.add(slow); + return new Pair<>(quick, slow); } - private Date getTimepointOfFirstNonExtrapolated(List fixesForCompetitor) { - for (GPSFixDTOWithSpeedWindTackAndLegType fix : fixesForCompetitor) { - if (!fix.extrapolated) { - return fix.timepoint; + /** + * Roughly speaking, this method finds the time range represented by the contiguous track segment cached for + * {@code competitor}. + *

      + * + * This first looks at what has been requested through + * {@link #computeFromAndTo(Date, Iterable, long, long, DetailType)} with the {@link PositionRequest} objects + * returned from it. These time ranges are stored in {@link #timeRangesRequestedByCompetitorIdAsString}, and this is what will be returned + * if any in-flight request will clear the {@code competitor}'s cache. + *

      + * + * If no in-flight request clears the {@code competitor}'s cache, and if no {@link #inFlightRequests in-flight + * request} ends at the end of the time range requested overall for {@code competitor} and doesn't ignore the fixes + * for that competitor then we take into account the possibility that new fixes may have been delivered late to the + * server after we last asked for them. In this case, this method will return a time range starting at the beginning + * of the time range requested so far for {@code competitor}, ending at the last fix received for that competitor + * (instead of the last time point requested). This way, fixes delivered late can be expected to still make + * it into the cache. + */ + private TimeRange getTimeRangeNotToRequestAgain(CompetitorDTO competitor) { + final TimeRange timeRangeRequestedForCompetitor = timeRangesRequestedByCompetitorIdAsString.get(competitor.getIdAsString()); + boolean inFlightRequestWillClearCacheForCompetitor = false; + for (final PositionRequest inFlightRequest : inFlightRequests) { + if (inFlightRequest.isMustClearCacheForCompetitor(competitor)) { + inFlightRequestWillClearCacheForCompetitor = true; + } + final TimePoint competitorTimeRangeEnd = inFlightRequest.getToTimepoint(competitor); + if (competitorTimeRangeEnd != null && competitorTimeRangeEnd.equals(timeRangeRequestedForCompetitor.to())) { + return timeRangeRequestedForCompetitor; // found an in-flight request ending at the last time point requested; we hope it delivers fixes up to there } } - return null; + final TimeRange result; + if (inFlightRequestWillClearCacheForCompetitor) { + result = timeRangeRequestedForCompetitor; + } else { + final List fixesForCompetitor = getFixes(competitor); + if (fixesForCompetitor == null) { + result = timeRangeRequestedForCompetitor; + } else { + final TimePoint timePointOfLastFix = TimePoint.of(getTimepointOfLastNonExtrapolated(fixesForCompetitor)); + if (timePointOfLastFix == null) { + result = timeRangeRequestedForCompetitor; + } else { + result = TimeRange.create(timeRangeRequestedForCompetitor.from(), timePointOfLastFix); + } + } + } + return result; + } + + private void ignoreResultsForCompetitorInPendingRequests(CompetitorDTO competitor) { + for (final PositionRequest inFlightRequest : inFlightRequests) { + inFlightRequest.ignoreFixesFor(competitor); + } } private Date getTimepointOfLastNonExtrapolated(List fixesForCompetitor) { @@ -665,164 +1180,121 @@ public class FixesAndTails { return null; } - /** - * Determines whether an index for a competitor lies in [firstShownFix, lastShownFix], i.e. the index is included in the competitors tail. - * @param competitor {@link CompetitorDTO} specifying the tail. - * @param index {@code int} the index in question. - * @return {@code boolean} {@code true} if {@code index} is shown. - */ - protected boolean isIndexShown(CompetitorDTO competitor, int index) { - if (getFirstShownFix(competitor) == null || lastShownFix.get(competitor) == null || lastShownFix.get(competitor).get() == null) { - return false; - } - final int first = firstShownFix.get(competitor).get(); - final int last = lastShownFix.get(competitor).get(); - if (index >= first && index <= last && first != -1 && last != -1) { - return true; - } - return false; - } - - /** - * Searches a competitor's shown fixes (firstShownFix to lastShownFix but usually a smaller range since many fixes - * have already been searched by a previous iteration) for the smallest and largest detailValue. - * @param competitor {@link CompetitorDTO} competitor whose tail to search in. + * Establishes that {@link #minDetailValueFixByCompetitorIdsAsStrings} and + * {@link #maxDetailValueFixByCompetitorIdsAsStrings} hold the correct values for {@code competitor}, unless the + * competitor has an empty tail or only {@code null} values for the + * {@link GPSFixDTOWithSpeedWindTackAndLegType#detailValue detailValue} fields of its fixes. If an index is already + * contained in one of the maps it is assumed to be correct, and no search for the corresponding extreme value is + * performed. Otherwise, all visible fixes of the competitor's {@link #tailsByCompetitorIdsAsStrings tail}, so + * between index {@link #firstShownFixByCompetitorIdsAsStrings} and index + * {@link #lastShownFixByCompetitorIdsAsStrings}, are searched for extreme values, and the index, relative to the + * fix list in {@link #fixesByCompetitorIdsAsStrings}, is stored in the respective map for the {@code competitor}'s + * ID. + *

      + * + * Note that in case of an empty tail or all empty detail values the + * {@link #minDetailValueFixByCompetitorIdsAsStrings} and/or {@link #maxDetailValueFixByCompetitorIdsAsStrings} map + * may still not contain the {@code competitor}'s ID as key when this method returns, so callers must still perform + * a {@code null} check. + * + * @param competitor + * {@link CompetitorDTO} competitor whose tail to search in */ protected void searchMinMaxDetailValue(CompetitorDTO competitor) { - Integer startIndex = null; - - double min = 0; - boolean minSet = false; - int minIndex = -1; - // Check if the previously found minimum is still shown and use its value - if (minDetailValueFix.containsKey(competitor)) { - int minFix = minDetailValueFix.get(competitor); - if (minFix < 0 || !isIndexShown(competitor, minFix)) { - minDetailValueFix.remove(competitor); - startIndex = getFirstShownFix(competitor); - } else if (minFix < fixes.get(competitor).size() && fixes.get(competitor).get(minFix).detailValue != null) { - min = fixes.get(competitor).get(minFix).detailValue; - minSet = true; - } - } - - double max = 0; - boolean maxSet = false; - int maxIndex = -1; - // Check if the previously found maximum is still shown and use its value - if (maxDetailValueFix.containsKey(competitor)) { - int maxFix = maxDetailValueFix.get(competitor); - if (maxFix < 0 || !isIndexShown(competitor, maxFix)) { - maxDetailValueFix.remove(competitor); - if (startIndex == null) { - startIndex = getFirstShownFix(competitor); - } else if (getFirstShownFix(competitor) != null) { - startIndex = Math.min(startIndex, getFirstShownFix(competitor)); + boolean minSet = minDetailValueFixByCompetitorIdsAsStrings.containsKey(competitor.getIdAsString()); + boolean maxSet = maxDetailValueFixByCompetitorIdsAsStrings.containsKey(competitor.getIdAsString()); + if (!minSet || !maxSet) { // only need to do something if min or max are not known + double min = Double.MAX_VALUE; + int minIndex = -1; + double max = Double.MIN_VALUE; + int maxIndex = -1; + // If the startIndex has not been reset to the beginning of the shown range because the min/max value has just + // left shown range it will now be set to the first not already searched index + final Integer startIndex = getFirstShownFix(competitor); + if (startIndex != null) { + final int endIndex = lastShownFixByCompetitorIdsAsStrings.get(competitor.getIdAsString()); + final List fixesForCompetitor = fixesByCompetitorIdsAsStrings.get(competitor.getIdAsString()); + int i = startIndex; + for (final GPSFixDTOWithSpeedWindTackAndLegType fix : fixesForCompetitor.subList(startIndex, endIndex+1)) { + final Double value = fix.detailValue; + if (value != null) { + final double doubleValue = value.doubleValue(); + if (!minSet && doubleValue <= min) { + min = doubleValue; + minIndex = i; + } + if (!maxSet && doubleValue >= max) { + max = doubleValue; + maxIndex = i; + } + } + i++; } - } else if (maxFix < fixes.get(competitor).size() && fixes.get(competitor).get(maxFix).detailValue != null) { - max = fixes.get(competitor).get(maxFix).detailValue; - maxSet = true; - } - } - - // If the startIndex has not been reset to the beginning of the shown range because the min/max value has just - // left shown range it will now be set to the first not already searched index - if (startIndex == null) { - if (lastSearchedFix.containsKey(competitor)) { - startIndex = lastSearchedFix.get(competitor) + 1; - } - if (startIndex == null || !isIndexShown(competitor, startIndex)) { - startIndex = getFirstShownFix(competitor) != null && getFirstShownFix(competitor) != -1 - ? getFirstShownFix(competitor) : 0; - } - } - if (startIndex < 0) { - // If a tail is present but has no path getFirstShownFix will return -1 - if (fixes.get(competitor).size() == 0) return; - startIndex = 0; - } - - int endIndex = lastShownFix.containsKey(competitor) && lastShownFix.get(competitor) != null - && lastShownFix.get(competitor).get() != null && lastShownFix.get(competitor).get() != -1 ? - lastShownFix.get(competitor).get() : fixes.get(competitor).size() - 1; - - List fixesForCompetitor = fixes.get(competitor); - for (int i = startIndex; i <= endIndex; i++) { - Double value = fixesForCompetitor.get(i).detailValue; - if (value != null) { - if (!minSet) { - min = value; - minIndex = i; - minSet = true; + if (!minSet && minIndex > -1) { + minDetailValueFixByCompetitorIdsAsStrings.put(competitor.getIdAsString(), minIndex); } - if (!maxSet) { - max = value; - maxIndex = i; - maxSet = true; - } - if (value <= min) { - min = value; - minIndex = i; - } - if (value >= max) { - max = value; - maxIndex = i; + if (!maxSet && maxIndex > -1) { + maxDetailValueFixByCompetitorIdsAsStrings.put(competitor.getIdAsString(), maxIndex); } } } - - if (minIndex > -1) minDetailValueFix.put(competitor, minIndex); - if (maxIndex > -1) maxDetailValueFix.put(competitor, maxIndex); - lastSearchedFix.put(competitor, endIndex); } /** * Resets the search so that the next iteration will start from the beginning. */ protected void resetDetailValueSearch() { - lastSearchedFix.clear(); - minDetailValueFix.clear(); - maxDetailValueFix.clear(); + minDetailValueFixByCompetitorIdsAsStrings.clear(); + maxDetailValueFixByCompetitorIdsAsStrings.clear(); } /** - * Updates the fleet wide {@link #detailValueBoundaries} with the current maximum and minimum detailValues. - * To do so each competitors (in parameter {@code competitors}) tail will be searched and then the maximum and minimum search - * results will be collected. - * Finally {@link #detailValueBoundaries} will be updated. - * @param competitors {@link Iterable}{@code <}{@link CompetitorDTO}{@code >} containing all competitors to include - * in the search. + * Updates the fleet wide {@link #detailValueBoundaries} with the current maximum and minimum detailValues. To do + * so, each competitor's (in parameter {@code competitors}) tail will be searched and then the maximum and minimum + * search results will be collected. The findings are recorded as a side effect into {@link #minDetailValueFixByCompetitorIdsAsStrings} and + * {@link #maxDetailValueFixByCompetitorIdsAsStrings}. Finally {@link #detailValueBoundaries} will be + * {@link ValueRangeFlexibleBoundaries#setMinMax(double, double) updated}. + * + * @param competitors + * {@link Iterable}{@code <}{@link CompetitorDTO}{@code >} containing all competitors to include in the + * search. */ protected void updateDetailValueBoundaries(Iterable competitors) { - double min = 0; + double min = Double.MAX_VALUE; boolean minSet = false; - double max = 0; + double max = Double.MIN_VALUE; boolean maxSet = false; for (CompetitorDTO competitor : competitors) { searchMinMaxDetailValue(competitor); // Find minimum value across all boats - if (minDetailValueFix.containsKey(competitor) && minDetailValueFix.get(competitor) != null && minDetailValueFix.get(competitor) != -1) { - int index = minDetailValueFix.get(competitor); - if (!fixes.containsKey(competitor) || fixes.get(competitor) == null || index >= fixes.get(competitor).size()) { - minDetailValueFix.put(competitor, -1); - } else if (!minSet || fixes.get(competitor).get(index).detailValue < min) { - min = fixes.get(competitor).get(index).detailValue; - minSet = true; + final Integer minIndex = minDetailValueFixByCompetitorIdsAsStrings.get(competitor.getIdAsString()); + final List competitorFixes = fixesByCompetitorIdsAsStrings.get(competitor.getIdAsString()); + if (minIndex != null) { + if (competitorFixes == null || minIndex >= competitorFixes.size()) { + minDetailValueFixByCompetitorIdsAsStrings.remove(competitor.getIdAsString()); + } else { + final GPSFixDTOWithSpeedWindTackAndLegType competitorFix = competitorFixes.get(minIndex); + if (!minSet || competitorFix.detailValue != null && competitorFix.detailValue < min) { + min = competitorFix.detailValue; + minSet = true; + } } } // Find maximum value across all boats - if (maxDetailValueFix.containsKey(competitor) && maxDetailValueFix.get(competitor) != null && maxDetailValueFix.get(competitor) != -1) { - int index = maxDetailValueFix.get(competitor); - if (!fixes.containsKey(competitor) || fixes.get(competitor) == null || index >= fixes.get(competitor).size()) { - maxDetailValueFix.put(competitor, -1); - } else if (!maxSet || fixes.get(competitor).get(index).detailValue > max) { - max = fixes.get(competitor).get(index).detailValue; - maxSet = true; + final Integer maxIndex = maxDetailValueFixByCompetitorIdsAsStrings.get(competitor.getIdAsString()); + if (maxIndex != null) { + if (competitorFixes == null || maxIndex >= competitorFixes.size()) { + maxDetailValueFixByCompetitorIdsAsStrings.remove(competitor.getIdAsString()); + } else { + final GPSFixDTOWithSpeedWindTackAndLegType competitorFix = competitorFixes.get(maxIndex); + if (!maxSet || competitorFix.detailValue != null && competitorFix.detailValue > max) { + max = competitorFix.detailValue; + maxSet = true; + } } } } - // If possible update detailValueBoundaries if (minSet && maxSet) { detailValueBoundaries.setMinMax(min, max); @@ -843,16 +1315,19 @@ public class FixesAndTails { /** * Gets the detail value at a specific index in a competitors tail. - * @param competitorDTO {@link CompetitorDTO} specifying the competitor. - * @param index {@code int} specifying the index. - * @return {@code null} if {@code index} is out of bounds or if a detail value cannot be found. Otherwise returns - * a {@link Double} of the respective value. + * + * @param competitorDTO + * {@link CompetitorDTO} specifying the competitor. + * @param fixIndexIntoTail + * {@code int} specifying the index, relative to the start of the visual tail + * @return {@code null} if {@code index} is out of bounds or if a detail value cannot be found. Otherwise returns a + * {@link Double} of the respective value. */ - protected Double getDetailValueAt(CompetitorDTO competitorDTO, int index) { - final Trigger firstShownFixForCompetitor = firstShownFix.get(competitorDTO); - int indexOfFirstShownFix = (firstShownFixForCompetitor == null || firstShownFixForCompetitor.get() == null) ? -1 : firstShownFixForCompetitor.get(); + protected Double getDetailValueAt(CompetitorDTO competitorDTO, int fixIndexIntoTail) { + final Integer firstShownFixForCompetitor = firstShownFixByCompetitorIdsAsStrings.get(competitorDTO.getIdAsString()); + int indexOfFirstShownFix = firstShownFixForCompetitor == null ? -1 : firstShownFixForCompetitor; try { - return getFixes(competitorDTO).get(indexOfFirstShownFix + index).detailValue; + return getFixes(competitorDTO).get(indexOfFirstShownFix + fixIndexIntoTail).detailValue; } catch (IndexOutOfBoundsException e) { return null; } @@ -862,23 +1337,22 @@ public class FixesAndTails { * Clears all tail data, removing them from the map and from this object's internal structures. GPS fixes remain * cached. Immediately after this call, {@link #getTail(CompetitorWithBoatDTO)} will return null for all * competitors. Tails will need to be created (again) using - * {@link #createTailAndUpdateIndices(CompetitorWithBoatDTO, Date, Date, TailFactory)}. + * {@link #createTailAndUpdateIndices(CompetitorWithBoatDTO, Date, Date, TailFactory, DetailType)}. */ protected void clearTails() { - for (final Trigger tail : tails.values()) { - tail.get().clear(); + for (final Colorline tail : tailsByCompetitorIdsAsStrings.values()) { + tail.clear(); } - tails.clear(); - firstShownFix.clear(); - lastShownFix.clear(); + tailsByCompetitorIdsAsStrings.clear(); + firstShownFixByCompetitorIdsAsStrings.clear(); + lastShownFixByCompetitorIdsAsStrings.clear(); resetDetailValueSearch(); } /** - * Tells whether a tail currently exists for the {@code competitor}. This does not trigger - * any {@link Triggerable}s that may be registered for an existing competitor's tail. + * Tells whether a tail currently exists for the {@code competitor}. */ public boolean hasTail(CompetitorDTO competitor) { - return tails.containsKey(competitor); + return tailsByCompetitorIdsAsStrings.containsKey(competitor.getIdAsString()); } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/Hoverline.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/Hoverline.java index b3389a79311..0a303a828fe 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/Hoverline.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/Hoverline.java @@ -36,7 +36,7 @@ public class Hoverline { } options.setVisible(false); options.setColorMode(ColorlineMode.MONOCHROMATIC); - options.setColorProvider((i) -> polylineOptions.getStrokeColor()); + options.setColorProvider(indexIntoTail -> polylineOptions.getStrokeColor()); hoverline = new Colorline(options); hoverline.setMap(polyline.getMap()); hoverline.setPath(polyline.getPath()); @@ -90,7 +90,6 @@ public class Hoverline { hoverline.setMap(colorline.getMap()); hoverline.setPath(MVCArray.newInstance(colorline.getPath().toArray(new LatLng[0]))); colorline.addPathChangeListener(hoverline); - options.setVisible(false); colorline.addMouseOverHandler(new MouseOverMapHandler() { @Override @@ -101,7 +100,7 @@ public class Hoverline { hoverline.setOptions(options); } }); - //Workaround for bug4480 (chrome does fire mouseOutMove on mouseclick) + // Workaround for bug4480 (chrome does fire mouseOutMove on mouseclick) hoverline.addMouseDownHandler(new MouseDownMapHandler() { @Override public void onEvent(MouseDownMapEvent event) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/QuickFlagDataProvider.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/QuickFlagDataProvider.java index 873a565a66a..2783c6f0660 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/QuickFlagDataProvider.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/QuickFlagDataProvider.java @@ -63,10 +63,11 @@ public interface QuickFlagDataProvider { * {@link LeaderboardDTO leaderboard} should ignore this call if it already has quick ranks information available * from a leaderboard. * - * @param quickSpeedsFromServerInKnots - * keys are the competitors, values are current speeds of competitors provided in knots (nautical miles per hour) + * @param quickSpeedsFromServerInKnotsByCompetitorIdAsString + * keys are the {@link CompetitorDTO#getIdAsString() competitor IDs as string}, values are current speeds + * of competitors provided in knots (nautical miles per hour) */ - void quickSpeedsInKnotsReceivedFromServer(Map quickSpeedsFromServerInKnots); + void quickSpeedsInKnotsReceivedFromServer(Map quickSpeedsFromServerInKnotsByCompetitorIdAsString, Map competitorsByIdAsString); /** * @return keys are the {@link CompetitorWithBoatDTO#getIdAsString() competitor IDs are string}, values are the diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java index 890cfa57945..4a3168af2a6 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java @@ -68,7 +68,6 @@ import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.HorizontalPanel; import com.google.gwt.user.client.ui.Image; import com.google.gwt.user.client.ui.Label; -import com.google.gwt.user.client.ui.ListBox; import com.google.gwt.user.client.ui.PopupPanel; import com.google.gwt.user.client.ui.PopupPanel.AnimationType; import com.google.gwt.user.client.ui.PopupPanel.PositionCallback; @@ -91,8 +90,10 @@ import com.sap.sailing.domain.common.impl.MeterDistance; import com.sap.sailing.domain.common.scalablevalue.impl.ScalableBearing; import com.sap.sailing.domain.common.scalablevalue.impl.ScalablePosition; import com.sap.sailing.domain.common.security.SecuredDomainType; +import com.sap.sailing.domain.common.security.SecuredDomainType.TrackedRaceActions; import com.sap.sailing.domain.common.windfinder.SpotDTO; import com.sap.sailing.gwt.common.client.FullscreenUtil; +import com.sap.sailing.gwt.common.client.premium.SailingPremiumListBox; import com.sap.sailing.gwt.common.client.sharing.FloatingSharingButtonsResources; import com.sap.sailing.gwt.ui.actions.GetBoatPositionsAction; import com.sap.sailing.gwt.ui.actions.GetBoatPositionsCallback; @@ -115,6 +116,7 @@ import com.sap.sailing.gwt.ui.client.WindSourceTypeFormatter; import com.sap.sailing.gwt.ui.client.media.MediaPlayerManagerComponent; import com.sap.sailing.gwt.ui.client.shared.filter.QuickFlagDataValuesProvider; import com.sap.sailing.gwt.ui.client.shared.racemap.BoatOverlay.DisplayMode; +import com.sap.sailing.gwt.ui.client.shared.racemap.FixesAndTails.PositionRequest; import com.sap.sailing.gwt.ui.client.shared.racemap.QuickFlagDataProvider.QuickFlagDataListener; import com.sap.sailing.gwt.ui.client.shared.racemap.RaceCompetitorSet.CompetitorsForRaceDefinedListener; import com.sap.sailing.gwt.ui.client.shared.racemap.RaceMapHelpLinesSettings.HelpLineTypes; @@ -155,7 +157,6 @@ import com.sap.sse.common.TimePoint; import com.sap.sse.common.TimeRange; import com.sap.sse.common.Util; import com.sap.sse.common.Util.Pair; -import com.sap.sse.common.Util.Triple; import com.sap.sse.common.ValueRangeFlexibleBoundaries; import com.sap.sse.common.filter.Filter; import com.sap.sse.common.filter.FilterSet; @@ -181,12 +182,14 @@ import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; import com.sap.sse.gwt.client.shared.settings.ComponentContext; import com.sap.sse.gwt.shared.ClientConfiguration; import com.sap.sse.gwt.shared.DebugConstants; +import com.sap.sse.security.shared.dto.UserDTO; +import com.sap.sse.security.ui.client.UserStatusEventHandler; import com.sap.sse.security.ui.client.premium.PaywallResolver; public class RaceMap extends AbstractCompositeComponent implements TimeListener, CompetitorSelectionChangeListener, RaceTimesInfoProviderListener, TailFactory, ColorMapperChangedListener, RequiresDataInitialization, RequiresResize, QuickFlagDataValuesProvider { /* Line colors */ - static private final RGBColor COURSE_MIDDLE_LINE_COLOR = new RGBColor("#0eed1d"); // selected by Larry Rosenfeld... + static private final RGBColor COURSE_MIDDLE_LINE_COLOR = new RGBColor("#0eed1d"); static final Color ADVANTAGE_LINE_COLOR = new RGBColor("#ff9900"); // orange static final Color START_LINE_COLOR = Color.WHITE; static final Color FINISH_LINE_COLOR = Color.BLACK; @@ -316,7 +319,7 @@ public class RaceMap extends AbstractCompositeComponent impleme /** * html5 canvases used as boat display on the map */ - private final Map boatOverlays; + private final Map boatOverlaysByCompetitorIdsAsStrings; /** * html5 canvases used for competitor info display on the map @@ -516,11 +519,11 @@ public class RaceMap extends AbstractCompositeComponent impleme private DetailType selectedDetailType; /** * Indicates if {@link #selectedDetailType} has changed. If that is the case {@link FixesAndTails} needs to - * overwrite its cache with new data and needs to reset its internal {@link ValueRangeFlexibleBoundaries} tracking - * the {@link DetailType} values. - * If set to {@code true} {@link #refreshMap(Date, long, boolean)} will pass the information along and set it back - * to {@code false} by {@link #updateBoatPositions(Date, long, Map, Iterable, Map, boolean, boolean)} once the - * first update with the new DetailType values arrives at the client. + * overwrite its cache with new data and needs to reset its internal {@link ValueRangeFlexibleBoundaries} which is + * tracking the {@link DetailType} values. If set to {@code true}, {@link #refreshMap(Date, long, boolean)} will pass + * the information along and set it back to {@code false} by + * {@link #updateBoatPositions(Date, long, Map, Iterable, Map, boolean, boolean, DetailType)} once the first update with the new + * {@link DetailType} values arrives at the client. */ private boolean selectedDetailTypeChanged; /** @@ -528,11 +531,6 @@ public class RaceMap extends AbstractCompositeComponent impleme */ private ColorMapper tailColorMapper; - private final MultiHashSet remoteCallsInExecution = new MultiHashSet<>(); - private final MultiHashSet remoteCallsToSkipInExecution = new MultiHashSet<>(); - private boolean currentlyDragging = false; - - private int zoomingAnimationsInProgress = 0; private final FloatingSharingButtonsResources floatingSharingButtonsResources; private final PaywallResolver paywallResolver; @@ -559,6 +557,10 @@ public class RaceMap extends AbstractCompositeComponent impleme } } + /** + * @return {@code true} if an object that equals {@code t} was found and removed in this multi-set, + * {@code false} otherwise + */ public boolean remove(T t) { List l = map.get(t); if (l != null) { @@ -664,7 +666,7 @@ public class RaceMap extends AbstractCompositeComponent impleme courseSidelines = new HashMap<>(); courseMiddleLines = new HashMap<>(); infoOverlaysForLinesForCourseGeometry = new HashMap<>(); - boatOverlays = new HashMap<>(); + boatOverlaysByCompetitorIdsAsStrings = new HashMap<>(); competitorInfoOverlays = new CompetitorInfoOverlays(this, stringMessages); quickFlagDataProvider.addQuickFlagDataListener(competitorInfoOverlays); quickFlagDataProvider.addQuickFlagDataListener(new AdvantageLineUpdater()); @@ -847,7 +849,6 @@ public class RaceMap extends AbstractCompositeComponent impleme map.addZoomChangeHandler(new ZoomChangeMapHandler() { @Override public void onEvent(ZoomChangeMapEvent event) { - remoteCallsToSkipInExecution.addAll(remoteCallsInExecution); if (!autoZoomIn && !autoZoomOut && !orientationChangeInProgress) { // stop automatic zoom after a manual zoom event; automatic zoom in zoomMapToNewBounds will // restore old settings @@ -856,11 +857,7 @@ public class RaceMap extends AbstractCompositeComponent impleme settings.getZoomSettings().isZoomToSelectedCompetitors()); settings = new RaceMapSettings(settings, clearedZoomSettings); simulationOverlay.setVisible(false); - if (zoomingAnimationsInProgress == 0) { - showLayoutsAfterAnimationFinishes(); - } else { - showLayoutsAfterAnimationFinishes(); - } + showLayoutsAfterAnimationFinishes(); } if ((streamletOverlay != null) && !map.getBounds().equals(currentMapBounds) && settings.isShowWindStreamletOverlay() @@ -870,15 +867,11 @@ public class RaceMap extends AbstractCompositeComponent impleme } private void showLayoutsAfterAnimationFinishes() { - zoomingAnimationsInProgress++; new com.google.gwt.user.client.Timer() { @Override public void run() { - if (zoomingAnimationsInProgress == 1) { - simulationOverlay.setVisible(settings.isShowSimulationOverlay() - && paywallResolver.hasPermission(SecuredDomainType.TrackedRaceActions.SIMULATOR, raceMapLifecycle.getRaceDTO())); - } - zoomingAnimationsInProgress--; + simulationOverlay.setVisible(settings.isShowSimulationOverlay() + && paywallResolver.hasPermission(SecuredDomainType.TrackedRaceActions.SIMULATOR, raceMapLifecycle.getRaceDTO())); } }.schedule(500); } @@ -893,7 +886,6 @@ public class RaceMap extends AbstractCompositeComponent impleme RaceMapZoomSettings clearedZoomSettings = new RaceMapZoomSettings(emptyList, settings.getZoomSettings().isZoomToSelectedCompetitors()); settings = new RaceMapSettings(settings, clearedZoomSettings); - currentlyDragging = false; refreshMapWithoutAnimation(); if (streamletOverlay != null && settings.isShowWindStreamletOverlay() @@ -903,7 +895,6 @@ public class RaceMap extends AbstractCompositeComponent impleme } }); map.addDragStartHandler(event -> { - currentlyDragging = true; if (streamletOverlay != null && settings.isShowWindStreamletOverlay() && paywallResolver.hasPermission(SecuredDomainType.TrackedRaceActions.VIEWSTREAMLETS, raceMapLifecycle.getRaceDTO())) { @@ -929,9 +920,7 @@ public class RaceMap extends AbstractCompositeComponent impleme && paywallResolver.hasPermission(SecuredDomainType.TrackedRaceActions.VIEWSTREAMLETS, raceMapLifecycle.getRaceDTO())) { streamletOverlay.setCanvasSettings(); } - if (!currentlyDragging) { - refreshMapWithoutAnimation(); - } + refreshMapWithoutAnimation(); if (!mapFirstZoomDone) { zoomMapToNewBounds(settings.getZoomSettings().getNewBounds(RaceMap.this)); redraw(); @@ -956,7 +945,6 @@ public class RaceMap extends AbstractCompositeComponent impleme currentMapBounds = map.getBounds(); currentZoomLevel = newZoomLevel; headerPanel.getElement().getStyle().setWidth(map.getOffsetWidth(), Unit.PX); - refreshMapWithoutAnimationButLeaveTransitionsAlive(); } }); // If there was a time change before the API was loaded, reset the time @@ -1005,7 +993,17 @@ public class RaceMap extends AbstractCompositeComponent impleme RaceMap.this.managedInfoWindow = new ManagedInfoWindow(map); } }; - GoogleMapsLoader.load(onLoad); + sailingService.getGoogleMapsLoaderAuthenticationParams(new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError(stringMessages.errorNoAuthenticationParamsForGoogleMapsFound(caught.getMessage())); + } + + @Override + public void onSuccess(String googleMapsLoaderAuthenticationParams) { + GoogleMapsLoader.load(onLoad, googleMapsLoaderAuthenticationParams); + } + }); } private void createAdvancedFunctionsButtonGroup(boolean showMapControls) { @@ -1269,7 +1267,7 @@ public class RaceMap extends AbstractCompositeComponent impleme private void removeTransitions() { // remove the canvas animations for boats - for (CanvasOverlayV3 boatOverlay : RaceMap.this.getBoatOverlays().values()) { + for (CanvasOverlayV3 boatOverlay : RaceMap.this.getBoatOverlaysByCompetitorIdAsString().values()) { boatOverlay.removeCanvasPositionAndRotationTransition(); } // remove the canvas animations for the info overlays of the selected boats @@ -1283,13 +1281,12 @@ public class RaceMap extends AbstractCompositeComponent impleme } } - public void redraw() { - remoteCallsToSkipInExecution.removeAll(timer.getTime()); + private void redraw() { timeChanged(timer.getTime(), null); } - Map getBoatOverlays() { - return Collections.unmodifiableMap(boatOverlays); + Map getBoatOverlaysByCompetitorIdAsString() { + return Collections.unmodifiableMap(boatOverlaysByCompetitorIdsAsStrings); } protected RaceCompetitorSelectionProvider getCompetitorSelection() { @@ -1339,13 +1336,8 @@ public class RaceMap extends AbstractCompositeComponent impleme return timer.getPlayMode() == PlayModes.Live; } - private void refreshMapWithoutAnimationButLeaveTransitionsAlive() { - remoteCallsToSkipInExecution.addAll(remoteCallsInExecution); - } - private void refreshMapWithoutAnimation() { removeTransitions(); - remoteCallsToSkipInExecution.addAll(remoteCallsInExecution); } private void updateMapWithWindInfo(final Date newTime, final long transitionTimeInMillis, @@ -1376,10 +1368,10 @@ public class RaceMap extends AbstractCompositeComponent impleme private void refreshMap(final Date newTime, final long transitionTimeInMillis, boolean isRedraw) { final Iterable competitorsToShow = getCompetitorsToShow(); - final com.sap.sse.common.Util.Triple, Map, Map> fromAndToAndOverlap = fixesAndTails - .computeFromAndTo(newTime, competitorsToShow, settings.getEffectiveTailLengthInMilliseconds(), selectedDetailTypeChanged); + final Pair quickAndSlowRequest = fixesAndTails + .computeFromAndTo(newTime, competitorsToShow, settings.getEffectiveTailLengthInMilliseconds(), transitionTimeInMillis, selectedDetailType); // Request map data update, possibly in two calls; see method details - callGetRaceMapDataForAllOverlappingAndTipsOfNonOverlappingAndGetBoatPositionsForAllOthers(fromAndToAndOverlap, + callGetRaceMapDataForAllOverlappingAndTipsOfNonOverlappingAndGetBoatPositionsForAllOthers(quickAndSlowRequest, raceIdentifier, newTime, transitionTimeInMillis, competitorsToShow, isRedraw, selectedDetailType, selectedDetailTypeChanged); // draw the wind into the map, get the combined wind @@ -1390,37 +1382,27 @@ public class RaceMap extends AbstractCompositeComponent impleme windSourceTypeNames.add(WindSourceType.EXPEDITION.name()); windSourceTypeNames.add(WindSourceType.WINDFINDER.name()); windSourceTypeNames.add(WindSourceType.COMBINED.name()); - if (remoteCallsInExecution.add(newTime)) { - if (currentlyDragging || zoomingAnimationsInProgress > 0) { - remoteCallsToSkipInExecution.add(newTime); - } - GetWindInfoAction getWindInfoAction = new GetWindInfoAction(sailingService, raceIdentifier, newTime, 1000L, - 1, windSourceTypeNames, - /* onlyUpToNewestEvent==false means get us any data we can get by a best effort */ false); - asyncActionsExecutor.execute(getWindInfoAction, GET_WIND_DATA_CATEGORY, - new AsyncCallback() { - @Override - public void onFailure(Throwable caught) { - remoteCallsInExecution.remove(newTime); - errorReporter.reportError("Error obtaining wind information: " + caught.getMessage(), - true /* silentMode */); - } + GetWindInfoAction getWindInfoAction = new GetWindInfoAction(sailingService, raceIdentifier, newTime, 1000L, + 1, windSourceTypeNames, + /* onlyUpToNewestEvent==false means get us any data we can get by a best effort */ false); + asyncActionsExecutor.execute(getWindInfoAction, GET_WIND_DATA_CATEGORY, + new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError("Error obtaining wind information: " + caught.getMessage(), + true /* silentMode */); + } - @Override - public void onSuccess(WindInfoForRaceDTO windInfo) { - remoteCallsInExecution.remove(newTime); - if (windInfo != null && !remoteCallsToSkipInExecution.remove(newTime)) { - List> windSourcesToShow = new ArrayList>(); - lastCombinedWindTrackInfoDTO = windInfo; - updateMapWithWindInfo(newTime, transitionTimeInMillis, competitorsToShow, windInfo, windSourcesToShow); - showWindSensorsOnMap(windSourcesToShow); - } + @Override + public void onSuccess(WindInfoForRaceDTO windInfo) { + if (windInfo != null) { + List> windSourcesToShow = new ArrayList>(); + lastCombinedWindTrackInfoDTO = windInfo; + updateMapWithWindInfo(newTime, transitionTimeInMillis, competitorsToShow, windInfo, windSourcesToShow); + showWindSensorsOnMap(windSourcesToShow); } - }); - } - else { - remoteCallsToSkipInExecution.add(newTime); - } + } + }); } @Override @@ -1464,75 +1446,71 @@ public class RaceMap extends AbstractCompositeComponent impleme *

      */ private void callGetRaceMapDataForAllOverlappingAndTipsOfNonOverlappingAndGetBoatPositionsForAllOthers( - final Triple, Map, Map> fromAndToAndOverlap, + final Pair quickAndSlowRequest, RegattaAndRaceIdentifier race, final Date newTime, final long transitionTimeInMillis, - final Iterable competitorsToShow, boolean isRedraw, DetailType detailType, boolean detailTypeChanged) { - final Map fromTimesForQuickCall = new HashMap<>(); - final Map toTimesForQuickCall = new HashMap<>(); - final Map fromTimesForNonOverlappingTailsCall = new HashMap<>(); - final Map toTimesForNonOverlappingTailsCall = new HashMap<>(); - for (Map.Entry e : fromAndToAndOverlap.getC().entrySet()) { - if (e.getValue()) { - // overlap: expect a quick response; add original request interval for the competitor - fromTimesForQuickCall.put(e.getKey(), fromAndToAndOverlap.getA().get(e.getKey())); - toTimesForQuickCall.put(e.getKey(), fromAndToAndOverlap.getB().get(e.getKey())); - } else { - // no overlap; add competitor to request with a zero-length interval asking only position at newTime, not the entire tail - fromTimesForQuickCall.put(e.getKey(), newTime); - toTimesForQuickCall.put(e.getKey(), newTime); - fromTimesForNonOverlappingTailsCall.put(e.getKey(), fromAndToAndOverlap.getA().get(e.getKey())); - toTimesForNonOverlappingTailsCall.put(e.getKey(), fromAndToAndOverlap.getB().get(e.getKey())); - } - } + final Iterable competitorsToShow, boolean isRedraw, final DetailType detailType, boolean detailTypeChanged) { + final Map fromTimesForQuickCall = quickAndSlowRequest.getA().getFromByCompetitorIdAsString(); + final Map toTimesForQuickCall = quickAndSlowRequest.getA().getToByCompetitorIdAsString(); + final Map fromTimesForNonOverlappingTailsCall = quickAndSlowRequest.getB().getFromByCompetitorIdAsString(); + final Map toTimesForNonOverlappingTailsCall = quickAndSlowRequest.getB().getToByCompetitorIdAsString(); final Map competitorsByIdAsString = new HashMap<>(); for (CompetitorDTO competitor : competitorSelection.getAllCompetitors()) { competitorsByIdAsString.put(competitor.getIdAsString(), competitor); } // only update the tails for these competitors - // Note: the fromAndToAndOverlap.getC() map will be UPDATED by the call to updateBoatPositions happening inside - // the callback provided by getRaceMapDataCallback(...) for those - // entries that are considered not overlapping; subsequently, fromAndToOverlap.getC() will contain true for - // all its entries so that the other response received for GetBoatPositionsAction will consider this an - // overlap if it happens after this update. - asyncActionsExecutor.execute(new GetRaceMapDataAction(sailingService, competitorsByIdAsString, - race, useNullAsTimePoint() ? null : newTime, fromTimesForQuickCall, toTimesForQuickCall, /* extrapolate */true, + asyncActionsExecutor.execute(new GetRaceMapDataAction(sailingService, timeRangeActionsExecutor, competitorsByIdAsString, + race, useNullAsTimePoint() ? null : newTime, fromTimesForQuickCall, toTimesForQuickCall, /* extrapolate */ true, (settings.isShowSimulationOverlay() ? simulationOverlay.getLegIdentifier() : null), raceCompetitorSet.getMd5OfIdsAsStringOfCompetitorParticipatingInRaceInAlphanumericOrderOfTheirID(), - newTime, settings.isShowEstimatedDuration(), detailType, leaderboardName, leaderboardGroupName, leaderboardGroupId), + newTime, settings.isShowEstimatedDuration(), detailType, leaderboardName, leaderboardGroupName, leaderboardGroupId, + // callback to use for alternative GetBoatPositionsAction fired when GetRaceMapDataAction gets dropped: + getBoatPositionsCallback(quickAndSlowRequest.getA(), newTime, transitionTimeInMillis, competitorsToShow, detailType, detailTypeChanged, competitorsByIdAsString)), GET_RACE_MAP_DATA_CATEGORY, - getRaceMapDataCallback(newTime, transitionTimeInMillis, fromAndToAndOverlap.getC(), competitorsToShow, - ++boatPositionRequestIDCounter, isRedraw, detailTypeChanged)); + getRaceMapDataCallback(newTime, transitionTimeInMillis, quickAndSlowRequest.getA(), competitorsToShow, + ++boatPositionRequestIDCounter, isRedraw, detailTypeChanged, detailType, fromTimesForQuickCall, toTimesForQuickCall, competitorsByIdAsString)); // next, if necessary, do the full thing; the two calls have different action classes, so throttling should not drop one for the other if (!fromTimesForNonOverlappingTailsCall.keySet().isEmpty()) { timeRangeActionsExecutor.execute(new GetBoatPositionsAction(sailingService, race, fromTimesForNonOverlappingTailsCall, toTimesForNonOverlappingTailsCall, /* extrapolate */ true, detailType, leaderboardName, leaderboardGroupName, leaderboardGroupId), - new GetBoatPositionsCallback(detailType, new AsyncCallback() { - @Override - public void onFailure(Throwable t) { - errorReporter.reportError("Error obtaining racemap data: " + t.getMessage(), true /*silentMode */); - } - - @Override - public void onSuccess(CompactBoatPositionsDTO result) { - // Note: the fromAndToAndOverlap.getC() map will be UPDATED by the call to updateBoatPositions for those - // entries that are considered not overlapping; subsequently, fromAndToOverlap.getC() will contain true for - // all its entries so that the other response received for GetRaceMapDataAction will consider this an - // overlap if it happens after this update. - updateBoatPositions(newTime, transitionTimeInMillis, fromAndToAndOverlap.getC(), - competitorsToShow, result.getBoatPositionsForCompetitors( - competitorsByIdAsString), /* updateTailsOnly */ true, detailTypeChanged); - } - })); + getBoatPositionsCallback(quickAndSlowRequest.getB(), newTime, transitionTimeInMillis, competitorsToShow, + detailType, detailTypeChanged, competitorsByIdAsString)); } } + private GetBoatPositionsCallback getBoatPositionsCallback( + final PositionRequest positionRequest, final Date newTime, + final long transitionTimeInMillis, final Iterable competitorsToShow, + final DetailType detailType, boolean detailTypeChanged, + final Map competitorsByIdAsString) { + return new GetBoatPositionsCallback(detailType, new AsyncCallback() { + @Override + public void onFailure(Throwable t) { + errorReporter.reportError("Error obtaining racemap data: " + t.getMessage(), true /*silentMode */); + } + + @Override + public void onSuccess(CompactBoatPositionsDTO result) { + // Note: the fromAndToAndOverlap.getC() map will be UPDATED by the call to updateBoatPositions for those + // entries that are considered not overlapping; subsequently, fromAndToOverlap.getC() will contain true for + // all its entries so that the other response received for GetRaceMapDataAction will consider this an + // overlap if it happens after this update. + final Map boatPositionsForCompetitors = result.getBoatPositionsForCompetitors( + competitorsByIdAsString); + positionRequest.processResponse(boatPositionsForCompetitors); + updateBoatPositions(newTime, transitionTimeInMillis, + competitorsToShow, boatPositionsForCompetitors, /* updateTailsOnly */ true, detailTypeChanged, detailType); + } + }); + } + private AsyncCallback getRaceMapDataCallback( final Date newTime, final long transitionTimeInMillis, - final Map hasTailOverlapForCompetitor, - final Iterable competitorsToShow, final int requestID, boolean isRedraw, boolean detailTypeChanged) { - remoteCallsInExecution.add(newTime); + final PositionRequest quickRequest, + final Iterable competitorsToShow, final int requestID, boolean isRedraw, boolean detailTypeChanged, + DetailType detailType, final Map fromTimesForQuickCall, final Map toTimesForQuickCall, + final Map competitorsByIdAsString) { return new MarkedAsyncCallback<>(new AsyncCallback() { @Override public void onFailure(Throwable caught) { @@ -1541,11 +1519,14 @@ public class RaceMap extends AbstractCompositeComponent impleme @Override public void onSuccess(RaceMapDataDTO raceMapDataDTO) { - remoteCallsInExecution.remove(newTime); - if (map != null && raceMapDataDTO != null && !remoteCallsToSkipInExecution.remove(newTime)) { - // process response only if not received out of order + if (map != null && raceMapDataDTO != null) { + final Map boatData = raceMapDataDTO.boatPositions; + quickRequest.processResponse(boatData); // stores the boat fixes received into the FixesAndTails cache + // process the rest of the response only if not received out of order if (startedProcessingRequestID < requestID) { startedProcessingRequestID = requestID; + // Uncomment the following for enhanced log output regarding getRaceMapData requests + // GWT.log("Processing race map data request "+requestID+" with detail type "+detailType+"\n"+getFromAndToTimesAsString()); if (raceMapDataDTO.raceCompetitorIdsAsStrings != null) { try { raceCompetitorSet.setIdsAsStringsOfCompetitorsInRace(raceMapDataDTO.raceCompetitorIdsAsStrings); @@ -1559,12 +1540,11 @@ public class RaceMap extends AbstractCompositeComponent impleme lastLegNumber = raceMapDataDTO.coursePositions.currentLegNumber; simulationOverlay.updateLeg(Math.max(lastLegNumber, 1), /* clearCanvas */ false, raceMapDataDTO.simulationResultVersion); } - Map boatData = raceMapDataDTO.boatPositions; - Map quickSpeedsFromServerInKnots = getCompetitorsSpeedInKnotsMap(boatData); - quickFlagDataProvider.quickSpeedsInKnotsReceivedFromServer(quickSpeedsFromServerInKnots); + final Map quickSpeedsFromServerInKnotsByCompetitorIdAsString = getCompetitorsSpeedInKnotsMap(boatData); // TODO: why do we need this from the *response*, and why couldn't this come straight from the FixesAndTails cache? + quickFlagDataProvider.quickSpeedsInKnotsReceivedFromServer(quickSpeedsFromServerInKnotsByCompetitorIdAsString, competitorsByIdAsString); // Do boat specific actions - updateBoatPositions(newTime, transitionTimeInMillis, hasTailOverlapForCompetitor, - competitorsToShow, boatData, /* updateTailsOnly */ false, detailTypeChanged); + updateBoatPositions(newTime, transitionTimeInMillis, + competitorsToShow, boatData, /* updateTailsOnly */ false, detailTypeChanged, detailType); if (!isRedraw) { // only remove markers if the time is actually changed if (douglasMarkers != null) { @@ -1606,22 +1586,40 @@ public class RaceMap extends AbstractCompositeComponent impleme } zoomMapToNewBounds(zoomToBounds); updateEstimatedDuration(raceMapDataDTO.estimatedDuration); + } else { + GWT.log("Dropped result from getRaceMapData(...) except for boat positions with detail type "+detailType+ + " because it was for request ID "+requestID+ + " while we already started processing request "+startedProcessingRequestID+"\n"+ + getFromAndToTimesAsString()); } - } else { + } else { // map was null or we didn't get a valid response; record time in case this was because map API hasn't loaded yet lastTimeChangeBeforeInitialization = newTime; } } - private Map getCompetitorsSpeedInKnotsMap( + private String getFromAndToTimesAsString() { + final StringBuilder result = new StringBuilder(); + for (final Entry from : fromTimesForQuickCall.entrySet()) { + result.append(from.getKey()); + result.append(": "); + result.append(from.getValue()); + result.append(".."); + result.append(toTimesForQuickCall.get(from.getKey())); + result.append("; "); + } + return result.toString(); + } + + private Map getCompetitorsSpeedInKnotsMap( Map boatData) { - Map quickSpeedsFromServerInKnots = new HashMap<>(); - for (CompetitorDTO competitor : boatData.keySet()) { - GPSFixDTOWithSpeedWindTackAndLegTypeIterable fixesList = boatData.get(competitor); + final Map quickSpeedsFromServerInKnots = new HashMap<>(); + for (Entry boatDataEntry : boatData.entrySet()) { + GPSFixDTOWithSpeedWindTackAndLegTypeIterable fixesList = boatDataEntry.getValue(); if (!fixesList.isEmpty()) { SpeedWithBearingDTO speedWithBearing = fixesList.last().speedWithBearing; if (speedWithBearing != null) { Double speedInKnots = speedWithBearing.speedInKnots; - quickSpeedsFromServerInKnots.put(competitor, speedInKnots); + quickSpeedsFromServerInKnots.put(boatDataEntry.getKey().getIdAsString(), speedInKnots); } } } @@ -1648,20 +1646,23 @@ public class RaceMap extends AbstractCompositeComponent impleme } /** + * Assumes that the fixes required for displaying the boat position have been received and updated to + * the {@link FixesAndTails} cache already. + * * @param hasTailOverlapForCompetitor - * if for a competitor whose fixes are provided in fixesForCompetitors this holds - * false, any fixes previously stored for that competitor are removed, and the tail is - * deleted from the map (see {@link #removeTail(CompetitorWithBoatDTO)}); the new fixes are then added to the - * {@link #fixes} map, and a new tail will have to be constructed as needed (does not happen here). If - * this map holds true, {@link #mergeFixes(CompetitorWithBoatDTO, List, long)} is used to merge the - * new fixes from fixesForCompetitors into the {@link #fixes} collection, and the tail is - * left unchanged. NOTE: When a non-overlapping set of fixes is updated (false), this + * if for a competitor whose fixes are provided in boatData this holds false, + * any fixes previously stored for that competitor are removed, and the tail is deleted from the map (see + * {@link #removeTail(CompetitorWithBoatDTO)}); the new fixes are then added to the {@link #fixes} map, + * and a new tail will have to be constructed as needed (does not happen here). If this map holds + * true, {@link #mergeFixes(CompetitorWithBoatDTO, List, long)} is used to merge the new + * fixes from fixesForCompetitors into the {@link #fixes} collection, and the tail is left + * unchanged. NOTE: When a non-overlapping set of fixes is updated (false), this * map's record for the competitor is UPDATED to true after the tail deletion and * {@link #fixes} replacement has taken place. This helps in cases where this update is only one of two * into which an original request was split (one quick update of the tail's head and another one for the * longer tail itself), such that the second request that uses the same map will be considered * having an overlap now, not leading to a replacement of the previous update originating from the same - * request. See also {@link FixesAndTails#updateFixes(Map, Map, TailFactory, long)}. + * request. See also {@link FixesAndTails#updateFixes(Map, Map, TailFactory, long, DetailType)}. * @param updateTailsOnly * if true, only the tails are updated according to boatData and * hasTailOverlapForCompetitor, but the advantage line is not updated, and neither are the @@ -1669,27 +1670,26 @@ public class RaceMap extends AbstractCompositeComponent impleme * which does update those structures. In particular, tails that do not appear in * boatData are not removed from the map in case updateTailsOnly is * true. + * @param detailType + * the detail type the {@code boatData} fixes contain detail values for; used for consistency check in + * {@link FixesAndTails} cache. */ private void updateBoatPositions(final Date newTime, final long transitionTimeInMillis, - final Map hasTailOverlapForCompetitor, final Iterable competitorsToShow, Map boatData, - boolean updateTailsOnly, boolean detailTypeChanged) { - if (zoomingAnimationsInProgress == 0) { - fixesAndTails.updateFixes(boatData, hasTailOverlapForCompetitor, RaceMap.this, transitionTimeInMillis, detailTypeChanged); - showBoatsOnMap(newTime, transitionTimeInMillis, - /* re-calculate; it could have changed since the asynchronous request was made: */ - getCompetitorsToShow(), updateTailsOnly); - if (detailTypeChanged) { - selectedDetailTypeChanged = false; - tailColorMapper.notifyListeners(); - } - if (!updateTailsOnly) { - showCompetitorInfoOnMap(newTime, transitionTimeInMillis, - competitorSelection.getSelectedFilteredCompetitors()); - // even though the wind data is retrieved by a separate call, re-draw the advantage line because it - // needs to adjust to new boat positions - showAdvantageLine(competitorsToShow, newTime, transitionTimeInMillis); - } + boolean updateTailsOnly, boolean detailTypeChanged, DetailType detailType) { + showBoatsOnMap(newTime, transitionTimeInMillis, + /* re-calculate; it could have changed since the asynchronous request was made: */ + getCompetitorsToShow(), updateTailsOnly, detailType); + if (detailTypeChanged) { + selectedDetailTypeChanged = false; + tailColorMapper.notifyListeners(); + } + if (!updateTailsOnly) { + showCompetitorInfoOnMap(newTime, transitionTimeInMillis, + competitorSelection.getSelectedFilteredCompetitors()); + // even though the wind data is retrieved by a separate call, re-draw the advantage line because it + // needs to adjust to new boat positions + showAdvantageLine(competitorsToShow, newTime, transitionTimeInMillis); } } @@ -1749,45 +1749,43 @@ public class RaceMap extends AbstractCompositeComponent impleme } private void showCourseMarksOnMap(CoursePositionsDTO courseDTO, long transitionTimeInMillis) { - if (zoomingAnimationsInProgress == 0 && !currentlyDragging) { - if (map != null && courseDTO != null) { - WaypointDTO endWaypointForCurrentLegNumber = null; - if (courseDTO.currentLegNumber > 0 && courseDTO.currentLegNumber <= courseDTO.totalLegsCount) { - endWaypointForCurrentLegNumber = courseDTO.getEndWaypointForLegNumber(courseDTO.currentLegNumber); - } - Map toRemoveCourseMarks = new HashMap(courseMarkOverlays); - if (courseDTO.marks != null) { - for (MarkDTO markDTO : courseDTO.marks) { - boolean isSelected = false; - if (endWaypointForCurrentLegNumber != null && Util.contains(endWaypointForCurrentLegNumber.controlPoint.getMarks(), markDTO)) { - isSelected = true; - } - CourseMarkOverlay courseMarkOverlay = courseMarkOverlays.get(markDTO.getIdAsString()); - if (courseMarkOverlay == null) { - courseMarkOverlay = new CourseMarkOverlay(map, RaceMapOverlaysZIndexes.COURSEMARK_ZINDEX, markDTO, coordinateSystem, courseDTO); - courseMarkOverlay.setShowBuoyZone(settings.getHelpLinesSettings().isVisible(HelpLineTypes.BUOYZONE)); - courseMarkOverlay.setBuoyZoneRadius(settings.getBuoyZoneRadius()); - courseMarkOverlay.setSelected(isSelected); - courseMarkOverlays.put(markDTO.getIdAsString(), courseMarkOverlay); - markDTOs.put(markDTO.getIdAsString(), markDTO); - registerCourseMarkInfoWindowClickHandler(markDTO.getIdAsString()); - courseMarkOverlay.addToMap(); - } else { - courseMarkOverlay.setMarkPosition(markDTO.position, transitionTimeInMillis); - courseMarkOverlay.setShowBuoyZone(settings.getHelpLinesSettings().isVisible(HelpLineTypes.BUOYZONE)); - courseMarkOverlay.setBuoyZoneRadius(settings.getBuoyZoneRadius()); - courseMarkOverlay.setSelected(isSelected); - courseMarkOverlay.setCourse(courseDTO); - courseMarkOverlay.draw(); - toRemoveCourseMarks.remove(markDTO.getIdAsString()); - } + if (map != null && courseDTO != null) { + WaypointDTO endWaypointForCurrentLegNumber = null; + if (courseDTO.currentLegNumber > 0 && courseDTO.currentLegNumber <= courseDTO.totalLegsCount) { + endWaypointForCurrentLegNumber = courseDTO.getEndWaypointForLegNumber(courseDTO.currentLegNumber); + } + Map toRemoveCourseMarks = new HashMap(courseMarkOverlays); + if (courseDTO.marks != null) { + for (MarkDTO markDTO : courseDTO.marks) { + boolean isSelected = false; + if (endWaypointForCurrentLegNumber != null && Util.contains(endWaypointForCurrentLegNumber.controlPoint.getMarks(), markDTO)) { + isSelected = true; + } + CourseMarkOverlay courseMarkOverlay = courseMarkOverlays.get(markDTO.getIdAsString()); + if (courseMarkOverlay == null) { + courseMarkOverlay = new CourseMarkOverlay(map, RaceMapOverlaysZIndexes.COURSEMARK_ZINDEX, markDTO, coordinateSystem, courseDTO); + courseMarkOverlay.setShowBuoyZone(settings.getHelpLinesSettings().isVisible(HelpLineTypes.BUOYZONE)); + courseMarkOverlay.setBuoyZoneRadius(settings.getBuoyZoneRadius()); + courseMarkOverlay.setSelected(isSelected); + courseMarkOverlays.put(markDTO.getIdAsString(), courseMarkOverlay); + markDTOs.put(markDTO.getIdAsString(), markDTO); + registerCourseMarkInfoWindowClickHandler(markDTO.getIdAsString()); + courseMarkOverlay.addToMap(); + } else { + courseMarkOverlay.setMarkPosition(markDTO.position, transitionTimeInMillis); + courseMarkOverlay.setShowBuoyZone(settings.getHelpLinesSettings().isVisible(HelpLineTypes.BUOYZONE)); + courseMarkOverlay.setBuoyZoneRadius(settings.getBuoyZoneRadius()); + courseMarkOverlay.setSelected(isSelected); + courseMarkOverlay.setCourse(courseDTO); + courseMarkOverlay.draw(); + toRemoveCourseMarks.remove(markDTO.getIdAsString()); } } - for (String toRemoveMarkIdAsString : toRemoveCourseMarks.keySet()) { - final CourseMarkOverlay removedOverlay = courseMarkOverlays.remove(toRemoveMarkIdAsString); - if (removedOverlay != null) { - removedOverlay.removeFromMap(); - } + } + for (String toRemoveMarkIdAsString : toRemoveCourseMarks.keySet()) { + final CourseMarkOverlay removedOverlay = courseMarkOverlays.remove(toRemoveMarkIdAsString); + if (removedOverlay != null) { + removedOverlay.removeFromMap(); } } } @@ -1889,11 +1887,11 @@ public class RaceMap extends AbstractCompositeComponent impleme if (timer.getPlayState() == PlayStates.Playing) { // choose 130% of the refresh interval as transition period to make it unlikely that the transition // stops before the next update has been received - long smoothIntervall = 1300 * timer.getRefreshInterval() / 1000; - if (timeForPositionTransitionMillis > 0 && timeForPositionTransitionMillis < smoothIntervall) { - timeForPositionTransitionMillisSmoothed = smoothIntervall; + long smoothInterval = 1300 * timer.getRefreshInterval() / 1000; + if (timeForPositionTransitionMillis > 0 && timeForPositionTransitionMillis < smoothInterval) { + timeForPositionTransitionMillisSmoothed = smoothInterval; } else { - // either a large transition positive transition happend or any negative one, do not use the smooth + // either a large positive transition happend or any negative one, do not use the smooth // value if (timeForPositionTransitionMillis > 0) { timeForPositionTransitionMillisSmoothed = timeForPositionTransitionMillis; @@ -1913,17 +1911,19 @@ public class RaceMap extends AbstractCompositeComponent impleme * @param updateTailsOnly * if false, tails of competitors not in competitorsToShow are removed from the * map + * @param detailTypeToShow + * the detail type expected to be shown on the fixes; used for consistency check for now */ private void showBoatsOnMap(final Date newTime, final long timeForPositionTransitionMillis, - final Iterable competitorsToShow, boolean updateTailsOnly) { + final Iterable competitorsToShow, boolean updateTailsOnly, DetailType detailTypeToShow) { if (map != null) { Date tailsFromTime = new Date(newTime.getTime() - settings.getEffectiveTailLengthInMilliseconds()); Date tailsToTime = newTime; - Set competitorDTOsOfUnusedTails = new HashSet<>(); - Set competitorDTOsOfUnusedBoatCanvases = new HashSet<>(); + Set competitorIdsAsStringOfUnusedTails = new HashSet<>(); + Set competitorIdsAsStringOfUnusedBoatCanvases = new HashSet<>(); if (!updateTailsOnly) { - competitorDTOsOfUnusedTails.addAll(fixesAndTails.getCompetitorsWithTails()); - competitorDTOsOfUnusedBoatCanvases.addAll(boatOverlays.keySet()); + competitorIdsAsStringOfUnusedTails.addAll(fixesAndTails.getCompetitorIdsAsStringWithTails()); + competitorIdsAsStringOfUnusedBoatCanvases.addAll(boatOverlaysByCompetitorIdsAsStrings.keySet()); } if (timeForPositionTransitionMillis > 3 * timer.getRefreshInterval()) { fixesAndTails.clearTails(); @@ -1931,19 +1931,19 @@ public class RaceMap extends AbstractCompositeComponent impleme for (CompetitorDTO competitorDTO : competitorsToShow) { if (fixesAndTails.hasFixesFor(competitorDTO)) { if (!fixesAndTails.hasTail(competitorDTO)) { - fixesAndTails.createTailAndUpdateIndices(competitorDTO, tailsFromTime, tailsToTime, this); + fixesAndTails.createTailAndUpdateIndices(competitorDTO, tailsFromTime, tailsToTime, this, detailTypeToShow); } else { fixesAndTails.updateTail(competitorDTO, tailsFromTime, tailsToTime, (int) (timeForPositionTransitionMillis == -1 ? -1 - : timeForPositionTransitionMillis / 2)); + : timeForPositionTransitionMillis / 2), detailTypeToShow); if (!updateTailsOnly) { - competitorDTOsOfUnusedTails.remove(competitorDTO); + competitorIdsAsStringOfUnusedTails.remove(competitorDTO.getIdAsString()); } } boolean usedExistingBoatCanvas = updateBoatCanvasForCompetitor(competitorDTO, newTime, timeForPositionTransitionMillis); if (usedExistingBoatCanvas && !updateTailsOnly) { - competitorDTOsOfUnusedBoatCanvases.remove(competitorDTO); + competitorIdsAsStringOfUnusedBoatCanvases.remove(competitorDTO.getIdAsString()); } } } @@ -1951,14 +1951,13 @@ public class RaceMap extends AbstractCompositeComponent impleme fixesAndTails.updateDetailValueBoundaries(competitorSelection.getSelectedCompetitors()); } if (!updateTailsOnly) { - for (CompetitorDTO unusedBoatCanvasCompetitorDTO : competitorDTOsOfUnusedBoatCanvases) { - CanvasOverlayV3 boatCanvas = boatOverlays.get(unusedBoatCanvasCompetitorDTO); + for (String unusedBoatCanvasCompetitorDTO : competitorIdsAsStringOfUnusedBoatCanvases) { + CanvasOverlayV3 boatCanvas = boatOverlaysByCompetitorIdsAsStrings.get(unusedBoatCanvasCompetitorDTO); boatCanvas.removeFromMap(); - boatOverlays.remove(unusedBoatCanvasCompetitorDTO); + boatOverlaysByCompetitorIdsAsStrings.remove(unusedBoatCanvasCompetitorDTO); } - for (CompetitorDTO unusedTailCompetitorDTO : competitorDTOsOfUnusedTails) { + for (String unusedTailCompetitorDTO : competitorIdsAsStringOfUnusedTails) { fixesAndTails.removeTail(unusedTailCompetitorDTO); - //competitorSelection.removeCompetitorSelectionChangeListener(); } } } @@ -2099,7 +2098,6 @@ public class RaceMap extends AbstractCompositeComponent impleme advantageLine.setPath(pointsAsArray); advantageLine.setMap(map); Hoverline advantageHoverline = new Hoverline(advantageLine, options, this); - advantageLineMouseOverHandler = new AdvantageLineMouseOverMapHandler( bearingOfCombinedWindInDeg, new Date(windFix.measureTimepoint)); advantageLine.addMouseOverHandler(advantageLineMouseOverHandler); @@ -2394,7 +2392,7 @@ public class RaceMap extends AbstractCompositeComponent impleme lineToShowOrRemoveOrUpdate = Polyline.newInstance(options); lineToShowOrRemoveOrUpdate.setPath(pointsAsArray); lineToShowOrRemoveOrUpdate.setMap(map); - Hoverline lineToShowOrRemoveOrUpdateHoverline = new Hoverline(lineToShowOrRemoveOrUpdate, options, this); + final Hoverline lineToShowOrRemoveOrUpdateHoverline = new Hoverline(lineToShowOrRemoveOrUpdate, options, this); lineToShowOrRemoveOrUpdate.addMouseOverHandler(new MouseOverMapHandler() { @Override public void onEvent(MouseOverMapEvent event) { @@ -2579,11 +2577,11 @@ public class RaceMap extends AbstractCompositeComponent impleme boolean usedExistingCanvas = false; GPSFixDTOWithSpeedWindTackAndLegType lastBoatFix = getBoatFix(competitorDTO, date); if (lastBoatFix != null) { - BoatOverlay boatOverlay = boatOverlays.get(competitorDTO); + BoatOverlay boatOverlay = boatOverlaysByCompetitorIdsAsStrings.get(competitorDTO.getIdAsString()); if (boatOverlay == null) { boatOverlay = createBoatOverlay(RaceMapOverlaysZIndexes.BOATS_ZINDEX, competitorDTO, displayHighlighted(competitorDTO)); if (boatOverlay != null) { - boatOverlays.put(competitorDTO, boatOverlay); + boatOverlaysByCompetitorIdsAsStrings.put(competitorDTO.getIdAsString(), boatOverlay); boatOverlay.setDisplayMode(displayHighlighted(competitorDTO)); boatOverlay.setBoatFix(lastBoatFix, timeForPositionTransitionMillis); boatOverlay.addToMap(); @@ -2829,33 +2827,35 @@ public class RaceMap extends AbstractCompositeComponent impleme final RegattaAndRaceIdentifier race = raceIdentifier; if (race != null) { final Map timeRange = new HashMap<>(); - final TimePoint from = new MillisecondsTimePoint(fixesAndTails.getFixes(competitorDTO) - .get(fixesAndTails.getFirstShownFix(competitorDTO)).timepoint); - final TimePoint to = new MillisecondsTimePoint(getBoatFix(competitorDTO, timer.getTime()).timepoint); - timeRange.put(competitorDTO, new TimeRangeImpl(from, to, true)); - if (settings.isShowDouglasPeuckerPoints()) { - sailingService.getDouglasPoints(race, timeRange, 3, - new AsyncCallback>>() { - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError("Error obtaining douglas positions: " + caught.getMessage(), true /*silentMode */); - } - - @Override - public void onSuccess(Map> result) { - lastDouglasPeuckerResult = result; - if (douglasMarkers != null) { - removeAllMarkDouglasPeuckerpoints(); + final Integer firstShownFix = fixesAndTails.getFirstShownFix(competitorDTO); + if (firstShownFix != null) { + final TimePoint from = new MillisecondsTimePoint(fixesAndTails.getFixes(competitorDTO).get(firstShownFix).timepoint); + final TimePoint to = new MillisecondsTimePoint(getBoatFix(competitorDTO, timer.getTime()).timepoint); + timeRange.put(competitorDTO, new TimeRangeImpl(from, to, true)); + if (settings.isShowDouglasPeuckerPoints()) { + sailingService.getDouglasPoints(race, timeRange, 3, + new AsyncCallback>>() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError("Error obtaining douglas positions: " + caught.getMessage(), true /*silentMode */); } - if (!(timer.getPlayState() == PlayStates.Playing)) { - if (settings.isShowDouglasPeuckerPoints()) { - showMarkDouglasPeuckerPoints(result); + + @Override + public void onSuccess(Map> result) { + lastDouglasPeuckerResult = result; + if (douglasMarkers != null) { + removeAllMarkDouglasPeuckerpoints(); + } + if (!(timer.getPlayState() == PlayStates.Playing)) { + if (settings.isShowDouglasPeuckerPoints()) { + showMarkDouglasPeuckerPoints(result); + } } } - } - }); + }); + } + maneuverMarkersAndLossIndicators.getAndShowManeuvers(race, timeRange); } - maneuverMarkersAndLossIndicators.getAndShowManeuvers(race, timeRange); } } // If a metric is shown a click on any competitor / competitors tail will @@ -2866,43 +2866,50 @@ public class RaceMap extends AbstractCompositeComponent impleme return vPanel; } - private ListBox createDetailTypeDropdown(CompetitorDTO competitor) { - final ListBox lb = new ListBox(); - final NodeList options = DOMUtils.getOptions(lb); - lb.addItem(stringMessages.none(), "none"); - if (sortedAvailableDetailTypes != null) { - for (int i = 0; i < sortedAvailableDetailTypes.size(); i++) { - final DetailType detail = sortedAvailableDetailTypes.get(i); - lb.addItem(DetailTypeFormatter.format(detail), detail.name()); - final String tooltip = DetailTypeFormatter.getTooltip(detail); - if (Util.hasLength(tooltip)) { - options.getItem(options.getLength()-1).setTitle(tooltip); - } - if (detail == selectedDetailType) { - lb.setSelectedIndex(i + 1); - } + private VerticalPanel createDetailTypeDropdown(CompetitorDTO competitor) { + final VerticalPanel vPanel = new VerticalPanel(); + vPanel.add(createDetailTypePremiumList(competitor)); + // reset component if user changes (login/out) + paywallResolver.registerUserStatusEventHandler(new UserStatusEventHandler() { + @Override + public void onUserStatusChange(UserDTO user, boolean preAuthenticated) { + vPanel.clear(); + vPanel.add(createDetailTypePremiumList(competitor)); } - } + }); + return vPanel; + } + + private SailingPremiumListBox createDetailTypePremiumList(CompetitorDTO competitor) { + // create new premium list box + final String EMPTY_VALUE = "none"; + SailingPremiumListBox lb = new SailingPremiumListBox(stringMessages.none(), EMPTY_VALUE, + TrackedRaceActions.COLORED_TAILS, paywallResolver, raceMapLifecycle.getRaceDTO()); + fillItemsFromAvailableDetailTypes(lb); lb.setVisibleItemCount(1); lb.addChangeHandler(new ChangeHandler() { @Override public void onChange(ChangeEvent event) { - String value = lb.getSelectedValue(); - DetailType previous = selectedDetailType; - if (value == null || value.equals("none")) { + final String value = lb.getSelectedValue(); + final DetailType previous = selectedDetailType; + if (value == null || value.equals(EMPTY_VALUE)) { selectedDetailType = null; metricOverlay.setVisible(false); + selectedDetailTypeChanged = previous != null; } else { selectedDetailType = DetailType.valueOfString(value); + selectedDetailTypeChanged = selectedDetailType != previous; metricOverlay.setVisible(true); if (!competitorSelection.isSelected(competitor)) { competitorSelection.setSelected(competitor, true); } } - if (selectedDetailType != previous) { + if (selectedDetailTypeChanged) { // Causes an overwrite of what are now wrong detailValues - selectedDetailTypeChanged = true; - setTailVisualizer(); + if (selectedDetailType != null) { + // start with fresh value boundaries + setTailVisualizer(); + } // In case the new values don't make it through this will make the tails visible tailColorMapper.notifyListeners(); // Forces update of tail values which subsequently results @@ -2914,6 +2921,25 @@ public class RaceMap extends AbstractCompositeComponent impleme return lb; } + private void fillItemsFromAvailableDetailTypes(SailingPremiumListBox lb) { + final NodeList options = DOMUtils.getOptions(lb.getListBox()); + if (lb.isEnabled() && sortedAvailableDetailTypes != null) { + for (int i = 0; i < sortedAvailableDetailTypes.size(); i++) { + final DetailType detail = sortedAvailableDetailTypes.get(i); + lb.addItem(DetailTypeFormatter.format(detail), detail.name()); + final String tooltip = DetailTypeFormatter.getTooltip(detail); + if (Util.hasLength(tooltip)) { + options.getItem(options.getLength()-1).setTitle(tooltip); + } + if (detail == selectedDetailType) { + lb.setSelectedIndex(i + 1); + } + } + } else { + lb.reset(); + } + } + /** * @return the {@link CompetitorSelectionProvider#getSelectedCompetitors()} if * {@link RaceMapSettings#isShowOnlySelectedCompetitors() only selected competitors are to be shown}, the @@ -2923,21 +2949,13 @@ public class RaceMap extends AbstractCompositeComponent impleme * the result set */ private Iterable getCompetitorsToShow() { - final Set result = new HashSet<>(); + final Iterable result; Iterable selection = competitorSelection.getSelectedCompetitors(); final Set raceCompetitorIdsAsString = raceCompetitorSet.getIdsOfCompetitorsParticipatingInRaceAsStrings(); if (!settings.isShowOnlySelectedCompetitors() || Util.isEmpty(selection)) { - for (final CompetitorDTO filteredCompetitor : competitorSelection.getFilteredCompetitors()) { - if (raceCompetitorIdsAsString == null || raceCompetitorIdsAsString.contains(filteredCompetitor.getIdAsString())) { - result.add(filteredCompetitor); - } - } + result = Util.filter(competitorSelection.getFilteredCompetitors(), filteredCompetitor -> raceCompetitorIdsAsString == null || raceCompetitorIdsAsString.contains(filteredCompetitor.getIdAsString())); } else { - for (final CompetitorDTO selectedCompetitor : selection) { - if (raceCompetitorIdsAsString == null || raceCompetitorIdsAsString.contains(selectedCompetitor.getIdAsString())) { - result.add(selectedCompetitor); - } - } + result = Util.filter(selection, selectedCompetitor -> raceCompetitorIdsAsString == null || raceCompetitorIdsAsString.contains(selectedCompetitor.getIdAsString())); } return result; } @@ -2981,8 +2999,8 @@ public class RaceMap extends AbstractCompositeComponent impleme * null if no fix is available */ private GPSFixDTOWithSpeedWindTackAndLegType getBoatFix(CompetitorDTO competitorDTO, Date date) { - GPSFixDTOWithSpeedWindTackAndLegType result = null; - List competitorFixes = fixesAndTails.getFixes(competitorDTO); + final GPSFixDTOWithSpeedWindTackAndLegType result; + final List competitorFixes = fixesAndTails.getFixes(competitorDTO); if (competitorFixes != null && !competitorFixes.isEmpty()) { int i = Collections.binarySearch(competitorFixes, new GPSFixDTOWithSpeedWindTackAndLegType(date, null, null, (WindDTO) null, null, null, false), new Comparator() { @@ -3013,7 +3031,7 @@ public class RaceMap extends AbstractCompositeComponent impleme // now compute a weighted average depending on the time difference to "date" (see also bug 1924) double factorForAfter = (double) (date.getTime()-fixBefore.timepoint.getTime()) / (double) (fixAfter.timepoint.getTime() - fixBefore.timepoint.getTime()); double factorForBefore = 1-factorForAfter; - DegreePosition betweenPosition = new DegreePosition(factorForBefore*fixBefore.position.getLatDeg() + factorForAfter*fixAfter.position.getLatDeg(), + final DegreePosition betweenPosition = new DegreePosition(factorForBefore*fixBefore.position.getLatDeg() + factorForAfter*fixAfter.position.getLatDeg(), factorForBefore*fixBefore.position.getLngDeg() + factorForAfter*fixAfter.position.getLngDeg()); final double betweenBearing; if (fixBefore.speedWithBearing == null) { @@ -3029,7 +3047,7 @@ public class RaceMap extends AbstractCompositeComponent impleme multiply(factorForBefore).add(new ScalableBearing(new DegreeBearingImpl(fixAfter.speedWithBearing.bearingInDegrees)). multiply(factorForAfter)).divide(1).getDegrees(); } - SpeedWithBearingDTO betweenSpeed = new SpeedWithBearingDTO( + final SpeedWithBearingDTO betweenSpeed = new SpeedWithBearingDTO( factorForBefore*(fixBefore.speedWithBearing==null?0:fixBefore.speedWithBearing.speedInKnots) + factorForAfter*(fixAfter.speedWithBearing==null?0:fixAfter.speedWithBearing.speedInKnots), betweenBearing); @@ -3041,6 +3059,8 @@ public class RaceMap extends AbstractCompositeComponent impleme final GPSFixDTOWithSpeedWindTackAndLegType fixAfter = competitorFixes.get(i); result = fixAfter; } + } else { + result = null; } return result; } @@ -3054,10 +3074,10 @@ public class RaceMap extends AbstractCompositeComponent impleme if (settings.isShowOnlySelectedCompetitors()) { if (Util.size(competitorSelection.getSelectedCompetitors()) == 1) { // first competitors selected; remove all others from map - Iterator> i = boatOverlays.entrySet().iterator(); + Iterator> i = boatOverlaysByCompetitorIdsAsStrings.entrySet().iterator(); while (i.hasNext()) { - Entry next = i.next(); - if (!next.getKey().equals(competitor)) { + Entry next = i.next(); + if (!next.getKey().equals(competitor.getIdAsString())) { CanvasOverlayV3 boatOverlay = next.getValue(); boatOverlay.removeFromMap(); fixesAndTails.removeTail(next.getKey()); @@ -3068,22 +3088,23 @@ public class RaceMap extends AbstractCompositeComponent impleme } } else { // only change highlighting - BoatOverlay boatCanvas = boatOverlays.get(competitor); + BoatOverlay boatCanvas = boatOverlaysByCompetitorIdsAsStrings.get(competitor.getIdAsString()); if (boatCanvas != null) { boatCanvas.setDisplayMode(displayHighlighted(competitor)); boatCanvas.draw(); showCompetitorInfoOnMap(timer.getTime(), -1, competitorSelection.getSelectedFilteredCompetitors()); } } - // Now update tails for all competitors because selection change may also affect all unselected competitors if (selectedDetailType != null && !selectedDetailTypeChanged) { + // assumes that the detail values have already been loaded, as the detail type hasn't changed fixesAndTails.updateDetailValueBoundaries(competitorSelection.getSelectedCompetitors()); } + // update tails for all competitors because selection change may also affect all unselected competitors for (CompetitorDTO oneOfAllCompetitors : competitorSelection.getAllCompetitors()) { Colorline tail = fixesAndTails.getTail(oneOfAllCompetitors); if (tail != null) { ColorlineOptions newOptions = createTailStyle(oneOfAllCompetitors, displayHighlighted(oneOfAllCompetitors)); - tail.setOptions(newOptions); + tail.setOptions(newOptions); // depends on the min/max boundaries computed above } } // Trigger auto-zoom if needed @@ -3091,7 +3112,7 @@ public class RaceMap extends AbstractCompositeComponent impleme if (!zoomSettings.containsZoomType(ZoomTypes.NONE) && zoomSettings.isZoomToSelectedCompetitors()) { zoomMapToNewBounds(zoomSettings.getNewBounds(this)); } - redraw(); + redraw(); } @Override @@ -3108,16 +3129,16 @@ public class RaceMap extends AbstractCompositeComponent impleme redraw(); } else { // otherwise remove only deselected competitor's boat images and tail - BoatOverlay removedBoatOverlay = boatOverlays.remove(competitor); + final BoatOverlay removedBoatOverlay = boatOverlaysByCompetitorIdsAsStrings.remove(competitor.getIdAsString()); if (removedBoatOverlay != null) { removedBoatOverlay.removeFromMap(); } - fixesAndTails.removeTail(competitor); + fixesAndTails.removeTail(competitor.getIdAsString()); showCompetitorInfoOnMap(timer.getTime(), -1, competitorSelection.getSelectedFilteredCompetitors()); } } else { // "lowlight" currently selected competitor - BoatOverlay boatCanvas = boatOverlays.get(competitor); + final BoatOverlay boatCanvas = boatOverlaysByCompetitorIdsAsStrings.get(competitor.getIdAsString()); if (boatCanvas != null) { boatCanvas.setDisplayMode(displayHighlighted(competitor)); boatCanvas.draw(); @@ -3136,7 +3157,7 @@ public class RaceMap extends AbstractCompositeComponent impleme tail.setOptions(newOptions); } } - //Trigger auto-zoom if needed + // Trigger auto-zoom if needed RaceMapZoomSettings zoomSettings = settings.getZoomSettings(); if (!zoomSettings.containsZoomType(ZoomTypes.NONE) && zoomSettings.isZoomToSelectedCompetitors()) { zoomMapToNewBounds(zoomSettings.getNewBounds(this)); @@ -3453,41 +3474,45 @@ public class RaceMap extends AbstractCompositeComponent impleme @Override public void onColorMappingChanged() { metricOverlay.updateLegend(fixesAndTails.getDetailValueBoundaries(), tailColorMapper, selectedDetailType); - for (CompetitorDTO competitor : competitorSelection.getSelectedCompetitors()) { - ColorlineOptions options = createTailStyle(competitor, displayHighlighted(competitor)); - fixesAndTails.getTail(competitor).setOptions(options); + for (final CompetitorDTO competitor : competitorSelection.getSelectedCompetitors()) { + final Colorline tail = fixesAndTails.getTail(competitor); + if (tail != null) { + final ColorlineOptions options = createTailStyle(competitor, displayHighlighted(competitor)); + tail.setOptions(options); + } } } @Override public ColorlineOptions createTailStyle(CompetitorDTO competitor, DisplayMode displayMode) { - ColorlineOptions options = new ColorlineOptions(); + final ColorlineOptions options = new ColorlineOptions(); options.setClickable(true); options.setGeodesic(true); options.setStrokeOpacity(1.0); switch (displayMode) { case DEFAULT: options.setColorMode(ColorlineMode.MONOCHROMATIC); - options.setColorProvider((i) -> competitorSelection.getColor(competitor, raceIdentifier).getAsHtml()); + options.setColorProvider(fixIndexInTail -> competitorSelection.getColor(competitor, raceIdentifier).getAsHtml()); options.setStrokeWeight(1); break; case SELECTED: options.setColorMode(ColorlineMode.POLYCHROMATIC); - options.setColorProvider(i -> { + options.setColorProvider(fixIndexInTail -> { + final String resultColor; + final Double detailValue; // If a DetailType has been selected and we are not currently waiting for the first update with the new values - if (selectedDetailType != null && !selectedDetailTypeChanged) { - Double detailValue = fixesAndTails.getDetailValueAt(competitor, i); - if (detailValue != null) { - return tailColorMapper.getColor(detailValue); - } + if (selectedDetailType != null && !selectedDetailTypeChanged && (detailValue = fixesAndTails.getDetailValueAt(competitor, fixIndexInTail)) != null) { + resultColor = tailColorMapper.getColor(detailValue); + } else { + resultColor = competitorSelection.getColor(competitor, raceIdentifier).getAsHtml(); } - return competitorSelection.getColor(competitor, raceIdentifier).getAsHtml(); + return resultColor; }); options.setStrokeWeight(2); break; case NOT_SELECTED: options.setColorMode(ColorlineMode.MONOCHROMATIC); - options.setColorProvider((i) -> LOWLIGHTED_TAIL_COLOR.getAsHtml()); + options.setColorProvider(fixIndexInTail -> LOWLIGHTED_TAIL_COLOR.getAsHtml()); options.setStrokeOpacity(LOWLIGHTED_TAIL_OPACITY); break; } @@ -3500,12 +3525,12 @@ public class RaceMap extends AbstractCompositeComponent impleme final BoatDTO boat = competitorSelection.getBoat(competitor); ColorlineOptions options = createTailStyle(competitor, displayHighlighted(competitor)); Colorline result = new Colorline(options); - MVCArray pointsAsArray = MVCArray.newInstance(points.toArray(new LatLng[0])); + MVCArray pointsAsArray = MVCArray.newInstance(points.toArray(new LatLng[points.size()])); result.setPath(pointsAsArray); result.setMap(map); ColorlineOptions hoverlineOptions = new ColorlineOptions(options); hoverlineOptions.setColorMode(ColorlineMode.MONOCHROMATIC); - hoverlineOptions.setColorProvider((i) -> competitorSelection.getColor(competitor, raceIdentifier).getAsHtml()); + hoverlineOptions.setColorProvider(fixIndexInTail -> competitorSelection.getColor(competitor, raceIdentifier).getAsHtml()); Hoverline resultHoverline = new Hoverline(result, hoverlineOptions, this); final ClickMapHandler clickHandler = new ClickMapHandler() { @Override diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/DataMiningSettingsInfoManagerImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/DataMiningSettingsInfoManagerImpl.java index fbb65ea3c07..9113abb6dfc 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/DataMiningSettingsInfoManagerImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/DataMiningSettingsInfoManagerImpl.java @@ -8,9 +8,11 @@ import com.sap.sailing.datamining.shared.ManeuverSettings; import com.sap.sailing.datamining.shared.ManeuverSettingsImpl; import com.sap.sailing.datamining.shared.ManeuverSpeedDetailsSettings; import com.sap.sailing.datamining.shared.ManeuverSpeedDetailsSettingsImpl; +import com.sap.sailing.datamining.shared.TackTypeSegmentsDataMiningSettings; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.datamining.presentation.ManeuverSettingsDialogComponent; import com.sap.sailing.gwt.ui.datamining.presentation.ManeuverSpeedDetailsSettingsDialogComponent; +import com.sap.sailing.gwt.ui.datamining.presentation.TackTypeSegmentsDataMiningSettingsDialogComponent; import com.sap.sailing.gwt.ui.polarmining.FoilingSegmentsDataMiningSettingsDialogComponent; import com.sap.sailing.gwt.ui.polarmining.PolarDataMiningSettingsDialogComponent; import com.sap.sailing.polars.datamining.shared.PolarDataMiningSettings; @@ -28,23 +30,21 @@ public class DataMiningSettingsInfoManagerImpl implements DataMiningSettingsInfo public DataMiningSettingsInfoManagerImpl(StringMessages stringMessages) { this.stringMessages = stringMessages; infosMappedBySettingsType = new HashMap<>(); - // GWT doesn't support Class.isAssignableFrom and Class.getInterfaces. // Adding every implementation of the desired type is necessary. PolarDataMiningSettingsInfo polarDataMiningSettingsInfo = new PolarDataMiningSettingsInfo(); infosMappedBySettingsType.put(PolarDataMiningSettings.class, polarDataMiningSettingsInfo); infosMappedBySettingsType.put(PolarDataMiningSettingsImpl.class, polarDataMiningSettingsInfo); - ManeuverSpeedDetailsSettingsInfo maneuverSpeedDetailsSettingsInfo = new ManeuverSpeedDetailsSettingsInfo(); infosMappedBySettingsType.put(ManeuverSpeedDetailsSettings.class, maneuverSpeedDetailsSettingsInfo); infosMappedBySettingsType.put(ManeuverSpeedDetailsSettingsImpl.class, maneuverSpeedDetailsSettingsInfo); - ManeuverSettingsInfo maneuverSettingsInfo = new ManeuverSettingsInfo(); infosMappedBySettingsType.put(ManeuverSettings.class, maneuverSettingsInfo); infosMappedBySettingsType.put(ManeuverSettingsImpl.class, maneuverSettingsInfo); - FoilingSegmentsDataMiningSettingsInfo foilingDataMiningSettingsInfo = new FoilingSegmentsDataMiningSettingsInfo(); infosMappedBySettingsType.put(FoilingSegmentsDataMiningSettings.class, foilingDataMiningSettingsInfo); + TackTypeSegmentsDataMiningSettingsInfo tackTypeDataMiningSettingsInfo = new TackTypeSegmentsDataMiningSettingsInfo(); + infosMappedBySettingsType.put(TackTypeSegmentsDataMiningSettings.class, tackTypeDataMiningSettingsInfo); } /* @@ -141,4 +141,24 @@ public class DataMiningSettingsInfoManagerImpl implements DataMiningSettingsInfo return "FoilingSegmentsDataMiningSettingsInfo"; } } + + private class TackTypeSegmentsDataMiningSettingsInfo implements DataMiningSettingsInfo { + @SuppressWarnings("unchecked") + @Override + public SettingsDialogComponent createSettingsDialogComponent( + SettingsType settings) { + return (SettingsDialogComponent) new TackTypeSegmentsDataMiningSettingsDialogComponent( + (TackTypeSegmentsDataMiningSettings) settings); + } + + @Override + public String getLocalizedName() { + return stringMessages.tackTypeSegments(); + } + + @Override + public String getId() { + return "TackTypeSegmentsDataMiningSettingsInfo"; + } + } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/TackTypeSegmentsDataMiningSettingsDialogComponent.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/TackTypeSegmentsDataMiningSettingsDialogComponent.java new file mode 100644 index 00000000000..854d5d4d18b --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/TackTypeSegmentsDataMiningSettingsDialogComponent.java @@ -0,0 +1,95 @@ +package com.sap.sailing.gwt.ui.datamining.presentation; + +import com.google.gwt.user.client.ui.FocusWidget; +import com.google.gwt.user.client.ui.Grid; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.VerticalPanel; +import com.google.gwt.user.client.ui.Widget; +import com.sap.sailing.datamining.shared.TackTypeSegmentsDataMiningSettings; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sse.common.Duration; +import com.sap.sse.common.impl.MillisecondsDurationImpl; +import com.sap.sse.gwt.client.dialog.DataEntryDialog; +import com.sap.sse.gwt.client.dialog.DataEntryDialog.Validator; +import com.sap.sse.gwt.client.dialog.DoubleBox; +import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; + +/** + * Settings dialog for maneuver settings. + * + * @author Vladislav Chumak (D069712) + * + */ +public class TackTypeSegmentsDataMiningSettingsDialogComponent implements SettingsDialogComponent { + + private TackTypeSegmentsDataMiningSettings settings; + private StringMessages stringMessages; + private DoubleBox minimumDurationBetweenAdjacentTackTypeSegmentsInSecondsBox; + private DoubleBox minimumTackTypeSegmentDurationInSecondsBox; + + public TackTypeSegmentsDataMiningSettingsDialogComponent(TackTypeSegmentsDataMiningSettings settings) { + this.settings = settings; + this.stringMessages = StringMessages.INSTANCE; + } + + @Override + public Widget getAdditionalWidget(DataEntryDialog dialog) { + VerticalPanel vp = new VerticalPanel(); + Grid grid = new Grid(2, 2); + grid.setCellPadding(5); + vp.add(grid); + setupGrid(grid, dialog); + return vp; + } + + private void setupGrid(Grid grid, DataEntryDialog dialog) { + final Label minDurationBetweenTackTypeSegmentsLabel = dialog.createLabel(stringMessages.minimumDurationBetweenAdjacentTackTypeSegmentsInSeconds()); + grid.setWidget(0, 0, minDurationBetweenTackTypeSegmentsLabel); + minimumDurationBetweenAdjacentTackTypeSegmentsInSecondsBox = dialog.createDoubleBox( + settings.getMinimumTackTypeSegmentDuration() == null ? null : settings.getMinimumDurationBetweenAdjacentTackTypeSegments().asSeconds(), 10); + grid.setWidget(0, 1, minimumDurationBetweenAdjacentTackTypeSegmentsInSecondsBox); + + final Label minTackTypeSegmentsDurationLabel = dialog.createLabel(stringMessages.minimumTackTypeSegmentsDurationInSeconds()); + grid.setWidget(1, 0, minTackTypeSegmentsDurationLabel); + minimumTackTypeSegmentDurationInSecondsBox = dialog.createDoubleBox( + settings.getMinimumTackTypeSegmentDuration() == null ? null : settings.getMinimumTackTypeSegmentDuration().asSeconds(), 10); + grid.setWidget(1, 1, minimumTackTypeSegmentDurationInSecondsBox); + } + + @Override + public TackTypeSegmentsDataMiningSettings getResult() { + return new TackTypeSegmentsDataMiningSettings( + getDurationFromSeconds(minimumDurationBetweenAdjacentTackTypeSegmentsInSecondsBox.getValue()), + getDurationFromSeconds(minimumTackTypeSegmentDurationInSecondsBox.getValue())); + } + + private Duration getDurationFromSeconds(Double seconds) { + return seconds == null ? null : new MillisecondsDurationImpl((long) (seconds * 1000)); + } + + @Override + public FocusWidget getFocusWidget() { + return minimumDurationBetweenAdjacentTackTypeSegmentsInSecondsBox; + } + + @Override + public Validator getValidator() { + return new Validator() { + @Override + public String getErrorMessage(TackTypeSegmentsDataMiningSettings valueToValidate) { + final String result; + if (valueToValidate.getMinimumDurationBetweenAdjacentTackTypeSegments() != null && + valueToValidate.getMinimumDurationBetweenAdjacentTackTypeSegments().compareTo(Duration.NULL) < 0) { + result = stringMessages.errorMinimumDurationBetweenAdjacentTackTypeSegmentsMustNotBeNegative(); + } else if (valueToValidate.getMinimumTackTypeSegmentDuration() != null && + valueToValidate.getMinimumTackTypeSegmentDuration().compareTo(Duration.NULL) < 0) { + result = stringMessages.errorMinimumTackTypeSegmentDurationMustNotBeNegative(); + } else { + result = null; + } + return result; + } + }; + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/polarmining/TackTypeSegmentsDataMiningSettingsDialogComponent.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/polarmining/TackTypeSegmentsDataMiningSettingsDialogComponent.java new file mode 100644 index 00000000000..f2cc3e5a40a --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/polarmining/TackTypeSegmentsDataMiningSettingsDialogComponent.java @@ -0,0 +1,120 @@ +//package com.sap.sailing.gwt.ui.polarmining; +// +//import com.google.gwt.user.client.ui.FocusWidget; +//import com.google.gwt.user.client.ui.Grid; +//import com.google.gwt.user.client.ui.Label; +//import com.google.gwt.user.client.ui.VerticalPanel; +//import com.google.gwt.user.client.ui.Widget; +//import com.sap.sailing.datamining.shared.FoilingSegmentsDataMiningSettings; +//import com.sap.sailing.datamining.shared.TackTypeSegmentsDataMiningSettings; +//import com.sap.sailing.domain.common.impl.KnotSpeedImpl; +//import com.sap.sailing.domain.common.impl.MeterDistance; +//import com.sap.sailing.gwt.ui.client.StringMessages; +//import com.sap.sailing.polars.datamining.shared.PolarDataMiningSettings; +//import com.sap.sse.common.impl.MillisecondsDurationImpl; +//import com.sap.sse.gwt.client.dialog.DataEntryDialog; +//import com.sap.sse.gwt.client.dialog.DataEntryDialog.Validator; +//import com.sap.sse.gwt.client.dialog.DoubleBox; +//import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; +// +///** +// * Provides a widget for configuring {@link PolarDataMiningSettings}, including validation. +// * +// * @author D054528 (Frederik Petersen) +// * +// */ +//public class TackTypeSegmentsDataMiningSettingsDialogComponent implements SettingsDialogComponent { +// +// private TackTypeSegmentsDataMiningSettings settings; +// private StringMessages stringMessages; +// private DoubleBox minimumTackTypeSegmentsDurationInSecondsBox; +// private DoubleBox minimumDurationBetweenAdjacentTackTypeSegmentsInSecondsBox; +// +// public TackTypeSegmentsDataMiningSettingsDialogComponent(TackTypeSegmentsDataMiningSettings settings) { +// this.settings = settings; +// this.stringMessages = StringMessages.INSTANCE; +// } +// +// @Override +// public Widget getAdditionalWidget(DataEntryDialog dialog) { +// VerticalPanel vp = new VerticalPanel(); +// Grid grid = new Grid(5, 2); +// grid.setCellPadding(5); +// vp.add(grid); +// setupGrid(grid, dialog); +// return vp; +// } +// +// private void setupGrid(Grid grid, DataEntryDialog dialog) { +// Label minimumFoilingSegmentsDurationInSecondsLabel = new Label(stringMessages.minimumFoilingSegmentsDurationInSeconds() + ":"); +// minimumFoilingSegmentsDurationInSecondsLabel.setTitle(stringMessages.minimumFoilingSegmentsDurationInSecondsTooltip()); +// grid.setWidget(0, 0, minimumFoilingSegmentsDurationInSecondsLabel); +// if (settings.getMinimumTackTypeSegmentDuration() == null) { +// minimumTackTypeSegmentsDurationInSecondsBox = dialog.createDoubleBox(6); +// } else { +// minimumTackTypeSegmentsDurationInSecondsBox = dialog.createDoubleBox(settings.getMinimumTackTypeSegmentDuration().asSeconds(), 6); +// } +// grid.setWidget(0, 1, minimumTackTypeSegmentsDurationInSecondsBox); +// Label minimumDurationBetweenAdjacentFoilingSegmentsInSecondsBoxLabel = new Label(stringMessages.minimumDurationBetweenAdjacentFoilingSegmentsInSeconds() + ":"); +// minimumDurationBetweenAdjacentFoilingSegmentsInSecondsBoxLabel.setTitle(stringMessages.minimumDurationBetweenAdjacentFoilingSegmentsInSecondsTooltip()); +// grid.setWidget(1, 0, minimumDurationBetweenAdjacentFoilingSegmentsInSecondsBoxLabel); +// if (settings.getMinimumDurationBetweenAdjacentTackTypeSegments() == null) { +// minimumDurationBetweenAdjacentTackTypeSegmentsInSecondsBox = dialog.createDoubleBox(6); +// } else { +// minimumDurationBetweenAdjacentTackTypeSegmentsInSecondsBox = dialog.createDoubleBox(settings.getMinimumFoilingSegmentDuration().asSeconds(), 6); +// } +// grid.setWidget(1, 1, minimumDurationBetweenAdjacentTackTypeSegmentsInSecondsBox); +// Label maximumSpeedNotFoilingInKnotsLabel = new Label(stringMessages.maximumSpeedNotFoilingInKnots() + ":"); +// maximumSpeedNotFoilingInKnotsLabel.setTitle(stringMessages.maximumSpeedNotFoilingInKnotsTooltip()); +// grid.setWidget(2, 0, maximumSpeedNotFoilingInKnotsLabel); +// if (settings.getMaximumSpeedNotFoiling() == null) { +// maximumSpeedNotTackTypeInKnotsBox = dialog.createDoubleBox(6); +// } else { +// maximumSpeedNotTackTypeInKnotsBox = dialog.createDoubleBox(settings.getMaximumSpeedNotTackType().getKnots(), 6); +// } +// grid.setWidget(2, 1, maximumSpeedNotTackTypeInKnotsBox); +// Label minimumSpeedForFoilingInKnotsLabel = new Label(stringMessages.minimumSpeedForFoilingInKnots() + ":"); +// minimumSpeedForFoilingInKnotsLabel.setTitle(stringMessages.minimumSpeedForFoilingInKnotsTooltip()); +// grid.setWidget(3, 0, minimumSpeedForFoilingInKnotsLabel); +// if (settings.getMinimumSpeedForTackType() == null) { +// minimumSpeedForTackTypeInKnotsBox = dialog.createDoubleBox(6); +// } else { +// minimumSpeedForTackTypeInKnotsBox = dialog.createDoubleBox(settings.getMinimumSpeedForTackType().getKnots(), 6); +// } +// grid.setWidget(3, 1, minimumSpeedForTackTypeInKnotsBox); +// Label minimumRideHeightInMetersLabel = new Label(stringMessages.minimumRideHeightInMeters() + ":"); +// minimumRideHeightInMetersLabel.setTitle(stringMessages.minimumRideHeightInMetersTooltip()); +// grid.setWidget(4, 0, minimumRideHeightInMetersLabel); +// if (settings.getMinimumRideHeight() == null) { +// minimumRideHeightInMetersBox = dialog.createDoubleBox(6); +// } else { +// minimumRideHeightInMetersBox = dialog.createDoubleBox(settings.getMinimumRideHeight().getMeters(), 6); +// } +// grid.setWidget(4, 1, minimumRideHeightInMetersBox); +// } +// +// @Override +// public FoilingSegmentsDataMiningSettings getResult() { +// return new FoilingSegmentsDataMiningSettings( +// minimumTackTypeSegmentsDurationInSecondsBox.getValue() == null ? null +// : new MillisecondsDurationImpl((long) (minimumTackTypeSegmentsDurationInSecondsBox.getValue() * 1000.)), +// minimumDurationBetweenAdjacentTackTypeSegmentsInSecondsBox.getValue() == null ? null +// : new MillisecondsDurationImpl((long) (minimumDurationBetweenAdjacentTackTypeSegmentsInSecondsBox.getValue() * 1000.)), +// minimumSpeedForTackTypeInKnotsBox.getValue() == null ? null +// : new KnotSpeedImpl(minimumSpeedForFoilingInKnotsBox.getValue()), +// maximumSpeedNotFoilingInKnotsBox.getValue() == null ? null +// : new KnotSpeedImpl(maximumSpeedNotFoilingInKnotsBox.getValue()), minimumRideHeightInMetersBox.getValue() == null ? null +// : new MeterDistance(minimumRideHeightInMetersBox.getValue())); +// } +// +// @Override +// public FocusWidget getFocusWidget() { +// return minimumTackTypeSegmentsDurationInSecondsBox; +// } +// +// @Override +// public Validator getValidator() { +// return new FoilingSegmentsDataMiningSettingsValidator(stringMessages); +// } +// +//} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/QuickFlagDataFromLeaderboardDTOProvider.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/QuickFlagDataFromLeaderboardDTOProvider.java index 74f78eacee1..1974655d845 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/QuickFlagDataFromLeaderboardDTOProvider.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/QuickFlagDataFromLeaderboardDTOProvider.java @@ -25,7 +25,7 @@ import com.sap.sse.common.Util; * whereas the {@link QuickRankDTO#legNumberOneBased leg numbers} will continue to be accepted from the quick ranks * coming from the server. Likewise, if the leaderboard does not provide the current speed over ground value, e.g., * because the leaderboard contains no leg details, the speed value is taken from the GPS fix, through - * {@link #quickSpeedsInKnotsReceivedFromServer(Map)}. IF the leaderboard contains the SOG values, + * {@link #quickSpeedsInKnotsReceivedFromServer(Map, Map)}. IF the leaderboard contains the SOG values, * {@link #quickSpeedsInKnots} is filled from the leaderboard. *

      * @@ -182,12 +182,12 @@ public class QuickFlagDataFromLeaderboardDTOProvider extends AbstractQuickFlagDa } @Override - public void quickSpeedsInKnotsReceivedFromServer(Map quickSpeedsFromServerInKnots) { - for (final Entry e : quickSpeedsFromServerInKnots.entrySet()) { - quickSpeedsInKnots.put(e.getKey().getIdAsString(), e.getValue()); + public void quickSpeedsInKnotsReceivedFromServer(Map quickSpeedsFromServerInKnotsByCompetitorIdAsString, Map competitorsByIdAsString) { + for (final Entry e : quickSpeedsFromServerInKnotsByCompetitorIdAsString.entrySet()) { + quickSpeedsInKnots.put(e.getKey(), e.getValue()); // pass the quick speed info on in case we have no speed info from the leaderboard - if (speedsFromLeaderboardInKnots.get(e.getKey().getIdAsString()) == null) { - notifyListenersSpeedInKnotsChanged(e.getKey(), e.getValue()); + if (speedsFromLeaderboardInKnots.get(e.getKey()) == null) { + notifyListenersSpeedInKnotsChanged(competitorsByIdAsString.get(e.getKey()), e.getValue()); } } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/tagging/TagFooterPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/tagging/TagFooterPanel.java index 63a05994648..7b56803e7e9 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/tagging/TagFooterPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/tagging/TagFooterPanel.java @@ -26,16 +26,12 @@ public class TagFooterPanel extends FlowPanel { */ protected TagFooterPanel(TaggingComponent taggingComponent, SailingServiceAsync sailingService, StringMessages stringMessages, UserService userService) { this.taggingComponent = taggingComponent; - tagModificationPanel = new TagModificationPanel(taggingComponent, this, sailingService, stringMessages, userService); tagButtonPanel = new TagButtonPanel(taggingComponent, this, stringMessages, userService); - // Tag-buttons are only shown if amount of tag-buttons is greater then 0! setTagButtonsVisibility(true); - // input fields are hidden by default setInputFieldsVisibility(false); - tagButtonPanel.loadAllTagButtons(); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/tagging/TaggingComponent.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/tagging/TaggingComponent.java index 4586ae37d54..0273e122ffc 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/tagging/TaggingComponent.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/tagging/TaggingComponent.java @@ -172,7 +172,6 @@ public class TaggingComponent extends ComponentWithoutSettings RaceTimesInfoProvider raceTimesInfoProvider, TimePoint timePointToHighlight, String tagToHighlight, StrippedLeaderboardDTO leaderboardDTO, SailingServiceWriteAsync sailingServiceWrite) { super(parent, context); - this.stringMessages = stringMessages; this.sailingService = sailingService; this.sailingServiceWrite = sailingServiceWrite; @@ -182,19 +181,15 @@ public class TaggingComponent extends ComponentWithoutSettings this.timePointToHighlight = timePointToHighlight; this.tagToHighlight = tagToHighlight; this.leaderboardDTO = leaderboardDTO; - style = TaggingPanelResources.INSTANCE.style(); style.ensureInjected(); TaggingPanelResources.INSTANCE.cellListStyle().ensureInjected(); TaggingPanelResources.INSTANCE.cellTableStyle().ensureInjected(); - tagCellList = new CellList(new TagCell(this, stringMessages, userService, false), TaggingPanelResources.INSTANCE); tagSelectionModel = new SingleSelectionModel(); tagListProvider = new TagListProvider(); - tagButtons = new ArrayList(); - taggingPanel = new DockLayoutPanel(Style.Unit.PX) { @Override public void onResize() { @@ -210,13 +205,10 @@ public class TaggingComponent extends ComponentWithoutSettings filterbarPanel = new TagFilterPanel(this, stringMessages, userService); contentPanel = new FlowPanel(); createTagsButton = new Button(); - userService.addUserStatusEventHandler(this); raceTimesInfoProvider.addRaceTimesInfoProviderListener(this); - generateRandomId(); registerStorageEventHandler(); - setCurrentState(State.VIEW); initializePanel(); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/Activator.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/Activator.java index c6a6f717837..15d242cae6b 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/Activator.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/Activator.java @@ -1,12 +1,27 @@ package com.sap.sailing.gwt.ui.server; +import java.util.logging.Logger; + import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; +import com.sap.sailing.gwt.ui.shared.racemap.GoogleMapsLoader; + public class Activator implements BundleActivator { + private static final Logger logger = Logger.getLogger(Activator.class.getName()); + private static BundleContext context; private SailingServiceImpl sailingServiceToStopWhenStopping; private static Activator INSTANCE; + + private final static String GOOGLE_MAPS_LOADER_AUTHENTICATION_PARAMS_PROPERTY_NAME = "google.maps.authenticationparams"; + + /** + * Required by {@link GoogleMapsLoader#load(Runnable, String)} and to be provided through a system property named + * after {@link GOOGLE_MAPS_LOADER_AUTHENTICATION_PARAMS_PROPERTY_NAME}. The value would be something like + * {@code client=abcde&channel=fghij}. + */ + private String googleMapsLoaderAuthenticationParams; public Activator() { INSTANCE = this; @@ -15,8 +30,14 @@ public class Activator implements BundleActivator { @Override public void start(BundleContext context) throws Exception { Activator.context = context; + googleMapsLoaderAuthenticationParams = context.getProperty(GOOGLE_MAPS_LOADER_AUTHENTICATION_PARAMS_PROPERTY_NAME); + if (googleMapsLoaderAuthenticationParams == null) { + googleMapsLoaderAuthenticationParams = "key=AIzaSyD1Se4tIkt-wglccbco3S7twaHiG20hR9E"; + logger.warning("Did not find a value for the "+GOOGLE_MAPS_LOADER_AUTHENTICATION_PARAMS_PROPERTY_NAME+ + " system property. Using a test key for the Google Maps API instead. Your mileage may vary."); + } } - + @Override public void stop(BundleContext context) throws Exception { if (sailingServiceToStopWhenStopping != null) { @@ -34,6 +55,15 @@ public class Activator implements BundleActivator { public static BundleContext getDefault() { return context; } + + /** + * Returns a URL parameter string, e.g., like {@code client=abcde&channel=fghij}, provided to this activator through + * a system property named after {@link GOOGLE_MAPS_LOADER_AUTHENTICATION_PARAMS_PROPERTY_NAME}. Won't be {@code null} + * because the entire bundle won't activate if not set. + */ + public String getGoogleMapsLoaderAuthenticationParams() { + return googleMapsLoaderAuthenticationParams; + } public void setSailingService(SailingServiceImpl sailingServiceImpl) { sailingServiceToStopWhenStopping = sailingServiceImpl; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index d812941f37e..b8220bc9b55 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -1790,7 +1790,6 @@ public class SailingServiceImpl extends ResultCachingProxiedRemoteServiceServlet final HashSet raceCompetitorIdsAsStrings; final TrackedRace trackedRace = getExistingTrackedRace(raceIdentifier); getSecurityService().checkCurrentUserReadPermission(trackedRace); - // if md5OfIdsAsStringOfCompetitorParticipatingInRaceInAlphanumericOrderOfTheirID is null, Arrays.equals will return false, and the // competitor set will be calculated and returned to the client if (trackedRace == null || Arrays.equals(md5OfIdsAsStringOfCompetitorParticipatingInRaceInAlphanumericOrderOfTheirID, trackedRace.getRace().getCompetitorMD5())) { @@ -6258,4 +6257,9 @@ public class SailingServiceImpl extends ResultCachingProxiedRemoteServiceServlet raceMetadata.getRaceUrl(), hasRememberedRegatta(raceMetadata.getRaceId()), raceMetadata.getTimePointOfLastFix(), raceMetadata.getNumberOfCompetitors()))); } + + @Override + public String getGoogleMapsLoaderAuthenticationParams() { + return Activator.getInstance().getGoogleMapsLoaderAuthenticationParams(); + } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SimulatorServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SimulatorServiceImpl.java index d340c51acfd..a2bdf59810c 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SimulatorServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SimulatorServiceImpl.java @@ -956,4 +956,9 @@ public class SimulatorServiceImpl extends DelegatingProxiedRemoteServiceServlet } return timepointAsMillis - startTimePoint2asMillis; } + + @Override + public String getGoogleMapsLoaderAuthenticationParams() { + return Activator.getInstance().getGoogleMapsLoaderAuthenticationParams(); + } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/GPSFixDTOWithSpeedWindTackAndLegType.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/GPSFixDTOWithSpeedWindTackAndLegType.java index 2906b646386..2464f2ea221 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/GPSFixDTOWithSpeedWindTackAndLegType.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/GPSFixDTOWithSpeedWindTackAndLegType.java @@ -70,4 +70,12 @@ public class GPSFixDTOWithSpeedWindTackAndLegType extends GPSFixDTO implements I this.degreesBoatToTheWind = degreesBoatToTheWind; this.detailValue = detailValue; } + + @Override + public String toString() { + return "GPSFixDTOWithSpeedWindTackAndLegType [speedWithBearing=" + speedWithBearing + ", extrapolated=" + + extrapolated + ", tack=" + tack + ", legType=" + legType + ", degreesBoatToTheWind=" + + degreesBoatToTheWind + ", detailValue=" + detailValue + ", timepoint=" + timepoint + ", position=" + + position + "]"; + } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/racemap/GoogleMapsLoader.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/racemap/GoogleMapsLoader.java index 6e621db3598..c692022cdeb 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/racemap/GoogleMapsLoader.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/racemap/GoogleMapsLoader.java @@ -8,20 +8,15 @@ import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.ScriptElement; /** - * The {@link #load(Runnable)} method can be used by clients to request the loading of the Google Maps API. + * The {@link #load(Runnable, String)} method can be used by clients to request the loading of the Google Maps API. * The callback passed will be invoked immediately if the API has already been loaded (e.g., by another - * client call to the {@link #load(Runnable)} method within the same frame / document); it will be queued + * client call to the {@link #load(Runnable, String)} method within the same frame / document); it will be queued * for invocation by a Google Maps API callback function registered otherwise. This callback function - * is injected at most once when the {@link #load(Runnable)} method is invoked for the first time and - * will trigger all callbacks registered through the {@link #load(Runnable)} method until the maps API + * is injected at most once when the {@link #load(Runnable, String)} method is invoked for the first time and + * will trigger all callbacks registered through the {@link #load(Runnable, String)} method until the maps API * invokes the callback registered. */ public class GoogleMapsLoader { - /** - * These params define the required information to authenticate with the Google Maps API. - */ - public static final String AUTHENTICATION_PARAMS = "client=gme-sapglobalmarketing&channel=sapsailing.com"; - /** * Note: If you use 3, it will take the newest stable available. We want that, although we didn't test with that yet! * Google Release notes: https://developers.google.com/maps/documentation/javascript/releases. @@ -45,7 +40,7 @@ public class GoogleMapsLoader { /** * @param callback must not be {@code null}. */ - public static void load(Runnable callback) { + public static void load(Runnable callback, String authenticationParams) { if (loaded) { Scheduler.get().scheduleDeferred(() -> callback.run()); } else { @@ -54,7 +49,7 @@ public class GoogleMapsLoader { loading = true; installCallback(); final ScriptElement scriptElement = Document.get().createScriptElement(); - scriptElement.setSrc("https://maps.googleapis.com/maps/api/js?v="+API_VERSION+"&" + AUTHENTICATION_PARAMS + scriptElement.setSrc("https://maps.googleapis.com/maps/api/js?v="+API_VERSION+"&" + authenticationParams + "&libraries="+LIBRARIES+"&callback=googleMapsLoadedCallback"); Document.get().getHead().appendChild(scriptElement); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/SimulatorEntryPoint.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/SimulatorEntryPoint.java index 91963f65d63..4c62dd04d9b 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/SimulatorEntryPoint.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/SimulatorEntryPoint.java @@ -32,7 +32,6 @@ import com.sap.sse.security.ui.authentication.generic.GenericAuthorizedContentDe import com.sap.sse.security.ui.client.premium.PaywallResolver; public class SimulatorEntryPoint extends AbstractSailingReadEntryPoint { - private final SimulatorServiceAsync simulatorService = GWT.create(SimulatorService.class); private int xRes = 40; private int yRes = 20; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/SimulatorMap.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/SimulatorMap.java index a7692ebc71a..ab97b877992 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/SimulatorMap.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/SimulatorMap.java @@ -134,7 +134,6 @@ public class SimulatorMap extends AbsolutePanel implements RequiresDataInitializ @Override public void onSuccess(SimulatorResultsDTO result) { - String notificationMessage = result.getNotificationMessage(); if (Util.hasLength(notificationMessage) && warningAlreadyShown == false) { errorReporter.reportError(notificationMessage, true); @@ -234,11 +233,8 @@ public class SimulatorMap extends AbsolutePanel implements RequiresDataInitializ if (windParams.isShowStreamlets()) { windStreamletsCanvasOverlay.addToMap(); } - refreshWindFieldOverlay(windFieldDTO); - timeListeners.clear(); - if (windParams.isShowArrows()) { timeListeners.add(windFieldCanvasOverlay); } @@ -257,7 +253,6 @@ public class SimulatorMap extends AbsolutePanel implements RequiresDataInitializ for (int i = 0; i < replayPathCanvasOverlays.size(); ++i) { timeListeners.add(replayPathCanvasOverlays.get(i)); } - if (summaryView) { if (windFieldCanvasOverlay != null) { windFieldCanvasOverlay.setVisible(false); @@ -279,45 +274,14 @@ public class SimulatorMap extends AbsolutePanel implements RequiresDataInitializ windStreamletsCanvasOverlay.setVisible(true); } } - legendCanvasOverlay.addToMap(); legendCanvasOverlay.setVisible(true); legendCanvasOverlay.draw(); busyIndicator.setBusy(false); } - } - /*public SimulatorMap(SimulatorServiceAsync simulatorSvc, StringMessages stringMessages, ErrorReporter errorReporter, int xRes, int yRes, int border, StreamletParameters streamletPars, Timer timer, - WindFieldGenParamsDTO windParams, SimpleBusyIndicator busyIndicator, char mode, - SimulatorMainPanel parent) { - this.simulatorService = simulatorSvc; - this.stringMessages = stringMessages; - this.errorReporter = errorReporter; - this.xRes = xRes; - this.yRes = yRes; - this.border = border; - this.timer = timer; - this.timePanel = null; - timer.addTimeListener(this); - this.windParams = windParams; - this.busyIndicator = busyIndicator; - this.mode = mode; - this.colorPalette = new ColorPaletteGenerator(); - this.dataInitialized = false; - this.overlaysInitialized = false; - this.windFieldCanvasOverlay = null; - this.windLineGuidesCanvasOverlay = null; - this.windGridCanvasOverlay = null; - this.windLineCanvasOverlay = null; - this.replayPathCanvasOverlays = null; - this.raceCourseCanvasOverlay = null; - this.timeListeners = new LinkedList(); - this.initializeData(); - this.parent = parent; - }*/ - public SimulatorMap(SimulatorServiceAsync simulatorSvc, StringMessages stringMessages, ErrorReporter errorReporter, int xRes, int yRes, int border, StreamletParameters streamletPars, Timer timer, TimePanel timePanel, WindFieldGenParamsDTO windParams, @@ -362,13 +326,9 @@ public class SimulatorMap extends AbsolutePanel implements RequiresDataInitializ mapOptions.setScaleControl(true); mapOptions.setRotateControl(true); mapOptions.setStreetViewControl(false); - MapTypeStyle[] mapTypeStyles; - if (windParams.isShowStreamlets()) { - mapTypeStyles = new MapTypeStyle[11]; - // hide all transit lines including ferry lines mapTypeStyles[0] = GoogleMapStyleHelper.createHiddenStyle(MapTypeStyleFeatureType.TRANSIT); // hide points of interest @@ -380,7 +340,6 @@ public class SimulatorMap extends AbsolutePanel implements RequiresDataInitializ mapTypeStyles[4] = GoogleMapStyleHelper.createColorStyle(MapTypeStyleFeatureType.LANDSCAPE, new RGBColor(255, 255, 255), -100, -70); mapTypeStyles[5] = GoogleMapStyleHelper.createColorStyle(MapTypeStyleFeatureType.POI, new RGBColor(255, 255, 255), -100, -70); mapTypeStyles[6] = GoogleMapStyleHelper.createElementStyleOnlyLightness(MapTypeStyleFeatureType.ROAD, MapTypeStyleElementType.ALL, -40); - MapTypeStyle mapStyle = MapTypeStyle.newInstance(); mapStyle.setFeatureType(MapTypeStyleFeatureType.ADMINISTRATIVE); mapStyle.setElementType(MapTypeStyleElementType.LABELS__TEXT__FILL); @@ -388,7 +347,6 @@ public class SimulatorMap extends AbsolutePanel implements RequiresDataInitializ typeStylers[0] = MapTypeStyler.newInvertLightnessStyler(true); mapStyle.setStylers(typeStylers); mapTypeStyles[7] = mapStyle; - mapStyle = MapTypeStyle.newInstance(); mapStyle.setFeatureType(MapTypeStyleFeatureType.ADMINISTRATIVE); mapStyle.setElementType(MapTypeStyleElementType.LABELS__TEXT__STROKE); @@ -396,7 +354,6 @@ public class SimulatorMap extends AbsolutePanel implements RequiresDataInitializ typeStylers[0] = MapTypeStyler.newInvertLightnessStyler(true); mapStyle.setStylers(typeStylers); mapTypeStyles[8] = mapStyle; - mapStyle = MapTypeStyle.newInstance(); mapStyle.setFeatureType(MapTypeStyleFeatureType.ADMINISTRATIVE); mapStyle.setElementType(MapTypeStyleElementType.GEOMETRY__FILL); @@ -406,7 +363,6 @@ public class SimulatorMap extends AbsolutePanel implements RequiresDataInitializ typeStylers[2] = MapTypeStyler.newSaturationStyler(-100); mapStyle.setStylers(typeStylers); mapTypeStyles[9] = mapStyle; - mapStyle = MapTypeStyle.newInstance(); mapStyle.setFeatureType(MapTypeStyleFeatureType.ADMINISTRATIVE); mapStyle.setElementType(MapTypeStyleElementType.GEOMETRY__STROKE); @@ -414,11 +370,8 @@ public class SimulatorMap extends AbsolutePanel implements RequiresDataInitializ typeStylers[0] = MapTypeStyler.newLightnessStyler(-70); mapStyle.setStylers(typeStylers); mapTypeStyles[10] = mapStyle; - } else { - mapTypeStyles = new MapTypeStyle[4]; - // hide all transit lines including ferry lines mapTypeStyles[0] = GoogleMapStyleHelper.createHiddenStyle(MapTypeStyleFeatureType.TRANSIT); // hide points of interest @@ -427,37 +380,27 @@ public class SimulatorMap extends AbsolutePanel implements RequiresDataInitializ mapTypeStyles[2] = GoogleMapStyleHelper.createSimplifiedStyle(MapTypeStyleFeatureType.ROAD); // set water color mapTypeStyles[3] = GoogleMapStyleHelper.createColorStyle(MapTypeStyleFeatureType.WATER, new RGBColor(0, 136, 255), -35, -34); - } - mapOptions.setMapTypeStyles(mapTypeStyles); - ScaleControlOptions scaleControlOptions = ScaleControlOptions.newInstance(); scaleControlOptions.setPosition(ControlPosition.BOTTOM_RIGHT); mapOptions.setScaleControlOptions(scaleControlOptions); - ZoomControlOptions zoomControlOptions = ZoomControlOptions.newInstance(); zoomControlOptions.setPosition(ControlPosition.TOP_RIGHT); mapOptions.setZoomControlOptions(zoomControlOptions); - PanControlOptions panControlOptions = PanControlOptions.newInstance(); panControlOptions.setPosition(ControlPosition.TOP_RIGHT); mapOptions.setPanControlOptions(panControlOptions); - map = new MapWidget(mapOptions); - map.setTitle(stringMessages.simulator() + " " + stringMessages.map()); mapOptions.setDisableDoubleClickZoom(true); - if (mode == SailingSimulatorConstants.ModeFreestyle) { map.setZoom(14); } else if (mode == SailingSimulatorConstants.ModeEvent) { map.setZoom(12); } - add(map, 0, 0); map.setSize("100%", "100%"); - if (windParams.isShowStreamlets()) { map.addBoundsChangeHandler(new BoundsChangeMapHandler() { @Override @@ -473,43 +416,7 @@ public class SimulatorMap extends AbsolutePanel implements RequiresDataInitializ } }); } - - /*map.addZoomChangeHandler(new ZoomChangeMapHandler() { - - @Override - public void onEvent(ZoomChangeMapEvent event) { - // TODO Auto-generated method stub - windRoseCanvasOverlay.removeFromMap(); - windNeedleCanvasOverlay.removeFromMap(); - mapPan = true; - } - - }); - - map.addDragStartHandler(new DragStartMapHandler() { - - @Override - public void onEvent(DragStartMapEvent event) { - // TODO Auto-generated method stub - windRoseCanvasOverlay.removeFromMap(); - windNeedleCanvasOverlay.removeFromMap(); - } - - }); - - map.addDragEndHandler(new DragEndMapHandler() { - - @Override - public void onEvent(DragEndMapEvent event) { - // TODO Auto-generated method stub - windRoseCanvasOverlay.addToMap(); - windNeedleCanvasOverlay.addToMap(); - } - - });*/ - map.addIdleHandler(new IdleMapHandler() { - @Override public void onEvent(IdleMapEvent event) { // TODO Auto-generated method stub @@ -520,13 +427,9 @@ public class SimulatorMap extends AbsolutePanel implements RequiresDataInitializ mapPan = false; } } - }); - initializeOverlays(); - dataInitialized = true; - if (mode == SailingSimulatorConstants.ModeFreestyle) { LatLng kiel = LatLng.newInstance(54.43450, 10.19559167); // regatta area for TV on Kieler Woche // LatLng trave = LatLng.newInstance(54.007063, 10.838356); // in front of Timmendorfer Strand @@ -534,7 +437,17 @@ public class SimulatorMap extends AbsolutePanel implements RequiresDataInitializ } } }; - GoogleMapsLoader.load(onLoad); + simulatorService.getGoogleMapsLoaderAuthenticationParams(new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError(stringMessages.errorNoAuthenticationParamsForGoogleMapsFound(caught.getMessage())); + } + + @Override + public void onSuccess(String googleMapsLoaderAuthenticationParams) { + GoogleMapsLoader.load(onLoad, googleMapsLoaderAuthenticationParams); + } + }); } private void initializeOverlays() { @@ -721,16 +634,13 @@ public class SimulatorMap extends AbsolutePanel implements RequiresDataInitializ private void generatePath(final WindPatternDisplay windPatternDisplay, boolean summaryView, final SimulatorUISelectionDTO selection) { LOGGER.info("In generatePath"); - if (windPatternDisplay == null) { errorReporter.reportError(stringMessages.pleaseSelectAValidWindPattern()); return; } - if ((windStreamletsCanvasOverlay != null)&&(windStreamletsCanvasOverlay.isVisible())) { windStreamletsCanvasOverlay.setVisible(false); } - if (mode != SailingSimulatorConstants.ModeMeasured) { Position startPointDTO = new DegreePosition(raceCourseCanvasOverlay.getStartPoint().getLatitude(), raceCourseCanvasOverlay.getStartPoint().getLongitude()); @@ -740,17 +650,13 @@ public class SimulatorMap extends AbsolutePanel implements RequiresDataInitializ raceCourseCanvasOverlay.getEndPoint().getLongitude()); windParams.setRaceCourseEnd(endPointDTO); } - windParams.setxRes(xRes); windParams.setyRes(yRes); windParams.setBorder(border); - busyIndicator.setBusy(true); timer.pause(); timer.setTime(windParams.getStartTime().getTime()); - simulatorService.getSimulatorResults(mode, raceCourseDirection, windParams, windPatternDisplay, true, selection, new ResultManager(summaryView)); - } private boolean isCourseSet() { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/WindLineCanvasOverlay.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/WindLineCanvasOverlay.java index 095f5d7b054..58f1ec9cb16 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/WindLineCanvasOverlay.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/WindLineCanvasOverlay.java @@ -238,21 +238,16 @@ public class WindLineCanvasOverlay extends FullCanvasOverlay implements TimeList final Point corner0 = getPointInDivPixel(corners[0]); final Point corner1 = getPointInDivPixel(corners[1]); final Point corner3 = getPointInDivPixel(corners[3]); - final int canvasWidth = (int) Math.sqrt(Math.pow(corner0.getX() - corner1.getX(), 2) + Math.pow(corner0.getY() - corner1.getY(), 2)); final int canvasHeight = (int) Math.sqrt(Math.pow(corner3.getX() - corner0.getX(), 2) + Math.pow(corner3.getY() - corner0.getY(), 2)); - final int canvasRadius = (int) Math.sqrt(canvasWidth * canvasWidth / 4 + canvasHeight * canvasHeight / 4); canvas.setSize("" + 2 * canvasRadius + "px", "" + 2 * canvasRadius + "px"); canvas.setCoordinateSpaceWidth(2 * canvasRadius); canvas.setCoordinateSpaceHeight(2 * canvasRadius); - final Point anchorPoint = getAnchorPoint(); - setWidgetPosLeft(anchorPoint.getX()); setWidgetPosTop(anchorPoint.getY()); - setCanvasPosition(anchorPoint.getX(), anchorPoint.getY()); } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/racemap/FullCanvasOverlay.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/racemap/FullCanvasOverlay.java index 3fe25f6a978..2437accb211 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/racemap/FullCanvasOverlay.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/racemap/FullCanvasOverlay.java @@ -69,17 +69,14 @@ public abstract class FullCanvasOverlay extends CanvasOverlayV3 implements Requi } int canvasWidth = mapWidth; int canvasHeight = mapHeight; - canvas.setWidth(String.valueOf(canvasWidth)); canvas.setHeight(String.valueOf(canvasHeight)); canvas.setCoordinateSpaceWidth(canvasWidth); canvas.setCoordinateSpaceHeight(canvasHeight); - Point sw = mapProjection.fromLatLngToDivPixel(getMap().getBounds().getSouthWest()); Point ne = mapProjection.fromLatLngToDivPixel(getMap().getBounds().getNorthEast()); setWidgetPosLeft(Math.min(sw.getX(), ne.getX())); setWidgetPosTop(Math.min(sw.getY(), ne.getY())); - setCanvasPosition(getWidgetPosLeft(), getWidgetPosTop()); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/racemap/MovingCanvasOverlay.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/racemap/MovingCanvasOverlay.java index 901500457ec..c66e2ad5db5 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/racemap/MovingCanvasOverlay.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/racemap/MovingCanvasOverlay.java @@ -25,49 +25,43 @@ public abstract class MovingCanvasOverlay extends FullCanvasOverlay { @Override public void setCanvasSettings() { - // do nothing, if mapProjection is not available - if (mapProjection == null) - return; - - int canvasWidth = getMap().getDiv().getClientWidth(); - int canvasHeight = getMap().getDiv().getClientHeight(); - - // calculate pixel-positions of old and new canvas-bounds using the same, current mapProjection - // start from LatLng, as the canvas might have jumped to new bounds that do not intersect with the old bounds - Point nwOldPx; - if (nw == null) { - nwOldPx = null; - } else { - nwOldPx = mapProjection.fromLatLngToDivPixel(nw); + if (mapProjection != null) { + final int canvasWidth = getMap().getDiv().getClientWidth(); + final int canvasHeight = getMap().getDiv().getClientHeight(); + // calculate pixel-positions of old and new canvas-bounds using the same, current mapProjection + // start from LatLng, as the canvas might have jumped to new bounds that do not intersect with the old bounds + final Point nwOldPx; + if (nw == null) { + nwOldPx = null; + } else { + nwOldPx = mapProjection.fromLatLngToDivPixel(nw); + } + nw = LatLng.newInstance(getMap().getBounds().getNorthEast().getLatitude(), getMap().getBounds().getSouthWest().getLongitude()); + final Point nwNewPx = mapProjection.fromLatLngToDivPixel(nw); + widgetPosLeft = Math.round(nwNewPx.getX()); + widgetPosTop = Math.round(nwNewPx.getY()); + // calculate the translation-vector between old and new origin in pixels + if (nwOldPx == null) { + diffPx = new Vector(0, 0); + } else { + double oldPosLeft = Math.round(nwOldPx.getX()); + double oldPosTop = Math.round(nwOldPx.getY()); + diffPx = new Vector(oldPosLeft - widgetPosLeft, oldPosTop - widgetPosTop); + } + // store canvas-content, because setWidth() and setHeight() will clear canvas and change Context2d + Context2d ctxt = canvas.getContext2d(); + final ImageData canvasContent = ctxt.getImageData(0, 0, canvas.getElement().getClientWidth(), canvas.getElement().getClientHeight()); + canvas.setWidth(String.valueOf(canvasWidth)); + canvas.setHeight(String.valueOf(canvasHeight)); + canvas.setCoordinateSpaceWidth(canvasWidth); + canvas.setCoordinateSpaceHeight(canvasHeight); + // get updated Context2d and restore canvas-content moved by translation-vector + ctxt = canvas.getContext2d(); + ctxt.putImageData(canvasContent, diffPx.x, diffPx.y); + // update canvas position + setCanvasPosition(widgetPosLeft, widgetPosTop); } - nw = LatLng.newInstance(getMap().getBounds().getNorthEast().getLatitude(), getMap().getBounds().getSouthWest().getLongitude()); - Point nwNewPx = mapProjection.fromLatLngToDivPixel(nw); - widgetPosLeft = Math.round(nwNewPx.getX()); - widgetPosTop = Math.round(nwNewPx.getY()); - - // calculate the translation-vector between old and new origin in pixels - if (nwOldPx == null) { - diffPx = new Vector(0, 0); - } else { - double oldPosLeft = Math.round(nwOldPx.getX()); - double oldPosTop = Math.round(nwOldPx.getY()); - diffPx = new Vector(oldPosLeft - widgetPosLeft, oldPosTop - widgetPosTop); - } - - // store canvas-content, because setWidth() and setHeight() will clear canvas and change Context2d - Context2d ctxt = canvas.getContext2d(); - ImageData canvasContent = ctxt.getImageData(0, 0, canvas.getElement().getClientWidth(), canvas.getElement().getClientHeight()); - canvas.setWidth(String.valueOf(canvasWidth)); - canvas.setHeight(String.valueOf(canvasHeight)); - canvas.setCoordinateSpaceWidth(canvasWidth); - canvas.setCoordinateSpaceHeight(canvasHeight); - // get updated Context2d and restore canvas-content moved by translation-vector - ctxt = canvas.getContext2d(); - ctxt.putImageData(canvasContent, diffPx.x, diffPx.y); - - // update canvas position - setCanvasPosition(widgetPosLeft, widgetPosTop); } @Override diff --git a/java/com.sap.sailing.server.gateway.interfaces/src/com/sap/sailing/server/gateway/interfaces/MasterDataImportConstants.java b/java/com.sap.sailing.server.gateway.interfaces/src/com/sap/sailing/server/gateway/interfaces/MasterDataImportConstants.java new file mode 100644 index 00000000000..79f16e83937 --- /dev/null +++ b/java/com.sap.sailing.server.gateway.interfaces/src/com/sap/sailing/server/gateway/interfaces/MasterDataImportConstants.java @@ -0,0 +1,10 @@ +package com.sap.sailing.server.gateway.interfaces; + +public interface MasterDataImportConstants { + String MASTER_DATA_RESOURCE_BASE_URL = "/v1/masterdata/leaderboardgroups"; + String QUERY_PARAM_UUIDS = "uuids[]"; + String QUERY_PARAM_COMPRESS = "compress"; + String QUERY_PARAM_EXPORT_WIND = "exportWind"; + String QUERY_PARAM_EXPORT_DEVICE_CONFIGS = "exportDeviceConfigs"; + String QUERY_PARAM_EXPORT_TRACKED_RACES_AND_START_TRACKING = "exportTrackedRacesAndStartTracking"; +} diff --git a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/spi/MasterDataResource.java b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/spi/MasterDataResource.java index 4edf5dc9cab..7a8bd3f5661 100644 --- a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/spi/MasterDataResource.java +++ b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/spi/MasterDataResource.java @@ -41,23 +41,24 @@ import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; import com.sap.sailing.domain.masterdataimport.TopLevelMasterData; import com.sap.sailing.domain.tracking.RaceTrackingConnectivityParameters; import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sailing.server.gateway.interfaces.MasterDataImportConstants; import com.sap.sailing.shared.server.gateway.jaxrs.AbstractSailingServerResource; import com.sap.sse.security.SecurityService; import com.sap.sse.security.shared.impl.SecuredSecurityTypes.PublicReadableActions; import com.sap.sse.security.shared.impl.SecuredSecurityTypes.ServerActions; import com.sap.sse.security.shared.impl.User; -@Path("/v1/masterdata/leaderboardgroups") +@Path(MasterDataImportConstants.MASTER_DATA_RESOURCE_BASE_URL) public class MasterDataResource extends AbstractSailingServerResource { private static final Logger logger = Logger.getLogger(MasterDataResource.class.getName()); @POST @Produces("application/x-java-serialized-object") - public Response getMasterDataByLeaderboardGroups(@QueryParam("uuids[]") List requestedLeaderboardGroupsUuids, - @QueryParam("compress") Boolean compress, @QueryParam("exportWind") Boolean exportWind, - @QueryParam("exportDeviceConfigs") Boolean exportDeviceConfigs, - @QueryParam("exportTrackedRacesAndStartTracking") Boolean exportTrackedRacesAndStartTracking) + public Response getMasterDataByLeaderboardGroups(@QueryParam(MasterDataImportConstants.QUERY_PARAM_UUIDS) List requestedLeaderboardGroupsUuids, + @QueryParam(MasterDataImportConstants.QUERY_PARAM_COMPRESS) Boolean compress, @QueryParam(MasterDataImportConstants.QUERY_PARAM_EXPORT_WIND) Boolean exportWind, + @QueryParam(MasterDataImportConstants.QUERY_PARAM_EXPORT_DEVICE_CONFIGS) Boolean exportDeviceConfigs, + @QueryParam(MasterDataImportConstants.QUERY_PARAM_EXPORT_TRACKED_RACES_AND_START_TRACKING) Boolean exportTrackedRacesAndStartTracking) throws UnsupportedEncodingException { final SecurityService securityService = getSecurityService(); User user = securityService.getCurrentUser(); diff --git a/java/com.sap.sailing.server.interface/src/com/sap/sailing/server/operationaltransformation/ImportMasterDataOperation.java b/java/com.sap.sailing.server.interface/src/com/sap/sailing/server/operationaltransformation/ImportMasterDataOperation.java index e39d809ae2b..4c39de00504 100755 --- a/java/com.sap.sailing.server.interface/src/com/sap/sailing/server/operationaltransformation/ImportMasterDataOperation.java +++ b/java/com.sap.sailing.server.interface/src/com/sap/sailing/server/operationaltransformation/ImportMasterDataOperation.java @@ -339,7 +339,7 @@ public class ImportMasterDataOperation extends } else if (override) { for (RaceColumn raceColumn : existingLeaderboards.get(leaderboard.getName()).getRaceColumns()) { for (Fleet fleet : raceColumn.getFleets()) { - TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); + final TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); if (trackedRace != null) { raceColumn.releaseTrackedRace(fleet); } @@ -414,7 +414,7 @@ public class ImportMasterDataOperation extends private void addAllImportedEvents(MongoObjectFactory mongoObjectFactory, RaceLogStore mongoRaceLogStore, final RaceLog log, RaceLogIdentifier identifier) { - RaceLogEventVisitor storeVisitor = MongoRaceLogStoreFactory.INSTANCE + final RaceLogEventVisitor storeVisitor = MongoRaceLogStoreFactory.INSTANCE .getMongoRaceLogStoreVisitor(identifier, mongoObjectFactory); log.lockForRead(); try { diff --git a/java/com.sap.sailing.server/META-INF/MANIFEST.MF b/java/com.sap.sailing.server/META-INF/MANIFEST.MF index aa8a24f2f57..c9ad01f08e4 100755 --- a/java/com.sap.sailing.server/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.server/META-INF/MANIFEST.MF @@ -8,6 +8,7 @@ Bundle-Activator: com.sap.sailing.server.impl.Activator Bundle-Vendor: SAP Bundle-RequiredExecutionEnvironment: JavaSE-1.8 Import-Package: com.sap.sailing.server.gateway.deserialization, + com.sap.sailing.server.gateway.interfaces, javax.mail;version="1.4.0", javax.mail.internet;version="1.4.0", javax.mail.util;version="1.4.0", diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy OSX).launch b/java/com.sap.sailing.server/SailingServer (No Proxy OSX).launch index 766c8027994..247a70facc5 100755 --- a/java/com.sap.sailing.server/SailingServer (No Proxy OSX).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy OSX).launch @@ -18,7 +18,7 @@ - + @@ -28,292 +28,288 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy).launch b/java/com.sap.sailing.server/SailingServer (No Proxy).launch index 18a676c1da4..3da5c75f305 100755 --- a/java/com.sap.sailing.server/SailingServer (No Proxy).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy).launch @@ -24,7 +24,7 @@ - + diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy, Axel).launch b/java/com.sap.sailing.server/SailingServer (No Proxy, Axel).launch index dd4deb00dd6..ff32435cbe9 100644 --- a/java/com.sap.sailing.server/SailingServer (No Proxy, Axel).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy, Axel).launch @@ -13,13 +13,14 @@ + - + @@ -29,292 +30,288 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy, Java11).launch b/java/com.sap.sailing.server/SailingServer (No Proxy, Java11).launch index 10922cfa3e2..74b8914b771 100755 --- a/java/com.sap.sailing.server/SailingServer (No Proxy, Java11).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy, Java11).launch @@ -13,6 +13,7 @@ + @@ -21,7 +22,7 @@ - + @@ -31,292 +32,288 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889).launch b/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889).launch index ea2c1f95cef..d3373c6ef67 100755 --- a/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889).launch @@ -12,6 +12,7 @@ + @@ -20,296 +21,292 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, Cached MTB).launch b/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, Cached MTB).launch index c3040ae44a6..681e9f9fba4 100755 --- a/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, Cached MTB).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, Cached MTB).launch @@ -1,314 +1,311 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, auto-replicate SS and SSD).launch b/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, auto-replicate SS and SSD).launch index 3e825d03f0d..b26acac0dac 100755 --- a/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, auto-replicate SS and SSD).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, auto-replicate SS and SSD).launch @@ -23,296 +23,292 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, auto-replicate dev).launch b/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, auto-replicate dev).launch index 4182fc35901..df1926ba70a 100755 --- a/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, auto-replicate dev).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, auto-replicate dev).launch @@ -21,7 +21,7 @@ - + diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, auto-replicate).launch b/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, auto-replicate).launch index d6c5e9855c6..f742ec8b9fa 100755 --- a/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, auto-replicate).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, auto-replicate).launch @@ -12,6 +12,7 @@ + @@ -22,296 +23,292 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8890).launch b/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8890).launch index 37dbda47910..ea6570e177a 100755 --- a/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8890).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8890).launch @@ -12,6 +12,7 @@ + @@ -20,296 +21,292 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8890, auto-replicate from replica at 8889).launch b/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8890, auto-replicate from replica at 8889).launch index fc93c6b4744..1a14e48af9d 100755 --- a/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8890, auto-replicate from replica at 8889).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8890, auto-replicate from replica at 8889).launch @@ -1,318 +1,315 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy, Remote Debug SAP VM).launch b/java/com.sap.sailing.server/SailingServer (No Proxy, Remote Debug SAP VM).launch index e6e712a75ce..23b0bc20ad5 100755 --- a/java/com.sap.sailing.server/SailingServer (No Proxy, Remote Debug SAP VM).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy, Remote Debug SAP VM).launch @@ -1,313 +1,310 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy, winddb Axel).launch b/java/com.sap.sailing.server/SailingServer (No Proxy, winddb Axel).launch index 2c9cd966b0b..b8f86fc095e 100644 --- a/java/com.sap.sailing.server/SailingServer (No Proxy, winddb Axel).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy, winddb Axel).launch @@ -1,319 +1,316 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy, winddbTest Axel).launch b/java/com.sap.sailing.server/SailingServer (No Proxy, winddbTest Axel).launch index b3aad4313dc..74ec00e141e 100755 --- a/java/com.sap.sailing.server/SailingServer (No Proxy, winddbTest Axel).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy, winddbTest Axel).launch @@ -1,321 +1,317 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy, winddbTest).launch b/java/com.sap.sailing.server/SailingServer (No Proxy, winddbTest).launch index 6d24d6573b3..84955539731 100755 --- a/java/com.sap.sailing.server/SailingServer (No Proxy, winddbTest).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy, winddbTest).launch @@ -22,7 +22,7 @@ - + @@ -32,294 +32,289 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com.sap.sailing.server/SailingServer (Proxy, Jetty on 8889, Polar & Wind estimation Import from 8888).launch b/java/com.sap.sailing.server/SailingServer (Proxy, Jetty on 8889, Polar & Wind estimation Import from 8888).launch index d36aa425d68..52ceb79c86a 100755 --- a/java/com.sap.sailing.server/SailingServer (Proxy, Jetty on 8889, Polar & Wind estimation Import from 8888).launch +++ b/java/com.sap.sailing.server/SailingServer (Proxy, Jetty on 8889, Polar & Wind estimation Import from 8888).launch @@ -12,6 +12,10 @@ + + + + @@ -20,296 +24,292 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 027aa3ee93c..631cfd03daf 100644 --- 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 @@ -253,6 +253,7 @@ import com.sap.sailing.server.gateway.deserialization.impl.LeaderboardGroupBaseJ import com.sap.sailing.server.gateway.deserialization.impl.LeaderboardSearchResultBaseJsonDeserializer; import com.sap.sailing.server.gateway.deserialization.impl.TrackingConnectorInfoJsonDeserializer; import com.sap.sailing.server.gateway.deserialization.impl.VenueJsonDeserializer; +import com.sap.sailing.server.gateway.interfaces.MasterDataImportConstants; import com.sap.sailing.server.impl.preferences.model.CompetitorNotificationPreference; import com.sap.sailing.server.impl.preferences.model.CompetitorNotificationPreferences; import com.sap.sailing.server.interfaces.CourseAndMarkConfigurationFactory; @@ -4161,12 +4162,12 @@ Replicator { @Override public void reloadRaceLog(String leaderboardName, String raceColumnName, String fleetName) { - Leaderboard leaderboard = getLeaderboardByName(leaderboardName); + final Leaderboard leaderboard = getLeaderboardByName(leaderboardName); if (leaderboard != null) { - RaceColumn raceColumn = leaderboard.getRaceColumnByName(raceColumnName); + final RaceColumn raceColumn = leaderboard.getRaceColumnByName(raceColumnName); if (raceColumn != null) { - Fleet fleetImpl = raceColumn.getFleetByName(fleetName); - RaceLog racelog = raceColumn.getRaceLog(fleetImpl); + final Fleet fleetImpl = raceColumn.getFleetByName(fleetName); + final RaceLog racelog = raceColumn.getRaceLog(fleetImpl); if (racelog != null) { raceColumn.reloadRaceLog(fleetImpl); logger.info("Reloaded race log for fleet " + fleetImpl + " for race column " + raceColumn.getName() @@ -5214,8 +5215,7 @@ Replicator { 0.5); final String query; try { - query = createLeaderboardQuery(leaderboardGroupIds, compress, exportWind, exportDeviceConfigurations, - exportTrackedRacesAndStartTracking); + query = createLeaderboardGroupQuery(leaderboardGroupIds, compress, exportWind, exportDeviceConfigurations, exportTrackedRacesAndStartTracking); } catch (UnsupportedEncodingException e1) { throw new RuntimeException(e1); } @@ -5278,16 +5278,22 @@ Replicator { } } - private String createLeaderboardQuery(UUID[] leaderboardGroupIds, boolean compress, boolean exportWind, + private String createLeaderboardGroupQuery(UUID[] leaderboardGroupIds, boolean compress, boolean exportWind, boolean exportDeviceConfigurations, boolean exportTrackedRacesAndStartTracking) throws UnsupportedEncodingException { - StringBuffer queryStringBuffer = new StringBuffer(""); + StringBuilder queryStringBuilder = new StringBuilder(); for (UUID uuid : leaderboardGroupIds) { - queryStringBuffer.append("uuids[]=" + uuid + "&"); + queryStringBuilder.append(MasterDataImportConstants.QUERY_PARAM_UUIDS); + queryStringBuilder.append('='); + queryStringBuilder.append(uuid); + queryStringBuilder.append('&'); } - queryStringBuffer.append(String.format("compress=%s&exportWind=%s&exportDeviceConfigs=%s&exportTrackedRacesAndStartTracking=%s", compress, - exportWind, exportDeviceConfigurations, exportTrackedRacesAndStartTracking)); - return queryStringBuffer.toString(); + queryStringBuilder.append(String.format(MasterDataImportConstants.QUERY_PARAM_COMPRESS+"=%s&" + +MasterDataImportConstants.QUERY_PARAM_EXPORT_WIND+"=%s&" + +MasterDataImportConstants.QUERY_PARAM_EXPORT_DEVICE_CONFIGS+"=%s&" + +MasterDataImportConstants.QUERY_PARAM_EXPORT_TRACKED_RACES_AND_START_TRACKING+"=%s", + compress, exportWind, exportDeviceConfigurations, exportTrackedRacesAndStartTracking)); + return queryStringBuilder.toString(); } private class TimeoutExtendingInputStream extends FilterInputStream { diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/MultiTimeRange.java b/java/com.sap.sse.common/src/com/sap/sse/common/MultiTimeRange.java index 9634e2cd202..8ea9ca765cf 100644 --- a/java/com.sap.sse.common/src/com/sap/sse/common/MultiTimeRange.java +++ b/java/com.sap.sse.common/src/com/sap/sse/common/MultiTimeRange.java @@ -2,6 +2,8 @@ package com.sap.sse.common; import java.io.Serializable; +import com.sap.sse.common.impl.MultiTimeRangeImpl; + /** * A minimal sequence of non-overlapping, non-touching, non-{@link TimeRange#isEmpty() empty} {@link TimeRange} objects. * The iteration order is from earlier to later time range. An object of this type may be empty. Only the last @@ -18,6 +20,14 @@ import java.io.Serializable; * */ public interface MultiTimeRange extends Iterable, Serializable { + static MultiTimeRange of(TimeRange... timeRanges) { + return new MultiTimeRangeImpl(timeRanges); + } + + static MultiTimeRange of(Iterable timeRanges) { + return new MultiTimeRangeImpl(timeRanges); + } + /** * @return a multi time range that {@link #includes(MultiTimeRange) includes} the {@code other} multi time range and * {@code this}, and that {@link #includes(TimePoint) includes} only time points that are diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/TimeRange.java b/java/com.sap.sse.common/src/com/sap/sse/common/TimeRange.java index 0919c308afd..5846fe2f0bc 100755 --- a/java/com.sap.sse.common/src/com/sap/sse/common/TimeRange.java +++ b/java/com.sap.sse.common/src/com/sap/sse/common/TimeRange.java @@ -159,7 +159,8 @@ public interface TimeRange extends Comparable, Serializable { * Produces a {@link TimeRange} that {@link TimeRange#includes(TimeRange) includes} both, {@code this} and * {@code other}. Other than {@link #union(TimeRange)}, this will also work in case {@code other} does not * {@link #touches(TimeRange) touch} {@code this} time range. If {@code this} already {@link #includes(TimeRange)} - * {@code other}, {@code this} time range is returned. + * {@code other}, {@code this} time range is returned. If {@code other} is {@code null}, {@code this} time range is + * returned. */ TimeRange extend(TimeRange other); diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/impl/TimeRangeImpl.java b/java/com.sap.sse.common/src/com/sap/sse/common/impl/TimeRangeImpl.java index dce4a9df54a..11838a17ba3 100644 --- a/java/com.sap.sse.common/src/com/sap/sse/common/impl/TimeRangeImpl.java +++ b/java/com.sap.sse.common/src/com/sap/sse/common/impl/TimeRangeImpl.java @@ -259,12 +259,16 @@ public class TimeRangeImpl extends Util.Pair implements Ti @Override public TimeRange extend(TimeRange other) { - final TimeRange preResult = this.extend(other.from()); final TimeRange result; - if (preResult.to().before(other.to())) { - result = new TimeRangeImpl(preResult.from(), other.to()); + if (other == null) { + result = this; } else { - result = preResult; + final TimeRange preResult = this.extend(other.from()); + if (preResult.to().before(other.to())) { + result = new TimeRangeImpl(preResult.from(), other.to()); + } else { + result = preResult; + } } return result; } diff --git a/java/com.sap.sse.datamining.ui/src/main/java/com/sap/sse/datamining/ui/client/selection/statistic/SuggestBoxStatisticProvider.java b/java/com.sap.sse.datamining.ui/src/main/java/com/sap/sse/datamining/ui/client/selection/statistic/SuggestBoxStatisticProvider.java index 2afccb53344..8707d0de83f 100644 --- a/java/com.sap.sse.datamining.ui/src/main/java/com/sap/sse/datamining/ui/client/selection/statistic/SuggestBoxStatisticProvider.java +++ b/java/com.sap.sse.datamining.ui/src/main/java/com/sap/sse/datamining/ui/client/selection/statistic/SuggestBoxStatisticProvider.java @@ -388,12 +388,11 @@ public class SuggestBoxStatisticProvider extends AbstractDataMiningComponent> createSettingsComponentsFor(final DataRetrieverChainDefinitionDTO retrieverChain) { - List> settingsComponents = new ArrayList<>(); - for (Entry retrieverLevelSettings : settingsMap.get(retrieverChain) - .entrySet()) { + final List> settingsComponents = new ArrayList<>(); + for (final Entry retrieverLevelSettings : settingsMap.get(retrieverChain).entrySet()) { final DataRetrieverLevelDTO retrieverLevel = retrieverLevelSettings.getKey(); final Class settingsType = retrieverLevelSettings.getValue().getClass(); - DataMiningSettingsInfo settingsInfo = settingsManager.getSettingsInfo(settingsType); + final DataMiningSettingsInfo settingsInfo = settingsManager.getSettingsInfo(settingsType); settingsComponents.add(new RetrieverLevelSettingsComponent(this, getComponentContext(), retrieverLevel, settingsInfo.getId(), settingsInfo.getLocalizedName()) { @Override diff --git a/java/com.sap.sse.datamining/src/com/sap/sse/datamining/impl/components/management/NullDataMiningQueryManager.java b/java/com.sap.sse.datamining/src/com/sap/sse/datamining/impl/components/management/NullDataMiningQueryManager.java index 7572bfe8a0f..a02a9d2e64a 100644 --- a/java/com.sap.sse.datamining/src/com/sap/sse/datamining/impl/components/management/NullDataMiningQueryManager.java +++ b/java/com.sap.sse.datamining/src/com/sap/sse/datamining/impl/components/management/NullDataMiningQueryManager.java @@ -23,7 +23,7 @@ public class NullDataMiningQueryManager implements DataMiningQueryManager { @Override public QueryResult runNewAndAbortPrevious(DataMiningSession session, Query query) { - logger.info("This query manager, doesn't manage anything. Just running the query " + query); + logger.info("This query manager doesn't manage anything. Just running the query " + query); queries.add(query); final QueryResult result = query.run(); queries.remove(query); diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/async/AsyncAction.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/async/AsyncAction.java index a8b0af7c25a..a24d092975c 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/async/AsyncAction.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/async/AsyncAction.java @@ -3,7 +3,10 @@ package com.sap.sse.gwt.client.async; import com.google.gwt.user.client.rpc.AsyncCallback; /** - * An action that which will be executed a asynchronous remote call to the server + * An action that which will be executed a asynchronous remote call to the server or may get dropped + * by the executor, e.g., if there is an excessive number of responses outstanding. When dropping an + * action, the executor will invoke the {@link #dropped()} method on this action. The action may + * decide to then take measures to compensate somehow for getting dropped. * * @param * The type of the returned value of the call @@ -12,4 +15,12 @@ import com.google.gwt.user.client.rpc.AsyncCallback; @FunctionalInterface public interface AsyncAction { void execute(AsyncCallback callback); + + /** + * Will be called by the {@link AsyncActionsExecutor} when dropping this action. This way, an action may react to + * the dropping and may, e.g., enqueue some other, maybe simpler, request for later execution, e.g., in order to + * achieve some eventual consistency. + */ + default void dropped(AsyncActionsExecutor executor) { + } } diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/async/AsyncActionsExecutor.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/async/AsyncActionsExecutor.java index 6b065b3fc0d..2847a935272 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/async/AsyncActionsExecutor.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/async/AsyncActionsExecutor.java @@ -45,6 +45,10 @@ public class AsyncActionsExecutor { this.action.execute(new MarkedAsyncCallback(this, getCategory())); } + public void dropped() { + action.dropped(AsyncActionsExecutor.this); + } + @Override public void onSuccess(T result) { try { @@ -57,7 +61,7 @@ public class AsyncActionsExecutor { @Override public void onFailure(Throwable caught) { try { - logger.warning("Execution failure for action of type " + getType() + ", category "+getCategory()); + logger.warning("Execution failure for action of type " + getType() + ", category "+getCategory()+": "+caught.getMessage()); this.callback.onFailure(caught); } finally { AsyncActionsExecutor.this.callCompleted(this); @@ -156,13 +160,10 @@ public class AsyncActionsExecutor { } private void execute(ExecutionJob job) { - Integer numActionsOfType = actionsPerTypeCounter.get(job.getType()); - if (numActionsOfType == null) { - numActionsOfType = Integer.valueOf(0); - } + Integer numActionsOfType = actionsPerTypeCounter.computeIfAbsent(job.getType(), j->Integer.valueOf(0)); if (numPendingCalls >= maxPendingCalls || (numActionsOfType >= maxPendingCallsPerType)) { - TimePoint now = MillisecondsTimePoint.now(); - TimePoint timePointToInspectForResetDecision = timePointOfTypeLastBeingExecuted.get(job.getType()) != null ? + final TimePoint now = MillisecondsTimePoint.now(); + final TimePoint timePointToInspectForResetDecision = timePointOfTypeLastBeingExecuted.get(job.getType()) != null ? timePointOfTypeLastBeingExecuted.get(job.getType()) : timePointOfFirstExecutorInit; if (timePointToInspectForResetDecision != null && now.minus(durationAfterToResetQueue).after(timePointToInspectForResetDecision)) { @@ -171,7 +172,7 @@ public class AsyncActionsExecutor { // reset number of pending actions per type - 0 is fine as checkForEmptyCallQueue // will check for a number less than maxPendingCallsPerType to send out the // last job pending for a given type - for (String jobPendingTypeKey : lastRequestedActionsNotBeingSentOut.keySet()) { + for (final String jobPendingTypeKey : lastRequestedActionsNotBeingSentOut.keySet()) { actionsPerTypeCounter.put(jobPendingTypeKey, 0); } numActionsOfType = 0; @@ -182,7 +183,12 @@ public class AsyncActionsExecutor { * are other jobs of that type that need execution and execute the last one thus * emptying the lastRequestedActionsQueue. * */ - lastRequestedActionsNotBeingSentOut.put(job.getType(), job); + final ExecutionJob droppedJob = lastRequestedActionsNotBeingSentOut.put(job.getType(), job); + if (droppedJob != null) { + // a job not sent out was replaced by the latest one not being sent out; + // the job replaced will definitely not be executed anymore; notify it: + droppedJob.dropped(); + } return; } } diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/async/TimeRangeResultCache.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/async/TimeRangeResultCache.java index dd6714b0b71..24de60ded8b 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/async/TimeRangeResultCache.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/async/TimeRangeResultCache.java @@ -384,7 +384,7 @@ public class TimeRangeResultCache { * {@link SubResult} of the trimmed request; this result will be cached */ public void registerResult(TimeRangeAsyncAction action, SubResult subResult) { - Request request = requestCache.get(action); + final Request request = requestCache.get(action); if (request == null) { throw new IllegalArgumentException("Attempted to register result for non-existent request: " + action.toString()); } @@ -400,7 +400,7 @@ public class TimeRangeResultCache { * {@link Throwable} that occurred */ public void registerFailure(TimeRangeAsyncAction action, Throwable cause) { - Request request = requestCache.get(action); + final Request request = requestCache.get(action); if (request != null) { request.onFailure(cause); } @@ -419,7 +419,7 @@ public class TimeRangeResultCache { * requested by {@code action} */ public List> getResults(TimeRangeAsyncAction action) throws IllegalArgumentException { - Request request = requestCache.get(action); + final Request request = requestCache.get(action); if (request == null) { throw new IllegalArgumentException("No request found for action: " + action.toString()); } @@ -433,7 +433,7 @@ public class TimeRangeResultCache { * corresponding {@link TimeRangeAsyncAction} */ public void removeRequest(TimeRangeAsyncAction action) throws IllegalArgumentException { - Request request = requestCache.get(action); + final Request request = requestCache.get(action); if (request == null) { throw new IllegalArgumentException("No request found for action: " + action.toString()); } @@ -457,8 +457,8 @@ public class TimeRangeResultCache { TimeRangeAsyncAction action, boolean forceTimeRange) { // TODO There is a lot of potential for improvements here TimeRange potentiallyTrimmed = toTrim; - List rangesToTrimWithAsList = new LinkedList<>(requestCache.values()); - List childrenList = new ArrayList<>(); + final List rangesToTrimWithAsList = new LinkedList<>(requestCache.values()); + final List childrenList = new ArrayList<>(); iterationsLoop: for (int i = 0; i < TRIM_MAX_ITERATIONS; i++) { boolean rangeWasTrimmedThisIteration = false; final Iterator iter = rangesToTrimWithAsList.iterator(); diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/slider/SliderBar.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/slider/SliderBar.java index 5e46eae2761..1a4a364607f 100755 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/slider/SliderBar.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/slider/SliderBar.java @@ -675,16 +675,14 @@ public class SliderBar extends FocusPanel implements RequiresResize, HasValue (stepSize / 2)) && ((newValue + stepSize) <= maxValue)) { newValue += stepSize; } - - boolean isValueChanged = !newValue.equals(this.curValue) && this.curValue != null; + final boolean isValueChanged = !newValue.equals(this.curValue) && this.curValue != null; this.curValue = newValue; // Redraw the knob drawKnob(); diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/premium/PremiumListBox.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/premium/PremiumListBox.java new file mode 100644 index 00000000000..553bbcb8f61 --- /dev/null +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/premium/PremiumListBox.java @@ -0,0 +1,182 @@ +package com.sap.sse.security.ui.client.premium; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.event.dom.client.ChangeHandler; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.HasAllKeyHandlers; +import com.google.gwt.event.dom.client.KeyDownHandler; +import com.google.gwt.event.dom.client.KeyPressHandler; +import com.google.gwt.event.dom.client.KeyUpHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.resources.client.CssResource; +import com.google.gwt.uibinder.client.UiBinder; +import com.google.gwt.uibinder.client.UiField; +import com.google.gwt.uibinder.client.UiHandler; +import com.google.gwt.user.client.ui.FocusPanel; +import com.google.gwt.user.client.ui.FocusWidget; +import com.google.gwt.user.client.ui.Image; +import com.google.gwt.user.client.ui.ListBox; +import com.sap.sse.gwt.client.dialog.ConfirmationDialog; +import com.sap.sse.security.shared.HasPermissions.Action; +import com.sap.sse.security.shared.dto.SecuredDTO; +import com.sap.sse.security.shared.dto.UserDTO; +import com.sap.sse.security.ui.client.UserStatusEventHandler; +import com.sap.sse.security.ui.client.i18n.StringMessages; + +public abstract class PremiumListBox extends PremiumUiElement implements HasAllKeyHandlers { + + private static PremiumCheckBoxUiBinder uiBinder = GWT.create(PremiumCheckBoxUiBinder.class); + + interface PremiumCheckBoxUiBinder extends UiBinder { + } + + interface Style extends CssResource { + @ClassName("premium-container") + String premiumContainer(); + + @ClassName("premium-list-box") + String premiumListBox(); + + @ClassName("premium-permitted") + String premiumPermitted(); + + @ClassName("premium-icon") + String premiumIcon(); + } + + @UiField + StringMessages i18n; + @UiField + Style style; + @UiField + FocusPanel container; + @UiField(provided = true) + protected final Image image; + @UiField(provided = true) + protected final ListBox listBox; + private final ConfirmationDialog subscribeDialog; + + /** + * A Composite component, that includes a list box and an additional premium icon, indicating that the feature to be + * enabled is a premium feature if the user does not have the permission. By definition there is always minimum one + * (empty) item with index 0. + */ + protected PremiumListBox(final String emptyLabel, String emptyValue, final Action action, + final PaywallResolver paywallResolver, final SecuredDTO contextDTO) { + super(action, paywallResolver, contextDTO); + this.listBox = new ListBox(); + this.listBox.addItem(emptyLabel, emptyValue); + this.listBox.setSelectedIndex(0); + this.image = createPremiumIcon(); + initWidget(uiBinder.createAndBindUi(this)); + this.subscribeDialog = ConfirmationDialog.create(i18n.subscriptionSuggestionTitle(), + i18n.pleaseSubscribeToUse(), i18n.takeMeToSubscriptions(), i18n.cancel(), () -> paywallResolver + .getUnlockingSubscriptionPlans(action, contextDTO, this::onSubscribeDialogConfirmation)); + updateUserPermission(); + paywallResolver.registerUserStatusEventHandler(new UserStatusEventHandler() { + @Override + public void onUserStatusChange(UserDTO user, boolean preAuthenticated) { + + } + }); + } + + public void reset() { + while (this.listBox.getItemCount() > 1) { + this.listBox.removeItem(1); + } + } + + protected abstract void onSubscribeDialogConfirmation(Iterable unlockingPlans); + + @Override + protected void onEnsureDebugId(final String baseID) { + this.listBox.ensureDebugId(baseID); + } + + @UiHandler("container") + void onContainerClicked(final ClickEvent event) { + if (!hasPermission()) { + this.updateUserPermission(); + subscribeDialog.center(); + } + } + + @Override + protected void onUserPermissionUpdate(final boolean isPermitted) { + listBox.setSelectedIndex(getSelectedIndex()); + listBox.setEnabled(isEnabled() && isPermitted); + container.setStyleName(style.premiumPermitted(), isPermitted); + } + + public void addItem(String item, String value) { + if (hasPermission()) { + this.listBox.addItem(item, value); + } + } + + public void setSelectedIndex(final int index) { + if (hasPermission()) { + this.listBox.setSelectedIndex(index); + } + } + + public int getSelectedIndex() { + final int selectedIndex; + if (hasPermission()) { + selectedIndex = this.listBox.getSelectedIndex(); + } else { + selectedIndex = 0; + } + return selectedIndex; + } + + /** + * If NO permission is granted select empty item (index 0) and return the default empty value. + */ + public String getSelectedValue() { + if (!hasPermission()) { + this.listBox.setSelectedIndex(0); + } + return this.listBox.getSelectedValue(); + } + + public void setVisibleItemCount(int visibleItems) { + if (hasPermission()) { + this.listBox.setVisibleItemCount(visibleItems); + } + } + + public HandlerRegistration addChangeHandler(final ChangeHandler handler) { + return listBox.addChangeHandler(handler); + } + + @Override + public void setEnabled(final boolean enabled) { + super.setEnabled(enabled); + listBox.setEnabled(enabled); + } + + @Override + public HandlerRegistration addKeyDownHandler(final KeyDownHandler handler) { + return container.addKeyDownHandler(handler); + } + + @Override + public HandlerRegistration addKeyPressHandler(final KeyPressHandler handler) { + return container.addKeyPressHandler(handler); + } + + @Override + public HandlerRegistration addKeyUpHandler(final KeyUpHandler handler) { + return container.addKeyUpHandler(handler); + } + + public FocusWidget getFocusWidget() { + return this.listBox; + } + + public ListBox getListBox() { + return this.listBox; + } +} diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/premium/PremiumListBox.ui.xml b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/premium/PremiumListBox.ui.xml new file mode 100644 index 00000000000..836a82fcdb4 --- /dev/null +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/premium/PremiumListBox.ui.xml @@ -0,0 +1,48 @@ + + + + + .premium-listbox { + display: inline-block; + } + .premium-container, + .premium-container select:disabled.premium-list-box + label { + cursor: pointer; + } + .premium-container .premium-list-box { + padding-right: 0; + } + .premium-container select.premium-list-box { + border-radius: 4px; + background-color: rgba(255,255,255,0.75); + border: 1px solid #efaa00; + } + .premium-container { + padding-right: 10px; + top: -3px; + position: relative; + } + .premium-permitted .premium-container, + .premium-permitted .premium-container select:disabled.premium-list-box + label { + cursor: default; + } + .premium-container select:disabled.premium-list-box { + pointer-events: none; + } + .premium-container .premium-icon { + width: 17px; + height: 17px; + margin: 2px; + vertical-align: top; + } + .premium-permitted .premium-container .premium-icon { + display: none; + } + + + + + + + + \ No newline at end of file diff --git a/java/pom.xml b/java/pom.xml index 6f95d8adcdd..a56bcbec43f 100755 --- a/java/pom.xml +++ b/java/pom.xml @@ -47,7 +47,7 @@ Can we move the system property -Dmongo.dbName to "parameters.mongodb"? If so the IP of the host would be the same for all profiles. --> - -Dfile.encoding=cp1252 -Dmongo.dbName=winddbTest -DspatialWind=true -Xss100m -Dgwt.rpc.version=9 + -Dfile.encoding=cp1252 -Dmongo.dbName=winddbTest -DspatialWind=true -Xss100m -Dgwt.rpc.version=9 -Dgoogle.maps.authenticationparams=key=AIzaSyD1Se4tIkt-wglccbco3S7twaHiG20hR9E -ea -Xmx8192m -XX:+UseG1GC diff --git a/java/target/env-default-rules.sh b/java/target/env-default-rules.sh index 46ed73c8c2e..99edd2a46a7 100755 --- a/java/target/env-default-rules.sh +++ b/java/target/env-default-rules.sh @@ -116,3 +116,6 @@ fi if [ -n ${IGTIMI_CLIENT_SECRET} ]; then ADDITIONAL_JAVA_ARGS="${ADDITIONAL_JAVA_ARGS} -Digtimi.client.secret=${IGTIMI_CLIENT_SECRET}" fi +if [ -n ${GOOGLE_MAPS_AUTHENTICATION_PARAMS} ]; then + ADDITIONAL_JAVA_ARGS="${ADDITIONAL_JAVA_ARGS} -Dgoogle.maps.authenticationparams=${GOOGLE_MAPS_AUTHENTICATION_PARAMS}" +fi diff --git a/wiki/howto/onboarding.md b/wiki/howto/onboarding.md index f7679644603..32c66b08ccf 100644 --- a/wiki/howto/onboarding.md +++ b/wiki/howto/onboarding.md @@ -114,6 +114,8 @@ Out of the box, multiple settings in Eclipse need to be changed. Go to Window - In "General ⇒ Editors ⇒ Text Editors" check Insert Spaces for Tabs - In "General ⇒ Editors ⇒ Text Editors ⇒ Quick Diff" change the reference source from 'Version on Disk' to 'A Git Revision'. If you like other colours for marking diffs change them here. (Example: Changes = Yellow, Additions = Green, Deletions = Red) - If you'd like to be able to import official results from the Manage2Sail regatta management system: In Run/Debug ⇒ String Substitution add a variable ``MANAGE2SAIL_ACCESS_TOKEN``. Ask your team lead for the value of such an access token you can uses for testing. The variable is used by the "Sailing Server (No Proxy)" launch configuration. and maybe others. +- For Google Maps API access, the server needs to know authentication parameters. These are provided through an Eclipse variable used by various launch configurations named ``GOOGLE_MAPS_AUTHENTICATION_PARAMS``. Ask for the official SAP Google Maps API credentials, or use ``key=AIzaSyD1Se4tIkt-wglccbco3S7twaHiG20hR9E`` for a test key that works for your localhost-based tests. +- If you'd like to work with the Igtimi YachtBot / WindBot API, you'll have to provide two additional Eclipse variables: ``IGTIMI_CLIENT_ID`` and ``IGTIMI_CLIENT_SECRET``. Again, ask your team lead for those official credentials or, if you have administrative permissions to our production landscape, look them up under each sailing-analytics-server instance's ``/root/secrets`` file. - In "GWT ⇒ Errors/Warnings" set "Missing SDK" to "Ignore" - In "GWT ⇒ GWT Settings ⇒ Add..." add the GWT SDK you downloaded and unpacked earlier - In "Java ⇒ Build Path ⇒ Classpath Variables" create a new classpath variable called `ANDROID_HOME`. Set its value to the installation location of your Android SDK, e.g., `C:\Users\'user'\AppData\Local\Android\Sdk` or `/usr/local/android-sdk-linux`. diff --git a/wiki/info/landscape/creating-ec2-image-for-hudson-from-scratch.md b/wiki/info/landscape/creating-ec2-image-for-hudson-from-scratch.md index 49d2333c58b..5311aafa641 100644 --- a/wiki/info/landscape/creating-ec2-image-for-hudson-from-scratch.md +++ b/wiki/info/landscape/creating-ec2-image-for-hudson-from-scratch.md @@ -15,7 +15,7 @@ This is an add-on to the regular EC2 image set-up described [here](https://wiki. * Ensure you have EC2 / EBS snapshot backups for the volumes by tagging them as follows: ``WeeklySailingInfrastructureBackup=Yes`` for ``/`` and ``/home/hudson``. -``/home/hudson/repo`` has the Hudson build repository. The Hudson WAR file is under ``/usr/lib/hudson/hudson.war``. ``/etc/init.d/hudson``, linked to from ``/etc/rc0.d/K29hudson``, ``/etc/rc1.d/K29hudson``, ``/etc/rc2.d/K29hudson``, ``/etc/rc3.d/S81hudson``, ``/etc/rc4.d/K29hudson``, ``/etc/rc5.d/S81hudson``, and ``/etc/rc6.d/K29hudson``, takes care of spinning up Hudson during instance re-boot. Hudson systemwide configuration is under ``/etc/sysconfig/hudson``: +``/home/hudson/repo`` has the Hudson build repository. The Hudson WAR file is under ``/usr/lib/hudson/hudson.war`` which has to be taken from [here](https://static.sapsailing.com/hudson.war.patched-with-mail-1.6.2). ``/etc/init.d/hudson``, linked to from ``/etc/rc0.d/K29hudson``, ``/etc/rc1.d/K29hudson``, ``/etc/rc2.d/K29hudson``, ``/etc/rc3.d/S81hudson``, ``/etc/rc4.d/K29hudson``, ``/etc/rc5.d/S81hudson``, and ``/etc/rc6.d/K29hudson``, takes care of spinning up Hudson during instance re-boot. It can be obtained from [here](https://static.sapsailing.com/etc-init.d-hudson). Hudson systemwide configuration is under ``/etc/sysconfig/hudson``: ``` ## Path: Development/Hudson ## Description: Configuration for the Hudson continuous build server diff --git a/wiki/projects/race-map-tracks-loading-and-caching.md b/wiki/projects/race-map-tracks-loading-and-caching.md new file mode 100644 index 00000000000..782318ba097 --- /dev/null +++ b/wiki/projects/race-map-tracks-loading-and-caching.md @@ -0,0 +1,118 @@ +# Race Map Tracks Loading and Caching + +One of the key features of the ``RaceMap`` is to display the boats and their tracks, in the form of so-called "tails" that show the boats' positions during the last so many seconds. The tails may be displayed in different colors, depending on whether a competitor is selected and if the user selected a metric to visualize using the tail's color. + +The data that represents the GPS fixes with wind data for their specific location and time points, as well as detail values that may be used in coloring tails, are managed in a client-side cache so as to reduce the number of server round trips required when a user moves the time slider. This cache is implemented by the ``FixesAndTails`` class which also keeps the visible lines used to draw the tails on the map in sync with the tracking data cached. + +With bugs [5921](https://bugzilla.sapsailing.com/bugzilla/show_bug.cgi?id=5921) and [5925](https://bugzilla.sapsailing.com/bugzilla/show_bug.cgi?id=5925), issues with the implementation were observed and documented. This document lists the requirements, the issues with the implementation as of around 2023-12-13, and then sketches a re-design that shall help fulfill the requirements in better, more consistent ways, with fewer glitches and better performance. + +## Requirements + +There are various requirements for this combination of track loading / caching / visualization: + +- Work efficiently in live scenarios where new data becomes available as time progresses; in particular, load only the new data not yet cached locally as it becomes available, with as few calls as possible +- Enable showing extrapolated or interpolated boat positions if a tail or boat icon canvas needs to be drawn for a time point for which no fix exists +- Distinguish expensive requests for long tails, perhaps even with an expensive-to-compute detail metric, from determining the "current" position of all boats to be displayed for the time point for which to display the boats on the map; in other words, don't delay the drawing of the boats until all tail data has arrived +- Delay updating the tails slightly, especially in replay mode; see the ``Triggerable`` pattern already partly employed by ``FixesAndTails`` +- Avoid re-loading data already present; particularly, when requesting data for a time range and the cache already has data for this time range but doesn't exactly reach the start and end of this time range, don't force a re-load of the parts of the track already loaded +- Handle out-of-order responses appropriately; different sorts of requests may cause very different computational efforts and therefore responses may arrive in an order different from the order in which their requests were sent +- Dropping high-rate dispensable requests when users "scrub" the time line, ensuring that the latest request comes through in order to finally show the boat and mark positions as well as the wind data for the time point where the time slider's thumb is; this must yet ensure that the track data is consistent and complete for the tails shown, avoiding any permanent "data holes" caused by requests dropped; cache invariants must at least eventually be established. +- Data delivered late to the server in live mode shall be picked up by the tails, even if an earlier request would have included such fixes already but didn't because they arrived late + +## Current Solution (as of 2023-12-13; around commit 970ae1747b4d09ab1c2774f0019cc9fbe8457e3a) + +The ``RaceMap`` class closely collaborates with the ``FixesAndTails`` class which acts as a cache for partial tracks loaded from the back-end. For the visualization on the map, ``FixesAndTails`` uses ``Colorline`` objects which can produce lines consisting of segments which may have different colors where needed. ``RaceMap.callGetRaceMapDataForAllOverlappingAndTipsOfNonOverlappingAndGetBoatPositionsForAllOthers(...)`` is the method putting together the requests for track data. ``RaceMap.refreshMap(...)`` asks ``FixesAndTails.computeFromAndTo(...)`` to determine which segments of the tracks need to be loaded for which competitors and with which detail type, based on the tail length and only for those competitors to actually be shown on the map. The ``computeFromAndTo`` method then tries to determine the segment to actually request, based on which data the cache already contains. While doing so, ``computeFromAndTo`` also records whether an overlap with existing data in the cache has been found which later affects the decision about whether to keep or discard previously cached data when adding the new data to the cache. This is trying to maintain the invariant of only keeping contiguous track segments in the cache. + +The overlap detection is then also used to decide how to split up requests for the data needed. There are two types of requests possible: a combined ``GetRaceMapDataAction`` and a ``GetBoatPositionsAction``. The ``GetRaceMapDataAction`` is a disposable action that is meant to be used with ``AsyncActionsExecutor`` which implements the dropping of excessive amounts of requests, preserving and running the latest one when throttling ends. The action asks for mark positions, sideline positions, boat positions, quick ranks, simulation results, and the estimated race duration, if requested. For all but the boat positions, single snapshots are returned; for the boat positions, a segment from each track may be returned which may contain several fixes each. ``GetBoatPositionsAction`` loads track segments including optional detail values for a ``DetailType`` selected by the user. The action is a ``TimeRangeAsyncAction`` which is passed for execution to a ``TimeRangeActionsExecutor``. While this executor won't drop requests, it may slice, split and trim requests for overlapping time ranges with equal detail type, and then merge their results to form the complete responses from partial requests. This way, no requests for redundant data should be made by the same client, saving server CPU resources. + +## Problems with the Current Solution + +### Dropped ``GetRaceMapDataAction`` Requests + +Whenever an "overlap" is recognized by ``FixesAndTails.computeFromAndTo`` then we incorrectly assume that the request resulting from trimming the time range to only what is missing will be quick to execute. However, an overlap was also recognized when the range requested expands beyond start and end of the cached segment, and the resulting request would then span the entire time frame instead of only asking the leading and trailing segment considered missing. The problem originally is caused really by fixes not always exactly matching up with tail range start and end. We don't remember which ranges were requested previously. Trimming against ranges requested instead of ranges received may help; however we always have to assume late delivery of live fixes into time ranges previously requested. + +### Handling Out-of-Order Responses for ``GetBoatPositionsAction`` + +Long-running requests for many long tails, perhaps even with expensive-to-compute detail values for tail coloring may be overtaken by later requests for less expensive or null detail values. There is no logic in place that would drop the response to the response received late. + +### ArrayOutOfBoundsException due to Inconsistent Updates of ``FixesAndTails`` Cache + +Only some but not all of the updates to the ``Colorline`` objects actually representing the tails graphically on the map are performed in so-called ``Triggerable`` objects which get executed either when a request for the tail or its start and end index is made, or based on a time schedule that aligns with the tick rate of the time slider so as to advance the tail only when the boat animation that moves it to the position of the next fix is at least half way. With this, there is less of an "overshoot" of tails extending beyond the boats' bows as the boat animation is still trying to catch up. + +This ``Triggerable`` pattern, however, is not used consistently for the updates to the tails and the first/last index variables, and across the different removeAt/insertAt/setAt methods used on the tails. While currently especially the ``removeAt`` calls happen with the ``Triggerable`` pattern, ``insertAt`` and ``setAt`` are invoked immediately. For the ``removeAt`` calls, the corresponding index manipulations (``indexOfFirstShownFix``, ``indexOfLastShownFix`` as well as the maps ``firstShownFix`` and ``lastShownFix``) happen immediately, potentially leading to an inconsistency between the fixes still part of the ``Colorline`` and the index variables telling which fixes are supposedly on that ``Colorline`` + +As a result, incorrect array access may occur, either dealing with the wrong fixes, or towards the end of the tail even leading to an ``ArrayOutOfBoundsException`` being thrown. Either we need to tie the index variable updates to the ``Triggerable`` objects actually carrying out the change on the ``Colorline``, or we have to challenge the whole process with these ``Triggerable`` objects. + +### Overlap Computed at Request Time + +With responses arriving in an order different from the order in which their corresponding requests were sent, and with requests potentially being dropped, when processing responses the overlap recognized at request construction time may no longer be valid when the response is being processed. If, for example, a larger number of ``GetRaceMapDataAction`` requests is sent, some of them may get dropped, and some ``GetBoatPositionsAction`` responses may have been processed in between, creating a whole new set of cached track segments. Basing the merge/replace decision on the overlap situation at request *creation* time can therefore lead to incorrect results, e.g., throwing away relevant data because *no* overlap was detected when the request was created, or by an overlap detected at request creation time that no longer applies, thus trying to merge fixes although they no longer form a contiguous segment with what's in the cache. + +### Redundant Fetching of GPS Fix Data When Only the Detail Type Changes + +We currently always load the detail values together with the ``GPSFixDTO`` objects. When only changing the detail type to be displayed as color on a tail, all data for the ``GPSFixDTO`` objects already known to the client have to be re-calculated redundantly, only in order to update the one ``detailValue`` field. + +Furthermore, if a user switches back and forth between different detail types for the tail color, detail values already loaded will be dropped and replaced by other detail values for another detail type because currently the ``FixesAndTails`` cache currently stores the detail values within the ``GPSFixDTO`` objects and there can only be one detail value per fix. + +### ``RaceMap.updateBoatPositions(...)`` and Out-of-Order Responses + +The call to ``updateBoatPositions`` by the two callback handlers for the quick and slow position requests both aim to adjust the tails based on the new results received. They compute the new time range to visualize with the tail based on the ``Timer`` time point at the time the data request was constructed. However, with out-of-order responses, a request made earlier may arrive later than a request for a newer time point. The slow request would then move the tail backwards in time while processing its response. + +### Incorrect Implementation of ``FixesAndTails.searchMinMaxDetailValue`` + +When moving backwards in time, fixes may be added to a competitor's tail that are older than the oldest fix part of the tail so far. However, ``FixesAndTails.searchMinMaxDetailValue`` uses ``lastSearchedFix+1`` as a start index to search for new min/max values. This will then not find extreme values that became part of the tail in ``FixesAndTails.updateTail(...)`` which manages ``firstShownFix`` and ``lastShownFix`` but not ``lastSearchedFix``. Furthermore, ``lastSearchedFix`` seems to be assumed to be inclusive by ``searchMinMaxDetailValue``, but ``mergeFixes(...)`` inserts the minimum insert index of new fixes added. This may even be before the tail, and then searching for extreme values potentially before the visible tail. + +## How to Improve + +### Holistically Maintain Ranges Requested and Repeat Position Requests for Dropped ``GetRaceMapDataActions`` + +At any time, for each competitor the ``FixesAndTails`` cache has a time range that it expects to have positions requested for, either already received or still in flight. When deciding to clear a competitor's fixes/tails cache, all callbacks for outstanding requests need to be informed to drop their responses for those competitors. + +Request trimming works against the start of the time range *requested*, but at the end of the requested time range the last fix *received* may be a good trimming point because in live scenarios the latest fixes sometimes may arrive a bit late. + +When trimming requests with an overlap, the "to-be" time ranges maintained by ``FixesAndTails`` are extended accordingly, and outstanding requests' callbacks are recorded so they can be notified in case they need to be invalidated. + +With this, eventual consistency shall be achieved. + +This has to take into account position fixes already received and cached, but also requests sent for which no response has been received yet. The callback objects for requests sent that haven't seen a response yet can be referenced, and if ``FixesAndTails`` decides that the data for their time range and competitor is to be dropped from the cache then the request callback could be informed about this; when the response arrives later, its parts for the competitors whose cached fixes were dropped must not be added to / merged into the cache anymore as this may create inconsistencies again. A special case for this is an in-flight request for fixes with detail values for a detail type, where the user has switched to a different detail type after the request has been sent. The response then must not be inserted into the cache anymore. + +The ``GetRaceMapDataAction`` requests should be limited to very short track segments only; probably some 5-10s at most. For low sampling rates this will then only produce an extrapolated fix. For typical sampling rates of 1/3-1Hz this will typically produce one or more actual fixes. Requests for longer track segments should immediately be moved to ``GetBoatPositionsAction`` requests to keep finding the "current" boat position quick. + +As ``GetRaceMapDataAction`` requests may be dropped, their dropping must trigger a separate ``GetBoatPositionsAction`` for the time range originally requested (unless the callback was informed about its boat positions result no longer to be added to the cache), only without extrapolation, as now we're interested only in real fixes and assume that other follow-up ``GetRaceMapDataAction`` requests will take care of the "current" boat position instead. These ``GetBoatPositionsAction`` requests that replace a dropped ``GetRaceMapDataAction`` will blend in through the ``TimeRangeActionsExecutor`` with other ongoing requests and may correspondingly get trimmed and merged. Request dropping is now signaled to the action by invoking its ``dropped(...)`` method. + +With this, out-of-order responses will add to the cache if an only if the cache hasn't informed the callback that its result is no longer desired/needed. In particular, out-of-order responses *may* reasonably add to the cache if needed. This also needs to be implemented for the ``GetRaceMapDataAction`` callback, restricted to the boat positions aspect of the response. For all its other aspects, only representing single instant snapshots, out-of-order responses do not have to be considered at all. + +### Keep Track of Desired Tail Time Range + +When the ``Timer`` "ticks", one or two asynchronous requests for track data are made. If the ``FixesAndTails`` cache already has the data desired, there wouldn't be a need to wait for the responses. Yet, at least one of the requests needs to receive a response, the fixes in the response---if any---need to be updated to the cache, and only then the visible tail will be updated. Boat position and tail updates could be quicker with already cached data if the ``RaceMap`` in its ``refreshMap`` method first told ``FixesAndTails`` which time range the tail shall visualize and then cared about fetching the data later. + +The same goes for the boat icon/canvas display: if the position data required is already in the cache, why wait for the round trip to deliver mark position, sideline, and wind data, and why not immediately update the canvas position based on the already cached fixes? + +Slow requests returning late should not mess with the desired tail time range. Several newer time ticks may already have adjusted the desired tail time range. + +### Fix the Search for Extreme Detail Values + +The ``FixesAndTails.lastSearchedFix`` map seems conceptually flawed. If the invariant is to be that at all times we know the minimum and maximum detailValues for all tails visible then we would in particular need to compute an update each time anything is added to or removed from any visible tail. While checking for a new minimum/maximum value upon addition, finding out what to do in case of removing a so far extreme fix is more difficult. + +For this, we already remember ``minDetailValueFix`` and ``maxDetailValueFix`` as indices into the ``fixes`` lists, for each competitor maintaining which of the fixes visible in their current tail has the minimum/maximum detail value. When such a maximum or minimum fix is removed from the visible tail (e.g., because the tail is shortened, or it moves backwards or forwards in time) then the new minimum/maximum must be found in the visible tail. + +The "add" case should entirely be handled in ``FixesAndTails.updateTail(...)``. (``FixesAndTails.createTailAndUpdateIndices`` currently would not need to search for a minimum/maximum because it only creates a monochromatic tail, assuming the competitor for which a new tail is required is not part of the selection yet.) If a new fix with a minimum/maximum value is added to the tail, ``minDetailValueFix``/``maxDetailValueFix`` can be adjusted immediately. + +The "remove" case could also be handled in ``FixesAndTails.updateTail(...)``, after having removed a fix from a tail that was the one with the minimal/maximal detail value. + +### Make Consistent Use of the ``Triggerable`` Pattern + +If we need this ``Triggerable`` pattern at all, it should be made consistent with the updating of the indexes that describe the first and last shown fix on the tails. This needs to maintain consistency at all times, across all asynchronous, Timer or Triggerable-based tail manipulations and hence avoid any ``ArrayIndexOutOfBoundsException`` in the future. + +### Optionally Separate Detail Value Requests from GPS Fix Requests + +This is addressing a part of the bug 5925 performance aspects and could be handled as an optional extension of the work on bug 5921. + +The ``FixesAndTails`` cache could manage separate detail value caches for different detail types per competitor and store those detail values separately from the GPS fixes. A separate RPC method may be provided to only request detail values for time ranges without the ``GPSFixDTO`` data. This would nicely support a user having cached the essential parts of the tracks already and now only switching between different detail types for tail coloring. + +The ``FixesAndTails.computeFromAndTo`` method then would have to consider the selected detail type and inspect the cache contents to see what has already been loaded and which detail values are still missing. As a result, the time ranges requested for a competitor in ``GetBoatPositionsAction`` may differ between GPS fixes and detail values, and the result structure should separate detail value segments from GPS fix segments. + +### Open Issues + +How should the collaboration between classes ``RaceMap`` and ``FixesAndTails`` be organized? ``FixesAndTails`.computeFromAndTo(...)`` is currently responsible for trimming requests and deciding about overlap. The overlap markers then are returned from ``computeFromAndTo``. ``RaceMap`` constructs the request actions and callbacks, and the callbacks know about the overlap markers as returned by ``computeFromAndTo``. The callbacks, when invoked upon success, talk to ``FixesAndTails`` again and pass the overlap markers to ``FixesAndTails.updateFixes`` which then decides whether to replace all of the competitor's fixes and the tail (in case no overlap) or to carefully merge the new fixes into the existing fix cache and update the tail in place. + +I find it strange that ``RaceMap.updateBoatPositions`` ignores the entire boat positions result if there is currently a zooming animation in progress. Why would we hope that the ``RaceMap.redraw()`` call at the end of such an interaction loads all this data again? Why drop all fixes data? But if we really want to stick with this pattern then we have to inform ``FixesAndTails`` or whichever component manages the relation between the cache, the outstanding requests and their callbacks, that a response to that request has been dropped. And this would apply for both, ``GetRaceMapDataAction`` as well as ``GetBoatPositionsAction``. If this was a request for a time range ending up in the middle of a contiguous segment, the cache would become inconsistent regarding the "contiguousness" invariant, unless we trimmed the cache content by, e.g., deleting the shorter of the two ends on either side of the "gap." \ No newline at end of file