diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/AbstractRaceBoardMode.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/AbstractRaceBoardMode.java index c99675b1315..a43881ad14c 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/AbstractRaceBoardMode.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/AbstractRaceBoardMode.java @@ -1,27 +1,49 @@ package com.sap.sailing.gwt.ui.raceboard; +import java.util.ArrayList; import java.util.Date; import java.util.Map; +import com.google.gwt.core.shared.GWT; +import com.google.gwt.user.client.rpc.AsyncCallback; import com.sap.sailing.domain.common.RaceIdentifier; import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.domain.common.dto.CompetitorDTO; import com.sap.sailing.domain.common.dto.LeaderboardDTO; import com.sap.sailing.domain.common.dto.RaceColumnDTO; +import com.sap.sailing.gwt.ui.actions.GetLeaderboardByNameAction; import com.sap.sailing.gwt.ui.client.LeaderboardUpdateListener; import com.sap.sailing.gwt.ui.client.RaceTimePanel; import com.sap.sailing.gwt.ui.client.RaceTimesInfoProviderListener; +import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.client.shared.racemap.RaceCompetitorSet.CompetitorsForRaceDefinedListener; import com.sap.sailing.gwt.ui.leaderboard.LeaderboardPanel; import com.sap.sailing.gwt.ui.shared.RaceTimesInfoDTO; import com.sap.sse.gwt.client.player.Timer; /** - * Abstract base class for implementing a {@link PlayerMode}. The {@link #applyTo(RaceBoardPanel)} method - * registers this object as a leaderboard update listener and as a race times info provider listener. The - * default implementations of the corresponding callback methods will simply unregister this object as a - * listener. If subclasses want to stick with this behavior they can call the {@code super} implementation - * in their overrides. + * Abstract base class for implementing a {@link RaceBoardMode}. The {@link #applyTo(RaceBoardPanel)} method registers + * this object as a leaderboard update listener and as a race times info provider listener, also as a + * {@link CompetitorsForRaceDefinedListener} in order to learn when the set of competitors for the race displayed by the + * player is known. The default implementations of the corresponding callback methods will simply unregister this object + * as a listener for the respective callback, enters the knowledge gained in the object's fields and calls the + * {@link #trigger} method each time (abstract in this class). Subclasses can and should do the following: + * + * + * + * Note that the {@link #updatedLeaderboard(LeaderboardDTO)} method will typically be called several times in more or + * less unpredictable order, asynchronously. Calling {@link Timer#setTime(long)} will trigger + * {@link #updatedLeaderboard(LeaderboardDTO)} eventually, but in between, other callbacks to the same method + * may occur, triggered by other, automatic adjustments or initialization of the {@link Timer}. To solve this + * problem, subclasses may decide to use a separate {@link GetLeaderboardByNameAction} action and execute + * it using the {@link #getLeaderboardPanel()}.{@link LeaderboardPanel#getExecutor() getExecutor()}. In the + * dedicated callback the subclass can then be sure that the {@link LeaderboardDTO} received matches the + * time previously set on the {@link Timer}. * * @author Axel Uhl (d043530) * @@ -33,6 +55,13 @@ public abstract class AbstractRaceBoardMode implements RaceBoardMode, RaceTimesI private LeaderboardPanel leaderboardPanel; private RaceBoardPanel raceBoardPanel; + private Map raceTimesInfo; + private RaceTimesInfoDTO raceTimesInfoForRace; + private LeaderboardDTO leaderboard; + private LeaderboardDTO leaderboardForSpecificTimePoint; + private Iterable competitorsInRace; + private RaceColumnDTO raceColumn; + @Override public void applyTo(RaceBoardPanel raceBoardPanel) { this.raceBoardPanel = raceBoardPanel; @@ -45,6 +74,12 @@ public abstract class AbstractRaceBoardMode implements RaceBoardMode, RaceTimesI raceBoardPanel.getMap().addCompetitorsForRaceDefinedListener(this); } + /** + * Called whenever new information of any sort has become available. Subclasses can use this + * to decide whether everything they need has been received in order to carry out their actions. + */ + protected abstract void trigger(); + protected Timer getTimer() { return timer; } @@ -66,13 +101,21 @@ public abstract class AbstractRaceBoardMode implements RaceBoardMode, RaceTimesI } /** - * Called after the {@link RaceTimePanel} has reacted to this update. We assume that now the timing for the race has been - * received, and it should be clear by now whether we're talking about a live or a replay race. In case of a replay race - * the timer is set to + * Called after the {@link RaceTimePanel} has reacted to this update. We assume that now the timing for the race has + * been received, and it should be clear by now whether we're talking about a live or a replay race. The + * {@link #raceTimesInfo} field is set to the {@code raceTimesInfo} object received as parameter, the + * {@link #raceTimesInfoForRace} field is set to the race times info specific to the race identified by + * {@link #getRaceIdentifier()}. */ @Override public void raceTimesInfosReceived(Map raceTimesInfo, long clientTimeWhenRequestWasSent, Date serverTimeDuringRequest, long clientTimeWhenResponseWasReceived) { + this.raceTimesInfo = raceTimesInfo; + this.raceTimesInfoForRace = raceTimesInfo.get(getRaceIdentifier()); + trigger(); + } + + protected void stopReceivingRaceTimesInfos() { raceTimePanel.removeRaceTimesInfoProviderListener(this); } @@ -81,16 +124,83 @@ public abstract class AbstractRaceBoardMode implements RaceBoardMode, RaceTimesI */ @Override public void updatedLeaderboard(LeaderboardDTO leaderboard) { + this.leaderboard = leaderboard; + trigger(); + } + + protected void stopReceivingLeaderboard() { leaderboardPanel.removeLeaderboardUpdateListener(this); } + /** + * Fetches the leaderboard named {@code leaderboardName} for {@code timePoint} with details for race column + * {@code raceColumnName} and stores it in {@link #leaderboardForSpecificTimePoint}, then invokes {@link #trigger()}. + * If a {@link #getLeaderboard()} has been received already, it is passed on in the call in order to reduce the + * bandwidth required, based on a differential approach. + */ + protected void loadLeaderboardForSpecificTimePoint(String leaderboardName, + String raceColumnName, Date timePoint) { + final AsyncCallback callback = new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + GWT.log("Error trying to load leaderboard", caught); + } + + @Override + public void onSuccess(LeaderboardDTO result) { + leaderboardForSpecificTimePoint = result; + trigger(); + } + }; + final ArrayList raceColumnNameAsList = new ArrayList<>(); + if (getRaceColumn() != null) { + raceColumnNameAsList.add(getRaceColumn().getName()); + } + final GetLeaderboardByNameAction getLeaderboardByNameAction = new GetLeaderboardByNameAction( + getLeaderboardPanel().getSailingService(), getLeaderboardPanel().getLeaderboard().name, + getTimer().getTime(), raceColumnNameAsList, /* addOverallDetails */ false, + getLeaderboard(), /* fillTotalPointsUncorrected */ false, + /* timerToAdjustOffsetIn */ getTimer(), /* errorReporter */ null, StringMessages.INSTANCE); + getLeaderboardPanel().getExecutor().execute(getLeaderboardByNameAction, LeaderboardPanel.LOAD_LEADERBOARD_DATA_CATEGORY, callback); + } + @Override public void currentRaceSelected(RaceIdentifier raceIdentifier, RaceColumnDTO raceColumn) { - // nothing to do + this.raceColumn = raceColumn; + trigger(); } @Override public void competitorsForRaceDefined(Iterable competitorsInRace) { - // nothing to do here + this.competitorsInRace = competitorsInRace; + trigger(); + } + + protected void stopReceivingCompetitorsInRace() { + raceBoardPanel.getMap().removeCompetitorsForRaceDefinedListener(this); + } + + protected Map getRaceTimesInfo() { + return raceTimesInfo; + } + + protected RaceTimesInfoDTO getRaceTimesInfoForRace() { + return raceTimesInfoForRace; + } + + protected LeaderboardDTO getLeaderboard() { + return leaderboard; + } + + protected LeaderboardDTO getLeaderboardForSpecificTimePoint() { + return leaderboardForSpecificTimePoint; + } + + protected Iterable getCompetitorsInRace() { + return competitorsInRace; + } + + protected RaceColumnDTO getRaceColumn() { + return raceColumn; } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/FullAnalysisMode.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/FullAnalysisMode.java index 599cef15f87..b74ab873c3d 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/FullAnalysisMode.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/FullAnalysisMode.java @@ -1,16 +1,10 @@ package com.sap.sailing.gwt.ui.raceboard; import java.util.ArrayList; -import java.util.Date; import java.util.List; -import java.util.Map; import com.sap.sailing.domain.common.DetailType; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sailing.domain.common.dto.LeaderboardDTO; -import com.sap.sailing.gwt.ui.client.RaceTimePanel; import com.sap.sailing.gwt.ui.leaderboard.LeaderboardSettings; -import com.sap.sailing.gwt.ui.shared.RaceTimesInfoDTO; import com.sap.sse.gwt.client.player.Timer.PlayModes; import com.sap.sse.gwt.client.player.Timer.PlayStates; @@ -22,30 +16,33 @@ import com.sap.sse.gwt.client.player.Timer.PlayStates; * */ public class FullAnalysisMode extends AbstractRaceBoardMode { - + private boolean leaderboardSettingsAdjusted; + private boolean timerAdjusted; + /** - * Called after the {@link RaceTimePanel} has reacted to this update. We assume that now the timing for the race has been - * received, and it should be clear by now whether we're talking about a live or a replay race. In case of a replay race - * the timer is set to + * Listening for race times info continues until information about the end of the race has been received. The leaderboard + * settings are adjusted as soon as a valid leaderboard has been received. */ @Override - public void raceTimesInfosReceived(Map raceTimesInfo, - long clientTimeWhenRequestWasSent, Date serverTimeDuringRequest, long clientTimeWhenResponseWasReceived) { - if (getTimer().getPlayMode() != PlayModes.Live && !raceTimesInfo.isEmpty() && raceTimesInfo.containsKey(getRaceIdentifier())) { - final RaceTimesInfoDTO times = raceTimesInfo.get(getRaceIdentifier()); - if (times.endOfRace != null) { - getTimer().setTime(times.endOfRace.getTime()); - // we've done our adjustments; remove listener and let go - super.raceTimesInfosReceived(raceTimesInfo, clientTimeWhenRequestWasSent, serverTimeDuringRequest, clientTimeWhenResponseWasReceived); + protected void trigger() { + if (!leaderboardSettingsAdjusted && getLeaderboard() != null) { + leaderboardSettingsAdjusted = true; + // it's important to first unregister the listener before updateSettings is called because + // updateSettings will trigger another leaderboard load, leading to an endless recursion otherwise + stopReceivingLeaderboard(); + adjustLeaderboardSettings(); + } + if (!timerAdjusted && getRaceTimesInfoForRace() != null && getRaceTimesInfoForRace().endOfRace != null) { + timerAdjusted = true; + stopReceivingRaceTimesInfos(); + if (getTimer().getPlayMode() == PlayModes.Live) { + getTimer().setPlayMode(PlayModes.Replay); } + getTimer().setTime(getRaceTimesInfoForRace().endOfRace.getTime()); } } - @Override - public void updatedLeaderboard(LeaderboardDTO leaderboard) { - // it's important to first unregister the listener before updateSettings is called because - // updateSettings will trigger another leaderboard load, leading to an endless recursion otherwise - super.updatedLeaderboard(leaderboard); + private void adjustLeaderboardSettings() { final LeaderboardSettings existingSettings = getLeaderboardPanel().getSettings(); final List raceDetailsToShow = new ArrayList<>(existingSettings.getRaceDetailsToShow()); raceDetailsToShow.add(DetailType.RACE_AVERAGE_SPEED_OVER_GROUND_IN_KNOTS); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/PlayerMode.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/PlayerMode.java index 43954dcd879..0e707f9ec88 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/PlayerMode.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/PlayerMode.java @@ -1,16 +1,10 @@ package com.sap.sailing.gwt.ui.raceboard; import java.util.ArrayList; -import java.util.Date; import java.util.List; -import java.util.Map; import com.sap.sailing.domain.common.DetailType; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sailing.domain.common.dto.LeaderboardDTO; -import com.sap.sailing.gwt.ui.client.RaceTimePanel; import com.sap.sailing.gwt.ui.leaderboard.LeaderboardSettings; -import com.sap.sailing.gwt.ui.shared.RaceTimesInfoDTO; import com.sap.sse.common.Duration; import com.sap.sse.common.impl.MillisecondsTimePoint; import com.sap.sse.gwt.client.player.Timer.PlayModes; @@ -26,30 +20,11 @@ import com.sap.sse.gwt.client.player.Timer.PlayStates; public class PlayerMode extends AbstractRaceBoardMode { private final Duration DURATION_BEFORE_START_TO_SET_TIMER_TO_FOR_REPLAY_RACES = Duration.ONE_SECOND.times(10); - /** - * Called after the {@link RaceTimePanel} has reacted to this update. We assume that now the timing for the race has been - * received, and it should be clear by now whether we're talking about a live or a replay race. In case of a replay race - * the timer is set to - */ - @Override - public void raceTimesInfosReceived(Map raceTimesInfo, - long clientTimeWhenRequestWasSent, Date serverTimeDuringRequest, long clientTimeWhenResponseWasReceived) { - if (getTimer().getPlayMode() != PlayModes.Live && !raceTimesInfo.isEmpty() && raceTimesInfo.containsKey(getRaceIdentifier())) { - final RaceTimesInfoDTO times = raceTimesInfo.get(getRaceIdentifier()); - if (times.startOfRace != null) { - getTimer().setTime(new MillisecondsTimePoint(times.startOfRace).minus(DURATION_BEFORE_START_TO_SET_TIMER_TO_FOR_REPLAY_RACES).asMillis()); - getTimer().play(); - // we've done our adjustments; remove listener and let go - super.raceTimesInfosReceived(raceTimesInfo, clientTimeWhenRequestWasSent, serverTimeDuringRequest, clientTimeWhenResponseWasReceived); - } - } - } + private boolean adjustedLeaderboardSettings; - @Override - public void updatedLeaderboard(LeaderboardDTO leaderboard) { - // it's important to first unregister the listener before updateSettings is called because - // updateSettings will trigger another leaderboard load, leading to an endless recursion otherwise - super.updatedLeaderboard(leaderboard); + private boolean timerAdjusted; + + private void adjustLeaderboardSettings() { final LeaderboardSettings existingSettings = getLeaderboardPanel().getSettings(); final List raceDetailsToShow = new ArrayList<>(existingSettings.getRaceDetailsToShow()); raceDetailsToShow.add(DetailType.RACE_CURRENT_SPEED_OVER_GROUND_IN_KNOTS); @@ -69,4 +44,22 @@ public class PlayerMode extends AbstractRaceBoardMode { existingSettings.isShowCompetitorFullNameColumn()); getLeaderboardPanel().updateSettings(newSettings); } + + @Override + protected void trigger() { + if (!adjustedLeaderboardSettings && getLeaderboard() != null) { + adjustedLeaderboardSettings = true; + // it's important to first unregister the listener before updateSettings is called because + // updateSettings will trigger another leaderboard load, leading to an endless recursion otherwise + stopReceivingLeaderboard(); + adjustLeaderboardSettings(); + } + if (!timerAdjusted && getTimer().getPlayMode() != PlayModes.Live && getRaceTimesInfoForRace() != null && getRaceTimesInfoForRace().startOfRace != null) { + timerAdjusted = true; + getTimer().setTime(new MillisecondsTimePoint(getRaceTimesInfoForRace().startOfRace).minus(DURATION_BEFORE_START_TO_SET_TIMER_TO_FOR_REPLAY_RACES).asMillis()); + getTimer().play(); + // we've done our adjustments; remove listener and let go + stopReceivingRaceTimesInfos(); + } + } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardModeWithPerRaceCompetitors.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardModeWithPerRaceCompetitors.java index 8fac0c85e68..4532853ca9d 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardModeWithPerRaceCompetitors.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardModeWithPerRaceCompetitors.java @@ -20,7 +20,6 @@ public abstract class RaceBoardModeWithPerRaceCompetitors extends AbstractRaceBo @Override public void competitorsForRaceDefined(Iterable competitorsInRace) { this.competitorsInRace = competitorsInRace; - updateCompetitorSelection(); } protected Iterable getCompetitorsInRace() { @@ -31,28 +30,19 @@ public abstract class RaceBoardModeWithPerRaceCompetitors extends AbstractRaceBo return leaderboard; } - protected void setLeaderboard(LeaderboardDTO leaderboard) { - this.leaderboard = leaderboard; - } - - @Override - public void updatedLeaderboard(LeaderboardDTO leaderboard) { - super.updatedLeaderboard(leaderboard); - setLeaderboard(leaderboard); - } - @Override public void currentRaceSelected(RaceIdentifier raceIdentifier, RaceColumnDTO raceColumn) { this.raceColumn = raceColumn; + trigger(); } protected RaceColumnDTO getRaceColumn() { return raceColumn; } - protected void updateCompetitorSelection(final int howManyTopCompetitorsInRaceToSelect) { - if (getLeaderboard() != null) { - final List competitorsFromBestToWorstInColumn = getLeaderboard().getCompetitorsFromBestToWorst(getRaceColumn()); + protected void updateCompetitorSelection(final int howManyTopCompetitorsInRaceToSelect, LeaderboardDTO leaderboard) { + if (leaderboard != null) { + final List competitorsFromBestToWorstInColumn = leaderboard.getCompetitorsFromBestToWorst(getRaceColumn()); final Set competitorsToSelect = new HashSet<>(); int numberOfSelectedCompetitors = 0; for (int i=0; numberOfSelectedCompetitors raceTimesInfo, - long clientTimeWhenRequestWasSent, Date serverTimeDuringRequest, long clientTimeWhenResponseWasReceived) { - if (!raceTimesInfo.isEmpty() && raceTimesInfo.containsKey(getRaceIdentifier())) { - final RaceTimesInfoDTO times = raceTimesInfo.get(getRaceIdentifier()); - if (times.startOfRace != null) { - // make sure we're no longer in live/playing mode/state; the start analysis is supposed to be a "frozen" display at first - if (getTimer().getPlayMode() == PlayModes.Live) { - getTimer().setPlayMode(PlayModes.Replay); - } - // the following call will always trigger a leaderboard load and therefore a callback - // to updatedLeaderboard; therefore, updatedLeaderboard can decide when it's time to - // trigger the chart display and the map settings application - getTimer().setTime(new MillisecondsTimePoint(times.startOfRace).plus(DURATION_AFTER_START_TO_SET_TIMER_TO_FOR_START_ANALYSIS).asMillis()); - // we've done our adjustments; remove listener and let go - super.raceTimesInfosReceived(raceTimesInfo, clientTimeWhenRequestWasSent, serverTimeDuringRequest, clientTimeWhenResponseWasReceived); - this.raceTimesInfo = times; - raceTimesInfoReceived = true; + protected void updateCompetitorSelection() { + updateCompetitorSelection(/* howManyTopCompetitorsInRaceToSelect */ 3, getLeaderboard()); + } + + private void adjustMapSettings() { + final RaceMapSettings existingMapSettings = getRaceBoardPanel().getMap().getSettings(); + final RaceMapSettings newMapSettings = new RaceMapSettings( + new RaceMapZoomSettings(Collections.singleton(ZoomTypes.BOATS), /* zoomToSelected */ false), + existingMapSettings.getHelpLinesSettings(), + existingMapSettings.getTransparentHoverlines(), + existingMapSettings.getHoverlineStrokeWeight(), + existingMapSettings.getTailLengthInMilliseconds(), + /* existingMapSettings.isWindUp() */ true, + existingMapSettings.getBuoyZoneRadiusInMeters(), + existingMapSettings.isShowOnlySelectedCompetitors(), + existingMapSettings.isShowSelectedCompetitorsInfo(), + existingMapSettings.isShowWindStreamletColors(), + existingMapSettings.isShowWindStreamletOverlay(), + existingMapSettings.isShowSimulationOverlay(), + existingMapSettings.isShowMapControls(), + existingMapSettings.getManeuverTypesToShow(), + existingMapSettings.isShowDouglasPeuckerPoints()); + // try to update the settings once; the problem is the "wind up" display; it changes pan/zoom + // which is a major source of instability for the map. Going twice in the map idle event handler + // to ensure the settings are really applied. + final Object[] registration = new Object[1]; + registration[0] = getRaceBoardPanel().getMap().getMap().addIdleHandler(new IdleMapHandler() { + @Override + public void onEvent(IdleMapEvent event) { + Scheduler.get().scheduleDeferred(new ScheduledCommand() { + @Override + public void execute() { + getRaceBoardPanel().getMap().updateSettings(newMapSettings); + } + }); + ((HandlerRegistration) registration[0]).removeHandler(); + } + }); + getRaceBoardPanel().getMap().updateSettings(newMapSettings); + } + + @Override + protected void trigger() { + if (!leaderboardSettingsAdjusted && getLeaderboard() != null) { + leaderboardSettingsAdjusted = true; + stopReceivingLeaderboard(); + adjustLeaderboardSettings(); + } + if (!timerAdjusted && getRaceTimesInfoForRace() != null && getRaceTimesInfoForRace().startOfRace != null) { + timerAdjusted = true; + // we've done our adjustments; remove listener and let go + stopReceivingRaceTimesInfos(); + // make sure we're no longer in live/playing mode/state; the start analysis is supposed to be a "frozen" display at first + if (getTimer().getPlayMode() == PlayModes.Live) { + getTimer().setPlayMode(PlayModes.Replay); + } + // the following call will always trigger a leaderboard load and therefore a callback + // to updatedLeaderboard; therefore, updatedLeaderboard can decide when it's time to + // trigger the chart display and the map settings application + getTimer().setTime(new MillisecondsTimePoint(getRaceTimesInfoForRace().startOfRace).plus(DURATION_AFTER_START_TO_SET_TIMER_TO_FOR_START_ANALYSIS).asMillis()); + // no need to do anything if we don't have the timing info yet; after obtaining the timing + // info we can start waiting for the simulation overlay and then adjust the map settings and, + // if available, show the competitor chart + if (!mapSettingsAndTimeZoomAdjusted) { + mapSettingsAndTimeZoomAdjusted = true; + Scheduler.get().scheduleFixedDelay(new RepeatingCommand() { + @Override + public boolean execute() { + // keep trying in 500ms steps until the simulation overlay is found; this way, changing the competitor chart + // settings is largely more successful + final boolean simulationOverlayFound = getRaceBoardPanel().getMap().getSimulationOverlay() != null; + if (simulationOverlayFound) { + if (isCompetitorChartEnabled()) { + final MultiCompetitorRaceChartSettings newCompetitorChartSettings = new MultiCompetitorRaceChartSettings( + new ChartSettings(/* stepSizeInMillis */ 1000), DetailType.RACE_CURRENT_SPEED_OVER_GROUND_IN_KNOTS, + /* no second series */ null); + competitorChart.updateSettings(newCompetitorChartSettings); + getRaceTimePanel().getTimeRangeProvider().setTimeZoom( + new MillisecondsTimePoint(getRaceTimesInfoForRace().startOfRace).minus(DURATION_BEFORE_START_TO_INCLUDE_IN_CHART_TIME_RANGE).asDate(), + new MillisecondsTimePoint(getRaceTimesInfoForRace().startOfRace).plus(DURATION_AFTER_START_TO_INCLUDE_IN_CHART_TIME_RANGE).asDate()); + } + adjustMapSettings(); + } + return !simulationOverlayFound; + } + }, /* delay in milliseconds */ 500); + } + } + if (getLeaderboardForSpecificTimePoint() == null && + getRaceColumn() != null && getLeaderboard() != null + && timerAdjusted) { + loadLeaderboardForSpecificTimePoint(getLeaderboard().name, getRaceColumn().getName(), getTimer().getTime()); + } + if (!competitorSelectionUpdated && getLeaderboardForSpecificTimePoint() != null && getCompetitorsInRace() != null) { + competitorSelectionUpdated = true; + stopReceivingCompetitorsInRace(); + updateCompetitorSelection(); + if (isCompetitorChartEnabled()) { + getRaceBoardPanel().setCompetitorChartVisible(true); } } } - - @Override - protected void updateCompetitorSelection() { - updateCompetitorSelection(/* howManyTopCompetitorsInRaceToSelect */ 3); - } - private void showCompetitorChartIfAllDataReceived() { - if (getRaceColumn() != null && leaderboardUpdateReveiced && raceTimesInfoReceived && getLeaderboard() != null - && getLeaderboard().getCompetitorsFromBestToWorst(getRaceColumn()) != null - && raceTimesInfo != null && raceTimesInfo.startOfRace != null) { - ArrayList raceColumnName = new ArrayList<>(); - raceColumnName.add(getRaceColumn().getName()); - // The problem with the leaderboard received in updatedLeaderboard(...) is that we don't know which request - // has caused it to be delivered; there is an early request for "now" and maybe even another request for start of race; - // therefore, we need to fire our own request for the time point desired and make sure we get the right - // LeaderboardDTO to determine the competitor ordering for the current race. - final GetLeaderboardByNameAction getLeaderboardByNameAction = new GetLeaderboardByNameAction( - getLeaderboardPanel().getSailingService(), getLeaderboardPanel().getLeaderboard().name, - getTimer().getTime(), raceColumnName, /* addOverallDetails */ false, - getLeaderboard(), /* fillTotalPointsUncorrected */ false, - /* timerToAdjustOffsetIn */ getTimer(), /* errorReporter */ null, StringMessages.INSTANCE); - getLeaderboardPanel().getExecutor().execute(getLeaderboardByNameAction, LeaderboardPanel.LOAD_LEADERBOARD_DATA_CATEGORY, - new AsyncCallback() { - @Override - public void onFailure(Throwable caught) { - GWT.log("Error trying to load leaderboard", caught); - } - - @Override - public void onSuccess(LeaderboardDTO result) { - setLeaderboard(result); - getLeaderboardPanel().updateLeaderboard(result); - updateCompetitorSelection(); - if (isCompetitorChartEnabled()) { - getRaceBoardPanel().setCompetitorChartVisible(true); - } - } - }); - Scheduler.get().scheduleFixedDelay(new RepeatingCommand() { - @Override - public boolean execute() { - // keep trying in 500ms steps until the simulation overlay is found; this way, changing the competitor chart - // settings is largely more successful because - boolean result = getRaceBoardPanel().getMap().getSimulationOverlay() == null; - if (!result) { - if (isCompetitorChartEnabled()) { - final MultiCompetitorRaceChartSettings newCompetitorChartSettings = new MultiCompetitorRaceChartSettings( - new ChartSettings(/* stepSizeInMillis */ 1000), DetailType.RACE_CURRENT_SPEED_OVER_GROUND_IN_KNOTS, - /* no second series */ null); - competitorChart.updateSettings(newCompetitorChartSettings); - getRaceTimePanel().getTimeRangeProvider().setTimeZoom( - new MillisecondsTimePoint(raceTimesInfo.startOfRace).minus(DURATION_BEFORE_START_TO_INCLUDE_IN_CHART_TIME_RANGE).asDate(), - new MillisecondsTimePoint(raceTimesInfo.startOfRace).plus(DURATION_AFTER_START_TO_INCLUDE_IN_CHART_TIME_RANGE).asDate()); - } - final RaceMapSettings existingMapSettings = getRaceBoardPanel().getMap().getSettings(); - final RaceMapSettings newMapSettings = new RaceMapSettings( - new RaceMapZoomSettings(Collections.singleton(ZoomTypes.BOATS), /* zoomToSelected */ false), - existingMapSettings.getHelpLinesSettings(), - existingMapSettings.getTransparentHoverlines(), - existingMapSettings.getHoverlineStrokeWeight(), - existingMapSettings.getTailLengthInMilliseconds(), - /* existingMapSettings.isWindUp() */ true, - existingMapSettings.getBuoyZoneRadiusInMeters(), - existingMapSettings.isShowOnlySelectedCompetitors(), - existingMapSettings.isShowSelectedCompetitorsInfo(), - existingMapSettings.isShowWindStreamletColors(), - existingMapSettings.isShowWindStreamletOverlay(), - existingMapSettings.isShowSimulationOverlay(), - existingMapSettings.isShowMapControls(), - existingMapSettings.getManeuverTypesToShow(), - existingMapSettings.isShowDouglasPeuckerPoints()); - // try to update the settings once; the problem is the "wind up" display; it changes pan/zoom - // which is a major source of instability for the map. Going twice in the map idle event handler - // to ensure the settings are really applied. - final Object[] registration = new Object[1]; - registration[0] = getRaceBoardPanel().getMap().getMap().addIdleHandler(new IdleMapHandler() { - @Override - public void onEvent(IdleMapEvent event) { - Scheduler.get().scheduleDeferred(new ScheduledCommand() { - @Override - public void execute() { - getRaceBoardPanel().getMap().updateSettings(newMapSettings); - } - }); - ((HandlerRegistration) registration[0]).removeHandler(); - } - }); - getRaceBoardPanel().getMap().updateSettings(newMapSettings); - } - return result; - } - }, /* delay in milliseconds */ 500); - } - } - - @Override - public void updatedLeaderboard(LeaderboardDTO leaderboard) { - // no need to do anything if we don't have the timing info yet; after obtaining the timing - // info there will always be at least one more leaderboard requested, triggering a callback - // to this method - if (raceTimesInfoReceived) { - // it's important to first unregister the listener before updateSettings is called because - // updateSettings will trigger another leaderboard load, leading to an endless recursion otherwise - super.updatedLeaderboard(leaderboard); - final LeaderboardSettings existingSettings = getLeaderboardPanel().getSettings(); - final List raceDetailsToShow = new ArrayList<>(existingSettings.getRaceDetailsToShow()); - raceDetailsToShow.add(DetailType.RACE_SPEED_OVER_GROUND_FIVE_SECONDS_BEFORE_START); - raceDetailsToShow.add(DetailType.RACE_DISTANCE_TO_START_FIVE_SECONDS_BEFORE_RACE_START); - raceDetailsToShow.add(DetailType.DISTANCE_TO_START_AT_RACE_START); - raceDetailsToShow.add(DetailType.DISTANCE_TO_START_LINE); - raceDetailsToShow.add(DetailType.DISTANCE_TO_STARBOARD_END_OF_STARTLINE_WHEN_PASSING_START_IN_METERS); - raceDetailsToShow.add(DetailType.SPEED_OVER_GROUND_AT_RACE_START); - raceDetailsToShow.add(DetailType.SPEED_OVER_GROUND_WHEN_PASSING_START); - raceDetailsToShow.add(DetailType.START_TACK); - raceDetailsToShow.remove(DetailType.DISPLAY_LEGS); - final LeaderboardSettings newSettings = new LeaderboardSettings(existingSettings.getManeuverDetailsToShow(), - existingSettings.getLegDetailsToShow(), - raceDetailsToShow, existingSettings.getOverallDetailsToShow(), existingSettings.getNamesOfRaceColumnsToShow(), - existingSettings.getNamesOfRacesToShow(), - existingSettings.getNumberOfLastRacesToShow(), /* auto-expand pre-selected race */ true, - existingSettings.getDelayBetweenAutoAdvancesInMilliseconds(), - existingSettings.getNameOfRaceToSort(), existingSettings.isSortAscending(), - existingSettings.isUpdateUponPlayStateChange(), - existingSettings.getActiveRaceColumnSelectionStrategy(), - existingSettings.isShowAddedScores(), - existingSettings.isShowOverallColumnWithNumberOfRacesCompletedPerCompetitor(), - existingSettings.isShowCompetitorSailIdColumn(), - existingSettings.isShowCompetitorFullNameColumn()); - getLeaderboardPanel().updateSettings(newSettings); - leaderboardUpdateReveiced = true; - showCompetitorChartIfAllDataReceived(); - } + private void adjustLeaderboardSettings() { + final LeaderboardSettings existingSettings = getLeaderboardPanel().getSettings(); + final List raceDetailsToShow = new ArrayList<>(existingSettings.getRaceDetailsToShow()); + raceDetailsToShow.add(DetailType.RACE_SPEED_OVER_GROUND_FIVE_SECONDS_BEFORE_START); + raceDetailsToShow.add(DetailType.RACE_DISTANCE_TO_START_FIVE_SECONDS_BEFORE_RACE_START); + raceDetailsToShow.add(DetailType.DISTANCE_TO_START_AT_RACE_START); + raceDetailsToShow.add(DetailType.DISTANCE_TO_START_LINE); + raceDetailsToShow.add(DetailType.DISTANCE_TO_STARBOARD_END_OF_STARTLINE_WHEN_PASSING_START_IN_METERS); + raceDetailsToShow.add(DetailType.SPEED_OVER_GROUND_AT_RACE_START); + raceDetailsToShow.add(DetailType.SPEED_OVER_GROUND_WHEN_PASSING_START); + raceDetailsToShow.add(DetailType.START_TACK); + raceDetailsToShow.remove(DetailType.DISPLAY_LEGS); + final LeaderboardSettings newSettings = new LeaderboardSettings(existingSettings.getManeuverDetailsToShow(), + existingSettings.getLegDetailsToShow(), + raceDetailsToShow, existingSettings.getOverallDetailsToShow(), existingSettings.getNamesOfRaceColumnsToShow(), + existingSettings.getNamesOfRacesToShow(), + existingSettings.getNumberOfLastRacesToShow(), /* auto-expand pre-selected race */ true, + existingSettings.getDelayBetweenAutoAdvancesInMilliseconds(), + existingSettings.getNameOfRaceToSort(), existingSettings.isSortAscending(), + existingSettings.isUpdateUponPlayStateChange(), + existingSettings.getActiveRaceColumnSelectionStrategy(), + existingSettings.isShowAddedScores(), + existingSettings.isShowOverallColumnWithNumberOfRacesCompletedPerCompetitor(), + existingSettings.isShowCompetitorSailIdColumn(), + existingSettings.isShowCompetitorFullNameColumn()); + getLeaderboardPanel().updateSettings(newSettings); } private boolean isCompetitorChartEnabled() { return competitorChart != null; } - } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/WinningLanesMode.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/WinningLanesMode.java index 82ccbf67e3f..3f6689ae76c 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/WinningLanesMode.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/WinningLanesMode.java @@ -3,16 +3,12 @@ package com.sap.sailing.gwt.ui.raceboard; import java.util.ArrayList; import java.util.Date; import java.util.List; -import java.util.Map; 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.domain.common.dto.LeaderboardDTO; import com.sap.sailing.gwt.ui.client.shared.racemap.RaceMapSettings; import com.sap.sailing.gwt.ui.leaderboard.LeaderboardSettings; import com.sap.sailing.gwt.ui.shared.MarkPassingTimesDTO; -import com.sap.sailing.gwt.ui.shared.RaceTimesInfoDTO; import com.sap.sse.common.Duration; import com.sap.sse.common.Util; import com.sap.sse.common.impl.MillisecondsTimePoint; @@ -30,93 +26,58 @@ import com.sap.sse.gwt.client.player.Timer.PlayModes; public class WinningLanesMode extends RaceBoardModeWithPerRaceCompetitors { private Duration tailLength; - /** - * When set to {@code true}, when {@link #updatedLeaderboard(LeaderboardDTO)} is called the next time it will - * stop the leaderboard update notifications and will adjust the tail length to {@link #tailLength}. Must only - * be set to {@code true} if {@link #tailLength} has been set to a valid duration before. - */ - private boolean stopReceivingLeaderboardUpdatesAndAdjustTailLength; - - @Override - public void raceTimesInfosReceived(Map raceTimesInfo, - long clientTimeWhenRequestWasSent, Date serverTimeDuringRequest, long clientTimeWhenResponseWasReceived) { - final Date startOfRace; - final RaceTimesInfoDTO raceTimesInfoForRace; - if (!raceTimesInfo.isEmpty() && raceTimesInfo.containsKey(getRaceIdentifier()) && - (startOfRace=(raceTimesInfoForRace=raceTimesInfo.get(getRaceIdentifier())).startOfRace) != null) { - if (getTimer().getPlayMode() == PlayModes.Live) { - // adjust tail length such that for the leading boat the tail is shown since the start time point - tailLength = new MillisecondsTimePoint(startOfRace).until(getTimer().getLiveTimePoint()); - stopReceivingLeaderboardUpdatesAndAdjustTailLength = true; - } else { - final List markPassingTimes = raceTimesInfoForRace.getMarkPassingTimes(); - final Date firstPassingOfLastWaypointPassed = markPassingTimes == null || markPassingTimes.isEmpty() ? null : - markPassingTimes.get(markPassingTimes.size()-1).firstPassingDate; - final Date endOfRace = raceTimesInfoForRace.endOfRace; - final Date end = firstPassingOfLastWaypointPassed != null ? firstPassingOfLastWaypointPassed : - endOfRace != null ? endOfRace : raceTimesInfoForRace.endOfTracking; - if (end != null) { - tailLength = new MillisecondsTimePoint(startOfRace).until(new MillisecondsTimePoint(end)); - stopReceivingLeaderboardUpdatesAndAdjustTailLength = true; - getTimer().setTime(end.getTime()); - } - // we've done our adjustments; remove listener and let go - super.raceTimesInfosReceived(raceTimesInfo, clientTimeWhenRequestWasSent, serverTimeDuringRequest, clientTimeWhenResponseWasReceived); - } - } - } + private boolean adjustedLeaderboardSettings; + + private boolean adjustedCompetitorSelection; @Override protected void updateCompetitorSelection() { final int howManyCompetitorsToSelect = getHowManyCompetitorsToSelect(getLeaderboardPanel().getCompetitors(getRaceIdentifier())); - updateCompetitorSelection(howManyCompetitorsToSelect); + updateCompetitorSelection(howManyCompetitorsToSelect, getLeaderboard()); } - @Override - public void updatedLeaderboard(LeaderboardDTO leaderboard) { - if (stopReceivingLeaderboardUpdatesAndAdjustTailLength) { - // it's important to first unregister the listener before updateSettings is called because - // updateSettings will trigger another leaderboard load, leading to an endless recursion otherwise - super.updatedLeaderboard(leaderboard); - final LeaderboardSettings existingSettings = getLeaderboardPanel().getSettings(); - final List raceDetailsToShow = new ArrayList<>(existingSettings.getRaceDetailsToShow()); - raceDetailsToShow.add(DetailType.RACE_AVERAGE_ABSOLUTE_CROSS_TRACK_ERROR_IN_METERS); - raceDetailsToShow.add(DetailType.RACE_AVERAGE_SIGNED_CROSS_TRACK_ERROR_IN_METERS); - raceDetailsToShow.add(DetailType.RACE_DISTANCE_TRAVELED); - raceDetailsToShow.add(DetailType.RACE_TIME_TRAVELED); - raceDetailsToShow.remove(DetailType.DISPLAY_LEGS); - final LeaderboardSettings newSettings = new LeaderboardSettings(existingSettings.getManeuverDetailsToShow(), - existingSettings.getLegDetailsToShow(), - raceDetailsToShow, existingSettings.getOverallDetailsToShow(), existingSettings.getNamesOfRaceColumnsToShow(), - existingSettings.getNamesOfRacesToShow(), - existingSettings.getNumberOfLastRacesToShow(), /* auto-expand pre-selected race */ true, - existingSettings.getDelayBetweenAutoAdvancesInMilliseconds(), - existingSettings.getNameOfRaceToSort(), existingSettings.isSortAscending(), - existingSettings.isUpdateUponPlayStateChange(), - existingSettings.getActiveRaceColumnSelectionStrategy(), - existingSettings.isShowAddedScores(), - existingSettings.isShowOverallColumnWithNumberOfRacesCompletedPerCompetitor(), - existingSettings.isShowCompetitorSailIdColumn(), - existingSettings.isShowCompetitorFullNameColumn()); - getLeaderboardPanel().updateSettings(newSettings); - final RaceMapSettings existingMapSettings = getRaceBoardPanel().getMap().getSettings(); - final RaceMapSettings newMapSettings = new RaceMapSettings(existingMapSettings.getZoomSettings(), - existingMapSettings.getHelpLinesSettings(), - existingMapSettings.getTransparentHoverlines(), - existingMapSettings.getHoverlineStrokeWeight(), - tailLength.asMillis(), - /* existingMapSettings.isWindUp() */ true, - existingMapSettings.getBuoyZoneRadiusInMeters(), - /* existingMapSettings.isShowOnlySelectedCompetitors() */ true, // show the top n competitors and their tails quickly - existingMapSettings.isShowSelectedCompetitorsInfo(), - existingMapSettings.isShowWindStreamletColors(), - existingMapSettings.isShowWindStreamletOverlay(), - existingMapSettings.isShowSimulationOverlay(), - existingMapSettings.isShowMapControls(), - existingMapSettings.getManeuverTypesToShow(), - existingMapSettings.isShowDouglasPeuckerPoints()); - getRaceBoardPanel().getMap().updateSettings(newMapSettings); - } + private void adjustLeaderboardSettings() { + final LeaderboardSettings existingSettings = getLeaderboardPanel().getSettings(); + final List raceDetailsToShow = new ArrayList<>(existingSettings.getRaceDetailsToShow()); + raceDetailsToShow.add(DetailType.RACE_AVERAGE_ABSOLUTE_CROSS_TRACK_ERROR_IN_METERS); + raceDetailsToShow.add(DetailType.RACE_AVERAGE_SIGNED_CROSS_TRACK_ERROR_IN_METERS); + raceDetailsToShow.add(DetailType.RACE_DISTANCE_TRAVELED); + raceDetailsToShow.add(DetailType.RACE_TIME_TRAVELED); + raceDetailsToShow.remove(DetailType.DISPLAY_LEGS); + final LeaderboardSettings newSettings = new LeaderboardSettings(existingSettings.getManeuverDetailsToShow(), + existingSettings.getLegDetailsToShow(), + raceDetailsToShow, existingSettings.getOverallDetailsToShow(), existingSettings.getNamesOfRaceColumnsToShow(), + existingSettings.getNamesOfRacesToShow(), + existingSettings.getNumberOfLastRacesToShow(), /* auto-expand pre-selected race */ true, + existingSettings.getDelayBetweenAutoAdvancesInMilliseconds(), + existingSettings.getNameOfRaceToSort(), existingSettings.isSortAscending(), + existingSettings.isUpdateUponPlayStateChange(), + existingSettings.getActiveRaceColumnSelectionStrategy(), + existingSettings.isShowAddedScores(), + existingSettings.isShowOverallColumnWithNumberOfRacesCompletedPerCompetitor(), + existingSettings.isShowCompetitorSailIdColumn(), + existingSettings.isShowCompetitorFullNameColumn()); + getLeaderboardPanel().updateSettings(newSettings); + } + + private void adjustMapSettings() { + final RaceMapSettings existingMapSettings = getRaceBoardPanel().getMap().getSettings(); + final RaceMapSettings newMapSettings = new RaceMapSettings(existingMapSettings.getZoomSettings(), + existingMapSettings.getHelpLinesSettings(), + existingMapSettings.getTransparentHoverlines(), + existingMapSettings.getHoverlineStrokeWeight(), + tailLength.asMillis(), + /* existingMapSettings.isWindUp() */ true, + existingMapSettings.getBuoyZoneRadiusInMeters(), + /* existingMapSettings.isShowOnlySelectedCompetitors() */ true, // show the top n competitors and their tails quickly + existingMapSettings.isShowSelectedCompetitorsInfo(), + existingMapSettings.isShowWindStreamletColors(), + existingMapSettings.isShowWindStreamletOverlay(), + existingMapSettings.isShowSimulationOverlay(), + existingMapSettings.isShowMapControls(), + existingMapSettings.getManeuverTypesToShow(), + existingMapSettings.isShowDouglasPeuckerPoints()); + getRaceBoardPanel().getMap().updateSettings(newMapSettings); } /** @@ -128,4 +89,46 @@ public class WinningLanesMode extends RaceBoardModeWithPerRaceCompetitors { final int numberOfCompetitors = Util.size(competitors); return numberOfCompetitors==0 ? 0 : numberOfCompetitors<=4 ? 1 : numberOfCompetitors <=9 ? 2 : 3; } + + @Override + protected void trigger() { + final Date startOfRace; + if (getRaceTimesInfoForRace() != null && (startOfRace=getRaceTimesInfoForRace().startOfRace) != null) { + if (getTimer().getPlayMode() == PlayModes.Live) { + // adjust tail length such that for the leading boat the tail is shown since the start time point + tailLength = new MillisecondsTimePoint(startOfRace).until(getTimer().getLiveTimePoint()); + } else { + final List markPassingTimes = getRaceTimesInfoForRace().getMarkPassingTimes(); + final Date firstPassingOfLastWaypointPassed = markPassingTimes == null || markPassingTimes.isEmpty() ? null : + markPassingTimes.get(markPassingTimes.size()-1).firstPassingDate; + final Date endOfRace = getRaceTimesInfoForRace().endOfRace; + final Date end = firstPassingOfLastWaypointPassed != null ? firstPassingOfLastWaypointPassed : + endOfRace != null ? endOfRace : getRaceTimesInfoForRace().endOfTracking; + if (end != null) { + tailLength = new MillisecondsTimePoint(startOfRace).until(new MillisecondsTimePoint(end)); + getTimer().setTime(end.getTime()); + } + } + // we've done our adjustments; remove listener and let go + stopReceivingRaceTimesInfos(); + } + if (!adjustedLeaderboardSettings && getLeaderboard() != null) { + adjustedLeaderboardSettings = true; + // it's important to first unregister the listener before updateSettings is called because + // updateSettings will trigger another leaderboard load, leading to an endless recursion otherwise + stopReceivingLeaderboard(); + adjustLeaderboardSettings(); + } + if (adjustedLeaderboardSettings && tailLength != null) { + adjustMapSettings(); + } + if (getLeaderboardForSpecificTimePoint() == null && tailLength != null && getLeaderboard() != null && getRaceColumn() != null) { + loadLeaderboardForSpecificTimePoint(getLeaderboard().name, getRaceColumn().getName(), getTimer().getTime()); + } + if (!adjustedCompetitorSelection && getLeaderboardForSpecificTimePoint() != null) { + adjustedCompetitorSelection = true; + updateCompetitorSelection(); + } + } + }