From 4fa94956c9a91b615f4f77894dc1892cef7cb854 Mon Sep 17 00:00:00 2001 From: Vladislav Chumak Date: Wed, 26 Apr 2017 18:45:23 +0200 Subject: [PATCH] bug4100 Refactored and added Javadoc --- .../shared/header/SAPHeaderComponent.java | 2 +- .../LeaderboardWithHeaderPerspective.java | 462 +- .../regatta/RegattaAnalyticsDataManager.java | 2 +- .../SharedLeaderboardRegattaTabView.java | 8 +- .../RegattaLeaderboardTabView.java | 2 +- .../EventSeriesAnalyticsDataManager.java | 2 +- .../SharedLeaderboardEventSeriesTabView.java | 8 +- .../EventSeriesLeaderboardsTabView.java | 310 +- .../EventSeriesOverallLeaderboardTabView.java | 234 +- .../client/RegattaOverviewPanel.java | 8 +- .../client/RegattaRaceStatesComponent.java | 2 +- .../AbstractTrackedRacesListComposite.java | 1066 +-- .../TrackedRacesListComposite.java | 420 +- .../sailing/gwt/ui/client/RaceTimePanel.java | 754 +- .../sap/sailing/gwt/ui/client/TimePanel.java | 1306 +-- .../media/MediaPlayerManagerComponent.java | 2 +- .../AbstractCompetitorLeaderboardChart.java | 2 +- .../charts/AbstractCompetitorRaceChart.java | 1682 ++-- .../shared/charts/AbstractRaceChart.java | 592 +- .../shared/charts/EditMarkPassingsPanel.java | 944 +-- .../shared/charts/EditMarkPositionPanel.java | 2040 ++--- .../ui/client/shared/charts/MarksPanel.java | 356 +- .../MultiCompetitorLeaderboardChart.java | 2 +- .../charts/MultiCompetitorRaceChart.java | 236 +- .../ui/client/shared/charts/WindChart.java | 1326 ++-- .../gwt/ui/client/shared/racemap/RaceMap.java | 5864 +++++++------- .../AnchorDataMiningSettingsControl.java | 280 +- .../developer/PredefinedQueryRunner.java | 2 +- .../developer/QueryDefinitionViewer.java | 2 +- .../execution/SimpleQueryRunner.java | 2 +- .../AbstractNumericResultsPresenter.java | 2 +- .../AbstractResultsPresenter.java | 2 +- .../presentation/MultiResultsPresenter.java | 380 +- .../PlainFilterSelectionPresenter.java | 2 +- .../presentation/PlainResultsPresenter.java | 2 +- .../datamining/presentation/ResultsChart.java | 2 +- .../presentation/TabbedResultsPresenter.java | 2 +- .../AbstractQueryDefinitionProvider.java | 2 +- .../MultiDimensionalGroupingProvider.java | 2 +- .../QueryDefinitionProviderWithControls.java | 2 +- .../RetrieverLevelSettingsComponent.java | 2 +- ...eDataRetrieverChainDefinitionProvider.java | 2 +- .../selection/SimpleStatisticProvider.java | 2 +- ...RetrieverChainFilterSelectionProvider.java | 2 +- ...RetrieverLevelFilterSelectionProvider.java | 2 +- .../AbstractLeaderboardViewer.java | 2 +- .../ui/leaderboard/LeaderboardEntryPoint.java | 6 +- .../gwt/ui/leaderboard/LeaderboardPanel.java | 7000 ++++++++--------- .../gwt/ui/leaderboard/LeaderboardViewer.java | 2 +- .../ui/leaderboard/MetaLeaderboardViewer.java | 2 +- .../ui/leaderboard/MultiLeaderboardPanel.java | 74 +- .../MultiLeaderboardProxyPanel.java | 12 +- .../leaderboard/OverallLeaderboardPanel.java | 76 +- .../PolarBackendResultsPresenter.java | 2 +- .../ui/polarmining/PolarResultsPresenter.java | 2 +- .../raceboard/RaceBoardComponentContext.java | 247 +- .../gwt/ui/raceboard/RaceBoardEntryPoint.java | 326 +- .../gwt/ui/raceboard/RaceBoardPanel.java | 2 +- .../settings/value/ValueCollection.java | 2 - .../settings/value/ValueCollectionValue.java | 1 - java/com.sap.sse.gwt/META-INF/MANIFEST.MF | 1 + .../shared/components/AbstractComponent.java | 62 +- .../AbstractCompositeComponent.java | 78 +- .../components/AbstractLazyComponent.java | 2 +- .../client/shared/components/Component.java | 150 +- .../shared/components/ComponentLifecycle.java | 15 + .../components/ComponentWithoutSettings.java | 2 +- .../components/LinkWithSettingsGenerator.java | 180 +- .../shared/components/SettingsDialog.java | 272 +- .../AbstractPerspectiveComposite.java | 1 + .../shared/perspective/ComponentContext.java | 69 - ...ositeLifecycleTabbedSettingsComponent.java | 185 +- ...ompositeLifecycleTabbedSettingsDialog.java | 79 +- .../perspective/SettingsBuildingPipeline.java | 20 - .../AbstractSettingsBuildingPipeline.java | 64 +- .../shared/settings/ComponentContext.java | 115 + .../ComponentContextWithSettingsStorage.java | 595 +- .../ComponentUtils.java | 198 +- .../DefaultOnSettingsLoadedCallback.java | 20 +- .../DummyOnSettingsStoredCallback.java | 26 +- .../OnSettingsLoadedCallback.java | 16 +- .../OnSettingsStoredCallback.java | 12 +- .../PipelineLevel.java | 40 +- .../settings/SettingsBuildingPipeline.java | 75 + .../SettingsJsons.java | 53 +- .../SettingsStorageManager.java | 145 +- .../SettingsStringConverter.java | 132 +- .../SettingsStrings.java | 49 +- .../SimpleComponentContext.java | 210 +- ...ContextWithSettingsStorageAndPatching.java | 93 +- ...laceBasedUserSettingsBuildingPipeline.java | 68 +- .../PlaceBasedUserSettingsStorageManager.java | 33 +- .../SimpleSettingsStorageManager.java | 165 +- .../settings/UrlSettingsBuildingPipeline.java | 46 +- .../UserSettingsBuildingPipeline.java | 74 +- ...rSettingsBuildingPipelineWithPatching.java | 252 +- .../settings/UserSettingsStorageManager.java | 667 +- ...serSettingsStorageManagerWithPatching.java | 88 +- 98 files changed, 15408 insertions(+), 15026 deletions(-) delete mode 100644 java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/ComponentContext.java delete mode 100644 java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/SettingsBuildingPipeline.java rename java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/{perspective => settings}/AbstractSettingsBuildingPipeline.java (92%) create mode 100644 java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/ComponentContext.java rename java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/{perspective => settings}/ComponentContextWithSettingsStorage.java (66%) rename java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/{perspective => settings}/ComponentUtils.java (96%) rename java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/{perspective => settings}/DefaultOnSettingsLoadedCallback.java (83%) rename java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/{perspective => settings}/DummyOnSettingsStoredCallback.java (76%) rename java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/{perspective => settings}/OnSettingsLoadedCallback.java (77%) rename java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/{perspective => settings}/OnSettingsStoredCallback.java (64%) rename java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/{perspective => settings}/PipelineLevel.java (89%) create mode 100644 java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/SettingsBuildingPipeline.java rename java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/{perspective => settings}/SettingsJsons.java (75%) rename java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/{perspective => settings}/SettingsStorageManager.java (54%) rename java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/{perspective => settings}/SettingsStringConverter.java (96%) rename java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/{perspective => settings}/SettingsStrings.java (73%) rename java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/{perspective => settings}/SimpleComponentContext.java (79%) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/shared/header/SAPHeaderComponent.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/shared/header/SAPHeaderComponent.java index f0782277e49..96bcc18a641 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/shared/header/SAPHeaderComponent.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/shared/header/SAPHeaderComponent.java @@ -9,7 +9,7 @@ import com.sap.sailing.gwt.common.client.FullscreenUtil; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sse.gwt.client.shared.components.AbstractCompositeComponent; import com.sap.sse.gwt.client.shared.components.Component; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; import com.sap.sse.security.ui.client.UserService; public class SAPHeaderComponent extends AbstractCompositeComponent { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/shared/leaderboard/LeaderboardWithHeaderPerspective.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/shared/leaderboard/LeaderboardWithHeaderPerspective.java index 7f76b643698..fe9a328e8d9 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/shared/leaderboard/LeaderboardWithHeaderPerspective.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/shared/leaderboard/LeaderboardWithHeaderPerspective.java @@ -1,231 +1,231 @@ -package com.sap.sailing.gwt.autoplay.client.shared.leaderboard; - -import com.google.gwt.core.client.Scheduler; -import com.google.gwt.core.client.Scheduler.RepeatingCommand; -import com.google.gwt.dom.client.Style.FontWeight; -import com.google.gwt.dom.client.Style.Unit; -import com.google.gwt.event.logical.shared.ResizeEvent; -import com.google.gwt.event.logical.shared.ResizeHandler; -import com.google.gwt.user.client.Window; -import com.google.gwt.user.client.ui.DockLayoutPanel; -import com.google.gwt.user.client.ui.Widget; -import com.sap.sailing.gwt.autoplay.client.shared.header.SAPHeaderComponent; -import com.sap.sailing.gwt.autoplay.client.shared.header.SAPHeaderComponentLifecycle; -import com.sap.sailing.gwt.autoplay.client.shared.header.SAPHeaderComponentSettings; -import com.sap.sailing.gwt.autoplay.client.shared.oldleaderboard.OldLeaderboard; -import com.sap.sailing.gwt.common.client.CSS3Util; -import com.sap.sailing.gwt.common.client.FullscreenUtil; -import com.sap.sailing.gwt.settings.client.leaderboard.LeaderboardPanelLifecycle; -import com.sap.sailing.gwt.settings.client.leaderboard.LeaderboardSettings; -import com.sap.sailing.gwt.ui.client.CompetitorSelectionProvider; -import com.sap.sailing.gwt.ui.client.LeaderboardUpdateListener; -import com.sap.sailing.gwt.ui.client.LeaderboardUpdateProvider; -import com.sap.sailing.gwt.ui.client.SailingServiceAsync; -import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sailing.gwt.ui.leaderboard.LeaderboardPanel; -import com.sap.sse.gwt.client.ErrorReporter; -import com.sap.sse.gwt.client.async.AsyncActionsExecutor; -import com.sap.sse.gwt.client.player.Timer; -import com.sap.sse.gwt.client.shared.components.Component; -import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; -import com.sap.sse.gwt.client.shared.perspective.AbstractPerspectiveComposite; -import com.sap.sse.gwt.client.shared.perspective.ComponentContextWithSettingsStorage; -import com.sap.sse.gwt.client.shared.perspective.PerspectiveCompositeSettings; -import com.sap.sse.security.ui.client.UserService; - -/** - * A perspective managing a header with a single leaderboard filling the rest of the screen. - * - * @author Frank Mittag - * - */ -public class LeaderboardWithHeaderPerspective extends AbstractPerspectiveComposite implements LeaderboardUpdateProvider { - private final DockLayoutPanel dockPanel; - private final static int SAP_HEADER_HEIGHT = 75; - private final Widget currentContentWidget; - private final LeaderboardPanel leaderboardPanel; - private final StringMessages stringMessages; - - public LeaderboardWithHeaderPerspective(Component parent, - ComponentContextWithSettingsStorage> componentContext, - LeaderboardWithHeaderPerspectiveLifecycle lifecycle, - PerspectiveCompositeSettings settings, - SailingServiceAsync sailingService, UserService userService, AsyncActionsExecutor asyncActionsExecutor, - CompetitorSelectionProvider competitorSelectionProvider, Timer timer, - String leaderboardName, final ErrorReporter errorReporter, final StringMessages stringMessages, - boolean startInFullScreenMode) { - super(parent, componentContext, lifecycle, settings); - this.stringMessages = stringMessages; - Window.addResizeHandler(new ResizeHandler() { - @Override - public void onResize(ResizeEvent event) { - if (LeaderboardWithHeaderPerspective.this.getPerspectiveSettings().isLeaderboardAutoZoom()) { - autoZoomContentWidget(SAP_HEADER_HEIGHT, currentContentWidget); - } - } - }); - SAPHeaderComponentLifecycle sapHeaderLifecycle = getPerspectiveLifecycle().getSapHeaderLifecycle(); - SAPHeaderComponent sapHeader = createSAPHeader(sapHeaderLifecycle, userService, - settings.findSettingsByComponentId(sapHeaderLifecycle.getComponentId()), - stringMessages, startInFullScreenMode); - leaderboardPanel = createLeaderboardPanel(lifecycle, settings, sailingService, asyncActionsExecutor, - competitorSelectionProvider, timer, leaderboardName, errorReporter, stringMessages); - leaderboardPanel.getContentWidget().getElement().getStyle().setFontWeight(FontWeight.BOLD); - addChildComponent(sapHeader); - addChildComponent(leaderboardPanel); - dockPanel = new DockLayoutPanel(Unit.PX); - dockPanel.addNorth(sapHeader, SAP_HEADER_HEIGHT); - OldLeaderboard oldLeaderboard = new OldLeaderboard(leaderboardPanel, stringMessages); - leaderboardPanel.addLeaderboardUpdateListener(oldLeaderboard); - currentContentWidget = oldLeaderboard.getContentWidget(); - if (getPerspectiveSettings().isLeaderboardAutoZoom()) { - autoZoomContentWidget(SAP_HEADER_HEIGHT, currentContentWidget); - } else { - Double zoom = getPerspectiveSettings().getLeaderboardZoomFactor(); - zoomContentWidget(SAP_HEADER_HEIGHT, currentContentWidget, zoom); - } - dockPanel.add(oldLeaderboard); - initWidget(dockPanel); - } - - @Override - public Widget getEntryWidget() { - return this; - } - - @Override - public boolean isVisible() { - return false; - } - - @Override - public void setVisible(boolean visibility) { - } - - @Override - public String getDependentCssClassName() { - return ""; - } - - private void zoomContentWidget(final int headerHeight, final Widget contentWidget, final double scaleFactor) { - if(contentWidget != null) { - Scheduler.get().scheduleFixedPeriod(new RepeatingCommand() { - public boolean execute () { - boolean invokeAgain = true; - if(currentContentWidget.getOffsetHeight() > 50) { - scaleContentWidget(headerHeight, contentWidget, scaleFactor); - FullscreenUtil.requestFullscreen(); - - invokeAgain = false; - } - return invokeAgain; - } - }, 1000); - } - } - - private void autoZoomContentWidget(final int headerHeight, final Widget contentWidget) { - if(contentWidget != null) { - Scheduler.get().scheduleFixedPeriod(new RepeatingCommand() { - public boolean execute () { - boolean invokeAgain = true; - if(currentContentWidget.getOffsetHeight() > 50) { - scaleContentWidget(headerHeight, contentWidget); - invokeAgain = false; - } - return invokeAgain; - } - }, 1000); - } - } - - private void scaleContentWidget(int headerHeight, Widget contentWidget, double scaleFactor) { - int clientWidth = Window.getClientWidth(); - int contentWidth = contentWidget.getOffsetWidth(); - - double diffX = clientWidth - contentWidth * scaleFactor; - - scaleContentWidget(headerHeight, contentWidget, scaleFactor, diffX); - } - - private void scaleContentWidget(int headerHeight, Widget contentWidget) { - int clientWidth = Window.getClientWidth(); - int clientHeight = Window.getClientHeight() - headerHeight; - - int contentWidth = contentWidget.getOffsetWidth(); - int contentHeight = contentWidget.getOffsetHeight(); - - double scaleFactorX = clientWidth / (double) contentWidth; - double scaleFactorY = clientHeight / (double) contentHeight; - - Double scaleFactor = scaleFactorX > scaleFactorY ? scaleFactorY : scaleFactorX; - if(scaleFactor < 1.0) { - scaleFactor = 1.0; - } - double diffX = clientWidth - contentWidth * scaleFactor; - - scaleContentWidget(headerHeight, contentWidget, scaleFactor, diffX); - } - - private void scaleContentWidget(int headerHeight, Widget contentWidget, double scaleFactor, double diffX) { - if(scaleFactor > 0.0) { - CSS3Util.setProperty(contentWidget.getElement().getStyle(), "transform", "translateX(" + diffX / 2.0 + "px) scale(" + scaleFactor + ")"); - CSS3Util.setProperty(contentWidget.getElement().getStyle(), "transformOrigin", "0 0"); - } - } - - @Override - public void addLeaderboardUpdateListener(LeaderboardUpdateListener listener) { - leaderboardPanel.addLeaderboardUpdateListener(listener); - } - - @Override - public void removeLeaderboardUpdateListener(LeaderboardUpdateListener listener) { - leaderboardPanel.removeLeaderboardUpdateListener(listener); - } - - private SAPHeaderComponent createSAPHeader(SAPHeaderComponentLifecycle componentLifecycle, UserService userService, SAPHeaderComponentSettings settings, - final StringMessages stringMessages, boolean withFullscreenButton) { - return new SAPHeaderComponent(this, getComponentContext(), componentLifecycle, userService, settings, - stringMessages, - withFullscreenButton); - } - - private LeaderboardPanel createLeaderboardPanel(LeaderboardWithHeaderPerspectiveLifecycle lifecycle, - PerspectiveCompositeSettings settings, - SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, - CompetitorSelectionProvider competitorSelectionProvider, Timer timer, - String leaderboardName, final ErrorReporter errorReporter, final StringMessages stringMessages) { - LeaderboardPanelLifecycle leaderboardPanelLifecycle = getPerspectiveLifecycle().getLeaderboardPanelLifecycle(); - LeaderboardSettings leaderboardSettings = settings - .findSettingsByComponentId(leaderboardPanelLifecycle.getComponentId()); - - LeaderboardPanel leaderboardPanel = new LeaderboardPanel(this, getComponentContext(), sailingService, - asyncActionsExecutor, - leaderboardSettings, /*isEmbedded*/true, /* preSelectedRace */null, - competitorSelectionProvider, timer, /* leaderboardGroupName */"", - leaderboardName, errorReporter, stringMessages, - /* showRaceDetails */false, /* competitorSearchTextBox */ null, /* showRegattaRank */ - /* showSelectionCheckbox */false, /* raceTimesInfoProvider */null, false, /* autoExpandLastRaceColumn */ - /* adjustTimerDelay */ true, /* autoApplyTopNFilter */ false, /* showCompetitorFilterStatus */ false, /* enableSyncScroller */ false); - - return leaderboardPanel; - } - - @Override - public SettingsDialogComponent getPerspectiveOwnSettingsDialogComponent() { - return new LeaderboardPerspectiveSettingsDialogComponent(getPerspectiveSettings(), stringMessages); - } - - @Override - public boolean hasPerspectiveOwnSettings() { - return true; - } - - @Override - public String getId() { - return LeaderboardWithHeaderPerspectiveLifecycle.ID; - } - -} +package com.sap.sailing.gwt.autoplay.client.shared.leaderboard; + +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.core.client.Scheduler.RepeatingCommand; +import com.google.gwt.dom.client.Style.FontWeight; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.event.logical.shared.ResizeEvent; +import com.google.gwt.event.logical.shared.ResizeHandler; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.ui.DockLayoutPanel; +import com.google.gwt.user.client.ui.Widget; +import com.sap.sailing.gwt.autoplay.client.shared.header.SAPHeaderComponent; +import com.sap.sailing.gwt.autoplay.client.shared.header.SAPHeaderComponentLifecycle; +import com.sap.sailing.gwt.autoplay.client.shared.header.SAPHeaderComponentSettings; +import com.sap.sailing.gwt.autoplay.client.shared.oldleaderboard.OldLeaderboard; +import com.sap.sailing.gwt.common.client.CSS3Util; +import com.sap.sailing.gwt.common.client.FullscreenUtil; +import com.sap.sailing.gwt.settings.client.leaderboard.LeaderboardPanelLifecycle; +import com.sap.sailing.gwt.settings.client.leaderboard.LeaderboardSettings; +import com.sap.sailing.gwt.ui.client.CompetitorSelectionProvider; +import com.sap.sailing.gwt.ui.client.LeaderboardUpdateListener; +import com.sap.sailing.gwt.ui.client.LeaderboardUpdateProvider; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.leaderboard.LeaderboardPanel; +import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.async.AsyncActionsExecutor; +import com.sap.sse.gwt.client.player.Timer; +import com.sap.sse.gwt.client.shared.components.Component; +import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; +import com.sap.sse.gwt.client.shared.perspective.AbstractPerspectiveComposite; +import com.sap.sse.gwt.client.shared.perspective.PerspectiveCompositeSettings; +import com.sap.sse.gwt.client.shared.settings.ComponentContextWithSettingsStorage; +import com.sap.sse.security.ui.client.UserService; + +/** + * A perspective managing a header with a single leaderboard filling the rest of the screen. + * + * @author Frank Mittag + * + */ +public class LeaderboardWithHeaderPerspective extends AbstractPerspectiveComposite implements LeaderboardUpdateProvider { + private final DockLayoutPanel dockPanel; + private final static int SAP_HEADER_HEIGHT = 75; + private final Widget currentContentWidget; + private final LeaderboardPanel leaderboardPanel; + private final StringMessages stringMessages; + + public LeaderboardWithHeaderPerspective(Component parent, + ComponentContextWithSettingsStorage> componentContext, + LeaderboardWithHeaderPerspectiveLifecycle lifecycle, + PerspectiveCompositeSettings settings, + SailingServiceAsync sailingService, UserService userService, AsyncActionsExecutor asyncActionsExecutor, + CompetitorSelectionProvider competitorSelectionProvider, Timer timer, + String leaderboardName, final ErrorReporter errorReporter, final StringMessages stringMessages, + boolean startInFullScreenMode) { + super(parent, componentContext, lifecycle, settings); + this.stringMessages = stringMessages; + Window.addResizeHandler(new ResizeHandler() { + @Override + public void onResize(ResizeEvent event) { + if (LeaderboardWithHeaderPerspective.this.getPerspectiveSettings().isLeaderboardAutoZoom()) { + autoZoomContentWidget(SAP_HEADER_HEIGHT, currentContentWidget); + } + } + }); + SAPHeaderComponentLifecycle sapHeaderLifecycle = getPerspectiveLifecycle().getSapHeaderLifecycle(); + SAPHeaderComponent sapHeader = createSAPHeader(sapHeaderLifecycle, userService, + settings.findSettingsByComponentId(sapHeaderLifecycle.getComponentId()), + stringMessages, startInFullScreenMode); + leaderboardPanel = createLeaderboardPanel(lifecycle, settings, sailingService, asyncActionsExecutor, + competitorSelectionProvider, timer, leaderboardName, errorReporter, stringMessages); + leaderboardPanel.getContentWidget().getElement().getStyle().setFontWeight(FontWeight.BOLD); + addChildComponent(sapHeader); + addChildComponent(leaderboardPanel); + dockPanel = new DockLayoutPanel(Unit.PX); + dockPanel.addNorth(sapHeader, SAP_HEADER_HEIGHT); + OldLeaderboard oldLeaderboard = new OldLeaderboard(leaderboardPanel, stringMessages); + leaderboardPanel.addLeaderboardUpdateListener(oldLeaderboard); + currentContentWidget = oldLeaderboard.getContentWidget(); + if (getPerspectiveSettings().isLeaderboardAutoZoom()) { + autoZoomContentWidget(SAP_HEADER_HEIGHT, currentContentWidget); + } else { + Double zoom = getPerspectiveSettings().getLeaderboardZoomFactor(); + zoomContentWidget(SAP_HEADER_HEIGHT, currentContentWidget, zoom); + } + dockPanel.add(oldLeaderboard); + initWidget(dockPanel); + } + + @Override + public Widget getEntryWidget() { + return this; + } + + @Override + public boolean isVisible() { + return false; + } + + @Override + public void setVisible(boolean visibility) { + } + + @Override + public String getDependentCssClassName() { + return ""; + } + + private void zoomContentWidget(final int headerHeight, final Widget contentWidget, final double scaleFactor) { + if(contentWidget != null) { + Scheduler.get().scheduleFixedPeriod(new RepeatingCommand() { + public boolean execute () { + boolean invokeAgain = true; + if(currentContentWidget.getOffsetHeight() > 50) { + scaleContentWidget(headerHeight, contentWidget, scaleFactor); + FullscreenUtil.requestFullscreen(); + + invokeAgain = false; + } + return invokeAgain; + } + }, 1000); + } + } + + private void autoZoomContentWidget(final int headerHeight, final Widget contentWidget) { + if(contentWidget != null) { + Scheduler.get().scheduleFixedPeriod(new RepeatingCommand() { + public boolean execute () { + boolean invokeAgain = true; + if(currentContentWidget.getOffsetHeight() > 50) { + scaleContentWidget(headerHeight, contentWidget); + invokeAgain = false; + } + return invokeAgain; + } + }, 1000); + } + } + + private void scaleContentWidget(int headerHeight, Widget contentWidget, double scaleFactor) { + int clientWidth = Window.getClientWidth(); + int contentWidth = contentWidget.getOffsetWidth(); + + double diffX = clientWidth - contentWidth * scaleFactor; + + scaleContentWidget(headerHeight, contentWidget, scaleFactor, diffX); + } + + private void scaleContentWidget(int headerHeight, Widget contentWidget) { + int clientWidth = Window.getClientWidth(); + int clientHeight = Window.getClientHeight() - headerHeight; + + int contentWidth = contentWidget.getOffsetWidth(); + int contentHeight = contentWidget.getOffsetHeight(); + + double scaleFactorX = clientWidth / (double) contentWidth; + double scaleFactorY = clientHeight / (double) contentHeight; + + Double scaleFactor = scaleFactorX > scaleFactorY ? scaleFactorY : scaleFactorX; + if(scaleFactor < 1.0) { + scaleFactor = 1.0; + } + double diffX = clientWidth - contentWidth * scaleFactor; + + scaleContentWidget(headerHeight, contentWidget, scaleFactor, diffX); + } + + private void scaleContentWidget(int headerHeight, Widget contentWidget, double scaleFactor, double diffX) { + if(scaleFactor > 0.0) { + CSS3Util.setProperty(contentWidget.getElement().getStyle(), "transform", "translateX(" + diffX / 2.0 + "px) scale(" + scaleFactor + ")"); + CSS3Util.setProperty(contentWidget.getElement().getStyle(), "transformOrigin", "0 0"); + } + } + + @Override + public void addLeaderboardUpdateListener(LeaderboardUpdateListener listener) { + leaderboardPanel.addLeaderboardUpdateListener(listener); + } + + @Override + public void removeLeaderboardUpdateListener(LeaderboardUpdateListener listener) { + leaderboardPanel.removeLeaderboardUpdateListener(listener); + } + + private SAPHeaderComponent createSAPHeader(SAPHeaderComponentLifecycle componentLifecycle, UserService userService, SAPHeaderComponentSettings settings, + final StringMessages stringMessages, boolean withFullscreenButton) { + return new SAPHeaderComponent(this, getComponentContext(), componentLifecycle, userService, settings, + stringMessages, + withFullscreenButton); + } + + private LeaderboardPanel createLeaderboardPanel(LeaderboardWithHeaderPerspectiveLifecycle lifecycle, + PerspectiveCompositeSettings settings, + SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, + CompetitorSelectionProvider competitorSelectionProvider, Timer timer, + String leaderboardName, final ErrorReporter errorReporter, final StringMessages stringMessages) { + LeaderboardPanelLifecycle leaderboardPanelLifecycle = getPerspectiveLifecycle().getLeaderboardPanelLifecycle(); + LeaderboardSettings leaderboardSettings = settings + .findSettingsByComponentId(leaderboardPanelLifecycle.getComponentId()); + + LeaderboardPanel leaderboardPanel = new LeaderboardPanel(this, getComponentContext(), sailingService, + asyncActionsExecutor, + leaderboardSettings, /*isEmbedded*/true, /* preSelectedRace */null, + competitorSelectionProvider, timer, /* leaderboardGroupName */"", + leaderboardName, errorReporter, stringMessages, + /* showRaceDetails */false, /* competitorSearchTextBox */ null, /* showRegattaRank */ + /* showSelectionCheckbox */false, /* raceTimesInfoProvider */null, false, /* autoExpandLastRaceColumn */ + /* adjustTimerDelay */ true, /* autoApplyTopNFilter */ false, /* showCompetitorFilterStatus */ false, /* enableSyncScroller */ false); + + return leaderboardPanel; + } + + @Override + public SettingsDialogComponent getPerspectiveOwnSettingsDialogComponent() { + return new LeaderboardPerspectiveSettingsDialogComponent(getPerspectiveSettings(), stringMessages); + } + + @Override + public boolean hasPerspectiveOwnSettings() { + return true; + } + + @Override + public String getId() { + return LeaderboardWithHeaderPerspectiveLifecycle.ID; + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/event/regatta/RegattaAnalyticsDataManager.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/event/regatta/RegattaAnalyticsDataManager.java index e9ebb5f4743..09ad00cc801 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/event/regatta/RegattaAnalyticsDataManager.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/event/regatta/RegattaAnalyticsDataManager.java @@ -16,7 +16,7 @@ import com.sap.sse.gwt.client.async.AsyncActionsExecutor; import com.sap.sse.gwt.client.player.Timer; import com.sap.sse.gwt.client.shared.components.Component; import com.sap.sse.gwt.client.shared.components.SettingsDialog; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; /** * A class managing analytical data on the regatta level (like leaderboard, regatta rank, etc.) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/event/regatta/SharedLeaderboardRegattaTabView.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/event/regatta/SharedLeaderboardRegattaTabView.java index bdcd173f744..716225c91e0 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/event/regatta/SharedLeaderboardRegattaTabView.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/event/regatta/SharedLeaderboardRegattaTabView.java @@ -13,10 +13,10 @@ import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.leaderboard.LeaderboardPanel; import com.sap.sse.gwt.client.mutationobserver.ElementSizeMutationObserver; import com.sap.sse.gwt.client.mutationobserver.ElementSizeMutationObserver.DomMutationCallback; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; -import com.sap.sse.gwt.client.shared.perspective.ComponentContextWithSettingsStorage; -import com.sap.sse.gwt.client.shared.perspective.DefaultOnSettingsLoadedCallback; -import com.sap.sse.gwt.client.shared.perspective.SettingsStorageManager; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; +import com.sap.sse.gwt.client.shared.settings.ComponentContextWithSettingsStorage; +import com.sap.sse.gwt.client.shared.settings.DefaultOnSettingsLoadedCallback; +import com.sap.sse.gwt.client.shared.settings.SettingsStorageManager; import com.sap.sse.gwt.shared.GwtHttpRequestUtils; import com.sap.sse.security.ui.client.UserService; import com.sap.sse.security.ui.settings.PlaceBasedUserSettingsStorageManager; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/event/regatta/leaderboardtab/RegattaLeaderboardTabView.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/event/regatta/leaderboardtab/RegattaLeaderboardTabView.java index 65c13dce65e..97093e996a8 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/event/regatta/leaderboardtab/RegattaLeaderboardTabView.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/event/regatta/leaderboardtab/RegattaLeaderboardTabView.java @@ -23,7 +23,7 @@ import com.sap.sailing.gwt.settings.client.leaderboard.LeaderboardSettings; import com.sap.sailing.gwt.ui.client.LeaderboardUpdateProvider; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.leaderboard.LeaderboardPanel; -import com.sap.sse.gwt.client.shared.perspective.DefaultOnSettingsLoadedCallback; +import com.sap.sse.gwt.client.shared.settings.DefaultOnSettingsLoadedCallback; /** * Created by pgtaboada on 25.11.14. diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/fakeseries/EventSeriesAnalyticsDataManager.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/fakeseries/EventSeriesAnalyticsDataManager.java index 91ba2730b41..23d5148fbc3 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/fakeseries/EventSeriesAnalyticsDataManager.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/fakeseries/EventSeriesAnalyticsDataManager.java @@ -25,7 +25,7 @@ import com.sap.sse.gwt.client.async.AsyncActionsExecutor; import com.sap.sse.gwt.client.player.Timer; import com.sap.sse.gwt.client.shared.components.Component; import com.sap.sse.gwt.client.shared.components.SettingsDialog; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; /** * A viewer for an overall series leaderboard. Additionally the viewer can render a chart for the series leaderboard and diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/fakeseries/SharedLeaderboardEventSeriesTabView.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/fakeseries/SharedLeaderboardEventSeriesTabView.java index 5c5e841bde0..536f063cd2c 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/fakeseries/SharedLeaderboardEventSeriesTabView.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/fakeseries/SharedLeaderboardEventSeriesTabView.java @@ -11,10 +11,10 @@ import com.sap.sailing.gwt.settings.client.utils.StorageDefinitionIdFactory; import com.sap.sailing.gwt.ui.client.LeaderboardUpdateListener; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.leaderboard.LeaderboardPanel; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; -import com.sap.sse.gwt.client.shared.perspective.ComponentContextWithSettingsStorage; -import com.sap.sse.gwt.client.shared.perspective.DefaultOnSettingsLoadedCallback; -import com.sap.sse.gwt.client.shared.perspective.SettingsStorageManager; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; +import com.sap.sse.gwt.client.shared.settings.ComponentContextWithSettingsStorage; +import com.sap.sse.gwt.client.shared.settings.DefaultOnSettingsLoadedCallback; +import com.sap.sse.gwt.client.shared.settings.SettingsStorageManager; import com.sap.sse.gwt.shared.GwtHttpRequestUtils; import com.sap.sse.security.ui.client.UserService; import com.sap.sse.security.ui.settings.PlaceBasedUserSettingsStorageManager; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/fakeseries/leaderboardstab/EventSeriesLeaderboardsTabView.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/fakeseries/leaderboardstab/EventSeriesLeaderboardsTabView.java index 9de833ee843..be3b475a801 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/fakeseries/leaderboardstab/EventSeriesLeaderboardsTabView.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/fakeseries/leaderboardstab/EventSeriesLeaderboardsTabView.java @@ -1,158 +1,158 @@ -package com.sap.sailing.gwt.home.desktop.places.fakeseries.leaderboardstab; - -import com.google.gwt.core.client.GWT; -import com.google.gwt.event.logical.shared.AttachEvent; -import com.google.gwt.event.logical.shared.AttachEvent.Handler; -import com.google.gwt.uibinder.client.UiBinder; -import com.google.gwt.uibinder.client.UiField; -import com.google.gwt.user.client.ui.AcceptsOneWidget; -import com.google.gwt.user.client.ui.Composite; -import com.google.gwt.user.client.ui.HTMLPanel; -import com.google.gwt.user.client.ui.Label; -import com.sap.sailing.domain.common.RaceIdentifier; -import com.sap.sailing.domain.common.dto.LeaderboardDTO; -import com.sap.sailing.domain.common.dto.RaceColumnDTO; -import com.sap.sailing.gwt.common.client.controls.tabbar.TabView; -import com.sap.sailing.gwt.home.communication.fakeseries.EventSeriesViewDTO.EventSeriesState; -import com.sap.sailing.gwt.home.desktop.partials.old.multileaderboard.OldMultiLeaderboard; -import com.sap.sailing.gwt.home.desktop.partials.old.multileaderboard.OldMultiLeaderboardDelegateFullscreenViewer; -import com.sap.sailing.gwt.home.desktop.places.fakeseries.EventSeriesAnalyticsDataManager; -import com.sap.sailing.gwt.home.desktop.places.fakeseries.SeriesTabView; -import com.sap.sailing.gwt.home.desktop.places.fakeseries.SeriesView; -import com.sap.sailing.gwt.settings.client.leaderboard.LeaderboardSettings; -import com.sap.sailing.gwt.settings.client.leaderboard.LeaderboardUrlSettings; -import com.sap.sailing.gwt.settings.client.leaderboard.MultiLeaderboardPanelLifecycle; -import com.sap.sailing.gwt.settings.client.utils.StorageDefinitionIdFactory; -import com.sap.sailing.gwt.ui.client.LeaderboardUpdateListener; -import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sailing.gwt.ui.leaderboard.MultiLeaderboardProxyPanel; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; -import com.sap.sse.gwt.client.shared.perspective.ComponentContextWithSettingsStorage; -import com.sap.sse.gwt.client.shared.perspective.DefaultOnSettingsLoadedCallback; -import com.sap.sse.gwt.client.shared.perspective.SettingsStorageManager; -import com.sap.sse.gwt.shared.GwtHttpRequestUtils; -import com.sap.sse.security.ui.client.UserService; -import com.sap.sse.security.ui.settings.PlaceBasedUserSettingsStorageManager; -import com.sap.sse.security.ui.settings.StorageDefinitionId; - -public class EventSeriesLeaderboardsTabView extends Composite implements SeriesTabView, - LeaderboardUpdateListener { - - interface MyBinder extends UiBinder { - } - - private static MyBinder ourUiBinder = GWT.create(MyBinder.class); - - private SeriesView.Presenter currentPresenter; - - @UiField(provided = true) - protected OldMultiLeaderboard leaderboard; - - public EventSeriesLeaderboardsTabView() { - leaderboard = new OldMultiLeaderboard(new OldMultiLeaderboardDelegateFullscreenViewer()); - } - - @Override - public Class getPlaceClassForActivation() { - return EventSeriesLeaderboardsPlace.class; - } - - @Override - public void setPresenter(SeriesView.Presenter currentPresenter) { - this.currentPresenter = currentPresenter; - } - - @Override - public TabView.State getState() { - return currentPresenter.getSeriesDTO().isHasAnalytics() ? TabView.State.VISIBLE : TabView.State.INVISIBLE; - } - - @Override - public void start(final EventSeriesLeaderboardsPlace myPlace, final AcceptsOneWidget contentArea) { - contentArea.setWidget(currentPresenter.getErrorAndBusyClientFactory().createBusyView()); - String leaderboardName = currentPresenter.getSeriesDTO().getLeaderboardId(); - - if (leaderboardName != null && !leaderboardName.isEmpty()) { - final EventSeriesAnalyticsDataManager regattaAnalyticsManager = currentPresenter.getCtx().getAnalyticsManager(); - final boolean autoExpandLastRaceColumn = GwtHttpRequestUtils.getBooleanParameter(LeaderboardUrlSettings.PARAM_AUTO_EXPAND_LAST_RACE_COLUMN, false); - - final ComponentContext componentContext = createLeaderboardComponentContext(leaderboardName, currentPresenter.getUserService(), /*FIXME placeToken */ null); - componentContext.getInitialSettings(new DefaultOnSettingsLoadedCallback() { - @Override - public void onSuccess(LeaderboardSettings settings) { - MultiLeaderboardProxyPanel leaderboardPanel = regattaAnalyticsManager.createMultiLeaderboardPanel(null, componentContext, - settings, - null, // TODO: preselectedLeaderboardName - null, - "leaderboardGroupName", - leaderboardName, - true, // TODO @FM this information came from place, now hard coded. check with frank - autoExpandLastRaceColumn); - leaderboardPanel.addAttachHandler(new Handler() { - - @Override - public void onAttachOrDetach(AttachEvent event) { - if(!event.isAttached()) { - componentContext.dispose(); - } - } - - }); - - initWidget(ourUiBinder.createAndBindUi(EventSeriesLeaderboardsTabView.this)); - - leaderboard.setMultiLeaderboard(leaderboardPanel, currentPresenter.getAutoRefreshTimer()); - leaderboardPanel.addLeaderboardUpdateListener(EventSeriesLeaderboardsTabView.this); - if (currentPresenter.getSeriesDTO().getState() != EventSeriesState.RUNNING) { - // TODO: this.leaderboard.hideRefresh(); - } else { +package com.sap.sailing.gwt.home.desktop.places.fakeseries.leaderboardstab; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.event.logical.shared.AttachEvent; +import com.google.gwt.event.logical.shared.AttachEvent.Handler; +import com.google.gwt.uibinder.client.UiBinder; +import com.google.gwt.uibinder.client.UiField; +import com.google.gwt.user.client.ui.AcceptsOneWidget; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.HTMLPanel; +import com.google.gwt.user.client.ui.Label; +import com.sap.sailing.domain.common.RaceIdentifier; +import com.sap.sailing.domain.common.dto.LeaderboardDTO; +import com.sap.sailing.domain.common.dto.RaceColumnDTO; +import com.sap.sailing.gwt.common.client.controls.tabbar.TabView; +import com.sap.sailing.gwt.home.communication.fakeseries.EventSeriesViewDTO.EventSeriesState; +import com.sap.sailing.gwt.home.desktop.partials.old.multileaderboard.OldMultiLeaderboard; +import com.sap.sailing.gwt.home.desktop.partials.old.multileaderboard.OldMultiLeaderboardDelegateFullscreenViewer; +import com.sap.sailing.gwt.home.desktop.places.fakeseries.EventSeriesAnalyticsDataManager; +import com.sap.sailing.gwt.home.desktop.places.fakeseries.SeriesTabView; +import com.sap.sailing.gwt.home.desktop.places.fakeseries.SeriesView; +import com.sap.sailing.gwt.settings.client.leaderboard.LeaderboardSettings; +import com.sap.sailing.gwt.settings.client.leaderboard.LeaderboardUrlSettings; +import com.sap.sailing.gwt.settings.client.leaderboard.MultiLeaderboardPanelLifecycle; +import com.sap.sailing.gwt.settings.client.utils.StorageDefinitionIdFactory; +import com.sap.sailing.gwt.ui.client.LeaderboardUpdateListener; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.leaderboard.MultiLeaderboardProxyPanel; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; +import com.sap.sse.gwt.client.shared.settings.ComponentContextWithSettingsStorage; +import com.sap.sse.gwt.client.shared.settings.DefaultOnSettingsLoadedCallback; +import com.sap.sse.gwt.client.shared.settings.SettingsStorageManager; +import com.sap.sse.gwt.shared.GwtHttpRequestUtils; +import com.sap.sse.security.ui.client.UserService; +import com.sap.sse.security.ui.settings.PlaceBasedUserSettingsStorageManager; +import com.sap.sse.security.ui.settings.StorageDefinitionId; + +public class EventSeriesLeaderboardsTabView extends Composite implements SeriesTabView, + LeaderboardUpdateListener { + + interface MyBinder extends UiBinder { + } + + private static MyBinder ourUiBinder = GWT.create(MyBinder.class); + + private SeriesView.Presenter currentPresenter; + + @UiField(provided = true) + protected OldMultiLeaderboard leaderboard; + + public EventSeriesLeaderboardsTabView() { + leaderboard = new OldMultiLeaderboard(new OldMultiLeaderboardDelegateFullscreenViewer()); + } + + @Override + public Class getPlaceClassForActivation() { + return EventSeriesLeaderboardsPlace.class; + } + + @Override + public void setPresenter(SeriesView.Presenter currentPresenter) { + this.currentPresenter = currentPresenter; + } + + @Override + public TabView.State getState() { + return currentPresenter.getSeriesDTO().isHasAnalytics() ? TabView.State.VISIBLE : TabView.State.INVISIBLE; + } + + @Override + public void start(final EventSeriesLeaderboardsPlace myPlace, final AcceptsOneWidget contentArea) { + contentArea.setWidget(currentPresenter.getErrorAndBusyClientFactory().createBusyView()); + String leaderboardName = currentPresenter.getSeriesDTO().getLeaderboardId(); + + if (leaderboardName != null && !leaderboardName.isEmpty()) { + final EventSeriesAnalyticsDataManager regattaAnalyticsManager = currentPresenter.getCtx().getAnalyticsManager(); + final boolean autoExpandLastRaceColumn = GwtHttpRequestUtils.getBooleanParameter(LeaderboardUrlSettings.PARAM_AUTO_EXPAND_LAST_RACE_COLUMN, false); + + final ComponentContext componentContext = createLeaderboardComponentContext(leaderboardName, currentPresenter.getUserService(), /*FIXME placeToken */ null); + componentContext.getInitialSettings(new DefaultOnSettingsLoadedCallback() { + @Override + public void onSuccess(LeaderboardSettings settings) { + MultiLeaderboardProxyPanel leaderboardPanel = regattaAnalyticsManager.createMultiLeaderboardPanel(null, componentContext, + settings, + null, // TODO: preselectedLeaderboardName + null, + "leaderboardGroupName", + leaderboardName, + true, // TODO @FM this information came from place, now hard coded. check with frank + autoExpandLastRaceColumn); + leaderboardPanel.addAttachHandler(new Handler() { + + @Override + public void onAttachOrDetach(AttachEvent event) { + if(!event.isAttached()) { + componentContext.dispose(); + } + } + + }); + + initWidget(ourUiBinder.createAndBindUi(EventSeriesLeaderboardsTabView.this)); + + leaderboard.setMultiLeaderboard(leaderboardPanel, currentPresenter.getAutoRefreshTimer()); + leaderboardPanel.addLeaderboardUpdateListener(EventSeriesLeaderboardsTabView.this); + if (currentPresenter.getSeriesDTO().getState() != EventSeriesState.RUNNING) { + // TODO: this.leaderboard.hideRefresh(); + } else { // Turn on auto refresh button at parent leaderboard leaderboard.turnOnAutoPlay(); - } - regattaAnalyticsManager.hideCompetitorChart(); - leaderboardPanel.setVisible(true); - contentArea.setWidget(EventSeriesLeaderboardsTabView.this); - - } - }); - } else { - contentArea.setWidget(new Label("No leaderboard specified, cannot proceed to leaderboardpage")); - new com.google.gwt.user.client.Timer() { - @Override - public void run() { - currentPresenter.getHomeNavigation().goToPlace(); - } - }.schedule(3000); - } - } - - private ComponentContext createLeaderboardComponentContext(String leaderboardName, UserService userService, - String placeToken) { - final MultiLeaderboardPanelLifecycle lifecycle = new MultiLeaderboardPanelLifecycle(null, StringMessages.INSTANCE); - final StorageDefinitionId storageDefinitionId = StorageDefinitionIdFactory.createStorageDefinitionIdForSeriesRegattaLeaderboards(leaderboardName); - final SettingsStorageManager settingsStorageManager = new PlaceBasedUserSettingsStorageManager<>( - userService, storageDefinitionId, placeToken); - - final ComponentContext componentContext = new ComponentContextWithSettingsStorage<>( - lifecycle, settingsStorageManager); - return componentContext; - } - - @Override - public void updatedLeaderboard(LeaderboardDTO leaderboard) { - this.leaderboard.updatedMultiLeaderboard(leaderboard, true); - } - - @Override - public void currentRaceSelected(RaceIdentifier raceIdentifier, RaceColumnDTO raceColumn) { - } - - @Override - public void stop() { - } - - @Override - public EventSeriesLeaderboardsPlace placeToFire() { - return new EventSeriesLeaderboardsPlace(currentPresenter.getCtx()); - } + } + regattaAnalyticsManager.hideCompetitorChart(); + leaderboardPanel.setVisible(true); + contentArea.setWidget(EventSeriesLeaderboardsTabView.this); + + } + }); + } else { + contentArea.setWidget(new Label("No leaderboard specified, cannot proceed to leaderboardpage")); + new com.google.gwt.user.client.Timer() { + @Override + public void run() { + currentPresenter.getHomeNavigation().goToPlace(); + } + }.schedule(3000); + } + } + + private ComponentContext createLeaderboardComponentContext(String leaderboardName, UserService userService, + String placeToken) { + final MultiLeaderboardPanelLifecycle lifecycle = new MultiLeaderboardPanelLifecycle(null, StringMessages.INSTANCE); + final StorageDefinitionId storageDefinitionId = StorageDefinitionIdFactory.createStorageDefinitionIdForSeriesRegattaLeaderboards(leaderboardName); + final SettingsStorageManager settingsStorageManager = new PlaceBasedUserSettingsStorageManager<>( + userService, storageDefinitionId, placeToken); + + final ComponentContext componentContext = new ComponentContextWithSettingsStorage<>( + lifecycle, settingsStorageManager); + return componentContext; + } + + @Override + public void updatedLeaderboard(LeaderboardDTO leaderboard) { + this.leaderboard.updatedMultiLeaderboard(leaderboard, true); + } + + @Override + public void currentRaceSelected(RaceIdentifier raceIdentifier, RaceColumnDTO raceColumn) { + } + + @Override + public void stop() { + } + + @Override + public EventSeriesLeaderboardsPlace placeToFire() { + return new EventSeriesLeaderboardsPlace(currentPresenter.getCtx()); + } } \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/fakeseries/overallleaderboardtab/EventSeriesOverallLeaderboardTabView.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/fakeseries/overallleaderboardtab/EventSeriesOverallLeaderboardTabView.java index c60dbe36419..eb4762a9f13 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/fakeseries/overallleaderboardtab/EventSeriesOverallLeaderboardTabView.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/fakeseries/overallleaderboardtab/EventSeriesOverallLeaderboardTabView.java @@ -1,118 +1,118 @@ -package com.sap.sailing.gwt.home.desktop.places.fakeseries.overallleaderboardtab; - -import com.google.gwt.core.client.GWT; -import com.google.gwt.uibinder.client.UiBinder; -import com.google.gwt.uibinder.client.UiField; -import com.google.gwt.user.client.ui.AcceptsOneWidget; -import com.google.gwt.user.client.ui.HTMLPanel; -import com.google.gwt.user.client.ui.Label; -import com.sap.sailing.domain.common.RaceIdentifier; -import com.sap.sailing.domain.common.dto.LeaderboardDTO; -import com.sap.sailing.domain.common.dto.RaceColumnDTO; -import com.sap.sailing.gwt.common.client.controls.tabbar.TabView; -import com.sap.sailing.gwt.home.desktop.partials.old.leaderboard.OldLeaderboard; -import com.sap.sailing.gwt.home.desktop.partials.old.leaderboard.OldLeaderboardDelegateFullscreenViewer; -import com.sap.sailing.gwt.home.desktop.places.Consumer; -import com.sap.sailing.gwt.home.desktop.places.fakeseries.EventSeriesAnalyticsDataManager; -import com.sap.sailing.gwt.home.desktop.places.fakeseries.SeriesView; -import com.sap.sailing.gwt.home.desktop.places.fakeseries.SharedLeaderboardEventSeriesTabView; -import com.sap.sailing.gwt.settings.client.leaderboard.LeaderboardSettings; -import com.sap.sailing.gwt.ui.leaderboard.LeaderboardPanel; -import com.sap.sse.gwt.client.shared.perspective.DefaultOnSettingsLoadedCallback; - -/** - * Created by pgtaboada on 25.11.14. - */ -public class EventSeriesOverallLeaderboardTabView extends SharedLeaderboardEventSeriesTabView { - - interface MyBinder extends UiBinder { - } - - private static MyBinder ourUiBinder = GWT.create(MyBinder.class); - - private SeriesView.Presenter currentPresenter; - - @UiField(provided = true) - protected OldLeaderboard leaderboard; - - public EventSeriesOverallLeaderboardTabView() { - leaderboard = new OldLeaderboard(new OldLeaderboardDelegateFullscreenViewer()); - } - - @Override - public Class getPlaceClassForActivation() { - return EventSeriesOverallLeaderboardPlace.class; - } - - @Override - public void setPresenter(SeriesView.Presenter currentPresenter) { - this.currentPresenter = currentPresenter; - } - - @Override - public TabView.State getState() { - return currentPresenter.getSeriesDTO().isHasAnalytics() ? TabView.State.VISIBLE : TabView.State.INVISIBLE; - } - - @SuppressWarnings("unused") - @Override - public void start(final EventSeriesOverallLeaderboardPlace myPlace, final AcceptsOneWidget contentArea) { - contentArea.setWidget(currentPresenter.getErrorAndBusyClientFactory().createBusyView()); - String leaderboardName = currentPresenter.getSeriesDTO().getLeaderboardId(); - if (leaderboardName != null && !leaderboardName.isEmpty()) { - EventSeriesAnalyticsDataManager eventSeriesAnalyticsManager = currentPresenter.getCtx().getAnalyticsManager(); - final Consumer leaderboardConsumer = new Consumer() { - @Override - public void consume(LeaderboardPanel leaderboardPanel) { - initWidget(ourUiBinder.createAndBindUi(EventSeriesOverallLeaderboardTabView.this)); - leaderboard.setLeaderboard(leaderboardPanel, currentPresenter.getAutoRefreshTimer()); - eventSeriesAnalyticsManager.hideCompetitorChart(); - contentArea.setWidget(EventSeriesOverallLeaderboardTabView.this); - if(leaderboardPanel.getLeaderboard() != null) { - leaderboard.updatedLeaderboard(leaderboardPanel.getLeaderboard()); - } - } - }; - LeaderboardPanel leaderboardPanel = eventSeriesAnalyticsManager.getLeaderboardPanel(); - if(leaderboardPanel == null) { - createSharedLeaderboardPanel(leaderboardName, eventSeriesAnalyticsManager, currentPresenter.getUserService(), /*FIXME placeToken */ null, leaderboardConsumer); - } else if( /*FIXME placeToken not empty */ false) { - createLeaderboardComponentContext(leaderboardName, currentPresenter.getUserService(), /*FIXME placeToken */ null).getInitialSettings(new DefaultOnSettingsLoadedCallback() { - @Override - public void onSuccess(LeaderboardSettings settings) { - leaderboardPanel.updateSettings(settings); - leaderboardConsumer.consume(leaderboardPanel); - } - }); - } else { - leaderboardConsumer.consume(leaderboardPanel); - } - } else { - contentArea.setWidget(new Label("No leaderboard specified, cannot proceed to leaderboardpage")); - new com.google.gwt.user.client.Timer() { - @Override - public void run() { - currentPresenter.getHomeNavigation().goToPlace(); - } - }.schedule(3000); - } - } - - @Override - public void updatedLeaderboard(LeaderboardDTO leaderboard) { - this.leaderboard.updatedLeaderboard(leaderboard); - } - - @Override - public void currentRaceSelected(RaceIdentifier raceIdentifier, RaceColumnDTO raceColumn) { - } - - @Override - public void stop() { - } - - @Override - public EventSeriesOverallLeaderboardPlace placeToFire() { - return new EventSeriesOverallLeaderboardPlace(currentPresenter.getCtx()); - } +package com.sap.sailing.gwt.home.desktop.places.fakeseries.overallleaderboardtab; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.uibinder.client.UiBinder; +import com.google.gwt.uibinder.client.UiField; +import com.google.gwt.user.client.ui.AcceptsOneWidget; +import com.google.gwt.user.client.ui.HTMLPanel; +import com.google.gwt.user.client.ui.Label; +import com.sap.sailing.domain.common.RaceIdentifier; +import com.sap.sailing.domain.common.dto.LeaderboardDTO; +import com.sap.sailing.domain.common.dto.RaceColumnDTO; +import com.sap.sailing.gwt.common.client.controls.tabbar.TabView; +import com.sap.sailing.gwt.home.desktop.partials.old.leaderboard.OldLeaderboard; +import com.sap.sailing.gwt.home.desktop.partials.old.leaderboard.OldLeaderboardDelegateFullscreenViewer; +import com.sap.sailing.gwt.home.desktop.places.Consumer; +import com.sap.sailing.gwt.home.desktop.places.fakeseries.EventSeriesAnalyticsDataManager; +import com.sap.sailing.gwt.home.desktop.places.fakeseries.SeriesView; +import com.sap.sailing.gwt.home.desktop.places.fakeseries.SharedLeaderboardEventSeriesTabView; +import com.sap.sailing.gwt.settings.client.leaderboard.LeaderboardSettings; +import com.sap.sailing.gwt.ui.leaderboard.LeaderboardPanel; +import com.sap.sse.gwt.client.shared.settings.DefaultOnSettingsLoadedCallback; + +/** + * Created by pgtaboada on 25.11.14. + */ +public class EventSeriesOverallLeaderboardTabView extends SharedLeaderboardEventSeriesTabView { + + interface MyBinder extends UiBinder { + } + + private static MyBinder ourUiBinder = GWT.create(MyBinder.class); + + private SeriesView.Presenter currentPresenter; + + @UiField(provided = true) + protected OldLeaderboard leaderboard; + + public EventSeriesOverallLeaderboardTabView() { + leaderboard = new OldLeaderboard(new OldLeaderboardDelegateFullscreenViewer()); + } + + @Override + public Class getPlaceClassForActivation() { + return EventSeriesOverallLeaderboardPlace.class; + } + + @Override + public void setPresenter(SeriesView.Presenter currentPresenter) { + this.currentPresenter = currentPresenter; + } + + @Override + public TabView.State getState() { + return currentPresenter.getSeriesDTO().isHasAnalytics() ? TabView.State.VISIBLE : TabView.State.INVISIBLE; + } + + @SuppressWarnings("unused") + @Override + public void start(final EventSeriesOverallLeaderboardPlace myPlace, final AcceptsOneWidget contentArea) { + contentArea.setWidget(currentPresenter.getErrorAndBusyClientFactory().createBusyView()); + String leaderboardName = currentPresenter.getSeriesDTO().getLeaderboardId(); + if (leaderboardName != null && !leaderboardName.isEmpty()) { + EventSeriesAnalyticsDataManager eventSeriesAnalyticsManager = currentPresenter.getCtx().getAnalyticsManager(); + final Consumer leaderboardConsumer = new Consumer() { + @Override + public void consume(LeaderboardPanel leaderboardPanel) { + initWidget(ourUiBinder.createAndBindUi(EventSeriesOverallLeaderboardTabView.this)); + leaderboard.setLeaderboard(leaderboardPanel, currentPresenter.getAutoRefreshTimer()); + eventSeriesAnalyticsManager.hideCompetitorChart(); + contentArea.setWidget(EventSeriesOverallLeaderboardTabView.this); + if(leaderboardPanel.getLeaderboard() != null) { + leaderboard.updatedLeaderboard(leaderboardPanel.getLeaderboard()); + } + } + }; + LeaderboardPanel leaderboardPanel = eventSeriesAnalyticsManager.getLeaderboardPanel(); + if(leaderboardPanel == null) { + createSharedLeaderboardPanel(leaderboardName, eventSeriesAnalyticsManager, currentPresenter.getUserService(), /*FIXME placeToken */ null, leaderboardConsumer); + } else if( /*FIXME placeToken not empty */ false) { + createLeaderboardComponentContext(leaderboardName, currentPresenter.getUserService(), /*FIXME placeToken */ null).getInitialSettings(new DefaultOnSettingsLoadedCallback() { + @Override + public void onSuccess(LeaderboardSettings settings) { + leaderboardPanel.updateSettings(settings); + leaderboardConsumer.consume(leaderboardPanel); + } + }); + } else { + leaderboardConsumer.consume(leaderboardPanel); + } + } else { + contentArea.setWidget(new Label("No leaderboard specified, cannot proceed to leaderboardpage")); + new com.google.gwt.user.client.Timer() { + @Override + public void run() { + currentPresenter.getHomeNavigation().goToPlace(); + } + }.schedule(3000); + } + } + + @Override + public void updatedLeaderboard(LeaderboardDTO leaderboard) { + this.leaderboard.updatedLeaderboard(leaderboard); + } + + @Override + public void currentRaceSelected(RaceIdentifier raceIdentifier, RaceColumnDTO raceColumn) { + } + + @Override + public void stop() { + } + + @Override + public EventSeriesOverallLeaderboardPlace placeToFire() { + return new EventSeriesOverallLeaderboardPlace(currentPresenter.getCtx()); + } } \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaOverviewPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaOverviewPanel.java index 4678b039bde..09128aa9fc1 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaOverviewPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaOverviewPanel.java @@ -40,10 +40,10 @@ import com.sap.sse.gwt.client.player.Timer.PlayModes; import com.sap.sse.gwt.client.player.Timer.PlayStates; import com.sap.sse.gwt.client.shared.components.LinkWithSettingsGenerator; import com.sap.sse.gwt.client.shared.components.SettingsDialog; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; -import com.sap.sse.gwt.client.shared.perspective.ComponentContextWithSettingsStorage; -import com.sap.sse.gwt.client.shared.perspective.DefaultOnSettingsLoadedCallback; -import com.sap.sse.gwt.client.shared.perspective.SettingsStorageManager; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; +import com.sap.sse.gwt.client.shared.settings.ComponentContextWithSettingsStorage; +import com.sap.sse.gwt.client.shared.settings.DefaultOnSettingsLoadedCallback; +import com.sap.sse.gwt.client.shared.settings.SettingsStorageManager; import com.sap.sse.security.ui.client.UserService; import com.sap.sse.security.ui.settings.StorageDefinitionId; import com.sap.sse.security.ui.settings.UserSettingsStorageManager; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaRaceStatesComponent.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaRaceStatesComponent.java index 37b680be128..da35ea474ce 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaRaceStatesComponent.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaRaceStatesComponent.java @@ -73,8 +73,8 @@ import com.sap.sse.gwt.client.player.Timer; import com.sap.sse.gwt.client.shared.components.AbstractCompositeComponent; import com.sap.sse.gwt.client.shared.components.Component; import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; import com.sap.sse.gwt.client.shared.perspective.PerspectiveCompositeSettings; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; /** * This component shows a table displaying the current state of races for a given event. Which races are shown depends diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractTrackedRacesListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractTrackedRacesListComposite.java index ca1ed70a5a9..bf4c47b7c0c 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractTrackedRacesListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractTrackedRacesListComposite.java @@ -1,522 +1,522 @@ -package com.sap.sailing.gwt.ui.adminconsole; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.Set; - -import com.google.gwt.cell.client.AbstractCell; -import com.google.gwt.core.client.GWT; -import com.google.gwt.dom.client.Document; -import com.google.gwt.dom.client.Style.FontWeight; -import com.google.gwt.dom.client.Style.Unit; -import com.google.gwt.event.dom.client.ChangeEvent; -import com.google.gwt.event.dom.client.ChangeHandler; -import com.google.gwt.event.dom.client.ClickEvent; -import com.google.gwt.event.dom.client.ClickHandler; -import com.google.gwt.event.dom.client.DomEvent; -import com.google.gwt.safehtml.shared.SafeHtml; -import com.google.gwt.safehtml.shared.SafeHtmlBuilder; -import com.google.gwt.safehtml.shared.SafeHtmlUtils; -import com.google.gwt.user.cellview.client.Column; -import com.google.gwt.user.cellview.client.ColumnSortEvent.ListHandler; -import com.google.gwt.user.cellview.client.TextColumn; -import com.google.gwt.user.client.rpc.AsyncCallback; -import com.google.gwt.user.client.ui.Button; -import com.google.gwt.user.client.ui.HasVerticalAlignment; -import com.google.gwt.user.client.ui.HorizontalPanel; -import com.google.gwt.user.client.ui.Label; -import com.google.gwt.user.client.ui.ListBox; -import com.google.gwt.user.client.ui.VerticalPanel; -import com.google.gwt.user.client.ui.Widget; -import com.google.gwt.view.client.ListDataProvider; -import com.google.gwt.view.client.SelectionChangeEvent; -import com.google.gwt.view.client.SelectionChangeEvent.Handler; -import com.sap.sailing.domain.common.RaceIdentifier; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sailing.domain.common.TrackedRaceStatusEnum; -import com.sap.sailing.domain.common.dto.RaceDTO; -import com.sap.sailing.gwt.ui.client.RegattaRefresher; -import com.sap.sailing.gwt.ui.client.RegattasDisplayer; -import com.sap.sailing.gwt.ui.client.SailingServiceAsync; -import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sailing.gwt.ui.client.shared.controls.FlushableCellTable; -import com.sap.sailing.gwt.ui.client.shared.controls.SelectionCheckboxColumn; -import com.sap.sailing.gwt.ui.common.client.DateAndTimeFormatterUtil; -import com.sap.sailing.gwt.ui.shared.RegattaDTO; -import com.sap.sse.common.Util; -import com.sap.sse.common.filter.Filter; -import com.sap.sse.common.util.NaturalComparator; -import com.sap.sse.gwt.client.ErrorReporter; -import com.sap.sse.gwt.client.async.MarkedAsyncCallback; -import com.sap.sse.gwt.client.celltable.EntityIdentityComparator; -import com.sap.sse.gwt.client.celltable.RefreshableSelectionModel; -import com.sap.sse.gwt.client.celltable.RefreshableSingleSelectionModel; -import com.sap.sse.gwt.client.panels.CustomizableFilterablePanel; -import com.sap.sse.gwt.client.shared.components.AbstractCompositeComponent; -import com.sap.sse.gwt.client.shared.components.Component; -import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; - -public abstract class AbstractTrackedRacesListComposite extends AbstractCompositeComponent implements - RegattasDisplayer { - - protected final long DEFAULT_LIVE_DELAY_IN_MILLISECONDS = 5000; - - private final boolean multiSelection; - - protected RefreshableSelectionModel refreshableSelectionModel; - - private SelectionCheckboxColumn selectionCheckboxColumn; - - private FlushableCellTable raceTable; - - private ListDataProvider raceList; - - private Iterable allRaces; - - private Label noTrackedRacesLabel; - - protected final SailingServiceAsync sailingService; - protected final ErrorReporter errorReporter; - protected final RegattaRefresher regattaRefresher; - protected final StringMessages stringMessages; - - private Button btnRefresh; - - private CustomizableFilterablePanel filterablePanelRaces; - - protected TrackedRacesSettings settings; - - private ListBox listBoxRegattas; - - public static class AnchorCell extends AbstractCell { - @Override - public void render(com.google.gwt.cell.client.Cell.Context context, SafeHtml safeHtml, SafeHtmlBuilder sb) { - sb.append(safeHtml); - } - } - - public AbstractTrackedRacesListComposite(Component parent, ComponentContext context, - final SailingServiceAsync sailingService, - final ErrorReporter errorReporter, final RegattaRefresher regattaRefresher, - final StringMessages stringMessages, boolean hasMultiSelection) { - super(parent, context); - this.sailingService = sailingService; - this.errorReporter = errorReporter; - this.regattaRefresher = regattaRefresher; - this.multiSelection = hasMultiSelection; - this.stringMessages = stringMessages; - } - - public void setRegattaFilterValue(String regattaName) { - for (int i = 0; i < listBoxRegattas.getItemCount(); i++) { - if (listBoxRegattas.getValue(i).equals(regattaName)) { - listBoxRegattas.setSelectedIndex(i); - // Firing change event on combobox to filter - DomEvent.fireNativeEvent(Document.get().createChangeEvent(), listBoxRegattas); - return; - } - } - - // Set 'All' option in case there are no tracked race related to regatta - if (listBoxRegattas.getItemCount() > 0) { - listBoxRegattas.setSelectedIndex(0); - DomEvent.fireNativeEvent(Document.get().createChangeEvent(), listBoxRegattas); - } - } - - protected void createUI() { - AdminConsoleTableResources tableResources = GWT.create(AdminConsoleTableResources.class); - raceList = new ListDataProvider<>(); - settings = new TrackedRacesSettings(); - settings.setDelayToLiveInSeconds(DEFAULT_LIVE_DELAY_IN_MILLISECONDS / 1000l); - VerticalPanel panel = new VerticalPanel(); - initWidget(panel); - HorizontalPanel filterPanel = new HorizontalPanel(); - panel.add(filterPanel); - - noTrackedRacesLabel = new Label(stringMessages.noRacesYet()); - noTrackedRacesLabel.setWordWrap(false); - panel.add(noTrackedRacesLabel); - AdminConsoleTableResources tableRes = GWT.create(AdminConsoleTableResources.class); - raceTable = new FlushableCellTable(/* pageSize */10000, tableRes); - raceTable.ensureDebugId("TrackedRacesCellTable"); - - Label lblFilterRaces = new Label(stringMessages.filterRaces()+":"); - lblFilterRaces.setWordWrap(false); - lblFilterRaces.getElement().getStyle().setFontWeight(FontWeight.BOLD); - lblFilterRaces.getElement().getStyle().setMarginRight(10, Unit.PX); - filterPanel.add(lblFilterRaces); - filterPanel.setCellVerticalAlignment(lblFilterRaces, HasVerticalAlignment.ALIGN_MIDDLE); - - filterablePanelRaces = new CustomizableFilterablePanel(allRaces, raceTable, raceList) { - @Override - public List getSearchableStrings(RaceDTO t) { - List strings = new ArrayList(); - strings.add(t.getName()); - strings.add(t.boatClass); - strings.add(t.getRegattaName()); - return strings; - } - }; - Label lblFilterByRegatta = new Label(stringMessages.filterByRegatta()); - lblFilterByRegatta.setWordWrap(false); - listBoxRegattas = new ListBox(); - listBoxRegattas.addChangeHandler(new ChangeHandler() { - @Override - public void onChange(ChangeEvent event) { - filterablePanelRaces.filter(); - } - }); - filterablePanelRaces.add(lblFilterByRegatta, listBoxRegattas, new Filter() { - @Override - public boolean matches(RaceDTO t) { - return listBoxRegattas.getSelectedIndex() == 0 /* All */ || Util.equalsWithNull(listBoxRegattas.getSelectedValue(), t.getRegattaName()); - } - @Override - public String getName() { - return "TrackedRacesByRegattaFilter"; - } - }); - - Label lblFilterRacesByName = new Label(stringMessages.filterByNameOrBoatClass() + ":"); - lblFilterRacesByName.setWordWrap(false); - filterablePanelRaces.add(lblFilterRacesByName); - filterablePanelRaces.addDefaultTextBox(); - filterablePanelRaces.getTextBox().ensureDebugId("TrackedRacesFilterTextBox"); - - filterPanel.add(filterablePanelRaces); - filterPanel.setCellVerticalAlignment(filterablePanelRaces, HasVerticalAlignment.ALIGN_MIDDLE); - - final EntityIdentityComparator entityIdentityComparator = new EntityIdentityComparator() { - @Override - public boolean representSameEntity(RaceDTO dto1, RaceDTO dto2) { - return dto1.getRaceIdentifier().equals(dto2.getRaceIdentifier()); - } - @Override - public int hashCode(RaceDTO t) { - return t.getRaceIdentifier().hashCode(); - } - }; - if (multiSelection) { - this.selectionCheckboxColumn = new SelectionCheckboxColumn( - tableResources.cellTableStyle().cellTableCheckboxSelected(), - tableResources.cellTableStyle().cellTableCheckboxDeselected(), - tableResources.cellTableStyle().cellTableCheckboxColumnCell(), entityIdentityComparator, - filterablePanelRaces.getAllListDataProvider(), raceTable); - refreshableSelectionModel = selectionCheckboxColumn.getSelectionModel(); - raceTable.setSelectionModel(refreshableSelectionModel, this.selectionCheckboxColumn.getSelectionManager()); - } else { - refreshableSelectionModel = new RefreshableSingleSelectionModel(entityIdentityComparator, - filterablePanelRaces.getAllListDataProvider()); - raceTable.setSelectionModel(refreshableSelectionModel); - } - - ListHandler columnSortHandler = setupTableColumns(stringMessages); - raceTable.setWidth("300px"); - - raceTable.setVisible(false); - panel.add(raceTable); - raceList.addDataDisplay(raceTable); - raceTable.addColumnSortHandler(columnSortHandler); - raceTable.getSelectionModel().addSelectionChangeHandler(new Handler() { - @Override - public void onSelectionChange(SelectionChangeEvent event) { - Set selectedRaces = refreshableSelectionModel.getSelectedSet(); - makeControlsReactToSelectionChange(selectedRaces); - } - }); - - HorizontalPanel trackedRacesButtonPanel = new HorizontalPanel(); - trackedRacesButtonPanel.setSpacing(10); - panel.add(trackedRacesButtonPanel); - - btnRefresh = new Button(stringMessages.refresh()); - btnRefresh.ensureDebugId("RefreshButton"); - btnRefresh.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - regattaRefresher.fillRegattas(); - } - }); - trackedRacesButtonPanel.add(btnRefresh); - addControlButtons(trackedRacesButtonPanel); - } - - abstract protected void makeControlsReactToSelectionChange(Set selectedRaces); - - abstract protected void makeControlsReactToFillRegattas(Iterable regattas); - - abstract protected void addControlButtons(HorizontalPanel trackedRacesButtonPanel); - - private ListHandler setupTableColumns(final StringMessages stringMessages) { - ListHandler columnSortHandler = new ListHandler(raceList.getList()); - TextColumn regattaNameColumn = new TextColumn() { - @Override - public String getValue(RaceDTO raceDTO) { - return raceDTO.getRegattaName(); - } - }; - regattaNameColumn.setSortable(true); - columnSortHandler.setComparator(regattaNameColumn, new Comparator() { - @Override - public int compare(RaceDTO r1, RaceDTO r2) { - return new NaturalComparator().compare(r1.getRegattaName(), r2.getRegattaName()); - } - }); - - TextColumn boatClassNameColumn = new TextColumn() { - @Override - public String getValue(RaceDTO raceDTO) { - return raceDTO.boatClass == null ? "" : raceDTO.boatClass; - } - }; - boatClassNameColumn.setSortable(true); - columnSortHandler.setComparator(boatClassNameColumn, new Comparator() { - @Override - public int compare(RaceDTO r1, RaceDTO r2) { - return new NaturalComparator(false).compare(r1.boatClass, r2.boatClass); - } - }); - - AnchorCell anchorCell = new AnchorCell(); - Column raceNameColumn = new Column(anchorCell) { - @Override - public SafeHtml getValue(RaceDTO raceDTO) { - return SafeHtmlUtils.fromString(raceDTO.getName()); - } - }; - raceNameColumn.setSortable(true); - columnSortHandler.setComparator(raceNameColumn, new Comparator() { - @Override - public int compare(RaceDTO r1, RaceDTO r2) { - return new NaturalComparator().compare(r1.getName(), r2.getName()); - } - }); - - TextColumn raceStartColumn = new TextColumn() { - @Override - public String getValue(RaceDTO raceDTO) { - if (raceDTO.startOfRace != null) { - return DateAndTimeFormatterUtil.defaultDateFormatter.render(raceDTO.startOfRace) + " " + - DateAndTimeFormatterUtil.defaultTimeFormatter.render(raceDTO.startOfRace); - } - - return ""; - } - }; - raceStartColumn.setSortable(true); - columnSortHandler.setComparator(raceStartColumn, new Comparator() { - @Override - public int compare(RaceDTO r1, RaceDTO r2) { - if (r1.startOfRace != null && r2.startOfRace != null) { - return r1.startOfRace.compareTo(r2.startOfRace); - } - - return r1.startOfRace == null ? (r2.startOfRace == null ? 0 : -1) : 1; - } - }); - - TextColumn hasWindDataColumn = new TextColumn() { - @Override - public String getValue(RaceDTO raceDTO) { - if (raceDTO.trackedRace != null && raceDTO.trackedRace.hasWindData == true) - return stringMessages.yes(); - else - return stringMessages.no(); - } - }; - hasWindDataColumn.setSortable(true); - columnSortHandler.setComparator(hasWindDataColumn, new Comparator() { - - @Override - public int compare(RaceDTO r1, RaceDTO r2) { - return new Boolean(hasWindData(r1)).compareTo(hasWindData(r2)); - } - - private boolean hasWindData(RaceDTO race) { - return race.trackedRace != null && race.trackedRace.hasWindData == true; - } - - }); - - TextColumn hasGPSDataColumn = new TextColumn() { - @Override - public String getValue(RaceDTO raceDTO) { - if (raceDTO.trackedRace != null && raceDTO.trackedRace.hasGPSData == true) { - return stringMessages.yes(); - } else { - return stringMessages.no(); - } - } - }; - hasGPSDataColumn.setSortable(true); - columnSortHandler.setComparator(hasGPSDataColumn, new Comparator() { - - @Override - public int compare(RaceDTO r1, RaceDTO r2) { - return new Boolean(hasGPSData(r1)).compareTo(hasGPSData(r2)); - } - - private boolean hasGPSData(RaceDTO race) { - return race.trackedRace != null && race.trackedRace.hasGPSData == true; - } - - }); - - TextColumn raceStatusColumn = new TextColumn() { - @Override - public String getValue(RaceDTO raceDTO) { - return raceDTO.status == null ? "" : raceDTO.status.toString(); - } - }; - raceStatusColumn.setSortable(true); - columnSortHandler.setComparator(raceStatusColumn, new Comparator() { - @Override - public int compare(RaceDTO r1, RaceDTO r2) { - if (r1.status != null && r2.status != null) { - if (r1.status.status == TrackedRaceStatusEnum.LOADING - && r2.status.status == TrackedRaceStatusEnum.LOADING) { - return new Double(r1.status.loadingProgress).compareTo(r2.status.loadingProgress); - } - return new Integer(r1.status.status.getOrder()).compareTo(r2.status.status.getOrder()); - } - - return r1.status == null ? (r2.status == null ? 0 : -1) : 1; - } - }); - - TextColumn raceLiveDelayColumn = new TextColumn() { - @Override - public String getValue(RaceDTO raceDTO) { - if (raceDTO.isTracked && raceDTO.trackedRace != null && raceDTO.trackedRace.delayToLiveInMs > 0) { - return "" + raceDTO.trackedRace.delayToLiveInMs / 1000; - } - return ""; - } - }; - raceLiveDelayColumn.setSortable(true); - columnSortHandler.setComparator(raceLiveDelayColumn, new Comparator() { - @Override - public int compare(RaceDTO r1, RaceDTO r2) { - Long r1Delay = getDelay(r1); - Long r2Delay = getDelay(r2); - if (r1Delay != null && r2Delay != null) { - return r1Delay.compareTo(r2Delay); - } - - return r1Delay == null ? (r2Delay == null ? 0 : -1) : 1; - } - - private Long getDelay(RaceDTO race) { - return race.isTracked && race.trackedRace != null ? race.trackedRace.delayToLiveInMs : null; - } - }); - if (multiSelection) { - columnSortHandler.setComparator(selectionCheckboxColumn, selectionCheckboxColumn.getComparator()); - raceTable.addColumn(selectionCheckboxColumn, selectionCheckboxColumn.getHeader()); - } - raceTable.addColumn(regattaNameColumn, stringMessages.regatta()); - raceTable.addColumn(boatClassNameColumn, stringMessages.boatClass()); - raceTable.addColumn(raceNameColumn, stringMessages.race()); - raceTable.addColumn(raceStartColumn, stringMessages.startTime()); - raceTable.addColumn(hasWindDataColumn, stringMessages.windData()); - raceTable.addColumn(hasGPSDataColumn, stringMessages.gpsData()); - raceTable.addColumn(raceStatusColumn, stringMessages.status()); - raceTable.addColumn(raceLiveDelayColumn, stringMessages.delayInSeconds()); - - return columnSortHandler; - } - - @Override - public boolean hasSettings() { - return true; - } - - @Override - public SettingsDialogComponent getSettingsDialogComponent() { - return new TrackedRacesSettingsDialogComponent(settings, stringMessages); - } - - @Override - public void updateSettings(TrackedRacesSettings newSettings) { - settings.setDelayToLiveInSeconds(newSettings.getDelayToLiveInSeconds()); - - // set the new delay to all selected races - List raceIdentifiersToUpdate = new ArrayList(); - for (RaceDTO raceDTO : refreshableSelectionModel.getSelectedSet()) { - raceIdentifiersToUpdate.add(raceDTO.getRaceIdentifier()); - } - - if (raceIdentifiersToUpdate != null && !raceIdentifiersToUpdate.isEmpty()) { - sailingService.updateRacesDelayToLive(raceIdentifiersToUpdate, settings.getDelayToLiveInSeconds() * 1000l, - new MarkedAsyncCallback( - new AsyncCallback() { - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError( - "Exception trying to set the delay to live for the selected tracked races: " - + caught.getMessage()); - } - - @Override - public void onSuccess(Void result) { - regattaRefresher.fillRegattas(); - } - } - )); - } - } - - @Override - public String getLocalizedShortName() { - return stringMessages.trackedRaces(); - } - - @Override - public Widget getEntryWidget() { - return this; - } - - public void addRaceSelectionChangeHandler(Handler handler) { - refreshableSelectionModel.addSelectionChangeHandler(handler); - } - - public RaceDTO getRaceByIdentifier(RaceIdentifier raceIdentifier) { - RaceDTO result = null; - if (raceList != null) { - for (RaceDTO race : raceList.getList()) { - if (race.getRaceIdentifier().equals(raceIdentifier)) { - result = race; - break; - } - } - } - return result; - } - - public void selectRaceByIdentifier(RegattaAndRaceIdentifier raceIdentifier) { - if (raceList != null) { - for (RaceDTO race : raceList.getList()) { - String regattaName = race.getRegattaName(); - if (regattaName.equals(raceIdentifier.getRegattaName()) - && race.getName().equals(raceIdentifier.getRaceName())) { - refreshableSelectionModel.setSelected(race, true); - break; - } - } - } - } - - public void clearSelection() { - refreshableSelectionModel.clear(); - } - - /** - * @param regattas - */ - @Override +package com.sap.sailing.gwt.ui.adminconsole; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Set; + +import com.google.gwt.cell.client.AbstractCell; +import com.google.gwt.core.client.GWT; +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.Style.FontWeight; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.event.dom.client.ChangeEvent; +import com.google.gwt.event.dom.client.ChangeHandler; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.event.dom.client.DomEvent; +import com.google.gwt.safehtml.shared.SafeHtml; +import com.google.gwt.safehtml.shared.SafeHtmlBuilder; +import com.google.gwt.safehtml.shared.SafeHtmlUtils; +import com.google.gwt.user.cellview.client.Column; +import com.google.gwt.user.cellview.client.ColumnSortEvent.ListHandler; +import com.google.gwt.user.cellview.client.TextColumn; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.HasVerticalAlignment; +import com.google.gwt.user.client.ui.HorizontalPanel; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.ListBox; +import com.google.gwt.user.client.ui.VerticalPanel; +import com.google.gwt.user.client.ui.Widget; +import com.google.gwt.view.client.ListDataProvider; +import com.google.gwt.view.client.SelectionChangeEvent; +import com.google.gwt.view.client.SelectionChangeEvent.Handler; +import com.sap.sailing.domain.common.RaceIdentifier; +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.domain.common.TrackedRaceStatusEnum; +import com.sap.sailing.domain.common.dto.RaceDTO; +import com.sap.sailing.gwt.ui.client.RegattaRefresher; +import com.sap.sailing.gwt.ui.client.RegattasDisplayer; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.client.shared.controls.FlushableCellTable; +import com.sap.sailing.gwt.ui.client.shared.controls.SelectionCheckboxColumn; +import com.sap.sailing.gwt.ui.common.client.DateAndTimeFormatterUtil; +import com.sap.sailing.gwt.ui.shared.RegattaDTO; +import com.sap.sse.common.Util; +import com.sap.sse.common.filter.Filter; +import com.sap.sse.common.util.NaturalComparator; +import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.async.MarkedAsyncCallback; +import com.sap.sse.gwt.client.celltable.EntityIdentityComparator; +import com.sap.sse.gwt.client.celltable.RefreshableSelectionModel; +import com.sap.sse.gwt.client.celltable.RefreshableSingleSelectionModel; +import com.sap.sse.gwt.client.panels.CustomizableFilterablePanel; +import com.sap.sse.gwt.client.shared.components.AbstractCompositeComponent; +import com.sap.sse.gwt.client.shared.components.Component; +import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; + +public abstract class AbstractTrackedRacesListComposite extends AbstractCompositeComponent implements + RegattasDisplayer { + + protected final long DEFAULT_LIVE_DELAY_IN_MILLISECONDS = 5000; + + private final boolean multiSelection; + + protected RefreshableSelectionModel refreshableSelectionModel; + + private SelectionCheckboxColumn selectionCheckboxColumn; + + private FlushableCellTable raceTable; + + private ListDataProvider raceList; + + private Iterable allRaces; + + private Label noTrackedRacesLabel; + + protected final SailingServiceAsync sailingService; + protected final ErrorReporter errorReporter; + protected final RegattaRefresher regattaRefresher; + protected final StringMessages stringMessages; + + private Button btnRefresh; + + private CustomizableFilterablePanel filterablePanelRaces; + + protected TrackedRacesSettings settings; + + private ListBox listBoxRegattas; + + public static class AnchorCell extends AbstractCell { + @Override + public void render(com.google.gwt.cell.client.Cell.Context context, SafeHtml safeHtml, SafeHtmlBuilder sb) { + sb.append(safeHtml); + } + } + + public AbstractTrackedRacesListComposite(Component parent, ComponentContext context, + final SailingServiceAsync sailingService, + final ErrorReporter errorReporter, final RegattaRefresher regattaRefresher, + final StringMessages stringMessages, boolean hasMultiSelection) { + super(parent, context); + this.sailingService = sailingService; + this.errorReporter = errorReporter; + this.regattaRefresher = regattaRefresher; + this.multiSelection = hasMultiSelection; + this.stringMessages = stringMessages; + } + + public void setRegattaFilterValue(String regattaName) { + for (int i = 0; i < listBoxRegattas.getItemCount(); i++) { + if (listBoxRegattas.getValue(i).equals(regattaName)) { + listBoxRegattas.setSelectedIndex(i); + // Firing change event on combobox to filter + DomEvent.fireNativeEvent(Document.get().createChangeEvent(), listBoxRegattas); + return; + } + } + + // Set 'All' option in case there are no tracked race related to regatta + if (listBoxRegattas.getItemCount() > 0) { + listBoxRegattas.setSelectedIndex(0); + DomEvent.fireNativeEvent(Document.get().createChangeEvent(), listBoxRegattas); + } + } + + protected void createUI() { + AdminConsoleTableResources tableResources = GWT.create(AdminConsoleTableResources.class); + raceList = new ListDataProvider<>(); + settings = new TrackedRacesSettings(); + settings.setDelayToLiveInSeconds(DEFAULT_LIVE_DELAY_IN_MILLISECONDS / 1000l); + VerticalPanel panel = new VerticalPanel(); + initWidget(panel); + HorizontalPanel filterPanel = new HorizontalPanel(); + panel.add(filterPanel); + + noTrackedRacesLabel = new Label(stringMessages.noRacesYet()); + noTrackedRacesLabel.setWordWrap(false); + panel.add(noTrackedRacesLabel); + AdminConsoleTableResources tableRes = GWT.create(AdminConsoleTableResources.class); + raceTable = new FlushableCellTable(/* pageSize */10000, tableRes); + raceTable.ensureDebugId("TrackedRacesCellTable"); + + Label lblFilterRaces = new Label(stringMessages.filterRaces()+":"); + lblFilterRaces.setWordWrap(false); + lblFilterRaces.getElement().getStyle().setFontWeight(FontWeight.BOLD); + lblFilterRaces.getElement().getStyle().setMarginRight(10, Unit.PX); + filterPanel.add(lblFilterRaces); + filterPanel.setCellVerticalAlignment(lblFilterRaces, HasVerticalAlignment.ALIGN_MIDDLE); + + filterablePanelRaces = new CustomizableFilterablePanel(allRaces, raceTable, raceList) { + @Override + public List getSearchableStrings(RaceDTO t) { + List strings = new ArrayList(); + strings.add(t.getName()); + strings.add(t.boatClass); + strings.add(t.getRegattaName()); + return strings; + } + }; + Label lblFilterByRegatta = new Label(stringMessages.filterByRegatta()); + lblFilterByRegatta.setWordWrap(false); + listBoxRegattas = new ListBox(); + listBoxRegattas.addChangeHandler(new ChangeHandler() { + @Override + public void onChange(ChangeEvent event) { + filterablePanelRaces.filter(); + } + }); + filterablePanelRaces.add(lblFilterByRegatta, listBoxRegattas, new Filter() { + @Override + public boolean matches(RaceDTO t) { + return listBoxRegattas.getSelectedIndex() == 0 /* All */ || Util.equalsWithNull(listBoxRegattas.getSelectedValue(), t.getRegattaName()); + } + @Override + public String getName() { + return "TrackedRacesByRegattaFilter"; + } + }); + + Label lblFilterRacesByName = new Label(stringMessages.filterByNameOrBoatClass() + ":"); + lblFilterRacesByName.setWordWrap(false); + filterablePanelRaces.add(lblFilterRacesByName); + filterablePanelRaces.addDefaultTextBox(); + filterablePanelRaces.getTextBox().ensureDebugId("TrackedRacesFilterTextBox"); + + filterPanel.add(filterablePanelRaces); + filterPanel.setCellVerticalAlignment(filterablePanelRaces, HasVerticalAlignment.ALIGN_MIDDLE); + + final EntityIdentityComparator entityIdentityComparator = new EntityIdentityComparator() { + @Override + public boolean representSameEntity(RaceDTO dto1, RaceDTO dto2) { + return dto1.getRaceIdentifier().equals(dto2.getRaceIdentifier()); + } + @Override + public int hashCode(RaceDTO t) { + return t.getRaceIdentifier().hashCode(); + } + }; + if (multiSelection) { + this.selectionCheckboxColumn = new SelectionCheckboxColumn( + tableResources.cellTableStyle().cellTableCheckboxSelected(), + tableResources.cellTableStyle().cellTableCheckboxDeselected(), + tableResources.cellTableStyle().cellTableCheckboxColumnCell(), entityIdentityComparator, + filterablePanelRaces.getAllListDataProvider(), raceTable); + refreshableSelectionModel = selectionCheckboxColumn.getSelectionModel(); + raceTable.setSelectionModel(refreshableSelectionModel, this.selectionCheckboxColumn.getSelectionManager()); + } else { + refreshableSelectionModel = new RefreshableSingleSelectionModel(entityIdentityComparator, + filterablePanelRaces.getAllListDataProvider()); + raceTable.setSelectionModel(refreshableSelectionModel); + } + + ListHandler columnSortHandler = setupTableColumns(stringMessages); + raceTable.setWidth("300px"); + + raceTable.setVisible(false); + panel.add(raceTable); + raceList.addDataDisplay(raceTable); + raceTable.addColumnSortHandler(columnSortHandler); + raceTable.getSelectionModel().addSelectionChangeHandler(new Handler() { + @Override + public void onSelectionChange(SelectionChangeEvent event) { + Set selectedRaces = refreshableSelectionModel.getSelectedSet(); + makeControlsReactToSelectionChange(selectedRaces); + } + }); + + HorizontalPanel trackedRacesButtonPanel = new HorizontalPanel(); + trackedRacesButtonPanel.setSpacing(10); + panel.add(trackedRacesButtonPanel); + + btnRefresh = new Button(stringMessages.refresh()); + btnRefresh.ensureDebugId("RefreshButton"); + btnRefresh.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + regattaRefresher.fillRegattas(); + } + }); + trackedRacesButtonPanel.add(btnRefresh); + addControlButtons(trackedRacesButtonPanel); + } + + abstract protected void makeControlsReactToSelectionChange(Set selectedRaces); + + abstract protected void makeControlsReactToFillRegattas(Iterable regattas); + + abstract protected void addControlButtons(HorizontalPanel trackedRacesButtonPanel); + + private ListHandler setupTableColumns(final StringMessages stringMessages) { + ListHandler columnSortHandler = new ListHandler(raceList.getList()); + TextColumn regattaNameColumn = new TextColumn() { + @Override + public String getValue(RaceDTO raceDTO) { + return raceDTO.getRegattaName(); + } + }; + regattaNameColumn.setSortable(true); + columnSortHandler.setComparator(regattaNameColumn, new Comparator() { + @Override + public int compare(RaceDTO r1, RaceDTO r2) { + return new NaturalComparator().compare(r1.getRegattaName(), r2.getRegattaName()); + } + }); + + TextColumn boatClassNameColumn = new TextColumn() { + @Override + public String getValue(RaceDTO raceDTO) { + return raceDTO.boatClass == null ? "" : raceDTO.boatClass; + } + }; + boatClassNameColumn.setSortable(true); + columnSortHandler.setComparator(boatClassNameColumn, new Comparator() { + @Override + public int compare(RaceDTO r1, RaceDTO r2) { + return new NaturalComparator(false).compare(r1.boatClass, r2.boatClass); + } + }); + + AnchorCell anchorCell = new AnchorCell(); + Column raceNameColumn = new Column(anchorCell) { + @Override + public SafeHtml getValue(RaceDTO raceDTO) { + return SafeHtmlUtils.fromString(raceDTO.getName()); + } + }; + raceNameColumn.setSortable(true); + columnSortHandler.setComparator(raceNameColumn, new Comparator() { + @Override + public int compare(RaceDTO r1, RaceDTO r2) { + return new NaturalComparator().compare(r1.getName(), r2.getName()); + } + }); + + TextColumn raceStartColumn = new TextColumn() { + @Override + public String getValue(RaceDTO raceDTO) { + if (raceDTO.startOfRace != null) { + return DateAndTimeFormatterUtil.defaultDateFormatter.render(raceDTO.startOfRace) + " " + + DateAndTimeFormatterUtil.defaultTimeFormatter.render(raceDTO.startOfRace); + } + + return ""; + } + }; + raceStartColumn.setSortable(true); + columnSortHandler.setComparator(raceStartColumn, new Comparator() { + @Override + public int compare(RaceDTO r1, RaceDTO r2) { + if (r1.startOfRace != null && r2.startOfRace != null) { + return r1.startOfRace.compareTo(r2.startOfRace); + } + + return r1.startOfRace == null ? (r2.startOfRace == null ? 0 : -1) : 1; + } + }); + + TextColumn hasWindDataColumn = new TextColumn() { + @Override + public String getValue(RaceDTO raceDTO) { + if (raceDTO.trackedRace != null && raceDTO.trackedRace.hasWindData == true) + return stringMessages.yes(); + else + return stringMessages.no(); + } + }; + hasWindDataColumn.setSortable(true); + columnSortHandler.setComparator(hasWindDataColumn, new Comparator() { + + @Override + public int compare(RaceDTO r1, RaceDTO r2) { + return new Boolean(hasWindData(r1)).compareTo(hasWindData(r2)); + } + + private boolean hasWindData(RaceDTO race) { + return race.trackedRace != null && race.trackedRace.hasWindData == true; + } + + }); + + TextColumn hasGPSDataColumn = new TextColumn() { + @Override + public String getValue(RaceDTO raceDTO) { + if (raceDTO.trackedRace != null && raceDTO.trackedRace.hasGPSData == true) { + return stringMessages.yes(); + } else { + return stringMessages.no(); + } + } + }; + hasGPSDataColumn.setSortable(true); + columnSortHandler.setComparator(hasGPSDataColumn, new Comparator() { + + @Override + public int compare(RaceDTO r1, RaceDTO r2) { + return new Boolean(hasGPSData(r1)).compareTo(hasGPSData(r2)); + } + + private boolean hasGPSData(RaceDTO race) { + return race.trackedRace != null && race.trackedRace.hasGPSData == true; + } + + }); + + TextColumn raceStatusColumn = new TextColumn() { + @Override + public String getValue(RaceDTO raceDTO) { + return raceDTO.status == null ? "" : raceDTO.status.toString(); + } + }; + raceStatusColumn.setSortable(true); + columnSortHandler.setComparator(raceStatusColumn, new Comparator() { + @Override + public int compare(RaceDTO r1, RaceDTO r2) { + if (r1.status != null && r2.status != null) { + if (r1.status.status == TrackedRaceStatusEnum.LOADING + && r2.status.status == TrackedRaceStatusEnum.LOADING) { + return new Double(r1.status.loadingProgress).compareTo(r2.status.loadingProgress); + } + return new Integer(r1.status.status.getOrder()).compareTo(r2.status.status.getOrder()); + } + + return r1.status == null ? (r2.status == null ? 0 : -1) : 1; + } + }); + + TextColumn raceLiveDelayColumn = new TextColumn() { + @Override + public String getValue(RaceDTO raceDTO) { + if (raceDTO.isTracked && raceDTO.trackedRace != null && raceDTO.trackedRace.delayToLiveInMs > 0) { + return "" + raceDTO.trackedRace.delayToLiveInMs / 1000; + } + return ""; + } + }; + raceLiveDelayColumn.setSortable(true); + columnSortHandler.setComparator(raceLiveDelayColumn, new Comparator() { + @Override + public int compare(RaceDTO r1, RaceDTO r2) { + Long r1Delay = getDelay(r1); + Long r2Delay = getDelay(r2); + if (r1Delay != null && r2Delay != null) { + return r1Delay.compareTo(r2Delay); + } + + return r1Delay == null ? (r2Delay == null ? 0 : -1) : 1; + } + + private Long getDelay(RaceDTO race) { + return race.isTracked && race.trackedRace != null ? race.trackedRace.delayToLiveInMs : null; + } + }); + if (multiSelection) { + columnSortHandler.setComparator(selectionCheckboxColumn, selectionCheckboxColumn.getComparator()); + raceTable.addColumn(selectionCheckboxColumn, selectionCheckboxColumn.getHeader()); + } + raceTable.addColumn(regattaNameColumn, stringMessages.regatta()); + raceTable.addColumn(boatClassNameColumn, stringMessages.boatClass()); + raceTable.addColumn(raceNameColumn, stringMessages.race()); + raceTable.addColumn(raceStartColumn, stringMessages.startTime()); + raceTable.addColumn(hasWindDataColumn, stringMessages.windData()); + raceTable.addColumn(hasGPSDataColumn, stringMessages.gpsData()); + raceTable.addColumn(raceStatusColumn, stringMessages.status()); + raceTable.addColumn(raceLiveDelayColumn, stringMessages.delayInSeconds()); + + return columnSortHandler; + } + + @Override + public boolean hasSettings() { + return true; + } + + @Override + public SettingsDialogComponent getSettingsDialogComponent() { + return new TrackedRacesSettingsDialogComponent(settings, stringMessages); + } + + @Override + public void updateSettings(TrackedRacesSettings newSettings) { + settings.setDelayToLiveInSeconds(newSettings.getDelayToLiveInSeconds()); + + // set the new delay to all selected races + List raceIdentifiersToUpdate = new ArrayList(); + for (RaceDTO raceDTO : refreshableSelectionModel.getSelectedSet()) { + raceIdentifiersToUpdate.add(raceDTO.getRaceIdentifier()); + } + + if (raceIdentifiersToUpdate != null && !raceIdentifiersToUpdate.isEmpty()) { + sailingService.updateRacesDelayToLive(raceIdentifiersToUpdate, settings.getDelayToLiveInSeconds() * 1000l, + new MarkedAsyncCallback( + new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError( + "Exception trying to set the delay to live for the selected tracked races: " + + caught.getMessage()); + } + + @Override + public void onSuccess(Void result) { + regattaRefresher.fillRegattas(); + } + } + )); + } + } + + @Override + public String getLocalizedShortName() { + return stringMessages.trackedRaces(); + } + + @Override + public Widget getEntryWidget() { + return this; + } + + public void addRaceSelectionChangeHandler(Handler handler) { + refreshableSelectionModel.addSelectionChangeHandler(handler); + } + + public RaceDTO getRaceByIdentifier(RaceIdentifier raceIdentifier) { + RaceDTO result = null; + if (raceList != null) { + for (RaceDTO race : raceList.getList()) { + if (race.getRaceIdentifier().equals(raceIdentifier)) { + result = race; + break; + } + } + } + return result; + } + + public void selectRaceByIdentifier(RegattaAndRaceIdentifier raceIdentifier) { + if (raceList != null) { + for (RaceDTO race : raceList.getList()) { + String regattaName = race.getRegattaName(); + if (regattaName.equals(raceIdentifier.getRegattaName()) + && race.getName().equals(raceIdentifier.getRaceName())) { + refreshableSelectionModel.setSelected(race, true); + break; + } + } + } + } + + public void clearSelection() { + refreshableSelectionModel.clear(); + } + + /** + * @param regattas + */ + @Override public void fillRegattas(Iterable regattas) { makeControlsReactToFillRegattas(regattas); displayRaceTableUI(regattas); @@ -552,7 +552,7 @@ public abstract class AbstractTrackedRacesListComposite extends AbstractComposit private void restoreListBoxRegattasSelection(String lastSelectedRegattaName) { for (int i = 0; i < listBoxRegattas.getItemCount(); i++) { if (listBoxRegattas.getValue(i).equals(lastSelectedRegattaName)) { - listBoxRegattas.setSelectedIndex(i); + listBoxRegattas.setSelectedIndex(i); break; } } @@ -576,16 +576,16 @@ public abstract class AbstractTrackedRacesListComposite extends AbstractComposit noTrackedRacesLabel.setVisible(true); } - /** - * Allows applying some sort of filter to the process of adding races. Defaults to true in standard implementation. - * Override for custom behavior - */ - protected boolean raceIsToBeAddedToList(RaceDTO race) { - return true; - } - - public RefreshableSelectionModel getSelectionModel() { - return refreshableSelectionModel; - } - -} + /** + * Allows applying some sort of filter to the process of adding races. Defaults to true in standard implementation. + * Override for custom behavior + */ + protected boolean raceIsToBeAddedToList(RaceDTO race) { + return true; + } + + public RefreshableSelectionModel getSelectionModel() { + return refreshableSelectionModel; + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TrackedRacesListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TrackedRacesListComposite.java index ddf1249e811..8fa58da5599 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TrackedRacesListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TrackedRacesListComposite.java @@ -1,210 +1,210 @@ -package com.sap.sailing.gwt.ui.adminconsole; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import com.google.gwt.event.dom.client.ClickEvent; -import com.google.gwt.event.dom.client.ClickHandler; -import com.google.gwt.user.client.rpc.AsyncCallback; -import com.google.gwt.user.client.ui.Button; -import com.google.gwt.user.client.ui.HorizontalPanel; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sailing.domain.common.RegattaNameAndRaceName; -import com.sap.sailing.domain.common.dto.RaceDTO; -import com.sap.sailing.gwt.ui.client.RegattaRefresher; -import com.sap.sailing.gwt.ui.client.RegattasDisplayer; -import com.sap.sailing.gwt.ui.client.SailingServiceAsync; -import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sailing.gwt.ui.shared.RegattaDTO; -import com.sap.sse.common.Util; -import com.sap.sse.gwt.client.ErrorReporter; -import com.sap.sse.gwt.client.async.MarkedAsyncCallback; -import com.sap.sse.gwt.client.shared.components.Component; -import com.sap.sse.gwt.client.shared.components.SettingsDialog; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; - -/** - * Shows the currently tracked events/races in a table. Updated if subscribed as an {@link RegattasDisplayer}, e.g., with - * the {@link AdminConsoleEntryPoint}. - */ -public class TrackedRacesListComposite extends AbstractTrackedRacesListComposite { - final Set raceIsTrackedRaceChangeListener; - private Button btnUntrack; - private Button btnRemoveRace; - private Button btnSetDelayToLive; - private Button btnExport; - private ExportPopup exportPopup; - private boolean actionButtonsEnabled; - - public TrackedRacesListComposite(Component parent, ComponentContext context, - final SailingServiceAsync sailingService, - final ErrorReporter errorReporter, - final RegattaRefresher regattaRefresher, final StringMessages stringMessages, boolean hasMultiSelection, boolean actionButtonsEnabled) { - super(parent, context, sailingService, errorReporter, regattaRefresher, stringMessages, hasMultiSelection); - this.raceIsTrackedRaceChangeListener = new HashSet(); - this.actionButtonsEnabled = actionButtonsEnabled; - createUI(); - } - - private void showSetDelayToLiveDialog() { - TrackedRacesSettings settings = new TrackedRacesSettings(); - settings.setDelayToLiveInSeconds(DEFAULT_LIVE_DELAY_IN_MILLISECONDS); - - SettingsDialog settingsDialog = new SettingsDialog(this, stringMessages); - settingsDialog.show(); - } - - public void addTrackedRaceChangeListener(TrackedRaceChangedListener listener) { - this.raceIsTrackedRaceChangeListener.add(listener); - } - - void stopTrackingRaces(final Iterable races) { - final List racesToStopTracking = new ArrayList(); - for (RaceDTO race : races) { - if (race.isTracked) { - racesToStopTracking.add(race.getRaceIdentifier()); - } - } - sailingService.stopTrackingRaces(racesToStopTracking, new MarkedAsyncCallback( - new AsyncCallback() { - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError("Exception trying to stop tracking races " + races + ": " + caught.getMessage()); - } - - @Override - public void onSuccess(Void result) { - regattaRefresher.fillRegattas(); - for (TrackedRaceChangedListener listener : raceIsTrackedRaceChangeListener) { - listener.racesStoppedTracking(racesToStopTracking); - } - } - })); - } - - private void removeAndUntrackRaces(final Iterable races) { - final List regattaNamesAndRaceNames = new ArrayList(); - for (RaceDTO race : races) { - regattaNamesAndRaceNames.add((RegattaNameAndRaceName) race.getRaceIdentifier()); - } - sailingService.removeAndUntrackRaces(regattaNamesAndRaceNames, new MarkedAsyncCallback( - new AsyncCallback() { - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError("Exception trying to remove races " + regattaNamesAndRaceNames + - ": " + caught.getMessage()); - } - - @Override - public void onSuccess(Void result) { - regattaRefresher.fillRegattas(); - for (TrackedRaceChangedListener listener : raceIsTrackedRaceChangeListener) { - listener.racesRemoved(regattaNamesAndRaceNames); - } - } - })); - } - - @Override - protected void addControlButtons(HorizontalPanel trackedRacesButtonPanel) { - if(actionButtonsEnabled) { - btnRemoveRace = new Button(stringMessages.remove()); - btnRemoveRace.ensureDebugId("RemoveRaceButton"); - btnRemoveRace.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - removeAndUntrackRaces(refreshableSelectionModel.getSelectedSet()); - } - }); - btnRemoveRace.setEnabled(false); - trackedRacesButtonPanel.add(btnRemoveRace); - - btnUntrack = new Button(stringMessages.stopTracking()); - btnUntrack.ensureDebugId("StopTrackingButton"); - btnUntrack.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent click) { - stopTrackingRaces(refreshableSelectionModel.getSelectedSet()); - } - }); - btnUntrack.setEnabled(false); - trackedRacesButtonPanel.add(btnUntrack); - - btnSetDelayToLive = new Button(stringMessages.setDelayToLive() + "..."); - btnSetDelayToLive.ensureDebugId("SetDelayToLiveButton"); - btnSetDelayToLive.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - showSetDelayToLiveDialog(); - } - }); - trackedRacesButtonPanel.add(btnSetDelayToLive); - - exportPopup = new ExportPopup(stringMessages); - btnExport = new Button(stringMessages.export()); - btnExport.ensureDebugId("ExportButton"); - btnExport.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - exportPopup.center(new ArrayList<>(refreshableSelectionModel.getSelectedSet())); - } - }); - btnExport.setEnabled(false); - trackedRacesButtonPanel.add(btnExport); - } - } - - @Override - protected void makeControlsReactToSelectionChange(Set selectedRaces) { - if (actionButtonsEnabled) { - if (selectedRaces.isEmpty()) { - btnRemoveRace.setEnabled(false); - btnUntrack.setEnabled(false); - btnExport.setEnabled(false); - } else { - btnRemoveRace.setEnabled(true); - btnUntrack.setEnabled(true); - btnExport.setEnabled(true); - } - } - } - - @Override - protected void makeControlsReactToFillRegattas(Iterable regattas) { - if(actionButtonsEnabled) { - if (Util.isEmpty(regattas)) { - btnUntrack.setVisible(false); - btnRemoveRace.setVisible(false); - btnSetDelayToLive.setVisible(false); - btnExport.setVisible(false); - } else { - btnUntrack.setVisible(true); - btnUntrack.setEnabled(false); - btnRemoveRace.setVisible(true); - btnRemoveRace.setEnabled(false); - btnSetDelayToLive.setVisible(true); - btnExport.setVisible(true); - } - } - } - - @Override - public String getDependentCssClassName() { - return "trackedRacesListComposite"; - } - - @Override - public TrackedRacesSettings getSettings() { - // TODO Auto-generated method stub - return null; - } - - @Override - public String getId() { - return "TrackedRacesListComposite"; - } - - -} +package com.sap.sailing.gwt.ui.adminconsole; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.HorizontalPanel; +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.domain.common.RegattaNameAndRaceName; +import com.sap.sailing.domain.common.dto.RaceDTO; +import com.sap.sailing.gwt.ui.client.RegattaRefresher; +import com.sap.sailing.gwt.ui.client.RegattasDisplayer; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.shared.RegattaDTO; +import com.sap.sse.common.Util; +import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.async.MarkedAsyncCallback; +import com.sap.sse.gwt.client.shared.components.Component; +import com.sap.sse.gwt.client.shared.components.SettingsDialog; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; + +/** + * Shows the currently tracked events/races in a table. Updated if subscribed as an {@link RegattasDisplayer}, e.g., with + * the {@link AdminConsoleEntryPoint}. + */ +public class TrackedRacesListComposite extends AbstractTrackedRacesListComposite { + final Set raceIsTrackedRaceChangeListener; + private Button btnUntrack; + private Button btnRemoveRace; + private Button btnSetDelayToLive; + private Button btnExport; + private ExportPopup exportPopup; + private boolean actionButtonsEnabled; + + public TrackedRacesListComposite(Component parent, ComponentContext context, + final SailingServiceAsync sailingService, + final ErrorReporter errorReporter, + final RegattaRefresher regattaRefresher, final StringMessages stringMessages, boolean hasMultiSelection, boolean actionButtonsEnabled) { + super(parent, context, sailingService, errorReporter, regattaRefresher, stringMessages, hasMultiSelection); + this.raceIsTrackedRaceChangeListener = new HashSet(); + this.actionButtonsEnabled = actionButtonsEnabled; + createUI(); + } + + private void showSetDelayToLiveDialog() { + TrackedRacesSettings settings = new TrackedRacesSettings(); + settings.setDelayToLiveInSeconds(DEFAULT_LIVE_DELAY_IN_MILLISECONDS); + + SettingsDialog settingsDialog = new SettingsDialog(this, stringMessages); + settingsDialog.show(); + } + + public void addTrackedRaceChangeListener(TrackedRaceChangedListener listener) { + this.raceIsTrackedRaceChangeListener.add(listener); + } + + void stopTrackingRaces(final Iterable races) { + final List racesToStopTracking = new ArrayList(); + for (RaceDTO race : races) { + if (race.isTracked) { + racesToStopTracking.add(race.getRaceIdentifier()); + } + } + sailingService.stopTrackingRaces(racesToStopTracking, new MarkedAsyncCallback( + new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError("Exception trying to stop tracking races " + races + ": " + caught.getMessage()); + } + + @Override + public void onSuccess(Void result) { + regattaRefresher.fillRegattas(); + for (TrackedRaceChangedListener listener : raceIsTrackedRaceChangeListener) { + listener.racesStoppedTracking(racesToStopTracking); + } + } + })); + } + + private void removeAndUntrackRaces(final Iterable races) { + final List regattaNamesAndRaceNames = new ArrayList(); + for (RaceDTO race : races) { + regattaNamesAndRaceNames.add((RegattaNameAndRaceName) race.getRaceIdentifier()); + } + sailingService.removeAndUntrackRaces(regattaNamesAndRaceNames, new MarkedAsyncCallback( + new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError("Exception trying to remove races " + regattaNamesAndRaceNames + + ": " + caught.getMessage()); + } + + @Override + public void onSuccess(Void result) { + regattaRefresher.fillRegattas(); + for (TrackedRaceChangedListener listener : raceIsTrackedRaceChangeListener) { + listener.racesRemoved(regattaNamesAndRaceNames); + } + } + })); + } + + @Override + protected void addControlButtons(HorizontalPanel trackedRacesButtonPanel) { + if(actionButtonsEnabled) { + btnRemoveRace = new Button(stringMessages.remove()); + btnRemoveRace.ensureDebugId("RemoveRaceButton"); + btnRemoveRace.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + removeAndUntrackRaces(refreshableSelectionModel.getSelectedSet()); + } + }); + btnRemoveRace.setEnabled(false); + trackedRacesButtonPanel.add(btnRemoveRace); + + btnUntrack = new Button(stringMessages.stopTracking()); + btnUntrack.ensureDebugId("StopTrackingButton"); + btnUntrack.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent click) { + stopTrackingRaces(refreshableSelectionModel.getSelectedSet()); + } + }); + btnUntrack.setEnabled(false); + trackedRacesButtonPanel.add(btnUntrack); + + btnSetDelayToLive = new Button(stringMessages.setDelayToLive() + "..."); + btnSetDelayToLive.ensureDebugId("SetDelayToLiveButton"); + btnSetDelayToLive.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + showSetDelayToLiveDialog(); + } + }); + trackedRacesButtonPanel.add(btnSetDelayToLive); + + exportPopup = new ExportPopup(stringMessages); + btnExport = new Button(stringMessages.export()); + btnExport.ensureDebugId("ExportButton"); + btnExport.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + exportPopup.center(new ArrayList<>(refreshableSelectionModel.getSelectedSet())); + } + }); + btnExport.setEnabled(false); + trackedRacesButtonPanel.add(btnExport); + } + } + + @Override + protected void makeControlsReactToSelectionChange(Set selectedRaces) { + if (actionButtonsEnabled) { + if (selectedRaces.isEmpty()) { + btnRemoveRace.setEnabled(false); + btnUntrack.setEnabled(false); + btnExport.setEnabled(false); + } else { + btnRemoveRace.setEnabled(true); + btnUntrack.setEnabled(true); + btnExport.setEnabled(true); + } + } + } + + @Override + protected void makeControlsReactToFillRegattas(Iterable regattas) { + if(actionButtonsEnabled) { + if (Util.isEmpty(regattas)) { + btnUntrack.setVisible(false); + btnRemoveRace.setVisible(false); + btnSetDelayToLive.setVisible(false); + btnExport.setVisible(false); + } else { + btnUntrack.setVisible(true); + btnUntrack.setEnabled(false); + btnRemoveRace.setVisible(true); + btnRemoveRace.setEnabled(false); + btnSetDelayToLive.setVisible(true); + btnExport.setVisible(true); + } + } + } + + @Override + public String getDependentCssClassName() { + return "trackedRacesListComposite"; + } + + @Override + public TrackedRacesSettings getSettings() { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getId() { + return "TrackedRacesListComposite"; + } + + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceTimePanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceTimePanel.java index b2ef4b02bd2..c70b911211d 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceTimePanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceTimePanel.java @@ -1,222 +1,222 @@ -package com.sap.sailing.gwt.ui.client; - -import java.util.Date; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sailing.domain.common.security.Permission; -import com.sap.sailing.domain.common.security.SailingPermissionsForRoleProvider; -import com.sap.sailing.gwt.ui.common.client.DateAndTimeFormatterUtil; -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.gwt.client.player.TimeRangeWithZoomProvider; -import com.sap.sse.gwt.client.player.Timer; -import com.sap.sse.gwt.client.player.Timer.PlayModes; -import com.sap.sse.gwt.client.player.Timer.PlayStates; -import com.sap.sse.gwt.client.shared.components.Component; -import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; -import com.sap.sse.security.ui.client.UserService; -import com.sap.sse.security.ui.client.UserStatusEventHandler; -import com.sap.sse.security.ui.shared.UserDTO; - -/** - * A specific {@link TimePanel} that shows and manages a time slider for a race. Receives {@link RaceTimesInfoDTO} - * objects when registered as a {@link RaceTimesInfoProviderListener} on a {@link RaceTimesInfoProvider} and adjusts the - * time panel including its time slider and the marks displayed and the zoom model accordingly. - *

- * - * It is possible to register as a "transitive" {@link RaceTimesInfoProviderListener} on this object in case a component - * is interested in being notified when this object is done with its adjustments based on a - * {@link RaceTimesInfoProviderListener} notification. - * - * @author Axel Uhl (d043530) - * - */ -public class RaceTimePanel extends TimePanel implements RaceTimesInfoProviderListener { - private final RaceTimesInfoProvider raceTimesInfoProvider; - - private RegattaAndRaceIdentifier selectedRace; - private boolean autoAdjustPlayMode; - private RaceTimesInfoDTO lastRaceTimesInfo; - private boolean hasCanReplayDuringLiveRacesPermission = false; - private final UserService userService; - private final RaceTimePanelLifecycle componentLifecycle; - - /** - * These listeners are notified transitively after this object has been - * {@link RaceTimesInfoProviderListener#raceTimesInfosReceived(Map, long, Date, long) notified} about race timing - * changes and has finished reacting accordingly. This way, e.g., a race board mode implementation can do its - * thing while assuming that the time slider has already been adjusted accordingly, therefore allowing for the - * timer to be set to a reasonable value. - */ - private final Set listeners; - - private final UserStatusEventHandler userStatusEventHandler = new UserStatusEventHandler() { - @Override - public void onUserStatusChange(UserDTO user) { - RaceTimePanel.this.hasCanReplayDuringLiveRacesPermission = user != null && user.hasPermission( - Permission.CAN_REPLAY_DURING_LIVE_RACES.getStringPermission(), SailingPermissionsForRoleProvider.INSTANCE); - } - }; - - /** - * When a marker update is required but min/max are not yet initialized, this flag is set. If later - * {@link #setMinMax(Date, Date, boolean)} is invoked and this flag is set, a {@link #redrawAllMarkers(RaceTimesInfoDTO)} - * will be issued and the flag is cleared. - */ - private boolean redrawAllMarkersPendingForMinMaxBeingInitialized; - +package com.sap.sailing.gwt.ui.client; + +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.domain.common.security.Permission; +import com.sap.sailing.domain.common.security.SailingPermissionsForRoleProvider; +import com.sap.sailing.gwt.ui.common.client.DateAndTimeFormatterUtil; +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.gwt.client.player.TimeRangeWithZoomProvider; +import com.sap.sse.gwt.client.player.Timer; +import com.sap.sse.gwt.client.player.Timer.PlayModes; +import com.sap.sse.gwt.client.player.Timer.PlayStates; +import com.sap.sse.gwt.client.shared.components.Component; +import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; +import com.sap.sse.security.ui.client.UserService; +import com.sap.sse.security.ui.client.UserStatusEventHandler; +import com.sap.sse.security.ui.shared.UserDTO; + +/** + * A specific {@link TimePanel} that shows and manages a time slider for a race. Receives {@link RaceTimesInfoDTO} + * objects when registered as a {@link RaceTimesInfoProviderListener} on a {@link RaceTimesInfoProvider} and adjusts the + * time panel including its time slider and the marks displayed and the zoom model accordingly. + *

+ * + * It is possible to register as a "transitive" {@link RaceTimesInfoProviderListener} on this object in case a component + * is interested in being notified when this object is done with its adjustments based on a + * {@link RaceTimesInfoProviderListener} notification. + * + * @author Axel Uhl (d043530) + * + */ +public class RaceTimePanel extends TimePanel implements RaceTimesInfoProviderListener { + private final RaceTimesInfoProvider raceTimesInfoProvider; + + private RegattaAndRaceIdentifier selectedRace; + private boolean autoAdjustPlayMode; + private RaceTimesInfoDTO lastRaceTimesInfo; + private boolean hasCanReplayDuringLiveRacesPermission = false; + private final UserService userService; + private final RaceTimePanelLifecycle componentLifecycle; + + /** + * These listeners are notified transitively after this object has been + * {@link RaceTimesInfoProviderListener#raceTimesInfosReceived(Map, long, Date, long) notified} about race timing + * changes and has finished reacting accordingly. This way, e.g., a race board mode implementation can do its + * thing while assuming that the time slider has already been adjusted accordingly, therefore allowing for the + * timer to be set to a reasonable value. + */ + private final Set listeners; + + private final UserStatusEventHandler userStatusEventHandler = new UserStatusEventHandler() { + @Override + public void onUserStatusChange(UserDTO user) { + RaceTimePanel.this.hasCanReplayDuringLiveRacesPermission = user != null && user.hasPermission( + Permission.CAN_REPLAY_DURING_LIVE_RACES.getStringPermission(), SailingPermissionsForRoleProvider.INSTANCE); + } + }; + + /** + * When a marker update is required but min/max are not yet initialized, this flag is set. If later + * {@link #setMinMax(Date, Date, boolean)} is invoked and this flag is set, a {@link #redrawAllMarkers(RaceTimesInfoDTO)} + * will be issued and the flag is cleared. + */ + private boolean redrawAllMarkersPendingForMinMaxBeingInitialized; + private final Duration initialTimeAfterRaceStartInReplayMode; - public RaceTimePanel(Component parent, ComponentContext context, RaceTimePanelLifecycle componentLifecycle, - UserService userService, + public RaceTimePanel(Component parent, ComponentContext context, RaceTimePanelLifecycle componentLifecycle, + UserService userService, Timer timer, TimeRangeWithZoomProvider timeRangeProvider, StringMessages stringMessages, RaceTimesInfoProvider raceTimesInfoProvider, boolean canReplayWhileLiveIsPossible, boolean forcePaddingRightToAlignToCharts, RegattaAndRaceIdentifier selectedRaceIdentifier, Duration initialTimeAfterRaceStartInReplayMode) { - super(parent, context, timer, timeRangeProvider, stringMessages, canReplayWhileLiveIsPossible, + super(parent, context, timer, timeRangeProvider, stringMessages, canReplayWhileLiveIsPossible, forcePaddingRightToAlignToCharts); - this.componentLifecycle = componentLifecycle; - this.userService = userService; - this.raceTimesInfoProvider = raceTimesInfoProvider; - this.listeners = new HashSet<>(); - selectedRace = null; - autoAdjustPlayMode = true; - selectedRace = selectedRaceIdentifier; + this.componentLifecycle = componentLifecycle; + this.userService = userService; + this.raceTimesInfoProvider = raceTimesInfoProvider; + this.listeners = new HashSet<>(); + selectedRace = null; + autoAdjustPlayMode = true; + selectedRace = selectedRaceIdentifier; this.initialTimeAfterRaceStartInReplayMode = initialTimeAfterRaceStartInReplayMode; - if (!raceTimesInfoProvider.containsRaceIdentifier(selectedRace)) { - raceTimesInfoProvider.addRaceIdentifier(selectedRace, true); - } - } - - @Override - protected void onLoad() { - super.onLoad(); - userService.addUserStatusEventHandler(userStatusEventHandler); - userStatusEventHandler.onUserStatusChange(userService.getCurrentUser()); - } - - @Override - protected void onUnload() { - super.onUnload(); - userService.removeUserStatusEventHandler(userStatusEventHandler); - } - - public void addRaceTimesInfoProviderListener(RaceTimesInfoProviderListener listener) { - this.listeners.add(listener); - } - - public void removeRaceTimesInfoProviderListener(RaceTimesInfoProviderListener listener) { - this.listeners.remove(listener); - } - - @Override - protected boolean canReplayWhileLiveIsPossible() { - return this.hasCanReplayDuringLiveRacesPermission; - } - - /** - * If the race's start time is already known and the time is before the start we can set the count-down label - */ - @Override - protected String getTimeToStartLabelText(Date time) { - String result = null; - RaceTimesInfoDTO selectedRaceTimes = raceTimesInfoProvider.getRaceTimesInfo(selectedRace); - if (selectedRaceTimes.startOfRace != null) { - if (time.before(selectedRaceTimes.startOfRace) || time.equals(selectedRaceTimes.startOfRace)) { - long timeToStartInMs = selectedRaceTimes.startOfRace.getTime() - time.getTime(); - result = timeToStartInMs < 1000 ? stringMessages.start() : stringMessages.timeToStart(DateAndTimeFormatterUtil.formatElapsedTime(timeToStartInMs)); - } else { - long timeSinceStartInMs = time.getTime() - selectedRaceTimes.startOfRace.getTime(); - result = stringMessages.timeSinceStart(DateAndTimeFormatterUtil.formatElapsedTime(timeSinceStartInMs)); - } - } - return result; - } - - @Override - public void updateSettings(RaceTimePanelSettings newSettings) { - super.updateSettings(newSettings); - raceTimesInfoProvider.setRequestInterval(newSettings.getRefreshInterval()); - } - - @Override - public RaceTimePanelSettings getSettings() { - return new RaceTimePanelSettings(timer.getRefreshInterval()); - } - - @Override - public SettingsDialogComponent getSettingsDialogComponent() { - return componentLifecycle.getSettingsDialogComponent(getSettings()); - } - - private void updateTimeInfo(RaceTimesInfoDTO raceTimesInfo) { - if (raceTimesInfo == null) { - // in case the race is not tracked anymore we reset the time slider - resetTimeSlider(); - } else { - // timer will only accept this update if the delay hasn't been updated explicitly - timer.setLivePlayDelayInMillis(raceTimesInfo.delayToLiveInMs); - if ((raceTimesInfo.startOfTracking != null || raceTimesInfo.startOfRace != null) && - (raceTimesInfo.newestTrackingEvent != null || raceTimesInfo.endOfRace != null || raceTimesInfo.endOfTracking != null)) { - // we set here the min and max of the time slider, the start and end of the race as well as the known - // leg markers - boolean liveModeToBeMadePossible = isLiveModeToBeMadePossible(); - setLiveGenerallyPossible(liveModeToBeMadePossible); - setJumpToLiveEnablement(liveModeToBeMadePossible && timer.getPlayMode() != PlayModes.Live); - if (autoAdjustPlayMode && liveModeToBeMadePossible) { - timer.setPlayMode(PlayModes.Live); - } - updatePlayPauseButtonsVisibility(timer.getPlayMode()); - if (liveModeToBeMadePossible && !canReplayWhileLiveIsPossible() && - timer.getPlayMode()==PlayModes.Replay && timer.getPlayState()==PlayStates.Playing) { - // pause timer because it must not be playing in replay mode while the race is live - timer.pause(); - } - - boolean timerAlreadyInitialized = getFromTime() != null && getToTime() != null && timeSlider.getCurrentValue() != null; - if (!timeRangeProvider.isZoomed()) { - updateMinMax(raceTimesInfo); - if (!timerAlreadyInitialized) { - initTimerPosition(raceTimesInfo); - } - updateLegMarkers(raceTimesInfo); - } - } else { - // the tracked race did not start yet or has no events yet - // maybe show a special state for this like "Race did not start yet" - } - } - lastRaceTimesInfo = raceTimesInfo; // TODO bug 3122: when not redrawn yet because min/max not initialized yet, further redraw is disabled by this. - } - - @Override - public void onTimeZoomChanged(Date zoomStartTimepoint, Date zoomEndTimepoint) { - super.onTimeZoomChanged(zoomStartTimepoint, zoomEndTimepoint); - timeSlider.setZoomed(true); - setMinMax(zoomStartTimepoint, zoomEndTimepoint, false); - timeSlider.clearMarkersAndLabelsAndTicks(); - redrawAllMarkers(lastRaceTimesInfo); - } - - @Override - public void onTimeZoomReset() { - super.onTimeZoomReset(); - timeSlider.setZoomed(false); - timeSlider.setMinValue(new Double(timeRangeProvider.getFromTime().getTime()), false); - timeSlider.setMaxValue(new Double(timeRangeProvider.getToTime().getTime()), false); - timeSlider.setCurrentValue(new Double(timer.getTime().getTime()), true); - timeSlider.clearMarkersAndLabelsAndTicks(); - redrawAllMarkers(lastRaceTimesInfo); - } - - /** - * If and only if the {@link #selectedRace}'s timing is described by the {@link #raceTimesInfoProvider} and + if (!raceTimesInfoProvider.containsRaceIdentifier(selectedRace)) { + raceTimesInfoProvider.addRaceIdentifier(selectedRace, true); + } + } + + @Override + protected void onLoad() { + super.onLoad(); + userService.addUserStatusEventHandler(userStatusEventHandler); + userStatusEventHandler.onUserStatusChange(userService.getCurrentUser()); + } + + @Override + protected void onUnload() { + super.onUnload(); + userService.removeUserStatusEventHandler(userStatusEventHandler); + } + + public void addRaceTimesInfoProviderListener(RaceTimesInfoProviderListener listener) { + this.listeners.add(listener); + } + + public void removeRaceTimesInfoProviderListener(RaceTimesInfoProviderListener listener) { + this.listeners.remove(listener); + } + + @Override + protected boolean canReplayWhileLiveIsPossible() { + return this.hasCanReplayDuringLiveRacesPermission; + } + + /** + * If the race's start time is already known and the time is before the start we can set the count-down label + */ + @Override + protected String getTimeToStartLabelText(Date time) { + String result = null; + RaceTimesInfoDTO selectedRaceTimes = raceTimesInfoProvider.getRaceTimesInfo(selectedRace); + if (selectedRaceTimes.startOfRace != null) { + if (time.before(selectedRaceTimes.startOfRace) || time.equals(selectedRaceTimes.startOfRace)) { + long timeToStartInMs = selectedRaceTimes.startOfRace.getTime() - time.getTime(); + result = timeToStartInMs < 1000 ? stringMessages.start() : stringMessages.timeToStart(DateAndTimeFormatterUtil.formatElapsedTime(timeToStartInMs)); + } else { + long timeSinceStartInMs = time.getTime() - selectedRaceTimes.startOfRace.getTime(); + result = stringMessages.timeSinceStart(DateAndTimeFormatterUtil.formatElapsedTime(timeSinceStartInMs)); + } + } + return result; + } + + @Override + public void updateSettings(RaceTimePanelSettings newSettings) { + super.updateSettings(newSettings); + raceTimesInfoProvider.setRequestInterval(newSettings.getRefreshInterval()); + } + + @Override + public RaceTimePanelSettings getSettings() { + return new RaceTimePanelSettings(timer.getRefreshInterval()); + } + + @Override + public SettingsDialogComponent getSettingsDialogComponent() { + return componentLifecycle.getSettingsDialogComponent(getSettings()); + } + + private void updateTimeInfo(RaceTimesInfoDTO raceTimesInfo) { + if (raceTimesInfo == null) { + // in case the race is not tracked anymore we reset the time slider + resetTimeSlider(); + } else { + // timer will only accept this update if the delay hasn't been updated explicitly + timer.setLivePlayDelayInMillis(raceTimesInfo.delayToLiveInMs); + if ((raceTimesInfo.startOfTracking != null || raceTimesInfo.startOfRace != null) && + (raceTimesInfo.newestTrackingEvent != null || raceTimesInfo.endOfRace != null || raceTimesInfo.endOfTracking != null)) { + // we set here the min and max of the time slider, the start and end of the race as well as the known + // leg markers + boolean liveModeToBeMadePossible = isLiveModeToBeMadePossible(); + setLiveGenerallyPossible(liveModeToBeMadePossible); + setJumpToLiveEnablement(liveModeToBeMadePossible && timer.getPlayMode() != PlayModes.Live); + if (autoAdjustPlayMode && liveModeToBeMadePossible) { + timer.setPlayMode(PlayModes.Live); + } + updatePlayPauseButtonsVisibility(timer.getPlayMode()); + if (liveModeToBeMadePossible && !canReplayWhileLiveIsPossible() && + timer.getPlayMode()==PlayModes.Replay && timer.getPlayState()==PlayStates.Playing) { + // pause timer because it must not be playing in replay mode while the race is live + timer.pause(); + } + + boolean timerAlreadyInitialized = getFromTime() != null && getToTime() != null && timeSlider.getCurrentValue() != null; + if (!timeRangeProvider.isZoomed()) { + updateMinMax(raceTimesInfo); + if (!timerAlreadyInitialized) { + initTimerPosition(raceTimesInfo); + } + updateLegMarkers(raceTimesInfo); + } + } else { + // the tracked race did not start yet or has no events yet + // maybe show a special state for this like "Race did not start yet" + } + } + lastRaceTimesInfo = raceTimesInfo; // TODO bug 3122: when not redrawn yet because min/max not initialized yet, further redraw is disabled by this. + } + + @Override + public void onTimeZoomChanged(Date zoomStartTimepoint, Date zoomEndTimepoint) { + super.onTimeZoomChanged(zoomStartTimepoint, zoomEndTimepoint); + timeSlider.setZoomed(true); + setMinMax(zoomStartTimepoint, zoomEndTimepoint, false); + timeSlider.clearMarkersAndLabelsAndTicks(); + redrawAllMarkers(lastRaceTimesInfo); + } + + @Override + public void onTimeZoomReset() { + super.onTimeZoomReset(); + timeSlider.setZoomed(false); + timeSlider.setMinValue(new Double(timeRangeProvider.getFromTime().getTime()), false); + timeSlider.setMaxValue(new Double(timeRangeProvider.getToTime().getTime()), false); + timeSlider.setCurrentValue(new Double(timer.getTime().getTime()), true); + timeSlider.clearMarkersAndLabelsAndTicks(); + redrawAllMarkers(lastRaceTimesInfo); + } + + /** + * If and only if the {@link #selectedRace}'s timing is described by the {@link #raceTimesInfoProvider} and * according to the timing the current live time point is after the start of tracking or at least after * {@link RaceTimesCalculationUtil.MIN_TIME_BEFORE_RACE_START} minutes before the race, and the current live time * point is not after the end of tracking then live mode will be made possible (true will be returned).

@@ -225,15 +225,15 @@ public class RaceTimePanel extends TimePanel implements R * {@link RaceTimesInfoDTO#newestTrackingEvent} and {@link RaceTimesInfoDTO#endOfRace} values to see if---with * a little leeway---an end for the live period may be determined. Without this logic, such races would remain * live until the end of time. - */ - @Override - protected boolean isLiveModeToBeMadePossible() { - long liveTimePointInMillis = timer.getLiveTimePointInMillis(); - RaceTimesInfoDTO lastRaceTimesInfo = raceTimesInfoProvider != null ? raceTimesInfoProvider.getRaceTimesInfo(selectedRace) : null; + */ + @Override + protected boolean isLiveModeToBeMadePossible() { + long liveTimePointInMillis = timer.getLiveTimePointInMillis(); + RaceTimesInfoDTO lastRaceTimesInfo = raceTimesInfoProvider != null ? raceTimesInfoProvider.getRaceTimesInfo(selectedRace) : null; final boolean isLiveModeToBeMadePossible; if (lastRaceTimesInfo != null && // check that we're after startOfTracking or startOfRace minus some leeway: - ((lastRaceTimesInfo.startOfTracking != null && liveTimePointInMillis > lastRaceTimesInfo.startOfTracking.getTime()) || + ((lastRaceTimesInfo.startOfTracking != null && liveTimePointInMillis > lastRaceTimesInfo.startOfTracking.getTime()) || (lastRaceTimesInfo.startOfRace != null && liveTimePointInMillis > lastRaceTimesInfo.startOfRace.getTime() - RaceTimesCalculationUtil.MIN_TIME_BEFORE_RACE_START))) { // now check that we cannot know about an end time or are reasonably before it; // we don't know about the end time if startOfTracking is valid but endOfTracking is not; @@ -249,14 +249,14 @@ public class RaceTimePanel extends TimePanel implements R endTimeOfLivePeriod = latestOfNewestTrackingEventAndEndOfRace == null ? null : new Date(latestOfNewestTrackingEventAndEndOfRace.getTime() + RaceTimesCalculationUtil.TIME_AFTER_LIVE); } else { - endTimeOfLivePeriod = lastRaceTimesInfo.endOfTracking == null ? null : lastRaceTimesInfo.endOfTracking; - if (lastRaceTimesInfo.endOfRace != null) { - // this is a failsafe, if the tracking was never finsihed, the race was always assumed to be live. - Date latestAllowedTime = new Date( - lastRaceTimesInfo.endOfRace.getTime() + RaceTimesCalculationUtil.MAX_TIME_AFTER_RACE_END); - if (endTimeOfLivePeriod == null || endTimeOfLivePeriod.after(latestAllowedTime)) { - endTimeOfLivePeriod = latestAllowedTime; - } + endTimeOfLivePeriod = lastRaceTimesInfo.endOfTracking == null ? null : lastRaceTimesInfo.endOfTracking; + if (lastRaceTimesInfo.endOfRace != null) { + // this is a failsafe, if the tracking was never finsihed, the race was always assumed to be live. + Date latestAllowedTime = new Date( + lastRaceTimesInfo.endOfRace.getTime() + RaceTimesCalculationUtil.MAX_TIME_AFTER_RACE_END); + if (endTimeOfLivePeriod == null || endTimeOfLivePeriod.after(latestAllowedTime)) { + endTimeOfLivePeriod = latestAllowedTime; + } } } isLiveModeToBeMadePossible = endTimeOfLivePeriod == null /* meaning we don't know an end time */ || @@ -265,157 +265,157 @@ public class RaceTimePanel extends TimePanel implements R isLiveModeToBeMadePossible = false; } return isLiveModeToBeMadePossible; - } - - @Override - public void playStateChanged(PlayStates playState, PlayModes playMode) { - super.playStateChanged(playState, playMode); - switch (playMode) { - case Replay: - autoAdjustPlayMode = false; - break; - case Live: - break; - } - } - - /** - * Obtains the min/max range for the slider bar from newRaceTimesInfo and adjusts the slider bar if - * needed. Since we extend the slider bar to the right when playing across the "end of time," we don't want to - * constrain it back again here. Therefore, the max value is never reduced here but at best initially set if it - * was null before, or extended to a later point in time. - */ - private void updateMinMax(RaceTimesInfoDTO newRaceTimesInfo) { + } + + @Override + public void playStateChanged(PlayStates playState, PlayModes playMode) { + super.playStateChanged(playState, playMode); + switch (playMode) { + case Replay: + autoAdjustPlayMode = false; + break; + case Live: + break; + } + } + + /** + * Obtains the min/max range for the slider bar from newRaceTimesInfo and adjusts the slider bar if + * needed. Since we extend the slider bar to the right when playing across the "end of time," we don't want to + * constrain it back again here. Therefore, the max value is never reduced here but at best initially set if it + * was null before, or extended to a later point in time. + */ + private void updateMinMax(RaceTimesInfoDTO newRaceTimesInfo) { Util.Pair raceMinMax = RaceTimesCalculationUtil.calculateRaceMinMax(timer, newRaceTimesInfo); - - Date min = raceMinMax.getA(); - Date max = raceMinMax.getB(); - - // never reduce max if it was already set - if (min != null && max != null && - (getToTime() == null || getToTime().before(max) || - getFromTime() == null || getFromTime().after(min))) { - setMinMax(min, max, /* fireEvent */ false); // no event because we guarantee time to be between min and max - } - } - - @Override - public void setMinMax(Date min, Date max, boolean fireEvent) { - super.setMinMax(min, max, fireEvent); - if (redrawAllMarkersPendingForMinMaxBeingInitialized && timeSlider.isMinMaxInitialized()) { - redrawAllMarkers(lastRaceTimesInfo); - redrawAllMarkersPendingForMinMaxBeingInitialized = false; - } - } - - /** - * When in {@link PlayModes#Replay} mode, tries to put the {@link #timer} to the time point when the last leg was - * finished first. If this time point is not (yet?) known, tries to put the {@link #timer} to the - * {@link RaceTimesInfoDTO#endOfRace end of the race}. If that happens to be undefined (null), the - * {@link RaceTimesInfoDTO#startOfRace start of the race} is used instead. If that isn't available either, the timer - * remains unchanged. - *

- * - * When in {@link PlayModes#Live} mode, tries to advance the timer to the time point of the - * {@link RaceTimesInfoDTO#timePointOfNewestEvent timePointOfNewestEvent} received from the tracking infrastructure - * so far and puts the timer into {@link PlayStates#Playing play mode}. - */ - private void initTimerPosition(RaceTimesInfoDTO newRaceTimesInfo) { - // initialize timer position - switch (timer.getPlayMode()) { - case Live: - if (newRaceTimesInfo.newestTrackingEvent != null) { - timer.setTime(newRaceTimesInfo.newestTrackingEvent.getTime()); - } - timer.play(); - break; - case Replay: - // set time to start of race - if (newRaceTimesInfo.startOfRace != null) { + + Date min = raceMinMax.getA(); + Date max = raceMinMax.getB(); + + // never reduce max if it was already set + if (min != null && max != null && + (getToTime() == null || getToTime().before(max) || + getFromTime() == null || getFromTime().after(min))) { + setMinMax(min, max, /* fireEvent */ false); // no event because we guarantee time to be between min and max + } + } + + @Override + public void setMinMax(Date min, Date max, boolean fireEvent) { + super.setMinMax(min, max, fireEvent); + if (redrawAllMarkersPendingForMinMaxBeingInitialized && timeSlider.isMinMaxInitialized()) { + redrawAllMarkers(lastRaceTimesInfo); + redrawAllMarkersPendingForMinMaxBeingInitialized = false; + } + } + + /** + * When in {@link PlayModes#Replay} mode, tries to put the {@link #timer} to the time point when the last leg was + * finished first. If this time point is not (yet?) known, tries to put the {@link #timer} to the + * {@link RaceTimesInfoDTO#endOfRace end of the race}. If that happens to be undefined (null), the + * {@link RaceTimesInfoDTO#startOfRace start of the race} is used instead. If that isn't available either, the timer + * remains unchanged. + *

+ * + * When in {@link PlayModes#Live} mode, tries to advance the timer to the time point of the + * {@link RaceTimesInfoDTO#timePointOfNewestEvent timePointOfNewestEvent} received from the tracking infrastructure + * so far and puts the timer into {@link PlayStates#Playing play mode}. + */ + private void initTimerPosition(RaceTimesInfoDTO newRaceTimesInfo) { + // initialize timer position + switch (timer.getPlayMode()) { + case Live: + if (newRaceTimesInfo.newestTrackingEvent != null) { + timer.setTime(newRaceTimesInfo.newestTrackingEvent.getTime()); + } + timer.play(); + break; + case Replay: + // set time to start of race + if (newRaceTimesInfo.startOfRace != null) { timer.setTime(newRaceTimesInfo.startOfRace.getTime() + (initialTimeAfterRaceStartInReplayMode == null ? 0l : initialTimeAfterRaceStartInReplayMode.asMillis())); - } - break; - } - } - - /** - * Expected to be called before {@link #lastRaceTimesInfo} has been updated to newRaceTimesInfo - */ - private void updateLegMarkers(RaceTimesInfoDTO newRaceTimesInfo) { - boolean requiresMarkerUpdate = true; - // updating the sliderbar markers requires a lot of time, therefore we need to do this only if required - if (lastRaceTimesInfo != null && lastRaceTimesInfo.markPassingTimes.size() == newRaceTimesInfo.markPassingTimes.size()) { - requiresMarkerUpdate = false; - int numberOfLegs = newRaceTimesInfo.markPassingTimes.size(); - for (int i = 0; i < numberOfLegs; i++) { - if (!Util.equalsWithNull(newRaceTimesInfo.markPassingTimes.get(i).firstPassingDate, lastRaceTimesInfo.markPassingTimes.get(i).firstPassingDate)) { - requiresMarkerUpdate = true; - break; - } - } - if ((lastRaceTimesInfo.startOfRace == null && newRaceTimesInfo.startOfRace != null) - || (lastRaceTimesInfo.startOfRace != null && newRaceTimesInfo.startOfRace == null) - || (lastRaceTimesInfo.startOfRace != null && newRaceTimesInfo.startOfRace != null - && lastRaceTimesInfo.startOfRace.getTime() != newRaceTimesInfo.startOfRace.getTime())) { - requiresMarkerUpdate = true; - } - if ((lastRaceTimesInfo.endOfRace == null && newRaceTimesInfo.endOfRace != null) - || (lastRaceTimesInfo.endOfRace != null && newRaceTimesInfo.endOfRace == null) - || (lastRaceTimesInfo.endOfRace != null && newRaceTimesInfo.endOfRace != null - && lastRaceTimesInfo.endOfRace.getTime() != newRaceTimesInfo.endOfRace.getTime())) { - requiresMarkerUpdate = true; - } - } - if (requiresMarkerUpdate) { - if (timeSlider.isMinMaxInitialized()) { // TODO bug 3122 later, the lastRaceTimesInfo will be updated even if not redrawn because min/max not initialized - redrawAllMarkers(newRaceTimesInfo); - } else { - redrawAllMarkersPendingForMinMaxBeingInitialized = true; - } - } - } - - private void redrawAllMarkers(RaceTimesInfoDTO newRaceTimesInfo) { - List markPassingTimes = newRaceTimesInfo.getMarkPassingTimes(); - timeSlider.clearMarkers(); - if (newRaceTimesInfo.startOfRace != null) { - long markerTime = newRaceTimesInfo.startOfRace.getTime(); - if (!timeSlider.isZoomed() || (timeSlider.isZoomed() && markerTime > timeSlider.getMinValue() && markerTime < timeSlider.getMaxValue())) { - timeSlider.addMarker("S", new Double(markerTime)); - } - } - int markPassingCounter = 1; - for (MarkPassingTimesDTO markPassingTimesDTO: markPassingTimes) { - // ignore the start mark passing - if (markPassingCounter > 1 && markPassingTimesDTO.firstPassingDate != null) { - long markerTime = markPassingTimesDTO.firstPassingDate.getTime(); - if (!timeSlider.isZoomed() || (timeSlider.isZoomed() && markerTime > timeSlider.getMinValue() && markerTime < timeSlider.getMaxValue())) { - timeSlider.addMarker(markPassingTimesDTO.getName(), new Double(markerTime)); - } - } - markPassingCounter++; - } - if (newRaceTimesInfo.endOfRace != null) { - long markerTime = newRaceTimesInfo.endOfRace.getTime(); - if (!timeSlider.isZoomed() || (timeSlider.isZoomed() && markerTime > timeSlider.getMinValue() && markerTime < timeSlider.getMaxValue())) { - timeSlider.addMarker("E", new Double(markerTime)); - } - } - timeSlider.redraw(); - } - - @Override - public void raceTimesInfosReceived(Map raceTimesInfos, long clientTimeWhenRequestWasSent, Date serverTimeDuringRequest, long clientTimeWhenResponseWasReceived) { - timer.adjustClientServerOffset(clientTimeWhenRequestWasSent, serverTimeDuringRequest, clientTimeWhenResponseWasReceived); - updateTimeInfo(raceTimesInfos.get(selectedRace)); - for (final RaceTimesInfoProviderListener listener : listeners) { - listener.raceTimesInfosReceived(raceTimesInfos, clientTimeWhenRequestWasSent, serverTimeDuringRequest, clientTimeWhenResponseWasReceived); - } - } - - @Override - public String getId() { - return componentLifecycle.getComponentId(); - } -} + } + break; + } + } + + /** + * Expected to be called before {@link #lastRaceTimesInfo} has been updated to newRaceTimesInfo + */ + private void updateLegMarkers(RaceTimesInfoDTO newRaceTimesInfo) { + boolean requiresMarkerUpdate = true; + // updating the sliderbar markers requires a lot of time, therefore we need to do this only if required + if (lastRaceTimesInfo != null && lastRaceTimesInfo.markPassingTimes.size() == newRaceTimesInfo.markPassingTimes.size()) { + requiresMarkerUpdate = false; + int numberOfLegs = newRaceTimesInfo.markPassingTimes.size(); + for (int i = 0; i < numberOfLegs; i++) { + if (!Util.equalsWithNull(newRaceTimesInfo.markPassingTimes.get(i).firstPassingDate, lastRaceTimesInfo.markPassingTimes.get(i).firstPassingDate)) { + requiresMarkerUpdate = true; + break; + } + } + if ((lastRaceTimesInfo.startOfRace == null && newRaceTimesInfo.startOfRace != null) + || (lastRaceTimesInfo.startOfRace != null && newRaceTimesInfo.startOfRace == null) + || (lastRaceTimesInfo.startOfRace != null && newRaceTimesInfo.startOfRace != null + && lastRaceTimesInfo.startOfRace.getTime() != newRaceTimesInfo.startOfRace.getTime())) { + requiresMarkerUpdate = true; + } + if ((lastRaceTimesInfo.endOfRace == null && newRaceTimesInfo.endOfRace != null) + || (lastRaceTimesInfo.endOfRace != null && newRaceTimesInfo.endOfRace == null) + || (lastRaceTimesInfo.endOfRace != null && newRaceTimesInfo.endOfRace != null + && lastRaceTimesInfo.endOfRace.getTime() != newRaceTimesInfo.endOfRace.getTime())) { + requiresMarkerUpdate = true; + } + } + if (requiresMarkerUpdate) { + if (timeSlider.isMinMaxInitialized()) { // TODO bug 3122 later, the lastRaceTimesInfo will be updated even if not redrawn because min/max not initialized + redrawAllMarkers(newRaceTimesInfo); + } else { + redrawAllMarkersPendingForMinMaxBeingInitialized = true; + } + } + } + + private void redrawAllMarkers(RaceTimesInfoDTO newRaceTimesInfo) { + List markPassingTimes = newRaceTimesInfo.getMarkPassingTimes(); + timeSlider.clearMarkers(); + if (newRaceTimesInfo.startOfRace != null) { + long markerTime = newRaceTimesInfo.startOfRace.getTime(); + if (!timeSlider.isZoomed() || (timeSlider.isZoomed() && markerTime > timeSlider.getMinValue() && markerTime < timeSlider.getMaxValue())) { + timeSlider.addMarker("S", new Double(markerTime)); + } + } + int markPassingCounter = 1; + for (MarkPassingTimesDTO markPassingTimesDTO: markPassingTimes) { + // ignore the start mark passing + if (markPassingCounter > 1 && markPassingTimesDTO.firstPassingDate != null) { + long markerTime = markPassingTimesDTO.firstPassingDate.getTime(); + if (!timeSlider.isZoomed() || (timeSlider.isZoomed() && markerTime > timeSlider.getMinValue() && markerTime < timeSlider.getMaxValue())) { + timeSlider.addMarker(markPassingTimesDTO.getName(), new Double(markerTime)); + } + } + markPassingCounter++; + } + if (newRaceTimesInfo.endOfRace != null) { + long markerTime = newRaceTimesInfo.endOfRace.getTime(); + if (!timeSlider.isZoomed() || (timeSlider.isZoomed() && markerTime > timeSlider.getMinValue() && markerTime < timeSlider.getMaxValue())) { + timeSlider.addMarker("E", new Double(markerTime)); + } + } + timeSlider.redraw(); + } + + @Override + public void raceTimesInfosReceived(Map raceTimesInfos, long clientTimeWhenRequestWasSent, Date serverTimeDuringRequest, long clientTimeWhenResponseWasReceived) { + timer.adjustClientServerOffset(clientTimeWhenRequestWasSent, serverTimeDuringRequest, clientTimeWhenResponseWasReceived); + updateTimeInfo(raceTimesInfos.get(selectedRace)); + for (final RaceTimesInfoProviderListener listener : listeners) { + listener.raceTimesInfosReceived(raceTimesInfos, clientTimeWhenRequestWasSent, serverTimeDuringRequest, clientTimeWhenResponseWasReceived); + } + } + + @Override + public String getId() { + return componentLifecycle.getComponentId(); + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/TimePanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/TimePanel.java index bd3d0ef15e8..e2d0802e8e0 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/TimePanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/TimePanel.java @@ -1,653 +1,653 @@ -package com.sap.sailing.gwt.ui.client; - -import java.util.Date; - -import com.google.gwt.core.client.GWT; -import com.google.gwt.dom.client.Style; -import com.google.gwt.dom.client.Style.Unit; -import com.google.gwt.event.dom.client.ClickEvent; -import com.google.gwt.event.dom.client.ClickHandler; -import com.google.gwt.event.logical.shared.ValueChangeEvent; -import com.google.gwt.event.logical.shared.ValueChangeHandler; -import com.google.gwt.i18n.client.DateTimeFormat; -import com.google.gwt.resources.client.ImageResource; -import com.google.gwt.user.client.ui.Button; -import com.google.gwt.user.client.ui.FlowPanel; -import com.google.gwt.user.client.ui.Image; -import com.google.gwt.user.client.ui.IntegerBox; -import com.google.gwt.user.client.ui.Label; -import com.google.gwt.user.client.ui.RequiresResize; -import com.google.gwt.user.client.ui.SimplePanel; -import com.google.gwt.user.client.ui.Widget; -import com.sap.sailing.gwt.ui.client.TimePanelCssResources.TimePanelCss; -import com.sap.sse.common.TimePoint; -import com.sap.sse.common.Util; -import com.sap.sse.common.impl.MillisecondsTimePoint; -import com.sap.sse.gwt.client.controls.slider.SliderBar; -import com.sap.sse.gwt.client.controls.slider.TimeSlider; -import com.sap.sse.gwt.client.player.PlayStateListener; -import com.sap.sse.gwt.client.player.TimeListener; -import com.sap.sse.gwt.client.player.TimeRangeChangeListener; -import com.sap.sse.gwt.client.player.TimeRangeWithZoomProvider; -import com.sap.sse.gwt.client.player.TimeZoomChangeListener; -import com.sap.sse.gwt.client.player.Timer; -import com.sap.sse.gwt.client.player.Timer.PlayModes; -import com.sap.sse.gwt.client.player.Timer.PlayStates; -import com.sap.sse.gwt.client.shared.components.AbstractCompositeComponent; -import com.sap.sse.gwt.client.shared.components.Component; -import com.sap.sse.gwt.client.shared.components.SettingsDialog; -import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; - -public class TimePanel extends AbstractCompositeComponent implements TimeListener, TimeZoomChangeListener, - TimeRangeChangeListener, PlayStateListener, RequiresResize { - protected final Timer timer; - protected final TimeRangeWithZoomProvider timeRangeProvider; - - /** - * Tells whether the user shall be enabled to run a replay with the timer auto-advancing while the race is still live. - * Live races are often watched by many. If in addition to that several users run the live race based on their own time line, - * too many re-calculations will be triggered as the live data will constantly invalidate many caches. Therefore, if scaling - * to many concurrent users is an issue, the capability to replay a live race with a non-live timing may be restricted using - * this attribute. - */ - private final boolean canReplayWhileLiveIsPossible; - - private final IntegerBox playSpeedBox; - private final Label timeDelayLabel; - private final Label timeLabel; - private final Label dateLabel; - private final Label timeToStartLabel; - private final FlowPanel timeToStartControlPanel; - private final Label playModeLabel; - protected final TimeSlider timeSlider; - private final Button backToLivePlayButton; - protected final StringMessages stringMessages; - protected final DateTimeFormat dateFormatter = DateTimeFormat.getFormat(DateTimeFormat.PredefinedFormat.DATE_FULL); - protected final DateTimeFormat timeFormatter = DateTimeFormat.getFormat("HH:mm:ss"); - private final ImageResource playSpeedImg; - private final ImageResource playModeLiveActiveImg; - private final ImageResource playModeReplayActiveImg; - private final ImageResource playModeInactiveImg; - private final Button playPauseButton; - private final Image playModeImage; - protected Date lastReceivedDataTimepoint; - private final Button slowDownButton; - private final Button speedUpButton; - private final Button toggleAdvancedModeButton; - - private final FlowPanel controlsPanel; - private final SimplePanel timePanelSlider; - private final FlowPanel timePanelSliderFlowWrapper; - private final FlowPanel playControlPanel; - private final FlowPanel timePanelInnerWrapper; - - /** - * the minimum time the slider extends its time when the end of the slider is reached - */ - private long MINIMUM_AUTO_ADVANCE_TIME_IN_MS = 5 * 60 * 1000; // 5 minutes - private boolean advancedModeShown; - - private static ClientResources resources = GWT.create(ClientResources.class); - protected static TimePanelCss timePanelCss = TimePanelCssResources.INSTANCE.css(); - - private final boolean forcePaddingRightToAlignToCharts; - - /** - * @param forcePaddingRightToAlignToCharts - * if true, the right padding will always be set such that the time panel lines up with - * charts such as the competitor chart or the wind chart shown above it, otherwise the padding depends - * on the flag set by {@link #setLiveGenerallyPossible(boolean)} - */ - public TimePanel(Component parent, ComponentContext context, Timer timer, - TimeRangeWithZoomProvider timeRangeProvider, - StringMessages stringMessages, - boolean canReplayWhileLiveIsPossible, boolean forcePaddingRightToAlignToCharts) { - super(parent, context); - this.timer = timer; - this.timeRangeProvider = timeRangeProvider; - this.stringMessages = stringMessages; - this.canReplayWhileLiveIsPossible = canReplayWhileLiveIsPossible; - this.forcePaddingRightToAlignToCharts = forcePaddingRightToAlignToCharts; - timer.addTimeListener(this); - timer.addPlayStateListener(this); - timeRangeProvider.addTimeRangeChangeListener(this); - timePanelInnerWrapper = new FlowPanel(); - timePanelInnerWrapper.setStyleName("timePanelInnerWrapper"); - timePanelInnerWrapper.setSize("100%", "100%"); - - timePanelSlider = new SimplePanel(); - timePanelSliderFlowWrapper = new FlowPanel(); - timePanelSlider.setStyleName("timePanelSlider"); - timePanelSlider.getElement().getStyle().setPaddingLeft(66, Unit.PX); - timePanelSliderFlowWrapper.add(timePanelSlider); - - playSpeedImg = resources.timesliderPlaySpeedIcon(); - playPauseButton = new Button(""); - - playModeLiveActiveImg = resources.timesliderPlayStateLiveActiveIcon(); - playModeReplayActiveImg = resources.timesliderPlayStateReplayActiveIcon(); - playModeInactiveImg = resources.timesliderPlayStateLiveInactiveIcon(); - playModeImage = new Image(playModeInactiveImg); - - timeSlider = new TimeSlider(); - timeSlider.setEnabled(true); - timeSlider.setLabelFormatter(new SliderBar.LabelFormatter() { - final DateTimeFormat timeWithMinutesFormatter = DateTimeFormat.getFormat("HH:mm"); - @Override - public String formatLabel(SliderBar slider, Double value, Double previousValue) { - Date date = new Date(value.longValue()); - return timeWithMinutesFormatter.format(date); - } - }); - - timeSlider.addValueChangeHandler(new ValueChangeHandler() { - @Override - public void onValueChange(ValueChangeEvent newValue) { - if (timeSlider.getCurrentValue() != null) { - if (TimePanel.this.timer.getPlayMode() == PlayModes.Live) { - // if canReplayWhileLive==false, playStateChanged(...) will ensure that the timer is paused when - // reaching Replay mode - TimePanel.this.timer.setPlayMode(PlayModes.Replay); - } - TimePanel.this.timer.setTime(timeSlider.getCurrentValue().longValue()); - } - } - }); - - timePanelInnerWrapper.add(timePanelSliderFlowWrapper); - timePanelSlider.add(timeSlider); - - controlsPanel = new FlowPanel(); - - controlsPanel.setStyleName("timePanel-controls"); - timePanelInnerWrapper.add(controlsPanel); - - // play button control - playControlPanel = new FlowPanel(); - playControlPanel.setStyleName("timePanel-controls-play"); - controlsPanel.add(playControlPanel); - - toggleAdvancedModeButton = createToggleAdvancedModeButton(); - playControlPanel.add(toggleAdvancedModeButton); - - playPauseButton.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - switch(TimePanel.this.timer.getPlayState()) { - case Paused: - TimePanel.this.timer.play(); - break; - case Playing: - TimePanel.this.timer.pause(); - break; - } - } - }); - playPauseButton.setTitle(stringMessages.startStopPlaying()); - playPauseButton.setStyleName("playPauseButton"); - playControlPanel.add(playPauseButton); - - backToLivePlayButton = new Button(stringMessages.raceIsInLiveTimePanelMode()); - backToLivePlayButton.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - TimePanel.this.timer.setPlayMode(PlayModes.Live); - TimePanel.this.timer.play(); - } - }); - backToLivePlayButton.setStyleName("backToLivePlayButton"); - backToLivePlayButton.setTitle(stringMessages.backToLive()); - playControlPanel.add(backToLivePlayButton); - - // current date and time control - FlowPanel timeControlPanel = new FlowPanel(); - timeControlPanel.setStyleName("timePanel-controls-time"); - dateLabel = new Label(); - timeLabel = new Label(); - timeControlPanel.add(dateLabel); - timeControlPanel.add(timeLabel); - - dateLabel.getElement().setClassName("dateLabel"); - timeLabel.getElement().setClassName("timeLabel"); - - // time to start control - timeToStartControlPanel = new FlowPanel(); - timeToStartControlPanel.setStyleName("timePanel-controls-timeToStart"); - timeToStartLabel = new Label(); - timeToStartControlPanel.add(timeToStartLabel); - timeToStartLabel.getElement().setClassName("timeToStartLabel"); - - FlowPanel playModeControlPanel = new FlowPanel(); - playModeControlPanel.setStyleName("timePanel-controls-playmode"); - playModeControlPanel.add(playModeImage); - playModeImage.getElement().getStyle().setFloat(Style.Float.LEFT); - - playModeLabel = new Label(); - playModeControlPanel .add(playModeLabel); - - playModeLabel.getElement().getStyle().setFloat(Style.Float.LEFT); - playModeLabel.getElement().setClassName("playModeLabel"); - - // play speed controls - FlowPanel playSpeedControlPanel = new FlowPanel(); - playSpeedControlPanel.setStyleName("timePanel-controls-playSpeed"); - - playSpeedBox = new IntegerBox(); - playSpeedBox.setVisibleLength(3); - playSpeedBox.setValue((int)timer.getPlaySpeedFactor()); // Christopher: initialize play speed box according to play speed factor - playSpeedBox.setTitle(stringMessages.playSpeedHelp()); - playSpeedBox.addValueChangeHandler(new ValueChangeHandler() { - @Override - public void onValueChange(ValueChangeEvent event) { - Integer newPlaySpeedFactor = playSpeedBox.getValue() == null ? 0 : playSpeedBox.getValue(); - TimePanel.this.timer.setPlaySpeedFactor(newPlaySpeedFactor); - } - }); - - Image playSpeedImage = new Image(playSpeedImg); - playSpeedControlPanel.add(playSpeedImage); - playSpeedControlPanel.add(playSpeedBox); - - slowDownButton = new Button("-1"); - slowDownButton.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - playSpeedBox.setValue(playSpeedBox.getValue() == null ? 0 : playSpeedBox.getValue() - 1); - TimePanel.this.timer.setPlaySpeedFactor(playSpeedBox.getValue()); - } - }); - slowDownButton.setTitle(stringMessages.slowPlaySpeedDown()); - slowDownButton.addStyleName("timePanelButton-SlowDown"); - playSpeedControlPanel.add(slowDownButton); - - speedUpButton = new Button("+1"); - speedUpButton.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - playSpeedBox.setValue(playSpeedBox.getValue() == null ? 0 : playSpeedBox.getValue() + 1); - TimePanel.this.timer.setPlaySpeedFactor(playSpeedBox.getValue()); - } - }); - speedUpButton.setTitle(stringMessages.speedPlaySpeedUp()); - speedUpButton.addStyleName("timePanelButton-SpeedUp"); - playSpeedControlPanel.add(speedUpButton); - - playSpeedImage.getElement().getStyle().setFloat(Style.Float.LEFT); - playSpeedImage.getElement().getStyle().setPadding(3, Style.Unit.PX); - playSpeedImage.getElement().getStyle().setMarginRight(3, Style.Unit.PX); - - playSpeedBox.getElement().getStyle().setFloat(Style.Float.LEFT); - playSpeedBox.getElement().getStyle().setPadding(2, Style.Unit.PX); - - speedUpButton.addStyleName("timePanelButton"); - slowDownButton.addStyleName("timePanelButton"); - - // time delay - FlowPanel timeDelayPanel = new FlowPanel(); - timeDelayPanel.setStyleName("timePanel-controls-timeDelay"); - - timeDelayLabel = new Label(); - timeDelayPanel.add(timeDelayLabel); - - timeDelayLabel.getElement().getStyle().setFloat(Style.Float.LEFT); - timeDelayLabel.getElement().getStyle().setPadding(3, Style.Unit.PX); - - timePanelCss.ensureInjected(); - controlsPanel.add(createSettingsButton()); - initWidget(timePanelInnerWrapper); - playStateChanged(timer.getPlayState(), timer.getPlayMode()); - - controlsPanel.add(playSpeedControlPanel); - controlsPanel.add(playModeControlPanel ); - controlsPanel.add(timeDelayPanel); - controlsPanel.add(timeControlPanel); - controlsPanel.add(timeToStartControlPanel); - - hideControlsPanel(); - } - - public TimeRangeWithZoomProvider getTimeRangeProvider() { - return timeRangeProvider; - } - - private Button createSettingsButton() { - Button settingsButton = SettingsDialog.createSettingsButton(this, stringMessages); - settingsButton.setStyleName(timePanelCss.settingsButtonStyle()); - settingsButton.addStyleName(timePanelCss.settingsButtonBackgroundImage()); - return settingsButton; - } - - private Button createToggleAdvancedModeButton() { - Button toggleAdvancedModeButton = new Button(); - toggleAdvancedModeButton.setStyleName("TimePanel-ShowExtended-Button"); - toggleAdvancedModeButton.addStyleDependentName("Closed"); - return toggleAdvancedModeButton; - } - - public boolean toggleAdvancedMode() { - if (advancedModeShown) { - hideControlsPanel(); - } else { - showControlsPanel(); - } - this.advancedModeShown = !advancedModeShown; - return this.advancedModeShown; - } - - protected void hideControlsPanel() { - controlsPanel.remove(playControlPanel); - timePanelInnerWrapper.remove(controlsPanel); - timePanelSliderFlowWrapper.insert(playControlPanel, 0); - playControlPanel.setStyleName("timePanel-timeslider-play"); - } - - protected void showControlsPanel() { - timePanelInnerWrapper.add(controlsPanel); - } - - @Override - public void timeChanged(Date newTime, Date oldTime) { - if (timeRangeProvider.isZoomed()) { - timeSlider.setCurrentValue(new Double(newTime.getTime()), false); - } else { - if (getFromTime() == null || getToTime() == null) { - initializeTimeRange(oldTime, newTime); - } - if (getFromTime() != null && getToTime() != null) { - // handle the case where time advances beyond slider's end. - if (newTime.after(getToTime())) { - // in live mode, expand the range - switch (timer.getPlayMode()) { - case Live: - Date newMaxTime = new Date(newTime.getTime()); - if (newMaxTime.getTime() - getToTime().getTime() < MINIMUM_AUTO_ADVANCE_TIME_IN_MS) { - newMaxTime.setTime(getToTime().getTime() + MINIMUM_AUTO_ADVANCE_TIME_IN_MS); - } - setMinMax(getFromTime(), newMaxTime, /* fireEvent */ false); // no event because we guarantee that time is between min/max - break; - case Replay: - // in replay, stop playing when the end of the range has been reached - timer.pause(); - break; - } - } - timeSlider.setCurrentValue(new Double(newTime.getTime()), false); - } - } - dateLabel.setText(getDateLabelText(newTime)); - timeLabel.setText(getTimeLabelText(newTime)); - String timeToStartLabelText = getTimeToStartLabelText(newTime); - if(timeToStartLabelText != null && !timeToStartLabelText.isEmpty()) { - timeToStartControlPanel.setVisible(true); - timeToStartLabel.setText(getTimeToStartLabelText(newTime)); - } else { - timeToStartControlPanel.setVisible(false); - timeToStartLabel.setText(""); - } - } - - /** - * When the time range is not yet properly initialized and we get a time change, adjust the time range so that old and - * new time are in range. A margin of one minute is used before and after. This method assumes that at least one of the - * two time points passed as parameters has a value other than null. Should the other be null, - * it is ignored. No assumptions are made about the temporal relation of the two time points to each other; in other - * words, oldTime may also be after newTime which could, e.g., be the case for a timer running - * backwards. - */ - private void initializeTimeRange(Date oldTime, Date newTime) { - TimePoint from = Util.getEarliestOfTimePoints(getFromTime()==null?null:new MillisecondsTimePoint(getFromTime()), oldTime==null?null:new MillisecondsTimePoint(oldTime)); - from = Util.getEarliestOfTimePoints(from, newTime==null?null:new MillisecondsTimePoint(newTime)); - TimePoint to = Util.getEarliestOfTimePoints(getToTime()==null?null:new MillisecondsTimePoint(getToTime()), oldTime==null?null:new MillisecondsTimePoint(oldTime)); - from = Util.getEarliestOfTimePoints(to, newTime==null?null:new MillisecondsTimePoint(newTime)); - timeRangeProvider.setTimeRange(from==null?null:from.asDate(), to==null?null:to.asDate()); - } - - protected String getTimeToStartLabelText(Date time) { - return null; - } - - protected String getTimeLabelText(Date time) { - final String timeLabelText; - if (lastReceivedDataTimepoint == null) { - timeLabelText = timeFormatter.format(time); - } else { - timeLabelText = timeFormatter.format(time) + " (" + timeFormatter.format(lastReceivedDataTimepoint) + ")"; - } - return timeLabelText; - } - - protected String getDateLabelText(Date time) { - return dateFormatter.format(time); - } - - protected Date getFromTime() { - return timeRangeProvider.getFromTime(); - } - - protected Date getToTime() { - return timeRangeProvider.getToTime(); - } - - /** - * @param min must not be null - * @param max must not be null - */ - public void setMinMax(Date min, Date max, boolean fireEvent) { - assert min != null && max != null; - - boolean changed = false; - changed = timeSlider.setMinAndMaxValue(new Double(min.getTime()), new Double(max.getTime()), fireEvent); - if (changed) { - if (!timeRangeProvider.isZoomed()) { - timeRangeProvider.setTimeRange(min, max, this); - } - - int numSteps = timeSlider.getElement().getClientWidth(); - if (numSteps > 0) { - timeSlider.setStepSize(numSteps, fireEvent); - } else { - timeSlider.setStepSize(1000, fireEvent); - } - - // Christopher: following setCurrentValue requires stepsize to be set <> 0 (otherwise division by zero; NaN) - if (timeSlider.getCurrentValue() == null) { - timeSlider.setCurrentValue(new Double(min.getTime()), fireEvent); - } - } - } - - public void resetTimeSlider() { - timeSlider.clearMarkers(); - timeSlider.redraw(); - } - - /** - * Makes sure that the invariant regarding {@link #canReplayWhileLiveIsPossible} is fulfilled. The timer will be - * {@link Timer#pause() paused} if the state is {@link PlayStates#Playing} and the the play mode is - * {@link PlayModes#Replay}. - * - */ - @Override - public void playStateChanged(PlayStates playState, PlayModes playMode) { - boolean liveModeToBeMadePossible = isLiveModeToBeMadePossible(); - setLiveGenerallyPossible(liveModeToBeMadePossible); - setJumpToLiveEnablement(liveModeToBeMadePossible && playMode != PlayModes.Live); - switch (playState) { - case Playing: - playPauseButton.getElement().addClassName("playPauseButtonPause"); - playPauseButton.setVisible(true); - if (playMode == PlayModes.Live) { - playModeImage.setResource(playModeLiveActiveImg); - } else { - playModeImage.setResource(playModeReplayActiveImg); - } - break; - case Paused: - updatePlayPauseButtonsVisibility(playMode); - playPauseButton.getElement().removeClassName("playPauseButtonPause"); - playModeImage.setResource(playModeInactiveImg); - break; - } - - switch (playMode) { - case Live: - playModeLabel.setText(stringMessages.playModeLive()); - timeDelayLabel.setText(stringMessages.timeDelay() + ": " + timer.getLivePlayDelayInMillis() / 1000 + " s"); - timeDelayLabel.setVisible(true); - timeSlider.setEnabled(true); - playSpeedBox.setEnabled(false); - slowDownButton.setEnabled(false); - speedUpButton.setEnabled(false); - break; - case Replay: - timeDelayLabel.setVisible(false); - timeDelayLabel.setText(""); - playModeLabel.setText(stringMessages.playModeReplay()); - timeSlider.setEnabled(true); - playSpeedBox.setEnabled(true); - slowDownButton.setEnabled(true); - speedUpButton.setEnabled(true); - if (!canReplayWhileLiveIsPossible() && playState == PlayStates.Playing && isLiveModeToBeMadePossible()) { - // can't leave the timer playing when race is still live and canReplayWhileLiveIsPossible==false - timer.pause(); - } - break; - } - } - - public void updatePlayPauseButtonsVisibility(PlayModes playMode) { - playPauseButton.setVisible(playMode == PlayModes.Live || canReplayWhileLiveIsPossible() || !isLiveModeToBeMadePossible()); - } - - @Override - public void playSpeedFactorChanged(double newPlaySpeedFactor) { - // nothing to do - } - - @Override - public void onTimeZoomChanged(Date zoomStartTimepoint, Date zoomEndTimepoint) { - } - - @Override - public void onTimeZoomReset() { - } - - @Override - public void onTimeRangeChanged(Date fromTime, Date toTime) { - setMinMax(fromTime, toTime, true); - } - - protected boolean isLiveModeToBeMadePossible() { - return false; - } - - /** - * Enables the {@link #backToLivePlayButton} if and only if enabled is true. - */ - protected void setJumpToLiveEnablement(boolean enabled) { - backToLivePlayButton.setEnabled(enabled); - if (enabled) { - backToLivePlayButton.setText(stringMessages.backToLiveTimePanelMode()); - backToLivePlayButton.removeStyleDependentName("Inactive"); - backToLivePlayButton.addStyleDependentName("Active"); - } else { - backToLivePlayButton.setText(stringMessages.raceIsInLiveTimePanelMode()); - backToLivePlayButton.removeStyleDependentName("Active"); - backToLivePlayButton.addStyleDependentName("Inactive"); - } - } - - /** - * Iff possible, makes the {@link #backToLivePlayButton} button visible. Furthermore, if - * possible==false and {@link #canReplayWhileLiveIsPossible}==false and the timer is in - * state {@link PlayStates#Paused} and in mode {@link PlayModes#Replay}, the play/pause button will be shown. - */ - protected void setLiveGenerallyPossible(boolean possible) { - backToLivePlayButton.setVisible(possible); - updateTimeSliderPadding(possible); - updatePlayPauseButtonsVisibility(timer.getPlayMode()); - } - - private void updateTimeSliderPadding(boolean backToLivePlayButtonVisible) { - Style timePanelStyle = timePanelSlider.getElement().getStyle(); - if (backToLivePlayButtonVisible || forcePaddingRightToAlignToCharts) { - timePanelStyle.setPaddingRight(66, Unit.PX); - } else { - timePanelStyle.clearPaddingRight(); - } - timeSlider.onResize(); - } - - @SuppressWarnings("unchecked") - public T getSettings() { - TimePanelSettings result = new TimePanelSettings(timer.getRefreshInterval()); - return (T) result; - } - - @Override - public Widget getEntryWidget() { - return this; - } - - @Override - public boolean hasSettings() { - return true; - } - - @Override - public SettingsDialogComponent getSettingsDialogComponent() { - return new TimePanelSettingsDialogComponent(getSettings(), stringMessages); - } - - @Override - public void updateSettings(T newSettings) { - timer.setRefreshInterval(newSettings.getRefreshInterval()); - } - - @Override - public String getLocalizedShortName() { - return stringMessages.timePanelName(); - } - - @Override - public void onResize() { - // handle what is required by @link{ProvidesResize} - Widget child = getWidget(); - if (child instanceof RequiresResize) { - ((RequiresResize) child).onResize(); - } - timeSlider.onResize(); - } - - public Date getLastReceivedDataTimepoint() { - return lastReceivedDataTimepoint; - } - - public void setLastReceivedDataTimepoint(Date lastReceivedDataTimepoint) { - this.lastReceivedDataTimepoint = lastReceivedDataTimepoint; - } - - protected boolean canReplayWhileLiveIsPossible() { - return canReplayWhileLiveIsPossible; - } - - public Button getAdvancedToggleButton() { - return this.toggleAdvancedModeButton; - } - - @Override - public String getDependentCssClassName() { - return "timePanel"; - } - - public Button getBackToLiveButton() { - return backToLivePlayButton; - } - - @Override - public String getId() { - return "TimePanel"; - } - -} +package com.sap.sailing.gwt.ui.client; + +import java.util.Date; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.dom.client.Style; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.event.logical.shared.ValueChangeEvent; +import com.google.gwt.event.logical.shared.ValueChangeHandler; +import com.google.gwt.i18n.client.DateTimeFormat; +import com.google.gwt.resources.client.ImageResource; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.Image; +import com.google.gwt.user.client.ui.IntegerBox; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.RequiresResize; +import com.google.gwt.user.client.ui.SimplePanel; +import com.google.gwt.user.client.ui.Widget; +import com.sap.sailing.gwt.ui.client.TimePanelCssResources.TimePanelCss; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.Util; +import com.sap.sse.common.impl.MillisecondsTimePoint; +import com.sap.sse.gwt.client.controls.slider.SliderBar; +import com.sap.sse.gwt.client.controls.slider.TimeSlider; +import com.sap.sse.gwt.client.player.PlayStateListener; +import com.sap.sse.gwt.client.player.TimeListener; +import com.sap.sse.gwt.client.player.TimeRangeChangeListener; +import com.sap.sse.gwt.client.player.TimeRangeWithZoomProvider; +import com.sap.sse.gwt.client.player.TimeZoomChangeListener; +import com.sap.sse.gwt.client.player.Timer; +import com.sap.sse.gwt.client.player.Timer.PlayModes; +import com.sap.sse.gwt.client.player.Timer.PlayStates; +import com.sap.sse.gwt.client.shared.components.AbstractCompositeComponent; +import com.sap.sse.gwt.client.shared.components.Component; +import com.sap.sse.gwt.client.shared.components.SettingsDialog; +import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; + +public class TimePanel extends AbstractCompositeComponent implements TimeListener, TimeZoomChangeListener, + TimeRangeChangeListener, PlayStateListener, RequiresResize { + protected final Timer timer; + protected final TimeRangeWithZoomProvider timeRangeProvider; + + /** + * Tells whether the user shall be enabled to run a replay with the timer auto-advancing while the race is still live. + * Live races are often watched by many. If in addition to that several users run the live race based on their own time line, + * too many re-calculations will be triggered as the live data will constantly invalidate many caches. Therefore, if scaling + * to many concurrent users is an issue, the capability to replay a live race with a non-live timing may be restricted using + * this attribute. + */ + private final boolean canReplayWhileLiveIsPossible; + + private final IntegerBox playSpeedBox; + private final Label timeDelayLabel; + private final Label timeLabel; + private final Label dateLabel; + private final Label timeToStartLabel; + private final FlowPanel timeToStartControlPanel; + private final Label playModeLabel; + protected final TimeSlider timeSlider; + private final Button backToLivePlayButton; + protected final StringMessages stringMessages; + protected final DateTimeFormat dateFormatter = DateTimeFormat.getFormat(DateTimeFormat.PredefinedFormat.DATE_FULL); + protected final DateTimeFormat timeFormatter = DateTimeFormat.getFormat("HH:mm:ss"); + private final ImageResource playSpeedImg; + private final ImageResource playModeLiveActiveImg; + private final ImageResource playModeReplayActiveImg; + private final ImageResource playModeInactiveImg; + private final Button playPauseButton; + private final Image playModeImage; + protected Date lastReceivedDataTimepoint; + private final Button slowDownButton; + private final Button speedUpButton; + private final Button toggleAdvancedModeButton; + + private final FlowPanel controlsPanel; + private final SimplePanel timePanelSlider; + private final FlowPanel timePanelSliderFlowWrapper; + private final FlowPanel playControlPanel; + private final FlowPanel timePanelInnerWrapper; + + /** + * the minimum time the slider extends its time when the end of the slider is reached + */ + private long MINIMUM_AUTO_ADVANCE_TIME_IN_MS = 5 * 60 * 1000; // 5 minutes + private boolean advancedModeShown; + + private static ClientResources resources = GWT.create(ClientResources.class); + protected static TimePanelCss timePanelCss = TimePanelCssResources.INSTANCE.css(); + + private final boolean forcePaddingRightToAlignToCharts; + + /** + * @param forcePaddingRightToAlignToCharts + * if true, the right padding will always be set such that the time panel lines up with + * charts such as the competitor chart or the wind chart shown above it, otherwise the padding depends + * on the flag set by {@link #setLiveGenerallyPossible(boolean)} + */ + public TimePanel(Component parent, ComponentContext context, Timer timer, + TimeRangeWithZoomProvider timeRangeProvider, + StringMessages stringMessages, + boolean canReplayWhileLiveIsPossible, boolean forcePaddingRightToAlignToCharts) { + super(parent, context); + this.timer = timer; + this.timeRangeProvider = timeRangeProvider; + this.stringMessages = stringMessages; + this.canReplayWhileLiveIsPossible = canReplayWhileLiveIsPossible; + this.forcePaddingRightToAlignToCharts = forcePaddingRightToAlignToCharts; + timer.addTimeListener(this); + timer.addPlayStateListener(this); + timeRangeProvider.addTimeRangeChangeListener(this); + timePanelInnerWrapper = new FlowPanel(); + timePanelInnerWrapper.setStyleName("timePanelInnerWrapper"); + timePanelInnerWrapper.setSize("100%", "100%"); + + timePanelSlider = new SimplePanel(); + timePanelSliderFlowWrapper = new FlowPanel(); + timePanelSlider.setStyleName("timePanelSlider"); + timePanelSlider.getElement().getStyle().setPaddingLeft(66, Unit.PX); + timePanelSliderFlowWrapper.add(timePanelSlider); + + playSpeedImg = resources.timesliderPlaySpeedIcon(); + playPauseButton = new Button(""); + + playModeLiveActiveImg = resources.timesliderPlayStateLiveActiveIcon(); + playModeReplayActiveImg = resources.timesliderPlayStateReplayActiveIcon(); + playModeInactiveImg = resources.timesliderPlayStateLiveInactiveIcon(); + playModeImage = new Image(playModeInactiveImg); + + timeSlider = new TimeSlider(); + timeSlider.setEnabled(true); + timeSlider.setLabelFormatter(new SliderBar.LabelFormatter() { + final DateTimeFormat timeWithMinutesFormatter = DateTimeFormat.getFormat("HH:mm"); + @Override + public String formatLabel(SliderBar slider, Double value, Double previousValue) { + Date date = new Date(value.longValue()); + return timeWithMinutesFormatter.format(date); + } + }); + + timeSlider.addValueChangeHandler(new ValueChangeHandler() { + @Override + public void onValueChange(ValueChangeEvent newValue) { + if (timeSlider.getCurrentValue() != null) { + if (TimePanel.this.timer.getPlayMode() == PlayModes.Live) { + // if canReplayWhileLive==false, playStateChanged(...) will ensure that the timer is paused when + // reaching Replay mode + TimePanel.this.timer.setPlayMode(PlayModes.Replay); + } + TimePanel.this.timer.setTime(timeSlider.getCurrentValue().longValue()); + } + } + }); + + timePanelInnerWrapper.add(timePanelSliderFlowWrapper); + timePanelSlider.add(timeSlider); + + controlsPanel = new FlowPanel(); + + controlsPanel.setStyleName("timePanel-controls"); + timePanelInnerWrapper.add(controlsPanel); + + // play button control + playControlPanel = new FlowPanel(); + playControlPanel.setStyleName("timePanel-controls-play"); + controlsPanel.add(playControlPanel); + + toggleAdvancedModeButton = createToggleAdvancedModeButton(); + playControlPanel.add(toggleAdvancedModeButton); + + playPauseButton.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + switch(TimePanel.this.timer.getPlayState()) { + case Paused: + TimePanel.this.timer.play(); + break; + case Playing: + TimePanel.this.timer.pause(); + break; + } + } + }); + playPauseButton.setTitle(stringMessages.startStopPlaying()); + playPauseButton.setStyleName("playPauseButton"); + playControlPanel.add(playPauseButton); + + backToLivePlayButton = new Button(stringMessages.raceIsInLiveTimePanelMode()); + backToLivePlayButton.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + TimePanel.this.timer.setPlayMode(PlayModes.Live); + TimePanel.this.timer.play(); + } + }); + backToLivePlayButton.setStyleName("backToLivePlayButton"); + backToLivePlayButton.setTitle(stringMessages.backToLive()); + playControlPanel.add(backToLivePlayButton); + + // current date and time control + FlowPanel timeControlPanel = new FlowPanel(); + timeControlPanel.setStyleName("timePanel-controls-time"); + dateLabel = new Label(); + timeLabel = new Label(); + timeControlPanel.add(dateLabel); + timeControlPanel.add(timeLabel); + + dateLabel.getElement().setClassName("dateLabel"); + timeLabel.getElement().setClassName("timeLabel"); + + // time to start control + timeToStartControlPanel = new FlowPanel(); + timeToStartControlPanel.setStyleName("timePanel-controls-timeToStart"); + timeToStartLabel = new Label(); + timeToStartControlPanel.add(timeToStartLabel); + timeToStartLabel.getElement().setClassName("timeToStartLabel"); + + FlowPanel playModeControlPanel = new FlowPanel(); + playModeControlPanel.setStyleName("timePanel-controls-playmode"); + playModeControlPanel.add(playModeImage); + playModeImage.getElement().getStyle().setFloat(Style.Float.LEFT); + + playModeLabel = new Label(); + playModeControlPanel .add(playModeLabel); + + playModeLabel.getElement().getStyle().setFloat(Style.Float.LEFT); + playModeLabel.getElement().setClassName("playModeLabel"); + + // play speed controls + FlowPanel playSpeedControlPanel = new FlowPanel(); + playSpeedControlPanel.setStyleName("timePanel-controls-playSpeed"); + + playSpeedBox = new IntegerBox(); + playSpeedBox.setVisibleLength(3); + playSpeedBox.setValue((int)timer.getPlaySpeedFactor()); // Christopher: initialize play speed box according to play speed factor + playSpeedBox.setTitle(stringMessages.playSpeedHelp()); + playSpeedBox.addValueChangeHandler(new ValueChangeHandler() { + @Override + public void onValueChange(ValueChangeEvent event) { + Integer newPlaySpeedFactor = playSpeedBox.getValue() == null ? 0 : playSpeedBox.getValue(); + TimePanel.this.timer.setPlaySpeedFactor(newPlaySpeedFactor); + } + }); + + Image playSpeedImage = new Image(playSpeedImg); + playSpeedControlPanel.add(playSpeedImage); + playSpeedControlPanel.add(playSpeedBox); + + slowDownButton = new Button("-1"); + slowDownButton.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + playSpeedBox.setValue(playSpeedBox.getValue() == null ? 0 : playSpeedBox.getValue() - 1); + TimePanel.this.timer.setPlaySpeedFactor(playSpeedBox.getValue()); + } + }); + slowDownButton.setTitle(stringMessages.slowPlaySpeedDown()); + slowDownButton.addStyleName("timePanelButton-SlowDown"); + playSpeedControlPanel.add(slowDownButton); + + speedUpButton = new Button("+1"); + speedUpButton.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + playSpeedBox.setValue(playSpeedBox.getValue() == null ? 0 : playSpeedBox.getValue() + 1); + TimePanel.this.timer.setPlaySpeedFactor(playSpeedBox.getValue()); + } + }); + speedUpButton.setTitle(stringMessages.speedPlaySpeedUp()); + speedUpButton.addStyleName("timePanelButton-SpeedUp"); + playSpeedControlPanel.add(speedUpButton); + + playSpeedImage.getElement().getStyle().setFloat(Style.Float.LEFT); + playSpeedImage.getElement().getStyle().setPadding(3, Style.Unit.PX); + playSpeedImage.getElement().getStyle().setMarginRight(3, Style.Unit.PX); + + playSpeedBox.getElement().getStyle().setFloat(Style.Float.LEFT); + playSpeedBox.getElement().getStyle().setPadding(2, Style.Unit.PX); + + speedUpButton.addStyleName("timePanelButton"); + slowDownButton.addStyleName("timePanelButton"); + + // time delay + FlowPanel timeDelayPanel = new FlowPanel(); + timeDelayPanel.setStyleName("timePanel-controls-timeDelay"); + + timeDelayLabel = new Label(); + timeDelayPanel.add(timeDelayLabel); + + timeDelayLabel.getElement().getStyle().setFloat(Style.Float.LEFT); + timeDelayLabel.getElement().getStyle().setPadding(3, Style.Unit.PX); + + timePanelCss.ensureInjected(); + controlsPanel.add(createSettingsButton()); + initWidget(timePanelInnerWrapper); + playStateChanged(timer.getPlayState(), timer.getPlayMode()); + + controlsPanel.add(playSpeedControlPanel); + controlsPanel.add(playModeControlPanel ); + controlsPanel.add(timeDelayPanel); + controlsPanel.add(timeControlPanel); + controlsPanel.add(timeToStartControlPanel); + + hideControlsPanel(); + } + + public TimeRangeWithZoomProvider getTimeRangeProvider() { + return timeRangeProvider; + } + + private Button createSettingsButton() { + Button settingsButton = SettingsDialog.createSettingsButton(this, stringMessages); + settingsButton.setStyleName(timePanelCss.settingsButtonStyle()); + settingsButton.addStyleName(timePanelCss.settingsButtonBackgroundImage()); + return settingsButton; + } + + private Button createToggleAdvancedModeButton() { + Button toggleAdvancedModeButton = new Button(); + toggleAdvancedModeButton.setStyleName("TimePanel-ShowExtended-Button"); + toggleAdvancedModeButton.addStyleDependentName("Closed"); + return toggleAdvancedModeButton; + } + + public boolean toggleAdvancedMode() { + if (advancedModeShown) { + hideControlsPanel(); + } else { + showControlsPanel(); + } + this.advancedModeShown = !advancedModeShown; + return this.advancedModeShown; + } + + protected void hideControlsPanel() { + controlsPanel.remove(playControlPanel); + timePanelInnerWrapper.remove(controlsPanel); + timePanelSliderFlowWrapper.insert(playControlPanel, 0); + playControlPanel.setStyleName("timePanel-timeslider-play"); + } + + protected void showControlsPanel() { + timePanelInnerWrapper.add(controlsPanel); + } + + @Override + public void timeChanged(Date newTime, Date oldTime) { + if (timeRangeProvider.isZoomed()) { + timeSlider.setCurrentValue(new Double(newTime.getTime()), false); + } else { + if (getFromTime() == null || getToTime() == null) { + initializeTimeRange(oldTime, newTime); + } + if (getFromTime() != null && getToTime() != null) { + // handle the case where time advances beyond slider's end. + if (newTime.after(getToTime())) { + // in live mode, expand the range + switch (timer.getPlayMode()) { + case Live: + Date newMaxTime = new Date(newTime.getTime()); + if (newMaxTime.getTime() - getToTime().getTime() < MINIMUM_AUTO_ADVANCE_TIME_IN_MS) { + newMaxTime.setTime(getToTime().getTime() + MINIMUM_AUTO_ADVANCE_TIME_IN_MS); + } + setMinMax(getFromTime(), newMaxTime, /* fireEvent */ false); // no event because we guarantee that time is between min/max + break; + case Replay: + // in replay, stop playing when the end of the range has been reached + timer.pause(); + break; + } + } + timeSlider.setCurrentValue(new Double(newTime.getTime()), false); + } + } + dateLabel.setText(getDateLabelText(newTime)); + timeLabel.setText(getTimeLabelText(newTime)); + String timeToStartLabelText = getTimeToStartLabelText(newTime); + if(timeToStartLabelText != null && !timeToStartLabelText.isEmpty()) { + timeToStartControlPanel.setVisible(true); + timeToStartLabel.setText(getTimeToStartLabelText(newTime)); + } else { + timeToStartControlPanel.setVisible(false); + timeToStartLabel.setText(""); + } + } + + /** + * When the time range is not yet properly initialized and we get a time change, adjust the time range so that old and + * new time are in range. A margin of one minute is used before and after. This method assumes that at least one of the + * two time points passed as parameters has a value other than null. Should the other be null, + * it is ignored. No assumptions are made about the temporal relation of the two time points to each other; in other + * words, oldTime may also be after newTime which could, e.g., be the case for a timer running + * backwards. + */ + private void initializeTimeRange(Date oldTime, Date newTime) { + TimePoint from = Util.getEarliestOfTimePoints(getFromTime()==null?null:new MillisecondsTimePoint(getFromTime()), oldTime==null?null:new MillisecondsTimePoint(oldTime)); + from = Util.getEarliestOfTimePoints(from, newTime==null?null:new MillisecondsTimePoint(newTime)); + TimePoint to = Util.getEarliestOfTimePoints(getToTime()==null?null:new MillisecondsTimePoint(getToTime()), oldTime==null?null:new MillisecondsTimePoint(oldTime)); + from = Util.getEarliestOfTimePoints(to, newTime==null?null:new MillisecondsTimePoint(newTime)); + timeRangeProvider.setTimeRange(from==null?null:from.asDate(), to==null?null:to.asDate()); + } + + protected String getTimeToStartLabelText(Date time) { + return null; + } + + protected String getTimeLabelText(Date time) { + final String timeLabelText; + if (lastReceivedDataTimepoint == null) { + timeLabelText = timeFormatter.format(time); + } else { + timeLabelText = timeFormatter.format(time) + " (" + timeFormatter.format(lastReceivedDataTimepoint) + ")"; + } + return timeLabelText; + } + + protected String getDateLabelText(Date time) { + return dateFormatter.format(time); + } + + protected Date getFromTime() { + return timeRangeProvider.getFromTime(); + } + + protected Date getToTime() { + return timeRangeProvider.getToTime(); + } + + /** + * @param min must not be null + * @param max must not be null + */ + public void setMinMax(Date min, Date max, boolean fireEvent) { + assert min != null && max != null; + + boolean changed = false; + changed = timeSlider.setMinAndMaxValue(new Double(min.getTime()), new Double(max.getTime()), fireEvent); + if (changed) { + if (!timeRangeProvider.isZoomed()) { + timeRangeProvider.setTimeRange(min, max, this); + } + + int numSteps = timeSlider.getElement().getClientWidth(); + if (numSteps > 0) { + timeSlider.setStepSize(numSteps, fireEvent); + } else { + timeSlider.setStepSize(1000, fireEvent); + } + + // Christopher: following setCurrentValue requires stepsize to be set <> 0 (otherwise division by zero; NaN) + if (timeSlider.getCurrentValue() == null) { + timeSlider.setCurrentValue(new Double(min.getTime()), fireEvent); + } + } + } + + public void resetTimeSlider() { + timeSlider.clearMarkers(); + timeSlider.redraw(); + } + + /** + * Makes sure that the invariant regarding {@link #canReplayWhileLiveIsPossible} is fulfilled. The timer will be + * {@link Timer#pause() paused} if the state is {@link PlayStates#Playing} and the the play mode is + * {@link PlayModes#Replay}. + * + */ + @Override + public void playStateChanged(PlayStates playState, PlayModes playMode) { + boolean liveModeToBeMadePossible = isLiveModeToBeMadePossible(); + setLiveGenerallyPossible(liveModeToBeMadePossible); + setJumpToLiveEnablement(liveModeToBeMadePossible && playMode != PlayModes.Live); + switch (playState) { + case Playing: + playPauseButton.getElement().addClassName("playPauseButtonPause"); + playPauseButton.setVisible(true); + if (playMode == PlayModes.Live) { + playModeImage.setResource(playModeLiveActiveImg); + } else { + playModeImage.setResource(playModeReplayActiveImg); + } + break; + case Paused: + updatePlayPauseButtonsVisibility(playMode); + playPauseButton.getElement().removeClassName("playPauseButtonPause"); + playModeImage.setResource(playModeInactiveImg); + break; + } + + switch (playMode) { + case Live: + playModeLabel.setText(stringMessages.playModeLive()); + timeDelayLabel.setText(stringMessages.timeDelay() + ": " + timer.getLivePlayDelayInMillis() / 1000 + " s"); + timeDelayLabel.setVisible(true); + timeSlider.setEnabled(true); + playSpeedBox.setEnabled(false); + slowDownButton.setEnabled(false); + speedUpButton.setEnabled(false); + break; + case Replay: + timeDelayLabel.setVisible(false); + timeDelayLabel.setText(""); + playModeLabel.setText(stringMessages.playModeReplay()); + timeSlider.setEnabled(true); + playSpeedBox.setEnabled(true); + slowDownButton.setEnabled(true); + speedUpButton.setEnabled(true); + if (!canReplayWhileLiveIsPossible() && playState == PlayStates.Playing && isLiveModeToBeMadePossible()) { + // can't leave the timer playing when race is still live and canReplayWhileLiveIsPossible==false + timer.pause(); + } + break; + } + } + + public void updatePlayPauseButtonsVisibility(PlayModes playMode) { + playPauseButton.setVisible(playMode == PlayModes.Live || canReplayWhileLiveIsPossible() || !isLiveModeToBeMadePossible()); + } + + @Override + public void playSpeedFactorChanged(double newPlaySpeedFactor) { + // nothing to do + } + + @Override + public void onTimeZoomChanged(Date zoomStartTimepoint, Date zoomEndTimepoint) { + } + + @Override + public void onTimeZoomReset() { + } + + @Override + public void onTimeRangeChanged(Date fromTime, Date toTime) { + setMinMax(fromTime, toTime, true); + } + + protected boolean isLiveModeToBeMadePossible() { + return false; + } + + /** + * Enables the {@link #backToLivePlayButton} if and only if enabled is true. + */ + protected void setJumpToLiveEnablement(boolean enabled) { + backToLivePlayButton.setEnabled(enabled); + if (enabled) { + backToLivePlayButton.setText(stringMessages.backToLiveTimePanelMode()); + backToLivePlayButton.removeStyleDependentName("Inactive"); + backToLivePlayButton.addStyleDependentName("Active"); + } else { + backToLivePlayButton.setText(stringMessages.raceIsInLiveTimePanelMode()); + backToLivePlayButton.removeStyleDependentName("Active"); + backToLivePlayButton.addStyleDependentName("Inactive"); + } + } + + /** + * Iff possible, makes the {@link #backToLivePlayButton} button visible. Furthermore, if + * possible==false and {@link #canReplayWhileLiveIsPossible}==false and the timer is in + * state {@link PlayStates#Paused} and in mode {@link PlayModes#Replay}, the play/pause button will be shown. + */ + protected void setLiveGenerallyPossible(boolean possible) { + backToLivePlayButton.setVisible(possible); + updateTimeSliderPadding(possible); + updatePlayPauseButtonsVisibility(timer.getPlayMode()); + } + + private void updateTimeSliderPadding(boolean backToLivePlayButtonVisible) { + Style timePanelStyle = timePanelSlider.getElement().getStyle(); + if (backToLivePlayButtonVisible || forcePaddingRightToAlignToCharts) { + timePanelStyle.setPaddingRight(66, Unit.PX); + } else { + timePanelStyle.clearPaddingRight(); + } + timeSlider.onResize(); + } + + @SuppressWarnings("unchecked") + public T getSettings() { + TimePanelSettings result = new TimePanelSettings(timer.getRefreshInterval()); + return (T) result; + } + + @Override + public Widget getEntryWidget() { + return this; + } + + @Override + public boolean hasSettings() { + return true; + } + + @Override + public SettingsDialogComponent getSettingsDialogComponent() { + return new TimePanelSettingsDialogComponent(getSettings(), stringMessages); + } + + @Override + public void updateSettings(T newSettings) { + timer.setRefreshInterval(newSettings.getRefreshInterval()); + } + + @Override + public String getLocalizedShortName() { + return stringMessages.timePanelName(); + } + + @Override + public void onResize() { + // handle what is required by @link{ProvidesResize} + Widget child = getWidget(); + if (child instanceof RequiresResize) { + ((RequiresResize) child).onResize(); + } + timeSlider.onResize(); + } + + public Date getLastReceivedDataTimepoint() { + return lastReceivedDataTimepoint; + } + + public void setLastReceivedDataTimepoint(Date lastReceivedDataTimepoint) { + this.lastReceivedDataTimepoint = lastReceivedDataTimepoint; + } + + protected boolean canReplayWhileLiveIsPossible() { + return canReplayWhileLiveIsPossible; + } + + public Button getAdvancedToggleButton() { + return this.toggleAdvancedModeButton; + } + + @Override + public String getDependentCssClassName() { + return "timePanel"; + } + + public Button getBackToLiveButton() { + return backToLivePlayButton; + } + + @Override + public String getId() { + return "TimePanel"; + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/MediaPlayerManagerComponent.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/MediaPlayerManagerComponent.java index 122c8520071..40b865edfb6 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/MediaPlayerManagerComponent.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/MediaPlayerManagerComponent.java @@ -51,7 +51,7 @@ import com.sap.sse.gwt.client.player.Timer.PlayStates; import com.sap.sse.gwt.client.shared.components.AbstractComponent; import com.sap.sse.gwt.client.shared.components.Component; import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; import com.sap.sse.gwt.client.useragent.UserAgentDetails; import com.sap.sse.gwt.client.useragent.UserAgentDetails.AgentTypes; import com.sap.sse.security.ui.client.UserService; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractCompetitorLeaderboardChart.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractCompetitorLeaderboardChart.java index 8f2550dd4e7..8d2cedfce97 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractCompetitorLeaderboardChart.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractCompetitorLeaderboardChart.java @@ -50,7 +50,7 @@ import com.sap.sse.gwt.client.player.Timer; import com.sap.sse.gwt.client.player.Timer.PlayModes; import com.sap.sse.gwt.client.shared.components.AbstractLazyComponent; import com.sap.sse.gwt.client.shared.components.Component; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; /** * A base class for a leaderboard chart showing competitor data for all race columns of a leaderboard. diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractCompetitorRaceChart.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractCompetitorRaceChart.java index c60df5840b1..2cd05276c5e 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractCompetitorRaceChart.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractCompetitorRaceChart.java @@ -1,841 +1,841 @@ -package com.sap.sailing.gwt.ui.client.shared.charts; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; - -import org.moxieapps.gwt.highcharts.client.Axis; -import org.moxieapps.gwt.highcharts.client.BaseChart; -import org.moxieapps.gwt.highcharts.client.Chart; -import org.moxieapps.gwt.highcharts.client.ChartSubtitle; -import org.moxieapps.gwt.highcharts.client.ChartTitle; -import org.moxieapps.gwt.highcharts.client.Color; -import org.moxieapps.gwt.highcharts.client.Credits; -import org.moxieapps.gwt.highcharts.client.PlotLine; -import org.moxieapps.gwt.highcharts.client.PlotLine.DashStyle; -import org.moxieapps.gwt.highcharts.client.Point; -import org.moxieapps.gwt.highcharts.client.Series; -import org.moxieapps.gwt.highcharts.client.ToolTip; -import org.moxieapps.gwt.highcharts.client.ToolTipData; -import org.moxieapps.gwt.highcharts.client.ToolTipFormatter; -import org.moxieapps.gwt.highcharts.client.events.ChartClickEvent; -import org.moxieapps.gwt.highcharts.client.events.ChartClickEventHandler; -import org.moxieapps.gwt.highcharts.client.events.ChartSelectionEvent; -import org.moxieapps.gwt.highcharts.client.events.ChartSelectionEventHandler; -import org.moxieapps.gwt.highcharts.client.labels.AxisLabelsData; -import org.moxieapps.gwt.highcharts.client.labels.AxisLabelsFormatter; -import org.moxieapps.gwt.highcharts.client.labels.XAxisLabels; -import org.moxieapps.gwt.highcharts.client.plotOptions.LinePlotOptions; -import org.moxieapps.gwt.highcharts.client.plotOptions.Marker; -import org.moxieapps.gwt.highcharts.client.plotOptions.ScatterPlotOptions; - -import com.google.gwt.core.client.Scheduler; -import com.google.gwt.core.client.Scheduler.ScheduledCommand; -import com.google.gwt.i18n.client.NumberFormat; -import com.google.gwt.user.client.rpc.AsyncCallback; -import com.google.gwt.user.client.ui.Label; -import com.google.gwt.user.client.ui.RequiresResize; -import com.google.gwt.user.client.ui.Widget; -import com.sap.sailing.domain.common.DetailType; -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.gwt.ui.actions.GetCompetitorsRaceDataAction; -import com.sap.sailing.gwt.ui.client.CompetitorSelectionChangeListener; -import com.sap.sailing.gwt.ui.client.CompetitorSelectionProvider; -import com.sap.sailing.gwt.ui.client.DetailTypeFormatter; -import com.sap.sailing.gwt.ui.client.SailingServiceAsync; -import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sailing.gwt.ui.shared.CompetitorRaceDataDTO; -import com.sap.sailing.gwt.ui.shared.CompetitorsRaceDataDTO; -import com.sap.sailing.gwt.ui.shared.SailingServiceConstants; -import com.sap.sse.common.Util; -import com.sap.sse.common.Util.Pair; -import com.sap.sse.common.filter.Filter; -import com.sap.sse.common.filter.FilterSet; -import com.sap.sse.gwt.client.ErrorReporter; -import com.sap.sse.gwt.client.async.AsyncActionsExecutor; -import com.sap.sse.gwt.client.player.TimeRangeWithZoomProvider; -import com.sap.sse.gwt.client.player.Timer; -import com.sap.sse.gwt.client.player.Timer.PlayModes; -import com.sap.sse.gwt.client.shared.components.Component; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; - -/** - * AbstractCompetitorChart is a chart that can show one sort of competitor data (e.g. current speed over ground, - * windward distance to leader) for different races in a chart. - * - * When calling the constructor a chart is created that creates a final amount of series (so the maximum number of - * competitors cannot be changed in one chart) which are connected to competitors, when the sailing service returns the - * data. So {@code seriesID, competitorID and markSeriesID} are linked with the index. So if u know for example the - * seriesID-index, you can get the competitor by calling competitorID.get(index). - * - * @author Benjamin Ebling (D056866), Axel Uhl (d043530) - * - */ -public abstract class AbstractCompetitorRaceChart extends AbstractRaceChart implements - CompetitorSelectionChangeListener, RequiresResize { - public static final String LOAD_COMPETITOR_CHART_DATA_CATEGORY = "loadCompetitorChartData"; - - public static final long DEFAULT_STEPSIZE = 5000; - - private static final int LINE_WIDTH = 1; - - private final Label noCompetitorsSelectedLabel; - private final Label noDataFoundLabel; - private final CompetitorSelectionProvider competitorSelectionProvider; - private DetailType selectedFirstDetailType; - private DetailType selectedSecondDetailType; - - private boolean compactChart; - private final boolean allowTimeAdjust; - private final String leaderboardGroupName; - private final String leaderboardName; - private long stepSizeInMillis = DEFAULT_STEPSIZE; - private final Map, Series> dataSeriesForDetailTypeAndCompetitor = new HashMap<>(); - private final Map, Series> markPassingSeriesByCompetitor = new HashMap<>(); - - private final TimingHolder primary = new TimingHolder(); - private final TimingHolder secondary = new TimingHolder(); - - AbstractCompetitorRaceChart(Component parent, ComponentContext context, SailingServiceAsync sailingService, - AsyncActionsExecutor asyncActionsExecutor, - CompetitorSelectionProvider competitorSelectionProvider, RegattaAndRaceIdentifier selectedRaceIdentifier, - Timer timer, TimeRangeWithZoomProvider timeRangeWithZoomProvider, final StringMessages stringMessages, - ErrorReporter errorReporter, DetailType firstDetailType, DetailType secondDetailType, boolean compactChart, - boolean allowTimeAdjust, - String leaderboardGroupName, String leaderboardName) { - super(parent, context, sailingService, selectedRaceIdentifier, timer, timeRangeWithZoomProvider, stringMessages, - asyncActionsExecutor, errorReporter); - this.competitorSelectionProvider = competitorSelectionProvider; - this.compactChart = compactChart; - this.allowTimeAdjust = allowTimeAdjust; - this.leaderboardGroupName = leaderboardGroupName; - this.leaderboardName = leaderboardName; - - setSize("100%", "100%"); - noCompetitorsSelectedLabel = new Label(stringMessages.selectAtLeastOneCompetitor() + "."); - noCompetitorsSelectedLabel.setStyleName("abstractChartPanel-importantMessageOfChart"); - noDataFoundLabel = new Label(stringMessages.noDataFound() + "."); - noDataFoundLabel.setStyleName("abstractChartPanel-importantMessageOfChart"); - createChart(); - setSelectedDetailTypes(firstDetailType, secondDetailType); - competitorSelectionProvider.addCompetitorSelectionChangeListener(this); - clearChart(); - if (selectedRaceIdentifier != null) { - timeChanged(timer.getTime(), null); - } - } - - /** - * Creates a new chart. - * Attention: We can't reuse the old chart when the detail changes because HighChart does not support the inverting of the Y-Axis - */ - private Chart createChart() { - - Chart chart = new Chart().setZoomType(BaseChart.ZoomType.X) - .setPersistent(true) - .setReflow(false) - .setWidth100() - .setAlignTicks(true) - .setHeight100() - .setMarginLeft(65) - .setMarginRight(65) - .setBorderColor(new Color("#CACACA")) - .setBackgroundColor(new Color("#FFFFFF")) - .setPlotBackgroundColor("#f8f8f8") - .setBorderWidth(0) - .setBorderRadius(0) - .setPlotBorderWidth(0) - .setCredits(new Credits().setEnabled(false)) - .setChartSubtitle(new ChartSubtitle().setText(stringMessages.clickAndDragToZoomIn())); - chart.setStyleName(chartsCss.chartStyle()); - ChartUtil.useCheckboxesToShowAndHide(chart); - - if (allowTimeAdjust) { - chart.setClickEventHandler(new ChartClickEventHandler() { - @Override - public boolean onClick(ChartClickEvent chartClickEvent) { - return AbstractCompetitorRaceChart.this.onClick(chartClickEvent); - } - }); - chart.setSelectionEventHandler(new ChartSelectionEventHandler() { - @Override - public boolean onSelection(ChartSelectionEvent chartSelectionEvent) { - return AbstractCompetitorRaceChart.this.onXAxisSelectionChange(chartSelectionEvent); - } - }); - } - - if (hasSecondYAxis()) { - chart.getYAxis(0).setStartOnTick(false).setShowFirstLabel(false); - chart.getYAxis(1).setStartOnTick(false).setShowFirstLabel(false).setOpposite(true); - } else { - chart.getYAxis(0).setStartOnTick(false).setShowFirstLabel(false); - chart.setLinePlotOptions(new LinePlotOptions() - .setLineWidth(LINE_WIDTH) - .setMarker(new Marker().setEnabled(false).setHoverState(new Marker().setEnabled(true).setRadius(4))) - .setShadow(false).setHoverStateLineWidth(LINE_WIDTH)); - } - chart.getXAxis().setType(Axis.Type.DATE_TIME).setMaxZoom(60 * 1000); // 1 minute - chart.getXAxis().setLabels(new XAxisLabels().setFormatter(new AxisLabelsFormatter() { - @Override - public String format(AxisLabelsData axisLabelsData) { - return dateFormatHoursMinutes.format(new Date(axisLabelsData.getValueAsLong())); - } - })); - timePlotLine = chart.getXAxis().createPlotLine().setColor("#656565").setWidth(1.5) - .setDashStyle(DashStyle.SOLID); - return chart; - } - - protected abstract Component getComponent(); - - /** - * Loads the needed data (data which isn't in the {@link #chartData cache}) for the - * {@link #getSelectedCompetitors() visible competitors} via - * {@link SailingServiceAsync#getCompetitorsRaceData(RaceIdentifier, List, long, DetailType, AsyncCallback)}. After - * loading, the method {@link #drawChartData()} is called. - *

- * If no data needs to be {@link #needsDataLoading() loaded}, the "no competitors selected" label is displayed. - */ - private void updateChart(Date from, Date to, boolean append) { - if (hasSelectedCompetitors() && isVisible()) { - remove(noCompetitorsSelectedLabel); - if (!getChildren().contains(chart)) { - add(chart); - } - ArrayList competitorsToLoad = new ArrayList(); - for (CompetitorDTO competitorDTO : getSelectedCompetitors()) { - competitorsToLoad.add(competitorDTO); - } - loadData(from, to, competitorsToLoad, append); - } else { - remove(chart); - if (!getChildren().contains(noCompetitorsSelectedLabel)) { - add(noCompetitorsSelectedLabel); - } - } - } - - private void loadData(final Date from, final Date to, final List competitors, final boolean append) { - if (isVisible()) { - // if no data is loaded yet, or if it is not playing and not live (append loading every second) show loading - // indicator - if (shouldShowLoading(primary.timeOfLatestRequestInMillis)) { - showLoading(stringMessages.loadingCompetitorData()); - } - ArrayList competitorsToLoad = new ArrayList(); - for (CompetitorDTO competitorDTO : competitors) { - competitorsToLoad.add(competitorDTO); - } - // If the time interval is too long and the step size too small, the number of fixes the query would have to - // produce may exceed any reasonable limit. Therefore, we limit the number of fixes that such a query may ask - // for: - doLoadDataForCompetitorsAndDataType(from, to, append, competitorsToLoad, getSelectedFirstDetailType(),primary); - if (getSelectedSecondDetailType() != null) { - doLoadDataForCompetitorsAndDataType(from, to, append, competitorsToLoad, getSelectedSecondDetailType(),secondary); - } - } - } - - private void doLoadDataForCompetitorsAndDataType(final Date from, final Date to, final boolean append, - ArrayList competitorsToLoad, final DetailType selectedDataTypeToRetrieve,final TimingHolder tholder) { - long stepSize = Math.max(getStepSizeInMillis(), - from==null||to==null ? 0 : Math.abs(to.getTime()-from.getTime())/SailingServiceConstants.MAX_NUMBER_OF_FIXES_TO_QUERY); - GetCompetitorsRaceDataAction getCompetitorsRaceDataAction = new GetCompetitorsRaceDataAction(sailingService, - selectedRaceIdentifier, competitorsToLoad, from, to, stepSize, selectedDataTypeToRetrieve, - leaderboardGroupName, leaderboardName); - - AsyncCallback dataLoadedCallback = new AsyncCallback() { - @Override - public void onSuccess(final CompetitorsRaceDataDTO result) { - hideLoading(); - if (result != null) { - if (result.isEmpty() && chartContainsNoData()) { - setWidget(noDataFoundLabel); - } else { - updateChartSeries(result, selectedDataTypeToRetrieve, append, tholder); - } - } else { - if (!append) { - clearChart(); - } - } - } - - @Override - public void onFailure(Throwable caught) { - hideLoading(); - errorReporter.reportError(stringMessages.errorFetchingChartData(caught.getMessage()), - timer.getPlayMode() == PlayModes.Live); - } - }; - - if (append) { - // this call is repeated, allow it to be throttled and dropped - asyncActionsExecutor.execute(getCompetitorsRaceDataAction, LOAD_COMPETITOR_CHART_DATA_CATEGORY, - dataLoadedCallback); - } else { - // ensure that non appending only once loading is reliable and cannot be dropped by not using - // asyncActionExecutor - getCompetitorsRaceDataAction.execute(dataLoadedCallback); - } - - } - - private boolean chartContainsNoData() { - for (Series competitorSeries : dataSeriesForDetailTypeAndCompetitor.values()) { - if (competitorSeries.getPoints().length != 0) { - return false; - } - } - return true; - } - - @Override - public void addedToSelection(CompetitorDTO competitor) { - if (isVisible()) { - showLoading(stringMessages.loadingCompetitorData()); - ArrayList competitorsToLoad = new ArrayList(); - competitorsToLoad.add(competitor); - - { - Date fromDate = primary.timeOfEarliestRequestInMillis == null ? null : new Date(primary.timeOfEarliestRequestInMillis); - Date toDate = primary.timeOfLatestRequestInMillis == null ? null : new Date(primary.timeOfLatestRequestInMillis); - loadData(fromDate, toDate, competitorsToLoad, false); - } - { - Date fromDate = secondary.timeOfEarliestRequestInMillis == null ? null : new Date(secondary.timeOfEarliestRequestInMillis); - Date toDate = secondary.timeOfLatestRequestInMillis == null ? null : new Date(secondary.timeOfLatestRequestInMillis); - loadData(fromDate, toDate, competitorsToLoad, false); - } - } - } - - @Override - public void removedFromSelection(CompetitorDTO competitor) { - for (Pair coDePair : dataSeriesForDetailTypeAndCompetitor.keySet()) { - if (!coDePair.getA().equals(competitor)) { - continue; - } - Series competitorSeries = dataSeriesForDetailTypeAndCompetitor.get(coDePair); - if (competitorSeries != null) { - chart.removeSeries(competitorSeries, false); - } - Series competitorMarkPassingSeries = markPassingSeriesByCompetitor.get(coDePair); - if (competitorMarkPassingSeries != null) { - chart.removeSeries(competitorMarkPassingSeries, false); - } - if (isVisible()) { - if (hasSelectedCompetitors()) { - chart.redraw(); - } else { - setWidget(noCompetitorsSelectedLabel); - } - } - } - } - - /** - * Creates the series for all selected competitors if these aren't created yet.
- * Fills the series for the selected competitors with the data in {@link AbstractCompetitorRaceChart#chartData}.
- */ - private synchronized void updateChartSeries(CompetitorsRaceDataDTO chartData, DetailType retrievedDataType, - boolean append,TimingHolder tholder) { - // Make sure the busy indicator is removed at this point, or plotting the data results in an exception - setWidget(chart); - for (CompetitorDTO competitor : chartData.getCompetitors()) { - Series competitorDataSeries = getOrCreateCompetitorDataSeries(retrievedDataType, competitor); - Series markPassingSeries = getOrCreateCompetitorMarkPassingSeries(competitorDataSeries, retrievedDataType, - competitor); - - CompetitorRaceDataDTO competitorData = chartData.getCompetitorData(competitor); - if (competitorData != null) { - Date toDate = timer.getLiveTimePointAsDate(); - List> markPassingsData = competitorData.getMarkPassingsData(); - List markPassingPoints = new ArrayList(); - for (com.sap.sse.common.Util.Triple markPassingData : markPassingsData) { - if (markPassingData.getB() != null && markPassingData.getC() != null) { - if (markPassingData.getB().before(toDate)) { - Point markPassingPoint = new Point(markPassingData.getB().getTime(), - markPassingData.getC()); - markPassingPoint.setName(markPassingData.getA()); - markPassingPoints.add(markPassingPoint); - } - } - } - markPassingSeries.setPoints(markPassingPoints.toArray(new Point[0]), false); - - Point[] oldRaceDataPoints = competitorDataSeries.getPoints(); - List> raceData = competitorData.getRaceData(); - - Point[] raceDataPointsToAdd = new Point[raceData.size()]; - int currentPointIndex = 0; - for (com.sap.sse.common.Util.Pair raceDataPoint : raceData) { - Double dataPointValue = raceDataPoint.getB(); - if(dataPointValue != null) { - long dataPointTimeAsMillis = raceDataPoint.getA().getTime(); - if(append == false || (tholder.timeOfEarliestRequestInMillis == null || dataPointTimeAsMillis < tholder.timeOfEarliestRequestInMillis) || - tholder.timeOfLatestRequestInMillis == null || dataPointTimeAsMillis > tholder.timeOfLatestRequestInMillis) { - raceDataPointsToAdd[currentPointIndex++] = new Point(dataPointTimeAsMillis, dataPointValue); - } - } - } - - Point[] newRaceDataPoints; - if (append) { - newRaceDataPoints = new Point[oldRaceDataPoints.length + currentPointIndex]; - System.arraycopy(oldRaceDataPoints, 0, newRaceDataPoints, 0, oldRaceDataPoints.length); - System.arraycopy(raceDataPointsToAdd, 0, newRaceDataPoints, oldRaceDataPoints.length, currentPointIndex); - } else { - newRaceDataPoints = new Point[currentPointIndex]; - System.arraycopy(raceDataPointsToAdd, 0, newRaceDataPoints, 0, currentPointIndex); - } - - setSeriesPoints(competitorDataSeries, newRaceDataPoints); - // Adding the series if chart doesn't contain it - List chartSeries = Arrays.asList(chart.getSeries()); - if (!chartSeries.contains(competitorDataSeries)) { - chart.addSeries(competitorDataSeries); - chart.addSeries(markPassingSeries); - //this is to prevent a bug, that will prohibit the initial rendering - chart.setSizeToMatchContainer(); - - } - } - } - if (tholder.timeOfEarliestRequestInMillis == null || tholder.timeOfEarliestRequestInMillis > chartData.getRequestedFromTime().getTime()) { - tholder.timeOfEarliestRequestInMillis = chartData.getRequestedFromTime().getTime(); - } - if (tholder.timeOfLatestRequestInMillis == null || tholder.timeOfLatestRequestInMillis < chartData.getRequestedToTime().getTime()) { - tholder.timeOfLatestRequestInMillis = chartData.getRequestedToTime().getTime(); - } - chart.redraw(); - } - - private Iterable getSelectedCompetitors() { - return competitorSelectionProvider.getSelectedCompetitors(); - } - - private boolean hasSelectedCompetitors() { - return Util.size(getSelectedCompetitors()) > 0; - } - - /** - * - * @param competitor - * @return A series in the chart, that can be used to show the data of a specific competitor. - */ - protected Series getOrCreateCompetitorDataSeries(DetailType seriesDetailType, final CompetitorDTO competitor) { - final Pair coDePair = new Pair(competitor, - seriesDetailType); - final int yAxisIndex = yAxisIndex(seriesDetailType); - Series result = this.dataSeriesForDetailTypeAndCompetitor.get(coDePair); - if (result == null) { - result = chart.createSeries().setType(Series.Type.LINE); - - if (hasSecondYAxis()) { - result.setName(DetailTypeFormatter.format(seriesDetailType) + " " + competitor.getName()); - } else { - result.setName(competitor.getName()); - } - result.setPlotOptions(new LinePlotOptions() - .setLineWidth(LINE_WIDTH) - .setDashStyle(yAxisIndex == 0 ? PlotLine.DashStyle.SOLID : PlotLine.DashStyle.LONG_DASH) - .setMarker(new Marker().setEnabled(false).setHoverState(new Marker().setEnabled(true).setRadius(4))) - .setShadow(false).setHoverStateLineWidth(LINE_WIDTH) - .setColor(competitorSelectionProvider.getColor(competitor, selectedRaceIdentifier).getAsHtml()) - .setSelected(true)); - result.setOption("turboThreshold", MAX_SERIES_POINTS); - dataSeriesForDetailTypeAndCompetitor.put(coDePair, result); - } - - result.setYAxis(yAxisIndex); - return result; - } - - /** - * - * @param competitor - * @return A series in the chart, that can be used to show the mark passings. - */ - private Series getOrCreateCompetitorMarkPassingSeries(Series linkedCompetitorSeries, DetailType seriesDetailType, - CompetitorDTO competitor) { - final Pair coDePair = new Pair(competitor, - seriesDetailType); - Series result = markPassingSeriesByCompetitor.get(coDePair); - final int yAxisIndex = yAxisIndex(seriesDetailType); - if (result == null) { - result = chart.createSeries().setType(Series.Type.SCATTER) - .setName(stringMessages.markPassing() + " " + competitor.getName()); - result.setPlotOptions(new ScatterPlotOptions().setLinkedTo(linkedCompetitorSeries) - .setColor( - competitorSelectionProvider.getColor(competitor, selectedRaceIdentifier).getAsHtml()) - .setSelected(true)); - markPassingSeriesByCompetitor.put(coDePair, result); - } - result.setYAxis(yAxisIndex); - return result; - } - - @Override - public void setVisible(boolean visible) { - super.setVisible(visible); - if (visible) { - Scheduler.get().scheduleDeferred(new ScheduledCommand() { - - @Override - public void execute() { - // Workaround for a highcharts bug: Set a chart title, overwrite the title, - // switch chart to invisible and visible again -> the old title appears - if (compactChart) { - applyCompactTitle(chartTitleFromDetailTypes()); - } else { - chart.setTitle(hasSecondYAxis() ? null : new ChartTitle().setText(chartTitleFromDetailTypes()), - null); - } - - chart.setSizeToMatchContainer(); - // it's important here to recall the redraw method, otherwise the bug fix for wrong checkbox - // positions - // (nativeAdjustCheckboxPosition) - // in the BaseChart class would not be called - chart.redraw(); - } - }); - } - - } - - private void applyCompactTitle(String title) { - chart.setSpacingLeft(10).setSpacingRight(10).setSpacingTop(20).setOption("legend/margin", 2) - .setChartSubtitle(null).getXAxis().setAxisTitleText(null); - chart.setTitle(hasSecondYAxis() ? null : new ChartTitle().setText(title).setOption("floating",true), null); - chart.redraw(); - } - - private int yAxisIndex(DetailType seriesDetailType) { - if (selectedSecondDetailType != null && seriesDetailType == selectedSecondDetailType) { - return 1; - } else { - return 0; - } - } - - private String chartTitleFromDetailTypes() { - StringBuilder titleBuilder = new StringBuilder(); - titleBuilder.append(DetailTypeFormatter.format(selectedFirstDetailType)); - if (selectedSecondDetailType != null) { - titleBuilder.append("/"); - titleBuilder.append(DetailTypeFormatter.format(selectedSecondDetailType)); - } - return titleBuilder.toString(); - } - - /** - * Clears the whole chart and empties cached data. - */ - protected void clearChart() { - primary.timeOfEarliestRequestInMillis = null; - primary.timeOfLatestRequestInMillis = null; - secondary.timeOfEarliestRequestInMillis = null; - secondary.timeOfLatestRequestInMillis = null; - dataSeriesForDetailTypeAndCompetitor.clear(); - markPassingSeriesByCompetitor.clear(); - chart.removeAllSeries(); - } - - public String getLocalizedShortName() { - return chartTitleFromDetailTypes(); - } - - public Widget getEntryWidget() { - return this; - } - - public boolean hasSettings() { - return true; - } - - public ChartSettings getAbstractSettings() { - return new ChartSettings(getStepSizeInMillis()); - } - - /** - * Updates the settings known to be contained in {@link ChartSettings}. Subclasses have to update settings provided - * by subclasses thereof. Subclasses also need to call {@link #clearChart()} and {@link #updateChart(boolean)}, if - * this method returns true; - * - * @return true if the settings had been changed and a clearing and loading is needed. - */ - protected boolean updateSettingsOnly(ChartSettings newSettings) { - boolean settingsChanged = false; - if (getStepSizeInMillis() != newSettings.getStepSizeInMillis()) { - setStepSizeInMillis(newSettings.getStepSizeInMillis()); - settingsChanged = true; - } - return settingsChanged; - } - - protected StringMessages getStringMessages() { - return stringMessages; - } - - /** - * The chart step size in milliseconds - */ - protected long getStepSizeInMillis() { - return stepSizeInMillis; - } - - protected void setStepSizeInMillis(long stepSizeInMillis) { - this.stepSizeInMillis = stepSizeInMillis; - } - - protected DetailType getSelectedFirstDetailType() { - return this.selectedFirstDetailType; - } - - protected DetailType getSelectedSecondDetailType() { - return this.selectedSecondDetailType; - } - - /** - * Updates the {@link #selectedFirstDetailType} field, clears the chart for the new selectedDetailType - * and clears the {@link #chartData}.
- * Doesn't {@link #updateChart(boolean) load the data}. - * - * @return true if the detail type changed - */ - protected boolean setSelectedDetailTypes(DetailType newSelectedFirstDetailType, - DetailType newSelectedSecondDetailType) { - boolean hasDetailTypeChanged = !Util.equalsWithNull(newSelectedFirstDetailType, this.selectedFirstDetailType) - || !Util.equalsWithNull(newSelectedSecondDetailType, this.selectedSecondDetailType); - if (hasDetailTypeChanged) { - // final boolean oldReversedY0Axis = isY0AxisReversed(); - // final boolean oldReversedY1Axis = isY1AxisReversed(); - this.selectedFirstDetailType = newSelectedFirstDetailType; - this.selectedSecondDetailType = newSelectedSecondDetailType; - // TODO There is a bug in the highcharts library which prevents to change the reverse property of the YAxis - // Because we need this functionality we need to recreate the chart each time the YAxis changes - // if (oldReversedY0Axis != isY0AxisReversed() || oldReversedY1Axis != isY1AxisReversed()) { - // WORKAROUND: re-creating chart every time since introduction of dual y-axis, since there is no way to - // reset/ delete axis. - chart = createChart(); - if (isZoomed) { - com.sap.sse.common.Util.Pair zoomRange = timeRangeWithZoomProvider.getTimeZoom(); - onTimeZoomChanged(zoomRange.getA(), zoomRange.getB()); - } else { - resetMinMaxAndExtremesInterval(/* redraw */ true); - } - // } - - final String unitY0 = DetailTypeFormatter.getUnit(getSelectedFirstDetailType()); - final String labelY0 = unitY0.isEmpty() ? "" : "[" + unitY0 + "]"; - final String unitY1 = hasSecondYAxis() ? DetailTypeFormatter.getUnit(getSelectedSecondDetailType()) : null; - final String labelY1 = hasSecondYAxis() ? (unitY1.isEmpty() ? "" : "[" + unitY1 + "]") : null; - - if (compactChart) { - applyCompactTitle(chartTitleFromDetailTypes()); - } else { - chart.setTitle(hasSecondYAxis() ? null : new ChartTitle().setText(chartTitleFromDetailTypes()), null); - } - if (hasSecondYAxis()) { - chart.getYAxis(0).setAxisTitleText(DetailTypeFormatter.format(selectedFirstDetailType) + " " + labelY0); - chart.getYAxis(1) - .setAxisTitleText(DetailTypeFormatter.format(selectedSecondDetailType) + " " + labelY1); - } else { - chart.getYAxis(0).setAxisTitleText(labelY0); - } - - if (hasSecondYAxis()) { - chart.getYAxis(0).setReversed(isY0AxisReversed()).setOpposite(false) - .setGridLineWidth(1) - .setMinorGridLineWidth(0).setMinorGridLineColor("transparent"); - - chart.getYAxis(1).setReversed(isY1AxisReversed()).setOpposite(true) - .setGridLineWidth(1) - .setGridLineDashStyle(DashStyle.LONG_DASH) - .setMinorGridLineWidth(0).setMinorGridLineColor("transparent") - .setMinorTickIntervalAuto(); - } else { - chart.getYAxis(0).setReversed(isY0AxisReversed()); - } - chart.setAlignTicks(hasSecondYAxis()); - final NumberFormat numberFormatY0 = DetailTypeFormatter.getNumberFormat(selectedFirstDetailType); - final NumberFormat numberFormatY1 = hasSecondYAxis() ? DetailTypeFormatter - .getNumberFormat(selectedSecondDetailType) : null; - - chart.setToolTip(new ToolTip().setEnabled(true).setFormatter(new ToolTipFormatter() { - @Override - public String format(ToolTipData toolTipData) { - String seriesName = toolTipData.getSeriesName(); - - StringBuilder ttb = new StringBuilder(); - if (seriesName.equals(stringMessages.time())) { - ttb.append("").append(seriesName).append(": ") - .append(dateFormat.format(new Date(toolTipData.getXAsLong()))).append("
(") - .append(stringMessages.clickChartToSetTime()).append(")"); - } else { - ttb.append("").append(seriesName); - if (toolTipData.getPointName() != null) { - ttb.append(" ").append(toolTipData.getPointName()); - } - ttb.append("
").append(dateFormat.format(new Date(toolTipData.getXAsLong()))) - .append(": "); - if (isSecondYAxis(toolTipData.getSeriesId())) { - ttb.append(numberFormatY1.format(toolTipData.getYAsDouble())).append(" ").append(unitY1); - } else { - ttb.append(numberFormatY0.format(toolTipData.getYAsDouble())).append(" ").append(unitY0); - } - } - return ttb.toString(); - } - })); - } - return hasDetailTypeChanged; - } - - private boolean isSecondYAxis(String seriesId) { - Series series = chart.getSeries(seriesId); - for (Entry, Series> entry : dataSeriesForDetailTypeAndCompetitor.entrySet()) { - if (entry.getValue().equals(series)) { - return entry.getKey().getB().equals(selectedSecondDetailType); - } - } - return false; - } - - private boolean hasSecondYAxis() { - return selectedSecondDetailType != null; - } - - private boolean isY0AxisReversed() { - return isYAxisReversed(selectedFirstDetailType); - } - - private boolean isY1AxisReversed() { - return isYAxisReversed(selectedSecondDetailType); - } - - private boolean isYAxisReversed(DetailType detailType) { - return detailType == DetailType.WINDWARD_DISTANCE_TO_COMPETITOR_FARTHEST_AHEAD - || detailType == DetailType.GAP_TO_LEADER_IN_SECONDS || detailType == DetailType.RACE_RANK - || detailType == DetailType.REGATTA_RANK || detailType == DetailType.OVERALL_RANK; - } - - /** - * Checks the relation of the mark passings to the selection range. - * - * @param markPassingInRange - * A Boolean matrix filled by - * {@link AbstractCompetitorRaceChart#fillPotentialXValues(double, double, ArrayList, ArrayList, ArrayList) - * fillPotentialXValues(...)} - * @return A pair of Booleans. Value A contains false if a passing is not in the selection (error), so that the - * selection range needs to be refactored. Value B returns true if two passings are in range before the - * error happened or false, if the error happens before two passings were in the selection. B can be - * null. - */ - public com.sap.sse.common.Util.Pair checkPassingRelationToSelection(ArrayList> markPassingInRange) { - boolean everyPassingInRange = true; - Boolean twoPassingsInRangeBeforeError = null; - ArrayList competitorPassings = markPassingInRange.get(0); - for (int i = 0; i < competitorPassings.size(); i++) { - Boolean passingInRange = competitorPassings.get(i); - for (int j = 1; j < markPassingInRange.size(); j++) { - Boolean passingToCompare = markPassingInRange.get(j).get(i); - if (passingInRange != null) { - if (passingToCompare != null && everyPassingInRange) { - everyPassingInRange = passingInRange.equals(passingToCompare); - if (passingInRange && passingToCompare) { - twoPassingsInRangeBeforeError = true; - } - } else if (passingToCompare != null) { - if (passingInRange && passingToCompare) { - twoPassingsInRangeBeforeError = false; - } - } - } else { - passingInRange = passingToCompare; - } - } - } - - return new com.sap.sse.common.Util.Pair(everyPassingInRange, twoPassingsInRangeBeforeError); - } - - @Override - public void timeChanged(Date newTime, Date oldTime) { - if (!isVisible()) { - return; - } - - if (allowTimeAdjust) { - updateTimePlotLine(newTime); - } - - switch (timer.getPlayMode()) { - case Live: { - // is date before first cache entry or is cache empty? - if (primary.timeOfEarliestRequestInMillis == null - || newTime.getTime() < primary.timeOfEarliestRequestInMillis) { - updateChart(timeRangeWithZoomProvider.getFromTime(), newTime, /* append */true); - } else if (newTime.getTime() > primary.timeOfLatestRequestInMillis) { - updateChart(new Date(primary.timeOfLatestRequestInMillis), timeRangeWithZoomProvider.getToTime(), /* append */true); - } - // otherwise the cache spans across date and so we don't need to load anything - break; - } - case Replay: { - if (primary.timeOfLatestRequestInMillis == null) { - // pure replay mode - updateChart(timeRangeWithZoomProvider.getFromTime(), timeRangeWithZoomProvider.getToTime(), /* append */false); - } else { - // replay mode during live play - if (primary.timeOfEarliestRequestInMillis == null || newTime.getTime() < primary.timeOfEarliestRequestInMillis) { - updateChart(timeRangeWithZoomProvider.getFromTime(), newTime, /* append */true); - } else if (newTime.getTime() > primary.timeOfLatestRequestInMillis) { - updateChart(new Date(primary.timeOfLatestRequestInMillis), timeRangeWithZoomProvider.getToTime(), /* append */true); - } - } - break; - } - } - } - - @Override - public void onResize() { - chart.setSizeToMatchContainer(); - // it's important here to recall the redraw method, otherwise the bug fix for wrong checkbox positions - // (nativeAdjustCheckboxPosition) - // in the BaseChart class would not be called - chart.redraw(); - } - - @Override - public void competitorsListChanged(Iterable competitors) { - timeChanged(timer.getTime(), null); - } - - @Override - public void filteredCompetitorsListChanged(Iterable filteredCompetitors) { - timeChanged(timer.getTime(), null); - } - - @Override - public void filterChanged(FilterSet> oldFilterSet, - FilterSet> newFilterSet) { - // nothing to do; if it changes the filtered competitor list, a separate call to filteredCompetitorsListChanged will occur - } - - static class TimingHolder{ - private Long timeOfEarliestRequestInMillis; - private Long timeOfLatestRequestInMillis; - } - -} +package com.sap.sailing.gwt.ui.client.shared.charts; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.moxieapps.gwt.highcharts.client.Axis; +import org.moxieapps.gwt.highcharts.client.BaseChart; +import org.moxieapps.gwt.highcharts.client.Chart; +import org.moxieapps.gwt.highcharts.client.ChartSubtitle; +import org.moxieapps.gwt.highcharts.client.ChartTitle; +import org.moxieapps.gwt.highcharts.client.Color; +import org.moxieapps.gwt.highcharts.client.Credits; +import org.moxieapps.gwt.highcharts.client.PlotLine; +import org.moxieapps.gwt.highcharts.client.PlotLine.DashStyle; +import org.moxieapps.gwt.highcharts.client.Point; +import org.moxieapps.gwt.highcharts.client.Series; +import org.moxieapps.gwt.highcharts.client.ToolTip; +import org.moxieapps.gwt.highcharts.client.ToolTipData; +import org.moxieapps.gwt.highcharts.client.ToolTipFormatter; +import org.moxieapps.gwt.highcharts.client.events.ChartClickEvent; +import org.moxieapps.gwt.highcharts.client.events.ChartClickEventHandler; +import org.moxieapps.gwt.highcharts.client.events.ChartSelectionEvent; +import org.moxieapps.gwt.highcharts.client.events.ChartSelectionEventHandler; +import org.moxieapps.gwt.highcharts.client.labels.AxisLabelsData; +import org.moxieapps.gwt.highcharts.client.labels.AxisLabelsFormatter; +import org.moxieapps.gwt.highcharts.client.labels.XAxisLabels; +import org.moxieapps.gwt.highcharts.client.plotOptions.LinePlotOptions; +import org.moxieapps.gwt.highcharts.client.plotOptions.Marker; +import org.moxieapps.gwt.highcharts.client.plotOptions.ScatterPlotOptions; + +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.google.gwt.i18n.client.NumberFormat; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.RequiresResize; +import com.google.gwt.user.client.ui.Widget; +import com.sap.sailing.domain.common.DetailType; +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.gwt.ui.actions.GetCompetitorsRaceDataAction; +import com.sap.sailing.gwt.ui.client.CompetitorSelectionChangeListener; +import com.sap.sailing.gwt.ui.client.CompetitorSelectionProvider; +import com.sap.sailing.gwt.ui.client.DetailTypeFormatter; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.shared.CompetitorRaceDataDTO; +import com.sap.sailing.gwt.ui.shared.CompetitorsRaceDataDTO; +import com.sap.sailing.gwt.ui.shared.SailingServiceConstants; +import com.sap.sse.common.Util; +import com.sap.sse.common.Util.Pair; +import com.sap.sse.common.filter.Filter; +import com.sap.sse.common.filter.FilterSet; +import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.async.AsyncActionsExecutor; +import com.sap.sse.gwt.client.player.TimeRangeWithZoomProvider; +import com.sap.sse.gwt.client.player.Timer; +import com.sap.sse.gwt.client.player.Timer.PlayModes; +import com.sap.sse.gwt.client.shared.components.Component; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; + +/** + * AbstractCompetitorChart is a chart that can show one sort of competitor data (e.g. current speed over ground, + * windward distance to leader) for different races in a chart. + * + * When calling the constructor a chart is created that creates a final amount of series (so the maximum number of + * competitors cannot be changed in one chart) which are connected to competitors, when the sailing service returns the + * data. So {@code seriesID, competitorID and markSeriesID} are linked with the index. So if u know for example the + * seriesID-index, you can get the competitor by calling competitorID.get(index). + * + * @author Benjamin Ebling (D056866), Axel Uhl (d043530) + * + */ +public abstract class AbstractCompetitorRaceChart extends AbstractRaceChart implements + CompetitorSelectionChangeListener, RequiresResize { + public static final String LOAD_COMPETITOR_CHART_DATA_CATEGORY = "loadCompetitorChartData"; + + public static final long DEFAULT_STEPSIZE = 5000; + + private static final int LINE_WIDTH = 1; + + private final Label noCompetitorsSelectedLabel; + private final Label noDataFoundLabel; + private final CompetitorSelectionProvider competitorSelectionProvider; + private DetailType selectedFirstDetailType; + private DetailType selectedSecondDetailType; + + private boolean compactChart; + private final boolean allowTimeAdjust; + private final String leaderboardGroupName; + private final String leaderboardName; + private long stepSizeInMillis = DEFAULT_STEPSIZE; + private final Map, Series> dataSeriesForDetailTypeAndCompetitor = new HashMap<>(); + private final Map, Series> markPassingSeriesByCompetitor = new HashMap<>(); + + private final TimingHolder primary = new TimingHolder(); + private final TimingHolder secondary = new TimingHolder(); + + AbstractCompetitorRaceChart(Component parent, ComponentContext context, SailingServiceAsync sailingService, + AsyncActionsExecutor asyncActionsExecutor, + CompetitorSelectionProvider competitorSelectionProvider, RegattaAndRaceIdentifier selectedRaceIdentifier, + Timer timer, TimeRangeWithZoomProvider timeRangeWithZoomProvider, final StringMessages stringMessages, + ErrorReporter errorReporter, DetailType firstDetailType, DetailType secondDetailType, boolean compactChart, + boolean allowTimeAdjust, + String leaderboardGroupName, String leaderboardName) { + super(parent, context, sailingService, selectedRaceIdentifier, timer, timeRangeWithZoomProvider, stringMessages, + asyncActionsExecutor, errorReporter); + this.competitorSelectionProvider = competitorSelectionProvider; + this.compactChart = compactChart; + this.allowTimeAdjust = allowTimeAdjust; + this.leaderboardGroupName = leaderboardGroupName; + this.leaderboardName = leaderboardName; + + setSize("100%", "100%"); + noCompetitorsSelectedLabel = new Label(stringMessages.selectAtLeastOneCompetitor() + "."); + noCompetitorsSelectedLabel.setStyleName("abstractChartPanel-importantMessageOfChart"); + noDataFoundLabel = new Label(stringMessages.noDataFound() + "."); + noDataFoundLabel.setStyleName("abstractChartPanel-importantMessageOfChart"); + createChart(); + setSelectedDetailTypes(firstDetailType, secondDetailType); + competitorSelectionProvider.addCompetitorSelectionChangeListener(this); + clearChart(); + if (selectedRaceIdentifier != null) { + timeChanged(timer.getTime(), null); + } + } + + /** + * Creates a new chart. + * Attention: We can't reuse the old chart when the detail changes because HighChart does not support the inverting of the Y-Axis + */ + private Chart createChart() { + + Chart chart = new Chart().setZoomType(BaseChart.ZoomType.X) + .setPersistent(true) + .setReflow(false) + .setWidth100() + .setAlignTicks(true) + .setHeight100() + .setMarginLeft(65) + .setMarginRight(65) + .setBorderColor(new Color("#CACACA")) + .setBackgroundColor(new Color("#FFFFFF")) + .setPlotBackgroundColor("#f8f8f8") + .setBorderWidth(0) + .setBorderRadius(0) + .setPlotBorderWidth(0) + .setCredits(new Credits().setEnabled(false)) + .setChartSubtitle(new ChartSubtitle().setText(stringMessages.clickAndDragToZoomIn())); + chart.setStyleName(chartsCss.chartStyle()); + ChartUtil.useCheckboxesToShowAndHide(chart); + + if (allowTimeAdjust) { + chart.setClickEventHandler(new ChartClickEventHandler() { + @Override + public boolean onClick(ChartClickEvent chartClickEvent) { + return AbstractCompetitorRaceChart.this.onClick(chartClickEvent); + } + }); + chart.setSelectionEventHandler(new ChartSelectionEventHandler() { + @Override + public boolean onSelection(ChartSelectionEvent chartSelectionEvent) { + return AbstractCompetitorRaceChart.this.onXAxisSelectionChange(chartSelectionEvent); + } + }); + } + + if (hasSecondYAxis()) { + chart.getYAxis(0).setStartOnTick(false).setShowFirstLabel(false); + chart.getYAxis(1).setStartOnTick(false).setShowFirstLabel(false).setOpposite(true); + } else { + chart.getYAxis(0).setStartOnTick(false).setShowFirstLabel(false); + chart.setLinePlotOptions(new LinePlotOptions() + .setLineWidth(LINE_WIDTH) + .setMarker(new Marker().setEnabled(false).setHoverState(new Marker().setEnabled(true).setRadius(4))) + .setShadow(false).setHoverStateLineWidth(LINE_WIDTH)); + } + chart.getXAxis().setType(Axis.Type.DATE_TIME).setMaxZoom(60 * 1000); // 1 minute + chart.getXAxis().setLabels(new XAxisLabels().setFormatter(new AxisLabelsFormatter() { + @Override + public String format(AxisLabelsData axisLabelsData) { + return dateFormatHoursMinutes.format(new Date(axisLabelsData.getValueAsLong())); + } + })); + timePlotLine = chart.getXAxis().createPlotLine().setColor("#656565").setWidth(1.5) + .setDashStyle(DashStyle.SOLID); + return chart; + } + + protected abstract Component getComponent(); + + /** + * Loads the needed data (data which isn't in the {@link #chartData cache}) for the + * {@link #getSelectedCompetitors() visible competitors} via + * {@link SailingServiceAsync#getCompetitorsRaceData(RaceIdentifier, List, long, DetailType, AsyncCallback)}. After + * loading, the method {@link #drawChartData()} is called. + *

+ * If no data needs to be {@link #needsDataLoading() loaded}, the "no competitors selected" label is displayed. + */ + private void updateChart(Date from, Date to, boolean append) { + if (hasSelectedCompetitors() && isVisible()) { + remove(noCompetitorsSelectedLabel); + if (!getChildren().contains(chart)) { + add(chart); + } + ArrayList competitorsToLoad = new ArrayList(); + for (CompetitorDTO competitorDTO : getSelectedCompetitors()) { + competitorsToLoad.add(competitorDTO); + } + loadData(from, to, competitorsToLoad, append); + } else { + remove(chart); + if (!getChildren().contains(noCompetitorsSelectedLabel)) { + add(noCompetitorsSelectedLabel); + } + } + } + + private void loadData(final Date from, final Date to, final List competitors, final boolean append) { + if (isVisible()) { + // if no data is loaded yet, or if it is not playing and not live (append loading every second) show loading + // indicator + if (shouldShowLoading(primary.timeOfLatestRequestInMillis)) { + showLoading(stringMessages.loadingCompetitorData()); + } + ArrayList competitorsToLoad = new ArrayList(); + for (CompetitorDTO competitorDTO : competitors) { + competitorsToLoad.add(competitorDTO); + } + // If the time interval is too long and the step size too small, the number of fixes the query would have to + // produce may exceed any reasonable limit. Therefore, we limit the number of fixes that such a query may ask + // for: + doLoadDataForCompetitorsAndDataType(from, to, append, competitorsToLoad, getSelectedFirstDetailType(),primary); + if (getSelectedSecondDetailType() != null) { + doLoadDataForCompetitorsAndDataType(from, to, append, competitorsToLoad, getSelectedSecondDetailType(),secondary); + } + } + } + + private void doLoadDataForCompetitorsAndDataType(final Date from, final Date to, final boolean append, + ArrayList competitorsToLoad, final DetailType selectedDataTypeToRetrieve,final TimingHolder tholder) { + long stepSize = Math.max(getStepSizeInMillis(), + from==null||to==null ? 0 : Math.abs(to.getTime()-from.getTime())/SailingServiceConstants.MAX_NUMBER_OF_FIXES_TO_QUERY); + GetCompetitorsRaceDataAction getCompetitorsRaceDataAction = new GetCompetitorsRaceDataAction(sailingService, + selectedRaceIdentifier, competitorsToLoad, from, to, stepSize, selectedDataTypeToRetrieve, + leaderboardGroupName, leaderboardName); + + AsyncCallback dataLoadedCallback = new AsyncCallback() { + @Override + public void onSuccess(final CompetitorsRaceDataDTO result) { + hideLoading(); + if (result != null) { + if (result.isEmpty() && chartContainsNoData()) { + setWidget(noDataFoundLabel); + } else { + updateChartSeries(result, selectedDataTypeToRetrieve, append, tholder); + } + } else { + if (!append) { + clearChart(); + } + } + } + + @Override + public void onFailure(Throwable caught) { + hideLoading(); + errorReporter.reportError(stringMessages.errorFetchingChartData(caught.getMessage()), + timer.getPlayMode() == PlayModes.Live); + } + }; + + if (append) { + // this call is repeated, allow it to be throttled and dropped + asyncActionsExecutor.execute(getCompetitorsRaceDataAction, LOAD_COMPETITOR_CHART_DATA_CATEGORY, + dataLoadedCallback); + } else { + // ensure that non appending only once loading is reliable and cannot be dropped by not using + // asyncActionExecutor + getCompetitorsRaceDataAction.execute(dataLoadedCallback); + } + + } + + private boolean chartContainsNoData() { + for (Series competitorSeries : dataSeriesForDetailTypeAndCompetitor.values()) { + if (competitorSeries.getPoints().length != 0) { + return false; + } + } + return true; + } + + @Override + public void addedToSelection(CompetitorDTO competitor) { + if (isVisible()) { + showLoading(stringMessages.loadingCompetitorData()); + ArrayList competitorsToLoad = new ArrayList(); + competitorsToLoad.add(competitor); + + { + Date fromDate = primary.timeOfEarliestRequestInMillis == null ? null : new Date(primary.timeOfEarliestRequestInMillis); + Date toDate = primary.timeOfLatestRequestInMillis == null ? null : new Date(primary.timeOfLatestRequestInMillis); + loadData(fromDate, toDate, competitorsToLoad, false); + } + { + Date fromDate = secondary.timeOfEarliestRequestInMillis == null ? null : new Date(secondary.timeOfEarliestRequestInMillis); + Date toDate = secondary.timeOfLatestRequestInMillis == null ? null : new Date(secondary.timeOfLatestRequestInMillis); + loadData(fromDate, toDate, competitorsToLoad, false); + } + } + } + + @Override + public void removedFromSelection(CompetitorDTO competitor) { + for (Pair coDePair : dataSeriesForDetailTypeAndCompetitor.keySet()) { + if (!coDePair.getA().equals(competitor)) { + continue; + } + Series competitorSeries = dataSeriesForDetailTypeAndCompetitor.get(coDePair); + if (competitorSeries != null) { + chart.removeSeries(competitorSeries, false); + } + Series competitorMarkPassingSeries = markPassingSeriesByCompetitor.get(coDePair); + if (competitorMarkPassingSeries != null) { + chart.removeSeries(competitorMarkPassingSeries, false); + } + if (isVisible()) { + if (hasSelectedCompetitors()) { + chart.redraw(); + } else { + setWidget(noCompetitorsSelectedLabel); + } + } + } + } + + /** + * Creates the series for all selected competitors if these aren't created yet.
+ * Fills the series for the selected competitors with the data in {@link AbstractCompetitorRaceChart#chartData}.
+ */ + private synchronized void updateChartSeries(CompetitorsRaceDataDTO chartData, DetailType retrievedDataType, + boolean append,TimingHolder tholder) { + // Make sure the busy indicator is removed at this point, or plotting the data results in an exception + setWidget(chart); + for (CompetitorDTO competitor : chartData.getCompetitors()) { + Series competitorDataSeries = getOrCreateCompetitorDataSeries(retrievedDataType, competitor); + Series markPassingSeries = getOrCreateCompetitorMarkPassingSeries(competitorDataSeries, retrievedDataType, + competitor); + + CompetitorRaceDataDTO competitorData = chartData.getCompetitorData(competitor); + if (competitorData != null) { + Date toDate = timer.getLiveTimePointAsDate(); + List> markPassingsData = competitorData.getMarkPassingsData(); + List markPassingPoints = new ArrayList(); + for (com.sap.sse.common.Util.Triple markPassingData : markPassingsData) { + if (markPassingData.getB() != null && markPassingData.getC() != null) { + if (markPassingData.getB().before(toDate)) { + Point markPassingPoint = new Point(markPassingData.getB().getTime(), + markPassingData.getC()); + markPassingPoint.setName(markPassingData.getA()); + markPassingPoints.add(markPassingPoint); + } + } + } + markPassingSeries.setPoints(markPassingPoints.toArray(new Point[0]), false); + + Point[] oldRaceDataPoints = competitorDataSeries.getPoints(); + List> raceData = competitorData.getRaceData(); + + Point[] raceDataPointsToAdd = new Point[raceData.size()]; + int currentPointIndex = 0; + for (com.sap.sse.common.Util.Pair raceDataPoint : raceData) { + Double dataPointValue = raceDataPoint.getB(); + if(dataPointValue != null) { + long dataPointTimeAsMillis = raceDataPoint.getA().getTime(); + if(append == false || (tholder.timeOfEarliestRequestInMillis == null || dataPointTimeAsMillis < tholder.timeOfEarliestRequestInMillis) || + tholder.timeOfLatestRequestInMillis == null || dataPointTimeAsMillis > tholder.timeOfLatestRequestInMillis) { + raceDataPointsToAdd[currentPointIndex++] = new Point(dataPointTimeAsMillis, dataPointValue); + } + } + } + + Point[] newRaceDataPoints; + if (append) { + newRaceDataPoints = new Point[oldRaceDataPoints.length + currentPointIndex]; + System.arraycopy(oldRaceDataPoints, 0, newRaceDataPoints, 0, oldRaceDataPoints.length); + System.arraycopy(raceDataPointsToAdd, 0, newRaceDataPoints, oldRaceDataPoints.length, currentPointIndex); + } else { + newRaceDataPoints = new Point[currentPointIndex]; + System.arraycopy(raceDataPointsToAdd, 0, newRaceDataPoints, 0, currentPointIndex); + } + + setSeriesPoints(competitorDataSeries, newRaceDataPoints); + // Adding the series if chart doesn't contain it + List chartSeries = Arrays.asList(chart.getSeries()); + if (!chartSeries.contains(competitorDataSeries)) { + chart.addSeries(competitorDataSeries); + chart.addSeries(markPassingSeries); + //this is to prevent a bug, that will prohibit the initial rendering + chart.setSizeToMatchContainer(); + + } + } + } + if (tholder.timeOfEarliestRequestInMillis == null || tholder.timeOfEarliestRequestInMillis > chartData.getRequestedFromTime().getTime()) { + tholder.timeOfEarliestRequestInMillis = chartData.getRequestedFromTime().getTime(); + } + if (tholder.timeOfLatestRequestInMillis == null || tholder.timeOfLatestRequestInMillis < chartData.getRequestedToTime().getTime()) { + tholder.timeOfLatestRequestInMillis = chartData.getRequestedToTime().getTime(); + } + chart.redraw(); + } + + private Iterable getSelectedCompetitors() { + return competitorSelectionProvider.getSelectedCompetitors(); + } + + private boolean hasSelectedCompetitors() { + return Util.size(getSelectedCompetitors()) > 0; + } + + /** + * + * @param competitor + * @return A series in the chart, that can be used to show the data of a specific competitor. + */ + protected Series getOrCreateCompetitorDataSeries(DetailType seriesDetailType, final CompetitorDTO competitor) { + final Pair coDePair = new Pair(competitor, + seriesDetailType); + final int yAxisIndex = yAxisIndex(seriesDetailType); + Series result = this.dataSeriesForDetailTypeAndCompetitor.get(coDePair); + if (result == null) { + result = chart.createSeries().setType(Series.Type.LINE); + + if (hasSecondYAxis()) { + result.setName(DetailTypeFormatter.format(seriesDetailType) + " " + competitor.getName()); + } else { + result.setName(competitor.getName()); + } + result.setPlotOptions(new LinePlotOptions() + .setLineWidth(LINE_WIDTH) + .setDashStyle(yAxisIndex == 0 ? PlotLine.DashStyle.SOLID : PlotLine.DashStyle.LONG_DASH) + .setMarker(new Marker().setEnabled(false).setHoverState(new Marker().setEnabled(true).setRadius(4))) + .setShadow(false).setHoverStateLineWidth(LINE_WIDTH) + .setColor(competitorSelectionProvider.getColor(competitor, selectedRaceIdentifier).getAsHtml()) + .setSelected(true)); + result.setOption("turboThreshold", MAX_SERIES_POINTS); + dataSeriesForDetailTypeAndCompetitor.put(coDePair, result); + } + + result.setYAxis(yAxisIndex); + return result; + } + + /** + * + * @param competitor + * @return A series in the chart, that can be used to show the mark passings. + */ + private Series getOrCreateCompetitorMarkPassingSeries(Series linkedCompetitorSeries, DetailType seriesDetailType, + CompetitorDTO competitor) { + final Pair coDePair = new Pair(competitor, + seriesDetailType); + Series result = markPassingSeriesByCompetitor.get(coDePair); + final int yAxisIndex = yAxisIndex(seriesDetailType); + if (result == null) { + result = chart.createSeries().setType(Series.Type.SCATTER) + .setName(stringMessages.markPassing() + " " + competitor.getName()); + result.setPlotOptions(new ScatterPlotOptions().setLinkedTo(linkedCompetitorSeries) + .setColor( + competitorSelectionProvider.getColor(competitor, selectedRaceIdentifier).getAsHtml()) + .setSelected(true)); + markPassingSeriesByCompetitor.put(coDePair, result); + } + result.setYAxis(yAxisIndex); + return result; + } + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + if (visible) { + Scheduler.get().scheduleDeferred(new ScheduledCommand() { + + @Override + public void execute() { + // Workaround for a highcharts bug: Set a chart title, overwrite the title, + // switch chart to invisible and visible again -> the old title appears + if (compactChart) { + applyCompactTitle(chartTitleFromDetailTypes()); + } else { + chart.setTitle(hasSecondYAxis() ? null : new ChartTitle().setText(chartTitleFromDetailTypes()), + null); + } + + chart.setSizeToMatchContainer(); + // it's important here to recall the redraw method, otherwise the bug fix for wrong checkbox + // positions + // (nativeAdjustCheckboxPosition) + // in the BaseChart class would not be called + chart.redraw(); + } + }); + } + + } + + private void applyCompactTitle(String title) { + chart.setSpacingLeft(10).setSpacingRight(10).setSpacingTop(20).setOption("legend/margin", 2) + .setChartSubtitle(null).getXAxis().setAxisTitleText(null); + chart.setTitle(hasSecondYAxis() ? null : new ChartTitle().setText(title).setOption("floating",true), null); + chart.redraw(); + } + + private int yAxisIndex(DetailType seriesDetailType) { + if (selectedSecondDetailType != null && seriesDetailType == selectedSecondDetailType) { + return 1; + } else { + return 0; + } + } + + private String chartTitleFromDetailTypes() { + StringBuilder titleBuilder = new StringBuilder(); + titleBuilder.append(DetailTypeFormatter.format(selectedFirstDetailType)); + if (selectedSecondDetailType != null) { + titleBuilder.append("/"); + titleBuilder.append(DetailTypeFormatter.format(selectedSecondDetailType)); + } + return titleBuilder.toString(); + } + + /** + * Clears the whole chart and empties cached data. + */ + protected void clearChart() { + primary.timeOfEarliestRequestInMillis = null; + primary.timeOfLatestRequestInMillis = null; + secondary.timeOfEarliestRequestInMillis = null; + secondary.timeOfLatestRequestInMillis = null; + dataSeriesForDetailTypeAndCompetitor.clear(); + markPassingSeriesByCompetitor.clear(); + chart.removeAllSeries(); + } + + public String getLocalizedShortName() { + return chartTitleFromDetailTypes(); + } + + public Widget getEntryWidget() { + return this; + } + + public boolean hasSettings() { + return true; + } + + public ChartSettings getAbstractSettings() { + return new ChartSettings(getStepSizeInMillis()); + } + + /** + * Updates the settings known to be contained in {@link ChartSettings}. Subclasses have to update settings provided + * by subclasses thereof. Subclasses also need to call {@link #clearChart()} and {@link #updateChart(boolean)}, if + * this method returns true; + * + * @return true if the settings had been changed and a clearing and loading is needed. + */ + protected boolean updateSettingsOnly(ChartSettings newSettings) { + boolean settingsChanged = false; + if (getStepSizeInMillis() != newSettings.getStepSizeInMillis()) { + setStepSizeInMillis(newSettings.getStepSizeInMillis()); + settingsChanged = true; + } + return settingsChanged; + } + + protected StringMessages getStringMessages() { + return stringMessages; + } + + /** + * The chart step size in milliseconds + */ + protected long getStepSizeInMillis() { + return stepSizeInMillis; + } + + protected void setStepSizeInMillis(long stepSizeInMillis) { + this.stepSizeInMillis = stepSizeInMillis; + } + + protected DetailType getSelectedFirstDetailType() { + return this.selectedFirstDetailType; + } + + protected DetailType getSelectedSecondDetailType() { + return this.selectedSecondDetailType; + } + + /** + * Updates the {@link #selectedFirstDetailType} field, clears the chart for the new selectedDetailType + * and clears the {@link #chartData}.
+ * Doesn't {@link #updateChart(boolean) load the data}. + * + * @return true if the detail type changed + */ + protected boolean setSelectedDetailTypes(DetailType newSelectedFirstDetailType, + DetailType newSelectedSecondDetailType) { + boolean hasDetailTypeChanged = !Util.equalsWithNull(newSelectedFirstDetailType, this.selectedFirstDetailType) + || !Util.equalsWithNull(newSelectedSecondDetailType, this.selectedSecondDetailType); + if (hasDetailTypeChanged) { + // final boolean oldReversedY0Axis = isY0AxisReversed(); + // final boolean oldReversedY1Axis = isY1AxisReversed(); + this.selectedFirstDetailType = newSelectedFirstDetailType; + this.selectedSecondDetailType = newSelectedSecondDetailType; + // TODO There is a bug in the highcharts library which prevents to change the reverse property of the YAxis + // Because we need this functionality we need to recreate the chart each time the YAxis changes + // if (oldReversedY0Axis != isY0AxisReversed() || oldReversedY1Axis != isY1AxisReversed()) { + // WORKAROUND: re-creating chart every time since introduction of dual y-axis, since there is no way to + // reset/ delete axis. + chart = createChart(); + if (isZoomed) { + com.sap.sse.common.Util.Pair zoomRange = timeRangeWithZoomProvider.getTimeZoom(); + onTimeZoomChanged(zoomRange.getA(), zoomRange.getB()); + } else { + resetMinMaxAndExtremesInterval(/* redraw */ true); + } + // } + + final String unitY0 = DetailTypeFormatter.getUnit(getSelectedFirstDetailType()); + final String labelY0 = unitY0.isEmpty() ? "" : "[" + unitY0 + "]"; + final String unitY1 = hasSecondYAxis() ? DetailTypeFormatter.getUnit(getSelectedSecondDetailType()) : null; + final String labelY1 = hasSecondYAxis() ? (unitY1.isEmpty() ? "" : "[" + unitY1 + "]") : null; + + if (compactChart) { + applyCompactTitle(chartTitleFromDetailTypes()); + } else { + chart.setTitle(hasSecondYAxis() ? null : new ChartTitle().setText(chartTitleFromDetailTypes()), null); + } + if (hasSecondYAxis()) { + chart.getYAxis(0).setAxisTitleText(DetailTypeFormatter.format(selectedFirstDetailType) + " " + labelY0); + chart.getYAxis(1) + .setAxisTitleText(DetailTypeFormatter.format(selectedSecondDetailType) + " " + labelY1); + } else { + chart.getYAxis(0).setAxisTitleText(labelY0); + } + + if (hasSecondYAxis()) { + chart.getYAxis(0).setReversed(isY0AxisReversed()).setOpposite(false) + .setGridLineWidth(1) + .setMinorGridLineWidth(0).setMinorGridLineColor("transparent"); + + chart.getYAxis(1).setReversed(isY1AxisReversed()).setOpposite(true) + .setGridLineWidth(1) + .setGridLineDashStyle(DashStyle.LONG_DASH) + .setMinorGridLineWidth(0).setMinorGridLineColor("transparent") + .setMinorTickIntervalAuto(); + } else { + chart.getYAxis(0).setReversed(isY0AxisReversed()); + } + chart.setAlignTicks(hasSecondYAxis()); + final NumberFormat numberFormatY0 = DetailTypeFormatter.getNumberFormat(selectedFirstDetailType); + final NumberFormat numberFormatY1 = hasSecondYAxis() ? DetailTypeFormatter + .getNumberFormat(selectedSecondDetailType) : null; + + chart.setToolTip(new ToolTip().setEnabled(true).setFormatter(new ToolTipFormatter() { + @Override + public String format(ToolTipData toolTipData) { + String seriesName = toolTipData.getSeriesName(); + + StringBuilder ttb = new StringBuilder(); + if (seriesName.equals(stringMessages.time())) { + ttb.append("").append(seriesName).append(": ") + .append(dateFormat.format(new Date(toolTipData.getXAsLong()))).append("
(") + .append(stringMessages.clickChartToSetTime()).append(")"); + } else { + ttb.append("").append(seriesName); + if (toolTipData.getPointName() != null) { + ttb.append(" ").append(toolTipData.getPointName()); + } + ttb.append("
").append(dateFormat.format(new Date(toolTipData.getXAsLong()))) + .append(": "); + if (isSecondYAxis(toolTipData.getSeriesId())) { + ttb.append(numberFormatY1.format(toolTipData.getYAsDouble())).append(" ").append(unitY1); + } else { + ttb.append(numberFormatY0.format(toolTipData.getYAsDouble())).append(" ").append(unitY0); + } + } + return ttb.toString(); + } + })); + } + return hasDetailTypeChanged; + } + + private boolean isSecondYAxis(String seriesId) { + Series series = chart.getSeries(seriesId); + for (Entry, Series> entry : dataSeriesForDetailTypeAndCompetitor.entrySet()) { + if (entry.getValue().equals(series)) { + return entry.getKey().getB().equals(selectedSecondDetailType); + } + } + return false; + } + + private boolean hasSecondYAxis() { + return selectedSecondDetailType != null; + } + + private boolean isY0AxisReversed() { + return isYAxisReversed(selectedFirstDetailType); + } + + private boolean isY1AxisReversed() { + return isYAxisReversed(selectedSecondDetailType); + } + + private boolean isYAxisReversed(DetailType detailType) { + return detailType == DetailType.WINDWARD_DISTANCE_TO_COMPETITOR_FARTHEST_AHEAD + || detailType == DetailType.GAP_TO_LEADER_IN_SECONDS || detailType == DetailType.RACE_RANK + || detailType == DetailType.REGATTA_RANK || detailType == DetailType.OVERALL_RANK; + } + + /** + * Checks the relation of the mark passings to the selection range. + * + * @param markPassingInRange + * A Boolean matrix filled by + * {@link AbstractCompetitorRaceChart#fillPotentialXValues(double, double, ArrayList, ArrayList, ArrayList) + * fillPotentialXValues(...)} + * @return A pair of Booleans. Value A contains false if a passing is not in the selection (error), so that the + * selection range needs to be refactored. Value B returns true if two passings are in range before the + * error happened or false, if the error happens before two passings were in the selection. B can be + * null. + */ + public com.sap.sse.common.Util.Pair checkPassingRelationToSelection(ArrayList> markPassingInRange) { + boolean everyPassingInRange = true; + Boolean twoPassingsInRangeBeforeError = null; + ArrayList competitorPassings = markPassingInRange.get(0); + for (int i = 0; i < competitorPassings.size(); i++) { + Boolean passingInRange = competitorPassings.get(i); + for (int j = 1; j < markPassingInRange.size(); j++) { + Boolean passingToCompare = markPassingInRange.get(j).get(i); + if (passingInRange != null) { + if (passingToCompare != null && everyPassingInRange) { + everyPassingInRange = passingInRange.equals(passingToCompare); + if (passingInRange && passingToCompare) { + twoPassingsInRangeBeforeError = true; + } + } else if (passingToCompare != null) { + if (passingInRange && passingToCompare) { + twoPassingsInRangeBeforeError = false; + } + } + } else { + passingInRange = passingToCompare; + } + } + } + + return new com.sap.sse.common.Util.Pair(everyPassingInRange, twoPassingsInRangeBeforeError); + } + + @Override + public void timeChanged(Date newTime, Date oldTime) { + if (!isVisible()) { + return; + } + + if (allowTimeAdjust) { + updateTimePlotLine(newTime); + } + + switch (timer.getPlayMode()) { + case Live: { + // is date before first cache entry or is cache empty? + if (primary.timeOfEarliestRequestInMillis == null + || newTime.getTime() < primary.timeOfEarliestRequestInMillis) { + updateChart(timeRangeWithZoomProvider.getFromTime(), newTime, /* append */true); + } else if (newTime.getTime() > primary.timeOfLatestRequestInMillis) { + updateChart(new Date(primary.timeOfLatestRequestInMillis), timeRangeWithZoomProvider.getToTime(), /* append */true); + } + // otherwise the cache spans across date and so we don't need to load anything + break; + } + case Replay: { + if (primary.timeOfLatestRequestInMillis == null) { + // pure replay mode + updateChart(timeRangeWithZoomProvider.getFromTime(), timeRangeWithZoomProvider.getToTime(), /* append */false); + } else { + // replay mode during live play + if (primary.timeOfEarliestRequestInMillis == null || newTime.getTime() < primary.timeOfEarliestRequestInMillis) { + updateChart(timeRangeWithZoomProvider.getFromTime(), newTime, /* append */true); + } else if (newTime.getTime() > primary.timeOfLatestRequestInMillis) { + updateChart(new Date(primary.timeOfLatestRequestInMillis), timeRangeWithZoomProvider.getToTime(), /* append */true); + } + } + break; + } + } + } + + @Override + public void onResize() { + chart.setSizeToMatchContainer(); + // it's important here to recall the redraw method, otherwise the bug fix for wrong checkbox positions + // (nativeAdjustCheckboxPosition) + // in the BaseChart class would not be called + chart.redraw(); + } + + @Override + public void competitorsListChanged(Iterable competitors) { + timeChanged(timer.getTime(), null); + } + + @Override + public void filteredCompetitorsListChanged(Iterable filteredCompetitors) { + timeChanged(timer.getTime(), null); + } + + @Override + public void filterChanged(FilterSet> oldFilterSet, + FilterSet> newFilterSet) { + // nothing to do; if it changes the filtered competitor list, a separate call to filteredCompetitorsListChanged will occur + } + + static class TimingHolder{ + private Long timeOfEarliestRequestInMillis; + private Long timeOfLatestRequestInMillis; + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractRaceChart.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractRaceChart.java index ceb308e24aa..d57444571d8 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractRaceChart.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractRaceChart.java @@ -1,296 +1,296 @@ -package com.sap.sailing.gwt.ui.client.shared.charts; - -import java.util.Date; -import java.util.Iterator; - -import org.moxieapps.gwt.highcharts.client.Chart; -import org.moxieapps.gwt.highcharts.client.PlotLine; -import org.moxieapps.gwt.highcharts.client.Point; -import org.moxieapps.gwt.highcharts.client.Series; -import org.moxieapps.gwt.highcharts.client.XAxis; -import org.moxieapps.gwt.highcharts.client.events.ChartClickEvent; -import org.moxieapps.gwt.highcharts.client.events.ChartSelectionEvent; - -import com.google.gwt.core.client.Scheduler; -import com.google.gwt.core.client.Scheduler.ScheduledCommand; -import com.google.gwt.dom.client.Style.Unit; -import com.google.gwt.i18n.client.DateTimeFormat; -import com.google.gwt.user.client.ui.AbsolutePanel; -import com.google.gwt.user.client.ui.Button; -import com.google.gwt.user.client.ui.SimplePanel; -import com.google.gwt.user.client.ui.Widget; -import com.google.gwt.user.client.ui.WidgetCollection; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sailing.gwt.ui.client.SailingServiceAsync; -import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sailing.gwt.ui.client.shared.charts.ChartCssResources.ChartsCss; -import com.sap.sse.common.settings.Settings; -import com.sap.sse.gwt.client.ErrorReporter; -import com.sap.sse.gwt.client.async.AsyncActionsExecutor; -import com.sap.sse.gwt.client.controls.busyindicator.SimpleBusyIndicator; -import com.sap.sse.gwt.client.controls.slider.TimeTicksCalculator; -import com.sap.sse.gwt.client.controls.slider.TimeTicksCalculator.NormalizedInterval; -import com.sap.sse.gwt.client.player.TimeListener; -import com.sap.sse.gwt.client.player.TimeRangeChangeListener; -import com.sap.sse.gwt.client.player.TimeRangeWithZoomProvider; -import com.sap.sse.gwt.client.player.TimeZoomChangeListener; -import com.sap.sse.gwt.client.player.Timer; -import com.sap.sse.gwt.client.player.Timer.PlayModes; -import com.sap.sse.gwt.client.player.Timer.PlayStates; -import com.sap.sse.gwt.client.shared.components.AbstractCompositeComponent; -import com.sap.sse.gwt.client.shared.components.Component; -import com.sap.sse.gwt.client.shared.components.SettingsDialog; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; - -public abstract class AbstractRaceChart extends AbstractCompositeComponent implements TimeListener, TimeZoomChangeListener, TimeRangeChangeListener { - /** - * Used as the turboThreshold for the Highcharts series; this is basically the maximum number of points in a series - * to be displayed. Default is 1000. See also bug 1742. - */ - protected static final int MAX_SERIES_POINTS = 1000000; - - public static class ExposedAbsolutePanel extends AbsolutePanel { - - public WidgetCollection getChildren() { - return super.getChildren(); - } - - } - - private ExposedAbsolutePanel rootPanel = new ExposedAbsolutePanel(); - - protected Chart chart; - protected PlotLine timePlotLine; - - protected final Timer timer; - protected final TimeRangeWithZoomProvider timeRangeWithZoomProvider; - - protected final RegattaAndRaceIdentifier selectedRaceIdentifier; - - protected final DateTimeFormat dateFormat = DateTimeFormat.getFormat("HH:mm:ss"); - protected final DateTimeFormat dateFormatHoursMinutes = DateTimeFormat.getFormat("HH:mm"); - - protected final StringMessages stringMessages; - protected final ErrorReporter errorReporter; - protected final AsyncActionsExecutor asyncActionsExecutor; - protected final SailingServiceAsync sailingService; - - protected boolean isLoading = false; - protected boolean isZoomed = false; - - protected static ChartsCss chartsCss = ChartCssResources.INSTANCE.css(); - - /** the tick count must be the same as TimeSlider.TICKCOUNT, otherwise the time ticks will be not synchronized */ - private final int TICKCOUNT = 10; - - public static final long MINUTE_IN_MILLIS = 60 * 1000; - - private boolean ignoreNextClickEvent; - - private final SimpleBusyIndicator busyIndicator; - - private final Button settingsButton; - - protected AbstractRaceChart(Component parent, ComponentContext context, SailingServiceAsync sailingService, - RegattaAndRaceIdentifier selectedRaceIdentifier, Timer timer, - TimeRangeWithZoomProvider timeRangeWithZoomProvider, - final StringMessages stringMessages, AsyncActionsExecutor asyncActionsExecutor, ErrorReporter errorReporter) { - super(parent, context); - this.sailingService = sailingService; - this.selectedRaceIdentifier = selectedRaceIdentifier; - this.timer = timer; - this.timeRangeWithZoomProvider = timeRangeWithZoomProvider; - this.stringMessages = stringMessages; - this.asyncActionsExecutor = asyncActionsExecutor; - this.errorReporter = errorReporter; - timer.addTimeListener(this); - timeRangeWithZoomProvider.addTimeZoomChangeListener(this); - timeRangeWithZoomProvider.addTimeRangeChangeListener(this); - chartsCss.ensureInjected(); - busyIndicator = new SimpleBusyIndicator(/* busy */ true, 2.0f); - busyIndicator.setPanelStyleClass(chartsCss.busyIndicatorStyle()); - busyIndicator.setImageStyleClass(chartsCss.busyIndicatorImageStyle()); - settingsButton = createSettingsButton(); - settingsButton.setStyleName(chartsCss.settingsButtonStyle()); - settingsButton.addStyleName(chartsCss.settingsButtonBackgroundImage()); - add(settingsButton); - initWidget(rootPanel); - getElement().getStyle().setMarginRight(12, Unit.PX); - getElement().getStyle().setMarginLeft(12, Unit.PX); - } - - /** - * Subclasses implement this, e.g., by calling {@link SettingsDialog#createSettingsButton(com.sap.sse.gwt.client.shared.components.Component, StringMessages)}. - * This class's constructor will add the {@link ChartsCss#settingsButtonStyle()} and the {@link ChartsCss#settingsButtonBackgroundImage()}. - */ - protected abstract Button createSettingsButton(); - - /** - * Subclasses need to provide a settings button which will be displayed at a useful position in the layout of this - * complex panel, e.g., in the top right corner. - */ - private Button getSettingsButton() { - return settingsButton; - } - - /** - * Simulates a {@link SimplePanel} behavior by replacing all widgets but the {@link #getSettingsButton() settings button} which is always - * supposed to be visible. If widget is already a child of this panel, it is left unchanged, and all other widgets except for - * the settings button are removed. - */ - protected void setWidget(Widget widget) { - Button settingsButton = getSettingsButton(); - boolean foundWidget = false; - for (Iterator i=rootPanel.getChildren().iterator(); i.hasNext(); ) { - Widget child = i.next(); - if (child == widget) { - foundWidget = true; - } else if (child != settingsButton) { - i.remove(); - } - } - if (!foundWidget) { - add(widget); - } - } - - protected void showLoading(String message) { - if (chart.isRendered()) { - chart.showLoading(message); - } else { - add(busyIndicator); - } - isLoading = true; - } - - protected void hideLoading() { - chart.hideLoading(); - isLoading = false; - remove(busyIndicator); - } - - protected boolean onXAxisSelectionChange(ChartSelectionEvent chartSelectionEvent) { - try { - long xAxisMin = chartSelectionEvent.getXAxisMinAsLong(); - long xAxisMax = chartSelectionEvent.getXAxisMaxAsLong(); - if (!isZoomed) { - isZoomed = true; - } - //Set a minute as max time zoom just as for chart - if (xAxisMax - xAxisMin > MINUTE_IN_MILLIS) { - timeRangeWithZoomProvider.setTimeZoom(new Date(xAxisMin), new Date(xAxisMax), this); - } else { - return false; - } - } catch (Exception e) { - Scheduler.get().scheduleDeferred(new ScheduledCommand() { - @Override - public void execute() { - // in case the user clicks the "reset zoom" button chartSelectionEvent.getXAxisMinAsLong() throws in exception - isZoomed = false; - timeRangeWithZoomProvider.resetTimeZoom(); - // redraw is triggered by the call to onTimeZoomReset() and therefore not necessary again here - // after the selection change event, another click event is sent with the mouse position on the "Reset Zoom" button; ignore that - ignoreNextClickEvent = true; - } - }); - } - return true; - } - - protected boolean onClick(ChartClickEvent chartClickEvent) { - if (ignoreNextClickEvent) { - ignoreNextClickEvent = false; - } else { - if (!isLoading) { - timer.setPlayMode(PlayModes.Replay); - timer.setTime(chartClickEvent.getXAxisValueAsLong()); - } - } - return true; - } - - protected void changeMinMaxAndExtremesInterval(Date minTimepoint, Date maxTimepoint, boolean redraw) { - if (chart != null) { - XAxis xAxis = chart.getXAxis(); - if (minTimepoint != null && maxTimepoint != null) { - xAxis.setExtremes(minTimepoint.getTime(), maxTimepoint.getTime(), /* redraw */ false, false); - long tickInterval = (maxTimepoint.getTime() - minTimepoint.getTime()) / TICKCOUNT; - TimeTicksCalculator calculator = new TimeTicksCalculator(); - NormalizedInterval normalizedInterval = calculator.normalizeTimeTickInterval(tickInterval); - xAxis.setTickInterval(normalizedInterval.count * normalizedInterval.unitRange); - } - if (minTimepoint != null) { - xAxis.setMin(minTimepoint.getTime()); - } - if (maxTimepoint != null) { - xAxis.setMax(maxTimepoint.getTime()); - } - if (redraw) { - chart.redraw(); - } - } - } - - protected void setSeriesPoints(Series series, Point[] points) { - if (timeRangeWithZoomProvider.isZoomed()) { - com.sap.sse.common.Util.Pair timeZoom = timeRangeWithZoomProvider.getTimeZoom(); - resetMinMaxAndExtremesInterval(/* redraw */ false); - series.setPoints(points, false); - changeMinMaxAndExtremesInterval(timeZoom.getA(), timeZoom.getB(), /* redraw */ false); - } else { - series.setPoints(points, false); - } - } - - protected void resetMinMaxAndExtremesInterval(boolean redraw) { - changeMinMaxAndExtremesInterval(timeRangeWithZoomProvider.getFromTime(), timeRangeWithZoomProvider.getToTime(), redraw); - } - - @Override - public void onTimeZoomChanged(Date zoomStartTimepoint, Date zoomEndTimepoint) { - changeMinMaxAndExtremesInterval(zoomStartTimepoint, zoomEndTimepoint, true); - chart.showResetZoom(); // Patched method - } - - @Override - public void onTimeRangeChanged(Date fromTime, Date toTime) { - if (!(isZoomed && timer.getPlayMode() == PlayModes.Live)) { - resetMinMaxAndExtremesInterval(/* redraw */ true); - } - } - - @Override - public void onTimeZoomReset() { - resetMinMaxAndExtremesInterval(true); - } - - protected void updateTimePlotLine(Date date) { - chart.getXAxis().removePlotLine(timePlotLine); - timePlotLine.setValue(date.getTime()); - chart.getXAxis().addPlotLines(timePlotLine); - } - - public void add(Widget widget) { - rootPanel.add(widget); - } - - public boolean remove(Widget widget) { - return rootPanel.remove(widget); - } - - public WidgetCollection getChildren() { - return rootPanel.getChildren(); - } - - - /** - * Determines if a standard loading message is allowed to appear over the chart or not. - * - * @return true if the message is allowed, false otherwise. - */ - protected boolean shouldShowLoading(Long timestamp) { - return timestamp == null - || (timer.getPlayState() != PlayStates.Playing && timer.getPlayMode() != PlayModes.Live); - } -} +package com.sap.sailing.gwt.ui.client.shared.charts; + +import java.util.Date; +import java.util.Iterator; + +import org.moxieapps.gwt.highcharts.client.Chart; +import org.moxieapps.gwt.highcharts.client.PlotLine; +import org.moxieapps.gwt.highcharts.client.Point; +import org.moxieapps.gwt.highcharts.client.Series; +import org.moxieapps.gwt.highcharts.client.XAxis; +import org.moxieapps.gwt.highcharts.client.events.ChartClickEvent; +import org.moxieapps.gwt.highcharts.client.events.ChartSelectionEvent; + +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.i18n.client.DateTimeFormat; +import com.google.gwt.user.client.ui.AbsolutePanel; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.SimplePanel; +import com.google.gwt.user.client.ui.Widget; +import com.google.gwt.user.client.ui.WidgetCollection; +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.client.shared.charts.ChartCssResources.ChartsCss; +import com.sap.sse.common.settings.Settings; +import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.async.AsyncActionsExecutor; +import com.sap.sse.gwt.client.controls.busyindicator.SimpleBusyIndicator; +import com.sap.sse.gwt.client.controls.slider.TimeTicksCalculator; +import com.sap.sse.gwt.client.controls.slider.TimeTicksCalculator.NormalizedInterval; +import com.sap.sse.gwt.client.player.TimeListener; +import com.sap.sse.gwt.client.player.TimeRangeChangeListener; +import com.sap.sse.gwt.client.player.TimeRangeWithZoomProvider; +import com.sap.sse.gwt.client.player.TimeZoomChangeListener; +import com.sap.sse.gwt.client.player.Timer; +import com.sap.sse.gwt.client.player.Timer.PlayModes; +import com.sap.sse.gwt.client.player.Timer.PlayStates; +import com.sap.sse.gwt.client.shared.components.AbstractCompositeComponent; +import com.sap.sse.gwt.client.shared.components.Component; +import com.sap.sse.gwt.client.shared.components.SettingsDialog; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; + +public abstract class AbstractRaceChart extends AbstractCompositeComponent implements TimeListener, TimeZoomChangeListener, TimeRangeChangeListener { + /** + * Used as the turboThreshold for the Highcharts series; this is basically the maximum number of points in a series + * to be displayed. Default is 1000. See also bug 1742. + */ + protected static final int MAX_SERIES_POINTS = 1000000; + + public static class ExposedAbsolutePanel extends AbsolutePanel { + + public WidgetCollection getChildren() { + return super.getChildren(); + } + + } + + private ExposedAbsolutePanel rootPanel = new ExposedAbsolutePanel(); + + protected Chart chart; + protected PlotLine timePlotLine; + + protected final Timer timer; + protected final TimeRangeWithZoomProvider timeRangeWithZoomProvider; + + protected final RegattaAndRaceIdentifier selectedRaceIdentifier; + + protected final DateTimeFormat dateFormat = DateTimeFormat.getFormat("HH:mm:ss"); + protected final DateTimeFormat dateFormatHoursMinutes = DateTimeFormat.getFormat("HH:mm"); + + protected final StringMessages stringMessages; + protected final ErrorReporter errorReporter; + protected final AsyncActionsExecutor asyncActionsExecutor; + protected final SailingServiceAsync sailingService; + + protected boolean isLoading = false; + protected boolean isZoomed = false; + + protected static ChartsCss chartsCss = ChartCssResources.INSTANCE.css(); + + /** the tick count must be the same as TimeSlider.TICKCOUNT, otherwise the time ticks will be not synchronized */ + private final int TICKCOUNT = 10; + + public static final long MINUTE_IN_MILLIS = 60 * 1000; + + private boolean ignoreNextClickEvent; + + private final SimpleBusyIndicator busyIndicator; + + private final Button settingsButton; + + protected AbstractRaceChart(Component parent, ComponentContext context, SailingServiceAsync sailingService, + RegattaAndRaceIdentifier selectedRaceIdentifier, Timer timer, + TimeRangeWithZoomProvider timeRangeWithZoomProvider, + final StringMessages stringMessages, AsyncActionsExecutor asyncActionsExecutor, ErrorReporter errorReporter) { + super(parent, context); + this.sailingService = sailingService; + this.selectedRaceIdentifier = selectedRaceIdentifier; + this.timer = timer; + this.timeRangeWithZoomProvider = timeRangeWithZoomProvider; + this.stringMessages = stringMessages; + this.asyncActionsExecutor = asyncActionsExecutor; + this.errorReporter = errorReporter; + timer.addTimeListener(this); + timeRangeWithZoomProvider.addTimeZoomChangeListener(this); + timeRangeWithZoomProvider.addTimeRangeChangeListener(this); + chartsCss.ensureInjected(); + busyIndicator = new SimpleBusyIndicator(/* busy */ true, 2.0f); + busyIndicator.setPanelStyleClass(chartsCss.busyIndicatorStyle()); + busyIndicator.setImageStyleClass(chartsCss.busyIndicatorImageStyle()); + settingsButton = createSettingsButton(); + settingsButton.setStyleName(chartsCss.settingsButtonStyle()); + settingsButton.addStyleName(chartsCss.settingsButtonBackgroundImage()); + add(settingsButton); + initWidget(rootPanel); + getElement().getStyle().setMarginRight(12, Unit.PX); + getElement().getStyle().setMarginLeft(12, Unit.PX); + } + + /** + * Subclasses implement this, e.g., by calling {@link SettingsDialog#createSettingsButton(com.sap.sse.gwt.client.shared.components.Component, StringMessages)}. + * This class's constructor will add the {@link ChartsCss#settingsButtonStyle()} and the {@link ChartsCss#settingsButtonBackgroundImage()}. + */ + protected abstract Button createSettingsButton(); + + /** + * Subclasses need to provide a settings button which will be displayed at a useful position in the layout of this + * complex panel, e.g., in the top right corner. + */ + private Button getSettingsButton() { + return settingsButton; + } + + /** + * Simulates a {@link SimplePanel} behavior by replacing all widgets but the {@link #getSettingsButton() settings button} which is always + * supposed to be visible. If widget is already a child of this panel, it is left unchanged, and all other widgets except for + * the settings button are removed. + */ + protected void setWidget(Widget widget) { + Button settingsButton = getSettingsButton(); + boolean foundWidget = false; + for (Iterator i=rootPanel.getChildren().iterator(); i.hasNext(); ) { + Widget child = i.next(); + if (child == widget) { + foundWidget = true; + } else if (child != settingsButton) { + i.remove(); + } + } + if (!foundWidget) { + add(widget); + } + } + + protected void showLoading(String message) { + if (chart.isRendered()) { + chart.showLoading(message); + } else { + add(busyIndicator); + } + isLoading = true; + } + + protected void hideLoading() { + chart.hideLoading(); + isLoading = false; + remove(busyIndicator); + } + + protected boolean onXAxisSelectionChange(ChartSelectionEvent chartSelectionEvent) { + try { + long xAxisMin = chartSelectionEvent.getXAxisMinAsLong(); + long xAxisMax = chartSelectionEvent.getXAxisMaxAsLong(); + if (!isZoomed) { + isZoomed = true; + } + //Set a minute as max time zoom just as for chart + if (xAxisMax - xAxisMin > MINUTE_IN_MILLIS) { + timeRangeWithZoomProvider.setTimeZoom(new Date(xAxisMin), new Date(xAxisMax), this); + } else { + return false; + } + } catch (Exception e) { + Scheduler.get().scheduleDeferred(new ScheduledCommand() { + @Override + public void execute() { + // in case the user clicks the "reset zoom" button chartSelectionEvent.getXAxisMinAsLong() throws in exception + isZoomed = false; + timeRangeWithZoomProvider.resetTimeZoom(); + // redraw is triggered by the call to onTimeZoomReset() and therefore not necessary again here + // after the selection change event, another click event is sent with the mouse position on the "Reset Zoom" button; ignore that + ignoreNextClickEvent = true; + } + }); + } + return true; + } + + protected boolean onClick(ChartClickEvent chartClickEvent) { + if (ignoreNextClickEvent) { + ignoreNextClickEvent = false; + } else { + if (!isLoading) { + timer.setPlayMode(PlayModes.Replay); + timer.setTime(chartClickEvent.getXAxisValueAsLong()); + } + } + return true; + } + + protected void changeMinMaxAndExtremesInterval(Date minTimepoint, Date maxTimepoint, boolean redraw) { + if (chart != null) { + XAxis xAxis = chart.getXAxis(); + if (minTimepoint != null && maxTimepoint != null) { + xAxis.setExtremes(minTimepoint.getTime(), maxTimepoint.getTime(), /* redraw */ false, false); + long tickInterval = (maxTimepoint.getTime() - minTimepoint.getTime()) / TICKCOUNT; + TimeTicksCalculator calculator = new TimeTicksCalculator(); + NormalizedInterval normalizedInterval = calculator.normalizeTimeTickInterval(tickInterval); + xAxis.setTickInterval(normalizedInterval.count * normalizedInterval.unitRange); + } + if (minTimepoint != null) { + xAxis.setMin(minTimepoint.getTime()); + } + if (maxTimepoint != null) { + xAxis.setMax(maxTimepoint.getTime()); + } + if (redraw) { + chart.redraw(); + } + } + } + + protected void setSeriesPoints(Series series, Point[] points) { + if (timeRangeWithZoomProvider.isZoomed()) { + com.sap.sse.common.Util.Pair timeZoom = timeRangeWithZoomProvider.getTimeZoom(); + resetMinMaxAndExtremesInterval(/* redraw */ false); + series.setPoints(points, false); + changeMinMaxAndExtremesInterval(timeZoom.getA(), timeZoom.getB(), /* redraw */ false); + } else { + series.setPoints(points, false); + } + } + + protected void resetMinMaxAndExtremesInterval(boolean redraw) { + changeMinMaxAndExtremesInterval(timeRangeWithZoomProvider.getFromTime(), timeRangeWithZoomProvider.getToTime(), redraw); + } + + @Override + public void onTimeZoomChanged(Date zoomStartTimepoint, Date zoomEndTimepoint) { + changeMinMaxAndExtremesInterval(zoomStartTimepoint, zoomEndTimepoint, true); + chart.showResetZoom(); // Patched method + } + + @Override + public void onTimeRangeChanged(Date fromTime, Date toTime) { + if (!(isZoomed && timer.getPlayMode() == PlayModes.Live)) { + resetMinMaxAndExtremesInterval(/* redraw */ true); + } + } + + @Override + public void onTimeZoomReset() { + resetMinMaxAndExtremesInterval(true); + } + + protected void updateTimePlotLine(Date date) { + chart.getXAxis().removePlotLine(timePlotLine); + timePlotLine.setValue(date.getTime()); + chart.getXAxis().addPlotLines(timePlotLine); + } + + public void add(Widget widget) { + rootPanel.add(widget); + } + + public boolean remove(Widget widget) { + return rootPanel.remove(widget); + } + + public WidgetCollection getChildren() { + return rootPanel.getChildren(); + } + + + /** + * Determines if a standard loading message is allowed to appear over the chart or not. + * + * @return true if the message is allowed, false otherwise. + */ + protected boolean shouldShowLoading(Long timestamp) { + return timestamp == null + || (timer.getPlayState() != PlayStates.Playing && timer.getPlayMode() != PlayModes.Live); + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/EditMarkPassingsPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/EditMarkPassingsPanel.java index 783a352a5dc..75cfe81b520 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/EditMarkPassingsPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/EditMarkPassingsPanel.java @@ -1,299 +1,299 @@ -package com.sap.sailing.gwt.ui.client.shared.charts; - -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; - -import com.google.gwt.cell.client.AbstractCell; -import com.google.gwt.event.dom.client.ClickEvent; -import com.google.gwt.event.dom.client.ClickHandler; -import com.google.gwt.i18n.client.DateTimeFormat; -import com.google.gwt.safehtml.shared.SafeHtml; -import com.google.gwt.safehtml.shared.SafeHtmlBuilder; -import com.google.gwt.user.cellview.client.CellTable; -import com.google.gwt.user.cellview.client.Column; -import com.google.gwt.user.client.Window; -import com.google.gwt.user.client.rpc.AsyncCallback; -import com.google.gwt.user.client.ui.AbsolutePanel; -import com.google.gwt.user.client.ui.Button; -import com.google.gwt.user.client.ui.HorizontalPanel; -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.google.gwt.view.client.ListDataProvider; -import com.google.gwt.view.client.SelectionChangeEvent; -import com.google.gwt.view.client.SelectionChangeEvent.Handler; -import com.google.gwt.view.client.SingleSelectionModel; -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.CompetitorSelectionChangeListener; -import com.sap.sailing.gwt.ui.client.CompetitorSelectionProvider; -import com.sap.sailing.gwt.ui.client.SailingServiceAsync; -import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sailing.gwt.ui.client.shared.charts.RaceIdentifierToLeaderboardRaceColumnAndFleetMapper.LeaderboardNameRaceColumnNameAndFleetName; -import com.sap.sailing.gwt.ui.shared.RaceCourseDTO; -import com.sap.sailing.gwt.ui.shared.WaypointDTO; -import com.sap.sse.common.Util; -import com.sap.sse.common.Util.Pair; -import com.sap.sse.common.filter.Filter; -import com.sap.sse.common.filter.FilterSet; -import com.sap.sse.common.settings.AbstractSettings; -import com.sap.sse.gwt.client.ErrorReporter; -import com.sap.sse.gwt.client.celltable.BaseCelltable; -import com.sap.sse.gwt.client.player.Timer; -import com.sap.sse.gwt.client.shared.components.AbstractCompositeComponent; -import com.sap.sse.gwt.client.shared.components.Component; -import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; - -public class EditMarkPassingsPanel extends AbstractCompositeComponent implements CompetitorSelectionChangeListener { - private static class AnchorCell extends AbstractCell { - @Override - public void render(com.google.gwt.cell.client.Cell.Context context, SafeHtml safeHtml, SafeHtmlBuilder sb) { - sb.append(safeHtml); - } - } - - private final SailingServiceAsync sailingService; - private RegattaAndRaceIdentifier raceIdentifier; - private final ErrorReporter errorReporter; - private final StringMessages stringMessages; - private final CompetitorSelectionProvider competitorSelectionModel; - private final RaceIdentifierToLeaderboardRaceColumnAndFleetMapper raceIdentifierToLeaderboardRaceColumnAndFleetMapper; - - private CompetitorDTO competitor; - - /** - * The fixed mark passings; keys are zero-based waypoint indexes - */ - private Map currentCompetitorEdits = new HashMap<>(); - - private Integer zeroBasedIndexOfFirstSuppressedWaypoint; - - /** - * The waypoints table uses pairs of zero-based waypoint numbers and current passing times. - */ - private final CellTable> wayPointSelectionTable; - private final ListDataProvider> waypointList; - private final SingleSelectionModel> waypointSelectionModel; - private List currentWaypoints; - - private final Button setTimeAsMarkPassingsButton; - private final Button removeFixedMarkPassingsButton; - private final Button suppressPassingsButton; - private final Button removeSuppressedPassingButton; - private Label selectCompetitorLabel = new Label(); - - public EditMarkPassingsPanel(Component parent, ComponentContext context, - final SailingServiceAsync sailingService, - final RegattaAndRaceIdentifier raceIdentifier, - final StringMessages stringMessages, final CompetitorSelectionProvider competitorSelectionModel, - final ErrorReporter errorReporter, final Timer timer) { - super(parent, context); - this.raceIdentifierToLeaderboardRaceColumnAndFleetMapper = new RaceIdentifierToLeaderboardRaceColumnAndFleetMapper(); - this.sailingService = sailingService; - this.raceIdentifier = raceIdentifier; - this.errorReporter = errorReporter; - this.competitorSelectionModel = competitorSelectionModel; - this.stringMessages = stringMessages; - competitorSelectionModel.addCompetitorSelectionChangeListener(this); - - // Waypoint list - currentWaypoints = new ArrayList<>(); - waypointList = new ListDataProvider<>(); - waypointSelectionModel = new SingleSelectionModel>(); - waypointSelectionModel.addSelectionChangeHandler(new Handler() { - @Override - public void onSelectionChange(SelectionChangeEvent event) { - Pair selectedObject = waypointSelectionModel.getSelectedObject(); - if (selectedObject != null) { - Date timePoint = selectedObject.getB(); - if (timePoint != null) { - timer.setTime(timePoint.getTime()); - } - } - enableButtons(); - } - }); - wayPointSelectionTable = new BaseCelltable>(); - wayPointSelectionTable.addColumn(new Column, SafeHtml>(new AnchorCell()) { - @Override - public SafeHtml getValue(final Util.Pair object) { - return new SafeHtml() { - private static final long serialVersionUID = 1L; - - @Override - public String asString() { - return currentWaypoints.get(object.getA()).getName(); - } - }; - } - }, stringMessages.waypoint()); - final DateTimeFormat timeFormat = DateTimeFormat.getFormat("HH:mm:ss"); - wayPointSelectionTable.addColumn(new Column, SafeHtml>(new AnchorCell()) { - @Override - public SafeHtml getValue(final Pair object) { - final Date date; - date = object.getB(); - return new SafeHtml() { - private static final long serialVersionUID = 1L; - - @Override - public String asString() { - // TODO this is really unclean (Problem: no calendar) - String string; - if (date != null) { - string = timeFormat.format(date); - if (currentCompetitorEdits.containsKey(object.getA())) { - string += " "+stringMessages.fixedMarkPassing(); - } - } else if (zeroBasedIndexOfFirstSuppressedWaypoint != null - && !(object.getA() < zeroBasedIndexOfFirstSuppressedWaypoint)) { - string = " "+stringMessages.suppressedMarkPassing(); - } else { - string = ""; - } - return string; - } - }; - } - }, stringMessages.markPassing()); - - waypointList.addDataDisplay(wayPointSelectionTable); - wayPointSelectionTable.setSelectionModel(waypointSelectionModel); - - // Buttons for fixing - removeFixedMarkPassingsButton = new Button(stringMessages.removeFixedPassings()); - removeFixedMarkPassingsButton.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - final LeaderboardNameRaceColumnNameAndFleetName leaderboardNameRaceColumnNameAndFleetName = - raceIdentifierToLeaderboardRaceColumnAndFleetMapper.getLeaderboardNameAndRaceColumnNameAndFleetName(raceIdentifier); - if (leaderboardNameRaceColumnNameAndFleetName != null) { - sailingService.updateFixedMarkPassing(leaderboardNameRaceColumnNameAndFleetName.getLeaderboardName(), - leaderboardNameRaceColumnNameAndFleetName.getRaceColumnName(), - leaderboardNameRaceColumnNameAndFleetName.getFleetName(), - waypointSelectionModel.getSelectedObject().getA(), null, competitor, new AsyncCallback() { - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError(stringMessages.errorRemovingFixedPassing(caught.getMessage())); - } - - @Override - public void onSuccess(Void result) { - refillList(); - } - }); - } - } - }); - setTimeAsMarkPassingsButton = new Button(stringMessages.setFixedPassing()); - setTimeAsMarkPassingsButton.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - final LeaderboardNameRaceColumnNameAndFleetName leaderboardNameRaceColumnNameAndFleetName = - raceIdentifierToLeaderboardRaceColumnAndFleetMapper.getLeaderboardNameAndRaceColumnNameAndFleetName(raceIdentifier); - if (leaderboardNameRaceColumnNameAndFleetName != null) { +package com.sap.sailing.gwt.ui.client.shared.charts; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import com.google.gwt.cell.client.AbstractCell; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.i18n.client.DateTimeFormat; +import com.google.gwt.safehtml.shared.SafeHtml; +import com.google.gwt.safehtml.shared.SafeHtmlBuilder; +import com.google.gwt.user.cellview.client.CellTable; +import com.google.gwt.user.cellview.client.Column; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.gwt.user.client.ui.AbsolutePanel; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.HorizontalPanel; +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.google.gwt.view.client.ListDataProvider; +import com.google.gwt.view.client.SelectionChangeEvent; +import com.google.gwt.view.client.SelectionChangeEvent.Handler; +import com.google.gwt.view.client.SingleSelectionModel; +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.CompetitorSelectionChangeListener; +import com.sap.sailing.gwt.ui.client.CompetitorSelectionProvider; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.client.shared.charts.RaceIdentifierToLeaderboardRaceColumnAndFleetMapper.LeaderboardNameRaceColumnNameAndFleetName; +import com.sap.sailing.gwt.ui.shared.RaceCourseDTO; +import com.sap.sailing.gwt.ui.shared.WaypointDTO; +import com.sap.sse.common.Util; +import com.sap.sse.common.Util.Pair; +import com.sap.sse.common.filter.Filter; +import com.sap.sse.common.filter.FilterSet; +import com.sap.sse.common.settings.AbstractSettings; +import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.celltable.BaseCelltable; +import com.sap.sse.gwt.client.player.Timer; +import com.sap.sse.gwt.client.shared.components.AbstractCompositeComponent; +import com.sap.sse.gwt.client.shared.components.Component; +import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; + +public class EditMarkPassingsPanel extends AbstractCompositeComponent implements CompetitorSelectionChangeListener { + private static class AnchorCell extends AbstractCell { + @Override + public void render(com.google.gwt.cell.client.Cell.Context context, SafeHtml safeHtml, SafeHtmlBuilder sb) { + sb.append(safeHtml); + } + } + + private final SailingServiceAsync sailingService; + private RegattaAndRaceIdentifier raceIdentifier; + private final ErrorReporter errorReporter; + private final StringMessages stringMessages; + private final CompetitorSelectionProvider competitorSelectionModel; + private final RaceIdentifierToLeaderboardRaceColumnAndFleetMapper raceIdentifierToLeaderboardRaceColumnAndFleetMapper; + + private CompetitorDTO competitor; + + /** + * The fixed mark passings; keys are zero-based waypoint indexes + */ + private Map currentCompetitorEdits = new HashMap<>(); + + private Integer zeroBasedIndexOfFirstSuppressedWaypoint; + + /** + * The waypoints table uses pairs of zero-based waypoint numbers and current passing times. + */ + private final CellTable> wayPointSelectionTable; + private final ListDataProvider> waypointList; + private final SingleSelectionModel> waypointSelectionModel; + private List currentWaypoints; + + private final Button setTimeAsMarkPassingsButton; + private final Button removeFixedMarkPassingsButton; + private final Button suppressPassingsButton; + private final Button removeSuppressedPassingButton; + private Label selectCompetitorLabel = new Label(); + + public EditMarkPassingsPanel(Component parent, ComponentContext context, + final SailingServiceAsync sailingService, + final RegattaAndRaceIdentifier raceIdentifier, + final StringMessages stringMessages, final CompetitorSelectionProvider competitorSelectionModel, + final ErrorReporter errorReporter, final Timer timer) { + super(parent, context); + this.raceIdentifierToLeaderboardRaceColumnAndFleetMapper = new RaceIdentifierToLeaderboardRaceColumnAndFleetMapper(); + this.sailingService = sailingService; + this.raceIdentifier = raceIdentifier; + this.errorReporter = errorReporter; + this.competitorSelectionModel = competitorSelectionModel; + this.stringMessages = stringMessages; + competitorSelectionModel.addCompetitorSelectionChangeListener(this); + + // Waypoint list + currentWaypoints = new ArrayList<>(); + waypointList = new ListDataProvider<>(); + waypointSelectionModel = new SingleSelectionModel>(); + waypointSelectionModel.addSelectionChangeHandler(new Handler() { + @Override + public void onSelectionChange(SelectionChangeEvent event) { + Pair selectedObject = waypointSelectionModel.getSelectedObject(); + if (selectedObject != null) { + Date timePoint = selectedObject.getB(); + if (timePoint != null) { + timer.setTime(timePoint.getTime()); + } + } + enableButtons(); + } + }); + wayPointSelectionTable = new BaseCelltable>(); + wayPointSelectionTable.addColumn(new Column, SafeHtml>(new AnchorCell()) { + @Override + public SafeHtml getValue(final Util.Pair object) { + return new SafeHtml() { + private static final long serialVersionUID = 1L; + + @Override + public String asString() { + return currentWaypoints.get(object.getA()).getName(); + } + }; + } + }, stringMessages.waypoint()); + final DateTimeFormat timeFormat = DateTimeFormat.getFormat("HH:mm:ss"); + wayPointSelectionTable.addColumn(new Column, SafeHtml>(new AnchorCell()) { + @Override + public SafeHtml getValue(final Pair object) { + final Date date; + date = object.getB(); + return new SafeHtml() { + private static final long serialVersionUID = 1L; + + @Override + public String asString() { + // TODO this is really unclean (Problem: no calendar) + String string; + if (date != null) { + string = timeFormat.format(date); + if (currentCompetitorEdits.containsKey(object.getA())) { + string += " "+stringMessages.fixedMarkPassing(); + } + } else if (zeroBasedIndexOfFirstSuppressedWaypoint != null + && !(object.getA() < zeroBasedIndexOfFirstSuppressedWaypoint)) { + string = " "+stringMessages.suppressedMarkPassing(); + } else { + string = ""; + } + return string; + } + }; + } + }, stringMessages.markPassing()); + + waypointList.addDataDisplay(wayPointSelectionTable); + wayPointSelectionTable.setSelectionModel(waypointSelectionModel); + + // Buttons for fixing + removeFixedMarkPassingsButton = new Button(stringMessages.removeFixedPassings()); + removeFixedMarkPassingsButton.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + final LeaderboardNameRaceColumnNameAndFleetName leaderboardNameRaceColumnNameAndFleetName = + raceIdentifierToLeaderboardRaceColumnAndFleetMapper.getLeaderboardNameAndRaceColumnNameAndFleetName(raceIdentifier); + if (leaderboardNameRaceColumnNameAndFleetName != null) { + sailingService.updateFixedMarkPassing(leaderboardNameRaceColumnNameAndFleetName.getLeaderboardName(), + leaderboardNameRaceColumnNameAndFleetName.getRaceColumnName(), + leaderboardNameRaceColumnNameAndFleetName.getFleetName(), + waypointSelectionModel.getSelectedObject().getA(), null, competitor, new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError(stringMessages.errorRemovingFixedPassing(caught.getMessage())); + } + + @Override + public void onSuccess(Void result) { + refillList(); + } + }); + } + } + }); + setTimeAsMarkPassingsButton = new Button(stringMessages.setFixedPassing()); + setTimeAsMarkPassingsButton.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + final LeaderboardNameRaceColumnNameAndFleetName leaderboardNameRaceColumnNameAndFleetName = + raceIdentifierToLeaderboardRaceColumnAndFleetMapper.getLeaderboardNameAndRaceColumnNameAndFleetName(raceIdentifier); + if (leaderboardNameRaceColumnNameAndFleetName != null) { final Integer waypoint = waypointSelectionModel.getSelectedObject().getA(); if (isSettingFixedTimePossible(timer, stringMessages)) { final Date time = timer.getTime(); - sailingService.updateFixedMarkPassing(leaderboardNameRaceColumnNameAndFleetName.getLeaderboardName(), - leaderboardNameRaceColumnNameAndFleetName.getRaceColumnName(), + sailingService.updateFixedMarkPassing(leaderboardNameRaceColumnNameAndFleetName.getLeaderboardName(), + leaderboardNameRaceColumnNameAndFleetName.getRaceColumnName(), leaderboardNameRaceColumnNameAndFleetName.getFleetName(), waypoint, time, competitor, - new AsyncCallback() { - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError(stringMessages.errorSettingFixedPassing(caught.getMessage())); - } - - @Override - public void onSuccess(Void result) { - refillList(); - } - }); + new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError(stringMessages.errorSettingFixedPassing(caught.getMessage())); + } + + @Override + public void onSuccess(Void result) { + refillList(); + } + }); } else { Window.alert(stringMessages.warningSettingFixedPassing(currentWaypoints.get(waypoint).getName())); - } - } + } } - }); - - // Button for suppressing - suppressPassingsButton = new Button(stringMessages.setSuppressedPassing()); - suppressPassingsButton.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - final LeaderboardNameRaceColumnNameAndFleetName leaderboardNameRaceColumnNameAndFleetName = - raceIdentifierToLeaderboardRaceColumnAndFleetMapper.getLeaderboardNameAndRaceColumnNameAndFleetName(raceIdentifier); - if (leaderboardNameRaceColumnNameAndFleetName != null) { - sailingService.updateSuppressedMarkPassings(leaderboardNameRaceColumnNameAndFleetName.getLeaderboardName(), - leaderboardNameRaceColumnNameAndFleetName.getRaceColumnName(), - leaderboardNameRaceColumnNameAndFleetName.getFleetName(), - waypointSelectionModel.getSelectedObject().getA(), - competitor, new AsyncCallback() { - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError(stringMessages.errorSuppressingPassing(caught.getMessage())); - } - - @Override - public void onSuccess(Void result) { - refillList(); - } - }); - } - } - }); - - removeSuppressedPassingButton = new Button(stringMessages.removeSuppressedPassing()); - removeSuppressedPassingButton.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - final LeaderboardNameRaceColumnNameAndFleetName leaderboardNameRaceColumnNameAndFleetName = - raceIdentifierToLeaderboardRaceColumnAndFleetMapper.getLeaderboardNameAndRaceColumnNameAndFleetName(raceIdentifier); - if (leaderboardNameRaceColumnNameAndFleetName != null) { - sailingService.updateSuppressedMarkPassings(leaderboardNameRaceColumnNameAndFleetName.getLeaderboardName(), - leaderboardNameRaceColumnNameAndFleetName.getRaceColumnName(), - leaderboardNameRaceColumnNameAndFleetName.getFleetName(), - null, competitor, - new AsyncCallback() { - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError(stringMessages.errorRemovingSupressedPassing(caught.getMessage())); - } - - @Override - public void onSuccess(Void result) { - refillList(); - } - }); - } - } - }); - selectCompetitorLabel.setText(stringMessages.selectCompetitor()); - refreshWaypoints(); - AbsolutePanel rootPanel = new AbsolutePanel(); - HorizontalPanel tableAndButtons = new HorizontalPanel(); - rootPanel.add(tableAndButtons, 0, 0); - tableAndButtons.setSpacing(3); - tableAndButtons.add(wayPointSelectionTable); - VerticalPanel buttonPanel = new VerticalPanel(); - buttonPanel.setSpacing(3); - tableAndButtons.add(buttonPanel); - buttonPanel.add(setTimeAsMarkPassingsButton); - buttonPanel.add(removeFixedMarkPassingsButton); - buttonPanel.add(suppressPassingsButton); - buttonPanel.add(removeSuppressedPassingButton); - buttonPanel.add(selectCompetitorLabel); - enableButtons(); - initWidget(rootPanel); - setVisible(false); - } - + } + }); + + // Button for suppressing + suppressPassingsButton = new Button(stringMessages.setSuppressedPassing()); + suppressPassingsButton.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + final LeaderboardNameRaceColumnNameAndFleetName leaderboardNameRaceColumnNameAndFleetName = + raceIdentifierToLeaderboardRaceColumnAndFleetMapper.getLeaderboardNameAndRaceColumnNameAndFleetName(raceIdentifier); + if (leaderboardNameRaceColumnNameAndFleetName != null) { + sailingService.updateSuppressedMarkPassings(leaderboardNameRaceColumnNameAndFleetName.getLeaderboardName(), + leaderboardNameRaceColumnNameAndFleetName.getRaceColumnName(), + leaderboardNameRaceColumnNameAndFleetName.getFleetName(), + waypointSelectionModel.getSelectedObject().getA(), + competitor, new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError(stringMessages.errorSuppressingPassing(caught.getMessage())); + } + + @Override + public void onSuccess(Void result) { + refillList(); + } + }); + } + } + }); + + removeSuppressedPassingButton = new Button(stringMessages.removeSuppressedPassing()); + removeSuppressedPassingButton.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + final LeaderboardNameRaceColumnNameAndFleetName leaderboardNameRaceColumnNameAndFleetName = + raceIdentifierToLeaderboardRaceColumnAndFleetMapper.getLeaderboardNameAndRaceColumnNameAndFleetName(raceIdentifier); + if (leaderboardNameRaceColumnNameAndFleetName != null) { + sailingService.updateSuppressedMarkPassings(leaderboardNameRaceColumnNameAndFleetName.getLeaderboardName(), + leaderboardNameRaceColumnNameAndFleetName.getRaceColumnName(), + leaderboardNameRaceColumnNameAndFleetName.getFleetName(), + null, competitor, + new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError(stringMessages.errorRemovingSupressedPassing(caught.getMessage())); + } + + @Override + public void onSuccess(Void result) { + refillList(); + } + }); + } + } + }); + selectCompetitorLabel.setText(stringMessages.selectCompetitor()); + refreshWaypoints(); + AbsolutePanel rootPanel = new AbsolutePanel(); + HorizontalPanel tableAndButtons = new HorizontalPanel(); + rootPanel.add(tableAndButtons, 0, 0); + tableAndButtons.setSpacing(3); + tableAndButtons.add(wayPointSelectionTable); + VerticalPanel buttonPanel = new VerticalPanel(); + buttonPanel.setSpacing(3); + tableAndButtons.add(buttonPanel); + buttonPanel.add(setTimeAsMarkPassingsButton); + buttonPanel.add(removeFixedMarkPassingsButton); + buttonPanel.add(suppressPassingsButton); + buttonPanel.add(removeSuppressedPassingButton); + buttonPanel.add(selectCompetitorLabel); + enableButtons(); + initWidget(rootPanel); + setVisible(false); + } + /** * Checks the possibility of setting a new fixed time for selected waypoint * @@ -312,193 +312,193 @@ public class EditMarkPassingsPanel extends AbstractCompositeComponent>() { - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError(stringMessages.errorTryingToObtainMarkPassing(caught.getMessage())); - } - - @Override - public void onSuccess(Map result) { - List> newMarkPassings = new ArrayList<>(); - for (WaypointDTO waypoint : currentWaypoints) { - int index = currentWaypoints.indexOf(waypoint); - newMarkPassings.add(new Util.Pair(index, result.get(index))); - } - waypointList.getList().clear(); - waypointList.getList().addAll(newMarkPassings); - // Get current edits - - final LeaderboardNameRaceColumnNameAndFleetName leaderboardNameRaceColumnNameAndFleetName = - raceIdentifierToLeaderboardRaceColumnAndFleetMapper.getLeaderboardNameAndRaceColumnNameAndFleetName(raceIdentifier); - if (leaderboardNameRaceColumnNameAndFleetName != null) { - sailingService.getCompetitorRaceLogMarkPassingData(leaderboardNameRaceColumnNameAndFleetName.getLeaderboardName(), - leaderboardNameRaceColumnNameAndFleetName.getRaceColumnName(), - leaderboardNameRaceColumnNameAndFleetName.getFleetName(), competitor, new AsyncCallback>() { - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError(stringMessages.errorTryingToObtainRaceLogMarkPassingData(caught.getMessage())); - } - - @Override - public void onSuccess(Map result) { - for (Entry data : result.entrySet()) { - if (data.getValue() == null) { - zeroBasedIndexOfFirstSuppressedWaypoint = data.getKey(); - } else { - currentCompetitorEdits.put(data.getKey(), data.getValue()); - } - } - enableButtons(); - wayPointSelectionTable.redraw(); - } - }); - } - } - }); - } - - /** - * Removes all competitor selection-specific data, particularly the fixed and suppressed mark - * passings and the reference to the competitor selected. - */ - private void clearInfo() { - currentCompetitorEdits.clear(); - competitor = null; - zeroBasedIndexOfFirstSuppressedWaypoint = null; - } - - private void refreshWaypoints() { - sailingService.getRaceCourse(raceIdentifier, new Date(), new AsyncCallback() { - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError(stringMessages.errorTryingToObtainRaceCourse(caught.getMessage())); - } - - @Override - public void onSuccess(RaceCourseDTO result) { - currentWaypoints = result.waypoints; - } - }); - } - - private void enableButtons() { - setTimeAsMarkPassingsButton.setEnabled(false); - removeFixedMarkPassingsButton.setEnabled(false); - suppressPassingsButton.setEnabled(false); - removeSuppressedPassingButton.setEnabled(false); - if (Util.size(competitorSelectionModel.getSelectedCompetitors()) == 1) { - if (zeroBasedIndexOfFirstSuppressedWaypoint != null) { - removeSuppressedPassingButton.setEnabled(true); - } - Pair selectedWaypoint = waypointSelectionModel.getSelectedObject(); - if (selectedWaypoint != null) { - setTimeAsMarkPassingsButton.setEnabled(true); - suppressPassingsButton.setEnabled(true); - if (currentCompetitorEdits.containsKey(selectedWaypoint.getA())) { - removeFixedMarkPassingsButton.setEnabled(true); - } - } - } - } - - public void setLeaderboard(LeaderboardDTO leaderboard) { - this.raceIdentifierToLeaderboardRaceColumnAndFleetMapper.setLeaderboard(leaderboard); - } - - @Override - public Widget getEntryWidget() { - return this; - } - - @Override - public boolean hasSettings() { - return false; - } - - @Override - public SettingsDialogComponent getSettingsDialogComponent() { - return null; - } - - @Override - public void updateSettings(AbstractSettings newSettings) { - } - - @Override - public String getLocalizedShortName() { - return null; - } - - @Override - public String getDependentCssClassName() { - return null; - } - - @Override - public void filterChanged(FilterSet> oldFilterSet, - FilterSet> newFilterSet) { - } - - @Override - public void competitorsListChanged(Iterable competitors) { - } - - @Override - public void filteredCompetitorsListChanged(Iterable filteredCompetitors) { - } - - @Override - public AbstractSettings getSettings() { - return null; - } - - @Override - public String getId() { - return "EditMarkPassingsPanel"; - } - -} + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError(stringMessages.errorTryingToObtainMarkPassing(caught.getMessage())); + } + + @Override + public void onSuccess(Map result) { + List> newMarkPassings = new ArrayList<>(); + for (WaypointDTO waypoint : currentWaypoints) { + int index = currentWaypoints.indexOf(waypoint); + newMarkPassings.add(new Util.Pair(index, result.get(index))); + } + waypointList.getList().clear(); + waypointList.getList().addAll(newMarkPassings); + // Get current edits + + final LeaderboardNameRaceColumnNameAndFleetName leaderboardNameRaceColumnNameAndFleetName = + raceIdentifierToLeaderboardRaceColumnAndFleetMapper.getLeaderboardNameAndRaceColumnNameAndFleetName(raceIdentifier); + if (leaderboardNameRaceColumnNameAndFleetName != null) { + sailingService.getCompetitorRaceLogMarkPassingData(leaderboardNameRaceColumnNameAndFleetName.getLeaderboardName(), + leaderboardNameRaceColumnNameAndFleetName.getRaceColumnName(), + leaderboardNameRaceColumnNameAndFleetName.getFleetName(), competitor, new AsyncCallback>() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError(stringMessages.errorTryingToObtainRaceLogMarkPassingData(caught.getMessage())); + } + + @Override + public void onSuccess(Map result) { + for (Entry data : result.entrySet()) { + if (data.getValue() == null) { + zeroBasedIndexOfFirstSuppressedWaypoint = data.getKey(); + } else { + currentCompetitorEdits.put(data.getKey(), data.getValue()); + } + } + enableButtons(); + wayPointSelectionTable.redraw(); + } + }); + } + } + }); + } + + /** + * Removes all competitor selection-specific data, particularly the fixed and suppressed mark + * passings and the reference to the competitor selected. + */ + private void clearInfo() { + currentCompetitorEdits.clear(); + competitor = null; + zeroBasedIndexOfFirstSuppressedWaypoint = null; + } + + private void refreshWaypoints() { + sailingService.getRaceCourse(raceIdentifier, new Date(), new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError(stringMessages.errorTryingToObtainRaceCourse(caught.getMessage())); + } + + @Override + public void onSuccess(RaceCourseDTO result) { + currentWaypoints = result.waypoints; + } + }); + } + + private void enableButtons() { + setTimeAsMarkPassingsButton.setEnabled(false); + removeFixedMarkPassingsButton.setEnabled(false); + suppressPassingsButton.setEnabled(false); + removeSuppressedPassingButton.setEnabled(false); + if (Util.size(competitorSelectionModel.getSelectedCompetitors()) == 1) { + if (zeroBasedIndexOfFirstSuppressedWaypoint != null) { + removeSuppressedPassingButton.setEnabled(true); + } + Pair selectedWaypoint = waypointSelectionModel.getSelectedObject(); + if (selectedWaypoint != null) { + setTimeAsMarkPassingsButton.setEnabled(true); + suppressPassingsButton.setEnabled(true); + if (currentCompetitorEdits.containsKey(selectedWaypoint.getA())) { + removeFixedMarkPassingsButton.setEnabled(true); + } + } + } + } + + public void setLeaderboard(LeaderboardDTO leaderboard) { + this.raceIdentifierToLeaderboardRaceColumnAndFleetMapper.setLeaderboard(leaderboard); + } + + @Override + public Widget getEntryWidget() { + return this; + } + + @Override + public boolean hasSettings() { + return false; + } + + @Override + public SettingsDialogComponent getSettingsDialogComponent() { + return null; + } + + @Override + public void updateSettings(AbstractSettings newSettings) { + } + + @Override + public String getLocalizedShortName() { + return null; + } + + @Override + public String getDependentCssClassName() { + return null; + } + + @Override + public void filterChanged(FilterSet> oldFilterSet, + FilterSet> newFilterSet) { + } + + @Override + public void competitorsListChanged(Iterable competitors) { + } + + @Override + public void filteredCompetitorsListChanged(Iterable filteredCompetitors) { + } + + @Override + public AbstractSettings getSettings() { + return null; + } + + @Override + public String getId() { + return "EditMarkPassingsPanel"; + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/EditMarkPositionPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/EditMarkPositionPanel.java index 5cdcd11c212..2935e2f6f9d 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/EditMarkPositionPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/EditMarkPositionPanel.java @@ -1,1020 +1,1020 @@ -package com.sap.sailing.gwt.ui.client.shared.charts; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Comparator; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.SortedMap; -import java.util.TreeMap; - -import org.moxieapps.gwt.highcharts.client.Axis; -import org.moxieapps.gwt.highcharts.client.BaseChart; -import org.moxieapps.gwt.highcharts.client.Chart; -import org.moxieapps.gwt.highcharts.client.ChartSubtitle; -import org.moxieapps.gwt.highcharts.client.ChartTitle; -import org.moxieapps.gwt.highcharts.client.Color; -import org.moxieapps.gwt.highcharts.client.Credits; -import org.moxieapps.gwt.highcharts.client.PlotLine.DashStyle; -import org.moxieapps.gwt.highcharts.client.Point; -import org.moxieapps.gwt.highcharts.client.Series; -import org.moxieapps.gwt.highcharts.client.ToolTip; -import org.moxieapps.gwt.highcharts.client.ToolTipData; -import org.moxieapps.gwt.highcharts.client.ToolTipFormatter; -import org.moxieapps.gwt.highcharts.client.XAxis; -import org.moxieapps.gwt.highcharts.client.events.ChartClickEvent; -import org.moxieapps.gwt.highcharts.client.events.ChartClickEventHandler; -import org.moxieapps.gwt.highcharts.client.events.ChartSelectionEvent; -import org.moxieapps.gwt.highcharts.client.events.ChartSelectionEventHandler; -import org.moxieapps.gwt.highcharts.client.labels.AxisLabelsData; -import org.moxieapps.gwt.highcharts.client.labels.AxisLabelsFormatter; -import org.moxieapps.gwt.highcharts.client.labels.XAxisLabels; -import org.moxieapps.gwt.highcharts.client.plotOptions.LinePlotOptions; -import org.moxieapps.gwt.highcharts.client.plotOptions.Marker; - -import com.google.gwt.ajaxloader.client.Properties.TypeException; -import com.google.gwt.core.client.Callback; -import com.google.gwt.core.client.JavaScriptObject; -import com.google.gwt.core.client.Scheduler.ScheduledCommand; -import com.google.gwt.core.shared.GWT; -import com.google.gwt.event.shared.HandlerRegistration; -import com.google.gwt.i18n.client.NumberFormat; -import com.google.gwt.maps.client.MapOptions; -import com.google.gwt.maps.client.MapWidget; -import com.google.gwt.maps.client.base.LatLng; -import com.google.gwt.maps.client.base.LatLngBounds; -import com.google.gwt.maps.client.controls.ControlPosition; -import com.google.gwt.maps.client.events.click.ClickMapEvent; -import com.google.gwt.maps.client.events.click.ClickMapHandler; -import com.google.gwt.maps.client.events.mousedown.MouseDownMapEvent; -import com.google.gwt.maps.client.events.mousedown.MouseDownMapHandler; -import com.google.gwt.maps.client.events.mousemove.MouseMoveMapEvent; -import com.google.gwt.maps.client.events.mousemove.MouseMoveMapHandler; -import com.google.gwt.maps.client.events.mouseout.MouseOutMapEvent; -import com.google.gwt.maps.client.events.mouseout.MouseOutMapHandler; -import com.google.gwt.maps.client.events.mouseup.MouseUpMapEvent; -import com.google.gwt.maps.client.events.mouseup.MouseUpMapHandler; -import com.google.gwt.maps.client.events.resize.ResizeMapEvent; -import com.google.gwt.maps.client.events.resize.ResizeMapHandler; -import com.google.gwt.maps.client.events.rightclick.RightClickMapEvent; -import com.google.gwt.maps.client.events.rightclick.RightClickMapHandler; -import com.google.gwt.maps.client.mvc.MVCArray; -import com.google.gwt.maps.client.overlays.Polyline; -import com.google.gwt.maps.client.overlays.PolylineOptions; -import com.google.gwt.user.client.rpc.AsyncCallback; -import com.google.gwt.user.client.ui.Button; -import com.google.gwt.user.client.ui.HTMLPanel; -import com.google.gwt.user.client.ui.Label; -import com.google.gwt.user.client.ui.MenuBar; -import com.google.gwt.user.client.ui.MenuItem; -import com.google.gwt.user.client.ui.PopupPanel; -import com.google.gwt.user.client.ui.RequiresResize; -import com.google.gwt.user.client.ui.Widget; -import com.google.gwt.view.client.ListDataProvider; -import com.google.gwt.view.client.SelectionChangeEvent; -import com.sap.sailing.domain.common.FixType; -import com.sap.sailing.domain.common.Position; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sailing.domain.common.dto.LeaderboardDTO; -import com.sap.sailing.domain.common.impl.DegreePosition; -import com.sap.sailing.gwt.ui.client.SailingServiceAsync; -import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sailing.gwt.ui.client.shared.charts.MarkPositionService.MarkTrackDTO; -import com.sap.sailing.gwt.ui.client.shared.racemap.BoundsUtil; -import com.sap.sailing.gwt.ui.client.shared.racemap.CourseMarkOverlay; -import com.sap.sailing.gwt.ui.client.shared.racemap.FixOverlay; -import com.sap.sailing.gwt.ui.client.shared.racemap.RaceMap; -import com.sap.sailing.gwt.ui.leaderboard.LeaderboardPanel; -import com.sap.sailing.gwt.ui.raceboard.SideBySideComponentViewer; -import com.sap.sailing.gwt.ui.shared.GPSFixDTO; -import com.sap.sailing.gwt.ui.shared.GPSFixDTOWithSpeedWindTackAndLegType; -import com.sap.sailing.gwt.ui.shared.MarkDTO; -import com.sap.sailing.gwt.ui.shared.WindDTO; -import com.sap.sailing.gwt.ui.shared.racemap.CanvasOverlayV3; -import com.sap.sse.common.Util.Pair; -import com.sap.sse.common.settings.AbstractSettings; -import com.sap.sse.gwt.client.ErrorReporter; -import com.sap.sse.gwt.client.async.AsyncActionsExecutor; -import com.sap.sse.gwt.client.player.TimeRangeWithZoomProvider; -import com.sap.sse.gwt.client.player.Timer; -import com.sap.sse.gwt.client.shared.components.Component; -import com.sap.sse.gwt.client.shared.components.SettingsDialog; -import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; - -public class EditMarkPositionPanel extends AbstractRaceChart implements RequiresResize, SelectionChangeEvent.Handler { - protected static final int FIX_OVERLAY_Z_ORDER = 230; - private final RaceMap raceMap; - private final LeaderboardPanel leaderboardPanel; - private final MarksPanel marksPanel; - private Series markSeries; - private LinePlotOptions markSeriesPlotOptions; - private final Label noMarkSelectedLabel; - private MapWidget map; - private boolean visible; - private List courseMarkClickHandlers; - - private Map> marksFromToTimes; - private Date raceFromTime; - private Date raceToTime; - - private Map> marks; - private MarkDTO selectedMark; - private Map polylines; - private final ListDataProvider markDataProvider; - private SideBySideComponentViewer sideBySideComponentViewer; - - private FixPositionChooser currentFixPositionChooser; - private List overlayClickHandlers; - private final MarkPositionService markPositionService; - - private final RaceIdentifierToLeaderboardRaceColumnAndFleetMapper raceIdentifierToLeaderboardRaceColumnAndFleetMapper; - - public EditMarkPositionPanel(Component parent, ComponentContext context, final RaceMap raceMap, - final LeaderboardPanel leaderboardPanel, - RegattaAndRaceIdentifier selectedRaceIdentifier, String leaderboardName, final StringMessages stringMessages, - SailingServiceAsync sailingService, Timer timer, TimeRangeWithZoomProvider timeRangeWithZoomProvider, - AsyncActionsExecutor asyncActionsExecutor, ErrorReporter errorReporter) { - super(parent, context, sailingService, selectedRaceIdentifier, timer, timeRangeWithZoomProvider, stringMessages, - asyncActionsExecutor, errorReporter); - this.markPositionService = new MarkPositionServiceForSailingService(sailingService); - this.raceIdentifierToLeaderboardRaceColumnAndFleetMapper = new RaceIdentifierToLeaderboardRaceColumnAndFleetMapper(); - this.raceMap = raceMap; - this.leaderboardPanel = leaderboardPanel; - this.polylines = new HashMap<>(); - this.markDataProvider = new ListDataProvider<>(); - this.marksPanel = new MarksPanel(this, context, markDataProvider, stringMessages); - this.noMarkSelectedLabel = new Label(stringMessages.pleaseSelectAMark()); - this.noMarkSelectedLabel.setStyleName("abstractChartPanel-importantMessageOfChart"); - this.courseMarkClickHandlers = new ArrayList<>(); - this.marksFromToTimes = new HashMap<>(); - this.getEntryWidget().setTitle(stringMessages.editMarkPositions()); - this.setVisible(false); - this.overlayClickHandlers = new ArrayList<>(); - chart = new Chart() - .setPersistent(true) - .setZoomType(BaseChart.ZoomType.X) - .setMarginLeft(65) - .setMarginRight(65) - .setWidth100() - .setHeight100() - .setBorderWidth(0) - .setBorderRadius(0) - .setBackgroundColor(new Color("#FFFFFF")) - .setPlotBackgroundColor("#f8f8f8") - .setPlotBorderWidth(0) - .setCredits(new Credits().setEnabled(false)) - .setChartTitle(new ChartTitle().setText(stringMessages.markFixes())) - .setChartSubtitle(new ChartSubtitle().setText(stringMessages.clickAndDragToZoomIn())) - .setPersistent(true); - chart.setClickEventHandler(new ChartClickEventHandler() { - @Override - public boolean onClick(ChartClickEvent chartClickEvent) { - return EditMarkPositionPanel.this.onClick(chartClickEvent); - } - }); - chart.setSelectionEventHandler(new ChartSelectionEventHandler() { - @Override - public boolean onSelection(ChartSelectionEvent chartSelectionEvent) { - return EditMarkPositionPanel.this.onXAxisSelectionChange(chartSelectionEvent); - } - }); - chart.getXAxis().setType(Axis.Type.DATE_TIME) - .setMaxZoom(60 * 1000) // 1 minute - .setAxisTitleText(stringMessages.time()); - chart.getXAxis().setLabels(new XAxisLabels().setFormatter(new AxisLabelsFormatter() { - @Override - public String format(AxisLabelsData axisLabelsData) { - return dateFormatHoursMinutes.format(new Date(axisLabelsData.getValueAsLong())); - } - })); - chart.getYAxis().setAxisTitleText(stringMessages.distanceFromAveragePosition() + " (" + stringMessages.metersUnit() + ")").setOption("labels/enabled", false); - timePlotLine = chart.getXAxis().createPlotLine().setColor("#656565").setWidth(1) - .setDashStyle(DashStyle.SOLID); - markSeriesPlotOptions = new LinePlotOptions().setSelected(true).setShowInLegend(false).setLineWidth(1).setColor("#000") - .setHoverStateLineWidth(1).setEnableMouseTracking(true); - markSeries = chart.createSeries().setType(Series.Type.LINE).setYAxis(0) - .setPlotOptions(markSeriesPlotOptions).setName(stringMessages.distanceFromAveragePosition()); - chart.addSeries(markSeries, false, false); - chart.setToolTip(new ToolTip().setEnabled(true).setFormatter(new ToolTipFormatter() { - @Override - public String format(ToolTipData toolTipData) { - final String seriesName = toolTipData.getSeriesName(); - if (seriesName.equals(stringMessages.time())) { - return "" + seriesName + ": " + dateFormat.format(new Date(toolTipData.getXAsLong())) - + "
(" + stringMessages.clickChartToSetTime() + ")"; - } else { - return "" + (dateFormat.format(new Date(toolTipData.getXAsLong()))) + ": " - + NumberFormat.getFormat("0.0").format(toolTipData.getYAsDouble()) + " m"; - } - } - })); - } - - private void canRemoveMarkFix(MarkDTO mark, GPSFixDTO fix, AsyncCallback callback) { - markPositionService.canRemoveMarkFix( - raceIdentifierToLeaderboardRaceColumnAndFleetMapper.getLeaderboardNameAndRaceColumnNameAndFleetName(selectedRaceIdentifier), - mark, fix, callback); - } - - private static class Pixel extends JavaScriptObject { - protected Pixel() { - } - - private native int getX() /*-{ - return this.x; - }-*/; - private native int getY() /*-{ - return this.y; - }-*/; - } - - private class OverlayClickHandler { - private final PopupPanel popup; - private final MenuItem moveMenuItem; - - private final FixOverlay overlay; - private FixOverlay moveOverlay; - - private final MouseDownMapHandler mouseDownHandler; - private HandlerRegistration mouseDownHandlerRegistration; - private final RightClickMapHandler rightClickHandler; - private HandlerRegistration rightClickHandlerRegistration; - - private int mouseDownX; - private int mouseDownY; - private boolean mouseDown; - private boolean dragging; - - //Currently the big problem with the OverlayClickHandler is that you cannot reliably detect if users use a touch device - //and even if they don't at first they could switch from touch to mouse or the other way around. Thus, both input methods - //are always present. Touch users should not accidentally drag fixes, because there is a buffer. - public OverlayClickHandler(final MarkDTO mark, final GPSFixDTO fix, final FixOverlay overlay) { - this.overlay = overlay; - final Polyline polyline = polylines.get(mark); - - map.addResizeHandler(new ResizeMapHandler() { - @Override - public void onEvent(ResizeMapEvent event) { - onResizeMap(event); - } - }); - rightClickHandler = new RightClickMapHandler() { - @Override - public void onEvent(RightClickMapEvent event) { - onRightClick(event); - } - }; - mouseDownHandler = new MouseDownMapHandler() { - @Override - public void onEvent(MouseDownMapEvent event) { - try { - if (event.getProperties().getNumber("which") == 1) { - mouseDownX = event.getProperties().getNumber("pageX").intValue() - map.getAbsoluteLeft(); - mouseDownY = event.getProperties().getNumber("pageY").intValue() - map.getAbsoluteTop(); - mouseDown = true; - dragging = false; - MapOptions options = MapOptions.newInstance(false); - options.setDraggable(false); - map.setOptions(options); - } - } catch (TypeException e) { - GWT.log("Exception trying to obtain pageX or pageY property on event "+event, e); - } - } - }; - map.addMouseMoveHandler(new MouseMoveMapHandler() { - @Override - public void onEvent(MouseMoveMapEvent event) { - if (mouseDown) { - int currentX = 0; - int currentY = 0; - try { - Pixel pixel = event.getProperties().getObject("pixel").cast(); - currentX = pixel.getX(); - currentY = pixel.getY(); - } catch (TypeException e) { - GWT.log("Exception trying to obtain pixel property on event "+event, e); - } - final int index = getIndexOfFixInPolyline(mark, fix); - if (!dragging && Math.sqrt(Math.pow(currentX - mouseDownX, 2) + - Math.pow(currentY - mouseDownY, 2)) > 15) { - dragging = true; - overlay.setVisible(false); - setRedPoint(index); - moveOverlay = new FixOverlay(map, overlay.getZIndex(), overlay.getGPSFixDTO(), overlay.getType(), - "#f00", raceMap.getCoordinateSystem(), stringMessages.dragToChangePosition()); - moveOverlay.addMouseUpHandler(new MouseUpMapHandler() { - @Override - public void onEvent(MouseUpMapEvent event) { - editMarkFix(mark, fix, fix.position); - overlay.setVisible(true); - resetPointColor(index); - moveOverlay.removeFromMap(); - moveOverlay = null; - - MapOptions options = MapOptions.newInstance(false); - options.setDraggable(true); - map.setOptions(options); - mouseDown = false; - } - }); - } - if (dragging) { - fix.position = raceMap.getCoordinateSystem().getPosition(event.getMouseEvent().getLatLng()); - overlay.setGPSFixDTO(fix); - moveOverlay.setGPSFixDTO(fix); - updateRedPoint(index); - polyline.getPath().setAt(index, event.getMouseEvent().getLatLng()); - } - } - } - }); - overlay.addMouseUpHandler(new MouseUpMapHandler() { - @Override - public void onEvent(MouseUpMapEvent event) { - if (mouseDown) { - if (!dragging) { - onClick(event); - } - MapOptions options = MapOptions.newInstance(false); - options.setDraggable(true); - map.setOptions(options); - mouseDown = false; - } - } - }); - map.addMouseOutMoveHandler(new MouseOutMapHandler() { - @Override - public void onEvent(MouseOutMapEvent event) { - if (mouseDown) { - if (dragging) { - editMarkFix(mark, fix, fix.position); - overlay.setVisible(true); - resetPointColor(getIndexOfFixInPolyline(mark, fix)); - moveOverlay.removeFromMap(); - moveOverlay = null; - } - MapOptions options = MapOptions.newInstance(false); - options.setDraggable(true); - map.setOptions(options); - mouseDown = false; - } - } - }); - - popup = new PopupPanel(true); - popup.setStyleName("EditMarkPositionPopup"); - final MenuBar menu = new MenuBar(true); - moveMenuItem = new MenuItem(stringMessages.moveFix(), new ScheduledCommand() { - @Override - public void execute() { - overlay.setVisible(false); - if (currentFixPositionChooser == null) { - OverlayClickHandler.this.unregisterAll(); - currentFixPositionChooser = new FixPositionChooser(EditMarkPositionPanel.this, stringMessages, map, getIndexOfFixInPolyline(mark, fix), polyline.getPath(), overlay, new Callback() { - @Override - public void onFailure(Exception reason) { - overlay.setVisible(true); - OverlayClickHandler.this.registerAll(); - resetCurrentFixPositionChooser(); - } - @Override - public void onSuccess(Position result) { - editMarkFix(mark, fix, result); - overlay.setVisible(true); - OverlayClickHandler.this.registerAll(); - resetCurrentFixPositionChooser(); - } - }); - } - popup.hide(); - } - }); - moveMenuItem.setTitle(stringMessages.useATouchOptimizedUI()); - menu.addItem(moveMenuItem); - canRemoveMarkFix(mark, fix, new AsyncCallback() { - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError(stringMessages.errorCommunicatingWithServer()+": "+caught.getMessage(), /* silent */ true); - addDeleteItemAndShowPopup(false); - } - - @Override - public void onSuccess(Boolean result) { - addDeleteItemAndShowPopup(result); - } - - private void addDeleteItemAndShowPopup(boolean enableDelete) { - final MenuItem delete; - if (enableDelete) { - delete = new MenuItem(stringMessages.deleteFix(), new ScheduledCommand() { - @Override - public void execute() { - removeMarkFix(mark, fix); - popup.hide(); - } - }); - } else { - delete = new MenuItem(stringMessages.deleteFix(), new ScheduledCommand() { - @Override - public void execute() { - } - }); - delete.setEnabled(false); - delete.setTitle(stringMessages.theDeletionOfThisFix()); - } - menu.addItem(delete); - popup.setWidget(menu); - } - }); - } - - private int getIndexOfFixInPolyline(MarkDTO mark, GPSFixDTO fix) { - int index; - if (marks == null) { - index = -1; - } else { - index = 0; - for (GPSFixDTO fixToCompare : marks.get(mark).keySet()) { - if (fixToCompare.timepoint.equals(fix.timepoint)) - return index; - index++; - } - } - return index; - } - - public void onClick(MouseUpMapEvent event) { - setPopupPosition(); - moveMenuItem.setVisible(true); - popup.show(); - } - - public OverlayClickHandler register() { - if (mouseDownHandlerRegistration == null) { - mouseDownHandlerRegistration = overlay.addMouseDownHandler(mouseDownHandler); - } - if (rightClickHandlerRegistration == null) { - rightClickHandlerRegistration = overlay.addRightClickHandler(rightClickHandler); - } - return this; - } - - public OverlayClickHandler unregister() { - if (mouseDownHandlerRegistration != null) { - mouseDownHandlerRegistration.removeHandler(); - mouseDownHandlerRegistration = null; - } - if (rightClickHandlerRegistration != null) { - rightClickHandlerRegistration.removeHandler(); - rightClickHandlerRegistration = null; - } - return this; - } - - public void unregisterAll() { - for (OverlayClickHandler handler : overlayClickHandlers) { - handler.unregister(); - } - } - - public void registerAll() { - for (OverlayClickHandler handler : overlayClickHandlers) { - handler.register(); - } - } - - private void setPopupPosition() { - popup.setPopupPosition(overlay.getCanvas().getAbsoluteLeft() + (int) overlay.getFixScaleAndSize().getB().getWidth() / 2 + 5, - overlay.getCanvas().getAbsoluteTop() + (int) overlay.getFixScaleAndSize().getB().getHeight() / 2 + 5); - } - - public void onResizeMap(ResizeMapEvent event) { - if (popup.isVisible()) { - popup.hide(); - } - } - - public void onRightClick(RightClickMapEvent event) { - setPopupPosition(); - moveMenuItem.setVisible(false); - popup.show(); - } - } - - public void setRedPoint(Point[] points, int index) { - points[index].setMarker(new Marker().setFillColor("#f00")); - } - - public void setRedPoint(int index) { - Point[] points = markSeries.getPoints(); - if (points.length > index) { - setRedPoint(points, index); - setSeriesPoints(markSeries, points); - } - } - - public void updateRedPoint(int index) { - if (selectedMark != null && marks != null) { - Point[] points = getSeriesPoints(marks.get(selectedMark).keySet()); - if (points.length > index) { - setRedPoint(points, index); - setSeriesPoints(markSeries, points); - chart.redraw(); - } - } - } - - public void resetPointColor(int index) { - Point[] points = markSeries.getPoints(); - if (points.length > index) { - points[index].setMarker(new Marker().setFillColor(selectedMark.color==null?null:selectedMark.color.getAsHtml())); - setSeriesPoints(markSeries, points); - } - } - - private void createMarkTrackUi(MarkDTO mark, Iterable track) { - Date fromTime = timeRangeWithZoomProvider.getFromTime(); - Date toTime = timeRangeWithZoomProvider.getToTime(); - PolylineOptions options = PolylineOptions.newInstance(); - if (map != null) { - options.setMap(map); - } - options.setStrokeWeight(1); - options.setVisible(false); - final Polyline polyline = Polyline.newInstance(options); // Line can not be dashed at the moment, because line symbols are not supported - polylines.put(mark, polyline); - - SortedMap fixOverlayMap = marks.get(mark); - - for (final GPSFixDTO fix : track) { - final FixOverlay overlay = new FixOverlay(map, FIX_OVERLAY_Z_ORDER, fix, FixType.BUOY, - mark.color==null?null:mark.color.getAsHtml(), raceMap.getCoordinateSystem(), stringMessages.dragToChangePosition()); - fixOverlayMap.put(fix, overlay); - overlay.setVisible(false); - overlayClickHandlers.add(new OverlayClickHandler(mark, fix, overlay).register()); - if (fromTime.after(fix.timepoint)) { - fromTime = fix.timepoint; - } else if (toTime.before(fix.timepoint)) { - toTime = fix.timepoint; - } - } - updatePolylinePoints(mark); - marksFromToTimes.put(mark, new Pair(fromTime, toTime)); - onResize(); - } - - private void loadData() { - if (selectedRaceIdentifier != null && marks == null) { - setWidget(chart); - showLoading(stringMessages.loadingMarkFixes()); - markPositionService.getMarksInTrackedRace(raceIdentifierToLeaderboardRaceColumnAndFleetMapper.getLeaderboardNameAndRaceColumnNameAndFleetName(selectedRaceIdentifier), - new AsyncCallback>() { - @Override - public void onFailure(Throwable caught) { - hideLoading(); - errorReporter.reportError(stringMessages.errorCommunicatingWithServer()+": "+caught.getMessage()); - } - - @Override - public void onSuccess(Iterable marks) { - EditMarkPositionPanel.this.marks = new HashMap<>(); - List markList = new ArrayList<>(); - for (MarkDTO mark : marks) { - SortedMap fixOverlayMap = new TreeMap<>(new Comparator() { - @Override - public int compare(GPSFixDTO o1, GPSFixDTO o2) { - return o1.timepoint.compareTo(o2.timepoint); - } - }); - EditMarkPositionPanel.this.marks.put(mark, fixOverlayMap); - markList.add(mark); - } - markDataProvider.setList(markList); - onSelectionChange(null); - hideLoading(); - } - }); - } - } - - public void addMarkFix(final MarkDTO mark, final Date timepoint, final Position fixPosition) { - final GPSFixDTOWithSpeedWindTackAndLegType fix = new GPSFixDTOWithSpeedWindTackAndLegType(timepoint, fixPosition, null, new WindDTO(), null, null, false); - markPositionService.addMarkFix( - raceIdentifierToLeaderboardRaceColumnAndFleetMapper.getLeaderboardNameAndRaceColumnNameAndFleetName(selectedRaceIdentifier), - mark, fix, new AsyncCallback() { - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError(stringMessages.errorCommunicatingWithServer()+": "+caught.getMessage()); - } - - @Override - public void onSuccess(Void result) { - FixOverlay overlay = new FixOverlay(map, FIX_OVERLAY_Z_ORDER, fix, FixType.BUOY, - mark.color==null?null:mark.color.getAsHtml(), - raceMap.getCoordinateSystem(), stringMessages.dragToChangePosition()); - overlayClickHandlers.add(new OverlayClickHandler(mark, fix, overlay).register()); - marks.get(mark).put(fix, overlay); - updatePolylinePoints(mark); - setSeriesPoints(mark); - onResize(); - showNotification(stringMessages.fixSuccessfullyAdded(), NotificationType.SUCCESS); - } - }); - } - - private void editMarkFix(final MarkDTO mark, final GPSFixDTO fix, final Position newPosition) { - markPositionService.editMarkFix( - raceIdentifierToLeaderboardRaceColumnAndFleetMapper.getLeaderboardNameAndRaceColumnNameAndFleetName(selectedRaceIdentifier), - mark, fix, newPosition, new AsyncCallback() { - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError(stringMessages.errorCommunicatingWithServer()+": "+caught.getMessage()); - } - - @Override - public void onSuccess(Void result) { - fix.position = newPosition; - marks.get(mark).get(fix).setGPSFixDTO(fix); - updatePolylinePoints(mark); - setSeriesPoints(mark); - onResize(); - showNotification(stringMessages.fixPositionSuccessfullyEdited(), NotificationType.SUCCESS); - timer.setTime(timer.getTime().getTime()+1); - } - }); - } - - private void removeMarkFix(final MarkDTO mark, final GPSFixDTO fix) { - markPositionService.removeMarkFix( - raceIdentifierToLeaderboardRaceColumnAndFleetMapper.getLeaderboardNameAndRaceColumnNameAndFleetName(selectedRaceIdentifier), - mark, fix, new AsyncCallback() { - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError(stringMessages.errorCommunicatingWithServer()+": "+caught.getMessage()); - } - - @Override - public void onSuccess(Void result) { - CanvasOverlayV3 overlay = marks.get(mark).remove(fix); - overlay.removeFromMap(); - updatePolylinePoints(mark); - setSeriesPoints(mark); - onResize(); - showNotification(stringMessages.fixSuccessfullyRemoved(), NotificationType.SUCCESS); - } - }); - } - - private void updatePolylinePoints(MarkDTO mark) { - MVCArray path = polylines.get(mark).getPath(); - path.clear(); - for (GPSFixDTO fix : marks.get(mark).keySet()) { - path.push(LatLng.newInstance(fix.position.getLatDeg(), fix.position.getLngDeg())); - } - } - - public Point[] getSeriesPoints(Collection fixes) { - double latAverage = 0; - double lngAverage = 0; - for (GPSFixDTO fix : fixes) { - latAverage += fix.position.getLatDeg(); - lngAverage += fix.position.getLngDeg(); - } - latAverage /= fixes.size(); - lngAverage /= fixes.size(); - Position averagePosition = new DegreePosition(latAverage, lngAverage); - - Point[] points = new Point[fixes.size()]; - int i = 0; - for (GPSFixDTO fix : fixes) { - final double metersFromAverage = fix.position.getDistance(averagePosition).getMeters(); - points[i] = new Point(fix.timepoint.getTime(), metersFromAverage); - i++; - } - return points; - } - - private void setSeriesPoints(MarkDTO mark) { - setSeriesPoints(markSeries, getSeriesPoints(marks.get(mark).keySet())); - } - - public void setSeriesPoints(Point[] points) { - setSeriesPoints(markSeries, points); - } - - public void setVisible(boolean visible) { - this.visible = visible; - if (map == null) { - map = raceMap.getMap(); - if (map != null) { - setMap(map); - } - } - if (this.visible) { - if (sideBySideComponentViewer != null) { - sideBySideComponentViewer.setLeftComponent(marksPanel); - sideBySideComponentViewer.setLeftComponentToggleButtonVisible(false); - } - for (final CourseMarkOverlay overlay : raceMap.getCourseMarkOverlays().values()) { - courseMarkClickHandlers.add(overlay.addClickHandler(new ClickMapHandler() { - @Override - public void onEvent(ClickMapEvent event) { - marksPanel.select(overlay.getMark()); - } - })); - } - raceMap.unregisterAllCourseMarkInfoWindowClickHandlers(); - } else { - if (currentFixPositionChooser != null) { - currentFixPositionChooser.cancel(); - currentFixPositionChooser = null; - } - marksPanel.deselectMark(); - selectedMark = null; - if (sideBySideComponentViewer != null) { - sideBySideComponentViewer.setLeftComponent(leaderboardPanel); - sideBySideComponentViewer.setLeftComponentToggleButtonVisible(true); - } - for (HandlerRegistration registration : courseMarkClickHandlers) { - registration.removeHandler(); - } - raceMap.unregisterAllCourseMarkInfoWindowClickHandlers(); - raceMap.registerAllCourseMarkInfoWindowClickHandlers(); - } - super.setVisible(this.visible); - } - - @Override - public void onResize() { - if (selectedMark != null) { - LatLngBounds bounds = null; - if (marks != null) { - for (GPSFixDTO fix : marks.get(selectedMark).keySet()) { - if (bounds == null) { - bounds = BoundsUtil.getAsBounds(raceMap.getCoordinateSystem().toLatLng(fix.position)); - } else { - bounds = bounds.extend(raceMap.getCoordinateSystem().toLatLng(fix.position)); - } - } - } - } - chart.setSizeToMatchContainer(); - chart.redraw(); - } - - public void redrawChart() { - chart.redraw(); - } - - @Override - public String getLocalizedShortName() { - return null; - } - - @Override - public Widget getEntryWidget() { - return this; - } - - @Override - public boolean hasSettings() { - return false; - } - - @Override - public SettingsDialogComponent getSettingsDialogComponent() { - return null; - } - - @Override - public void updateSettings(AbstractSettings newSettings) { - } - - @Override - public String getDependentCssClassName() { - return null; - } - - public void setMap(final MapWidget map) { - this.map = map; - for (Map.Entry polyline : polylines.entrySet()) { - polyline.getValue().setMap(map); - } - } - - public Date getTimepoint() { - return timer.getTime(); - } - - @Override - protected Button createSettingsButton() { - Button settingsButton = SettingsDialog.createSettingsButton(this, stringMessages); - settingsButton.setVisible(false); - return settingsButton; - } - - @Override - public void timeChanged(Date newTime, Date oldTime) { - if (isVisible()) { - loadData(); - updateTimePlotLine(newTime); - } - } - - public void setComponentViewer(SideBySideComponentViewer sideBySideComponentViewer) { - this.sideBySideComponentViewer = sideBySideComponentViewer; - } - - private void selectMark(MarkDTO mark) { - selectedMark = mark; - if (currentFixPositionChooser != null) { - currentFixPositionChooser.cancel(); - currentFixPositionChooser = null; - } - if (selectedMark != null) { - if (marksFromToTimes.get(selectedMark) != null) { - // For some reason the time slider does not change with this method only if you comment out line 430 and 432 in TimePanel it works - timeRangeWithZoomProvider.setTimeRange(marksFromToTimes.get(selectedMark).getA(), marksFromToTimes.get(selectedMark).getB()); - } - setWidget(chart); - markSeries.remove(); - markSeries.setPlotOptions(markSeriesPlotOptions.setMarker( - new Marker().setFillColor(selectedMark.color != null ? selectedMark.color.getAsHtml() : "#efab00") - .setLineColor("#fff").setLineWidth(2))); - chart.addSeries(markSeries); - setSeriesPoints(selectedMark); - onResize(); // redraw chart - hideAllCourseMarkOverlaysExceptSelected(); - raceMap.hideAllHelplines(); - if (marks != null) { - for (final Entry> e : marks.entrySet()) { - for (CanvasOverlayV3 overlay : e.getValue().values()) { - // show only tail of selected mark, hide all others - overlay.setVisible(selectedMark.equals(e.getKey())); - } - } - } - for (final Entry e : polylines.entrySet()) { - e.getValue().setVisible(selectedMark.equals(e.getKey())); - } - } else { - if (raceFromTime != null && raceToTime != null) { - timeRangeWithZoomProvider.setTimeRange(raceFromTime, raceToTime); - } - setWidget(noMarkSelectedLabel); - showAllCourseMarkOverlays(); - raceMap.showAllHelplinesToShow(); - hideAllFixOverlays(); - hideAllPolylines(); - } - } - - @Override - public void onSelectionChange(SelectionChangeEvent event) { - final MarkDTO mark = marksPanel.getSelectedMark(); - if (mark != null && (marks.get(mark) == null || marks.get(mark).isEmpty())) { - if (mark != null) { - markPositionService.getMarkTrack(raceIdentifierToLeaderboardRaceColumnAndFleetMapper.getLeaderboardNameAndRaceColumnNameAndFleetName(selectedRaceIdentifier), - mark.getIdAsString(), new AsyncCallback() { - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError(stringMessages.errorCommunicatingWithServer()+": "+caught.getMessage()); - } - - @Override - public void onSuccess(MarkTrackDTO result) { - createMarkTrackUi(mark, result.getFixes()); - selectMark(mark); - } - }); - } - } else { - selectMark(mark); - } - } - - public void showAllCourseMarkOverlays() { - for (Map.Entry overlay : raceMap.getCourseMarkOverlays().entrySet()) { - overlay.getValue().setVisible(true); - } - } - - public void hideAllCourseMarkOverlaysExceptSelected() { - for (Map.Entry overlay : raceMap.getCourseMarkOverlays().entrySet()) { - overlay.getValue().setVisible(overlay.getKey().equals(selectedMark.getName())); - } - } - - public void hideAllFixOverlays() { - if (marks != null) { - for (Map.Entry> mark : marks.entrySet()) { - for (CanvasOverlayV3 overlay : mark.getValue().values()) { - overlay.setVisible(false); - } - } - } - } - - public void hideAllPolylines() { - for (Map.Entry polyline : polylines.entrySet()) { - polyline.getValue().setVisible(false); - } - } - - /** - * Call {@link #resetCurrentFixPositionChooser()} after the fix position chooser is done. - * - * @throws MultipleFixPositionChooserException - * Is thrown when another fix position chooser is still open - */ - public void createFixPositionChooserToAddFixToMark(MarkDTO mark, Callback callback) { - if (currentFixPositionChooser != null) { - return; - } - int index = 0; - for (GPSFixDTO fix : marks.get(mark).keySet()) { - if (fix.timepoint.after(timer.getTime())) - break; - index++; - } - Polyline polyline = polylines.get(mark); - currentFixPositionChooser = new FixPositionChooser(this, stringMessages, map, index, polyline != null ? polyline.getPath() : null, map.getCenter(), raceMap.getCoordinateSystem(), callback); - } - - public void resetCurrentFixPositionChooser() { - currentFixPositionChooser = null; - } - - private com.google.gwt.user.client.Timer notificationTimer; - - public enum NotificationType { - INFO, - ERROR, - SUCCESS - } - - public void showNotification(String message) { - showNotification(message, NotificationType.INFO); - } - - public void showNotification(String message, NotificationType type) { - if (notificationTimer != null && notificationTimer.isRunning()) { - notificationTimer.run(); - } - final HTMLPanel text = new HTMLPanel("

" + message + "
"); - text.setStyleName("EditMarkPositionNotification"); - map.setControls(ControlPosition.TOP_CENTER, text); - - notificationTimer = new com.google.gwt.user.client.Timer(){ - @Override - public void run() { - text.removeFromParent(); - } - }; - notificationTimer.schedule(5000); - } - - public boolean hasFixAtTimePoint(MarkDTO mark, Date timePoint) { - boolean result = false; - for (GPSFixDTO fix : marks.get(mark).keySet()) { - if (fix.timepoint.equals(timePoint)) { - result = true; - break; - } - } - return result; - } - - public List getMarkFixes() { - if (selectedMark != null) { - List set = new ArrayList<>(); - set.addAll(marks.get(selectedMark).keySet()); - return set; - } else { - return null; - } - } - - public XAxis getXAxis() { - return chart.getXAxis(); - } - - public void setLeaderboard(LeaderboardDTO leaderboard) { - raceIdentifierToLeaderboardRaceColumnAndFleetMapper.setLeaderboard(leaderboard); - } - - @Override - public AbstractSettings getSettings() { - return null; - } - - @Override - public String getId() { - return "EditMarkPositionPanel"; - } -} +package com.sap.sailing.gwt.ui.client.shared.charts; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.SortedMap; +import java.util.TreeMap; + +import org.moxieapps.gwt.highcharts.client.Axis; +import org.moxieapps.gwt.highcharts.client.BaseChart; +import org.moxieapps.gwt.highcharts.client.Chart; +import org.moxieapps.gwt.highcharts.client.ChartSubtitle; +import org.moxieapps.gwt.highcharts.client.ChartTitle; +import org.moxieapps.gwt.highcharts.client.Color; +import org.moxieapps.gwt.highcharts.client.Credits; +import org.moxieapps.gwt.highcharts.client.PlotLine.DashStyle; +import org.moxieapps.gwt.highcharts.client.Point; +import org.moxieapps.gwt.highcharts.client.Series; +import org.moxieapps.gwt.highcharts.client.ToolTip; +import org.moxieapps.gwt.highcharts.client.ToolTipData; +import org.moxieapps.gwt.highcharts.client.ToolTipFormatter; +import org.moxieapps.gwt.highcharts.client.XAxis; +import org.moxieapps.gwt.highcharts.client.events.ChartClickEvent; +import org.moxieapps.gwt.highcharts.client.events.ChartClickEventHandler; +import org.moxieapps.gwt.highcharts.client.events.ChartSelectionEvent; +import org.moxieapps.gwt.highcharts.client.events.ChartSelectionEventHandler; +import org.moxieapps.gwt.highcharts.client.labels.AxisLabelsData; +import org.moxieapps.gwt.highcharts.client.labels.AxisLabelsFormatter; +import org.moxieapps.gwt.highcharts.client.labels.XAxisLabels; +import org.moxieapps.gwt.highcharts.client.plotOptions.LinePlotOptions; +import org.moxieapps.gwt.highcharts.client.plotOptions.Marker; + +import com.google.gwt.ajaxloader.client.Properties.TypeException; +import com.google.gwt.core.client.Callback; +import com.google.gwt.core.client.JavaScriptObject; +import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.google.gwt.core.shared.GWT; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.i18n.client.NumberFormat; +import com.google.gwt.maps.client.MapOptions; +import com.google.gwt.maps.client.MapWidget; +import com.google.gwt.maps.client.base.LatLng; +import com.google.gwt.maps.client.base.LatLngBounds; +import com.google.gwt.maps.client.controls.ControlPosition; +import com.google.gwt.maps.client.events.click.ClickMapEvent; +import com.google.gwt.maps.client.events.click.ClickMapHandler; +import com.google.gwt.maps.client.events.mousedown.MouseDownMapEvent; +import com.google.gwt.maps.client.events.mousedown.MouseDownMapHandler; +import com.google.gwt.maps.client.events.mousemove.MouseMoveMapEvent; +import com.google.gwt.maps.client.events.mousemove.MouseMoveMapHandler; +import com.google.gwt.maps.client.events.mouseout.MouseOutMapEvent; +import com.google.gwt.maps.client.events.mouseout.MouseOutMapHandler; +import com.google.gwt.maps.client.events.mouseup.MouseUpMapEvent; +import com.google.gwt.maps.client.events.mouseup.MouseUpMapHandler; +import com.google.gwt.maps.client.events.resize.ResizeMapEvent; +import com.google.gwt.maps.client.events.resize.ResizeMapHandler; +import com.google.gwt.maps.client.events.rightclick.RightClickMapEvent; +import com.google.gwt.maps.client.events.rightclick.RightClickMapHandler; +import com.google.gwt.maps.client.mvc.MVCArray; +import com.google.gwt.maps.client.overlays.Polyline; +import com.google.gwt.maps.client.overlays.PolylineOptions; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.HTMLPanel; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.MenuBar; +import com.google.gwt.user.client.ui.MenuItem; +import com.google.gwt.user.client.ui.PopupPanel; +import com.google.gwt.user.client.ui.RequiresResize; +import com.google.gwt.user.client.ui.Widget; +import com.google.gwt.view.client.ListDataProvider; +import com.google.gwt.view.client.SelectionChangeEvent; +import com.sap.sailing.domain.common.FixType; +import com.sap.sailing.domain.common.Position; +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.domain.common.dto.LeaderboardDTO; +import com.sap.sailing.domain.common.impl.DegreePosition; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.client.shared.charts.MarkPositionService.MarkTrackDTO; +import com.sap.sailing.gwt.ui.client.shared.racemap.BoundsUtil; +import com.sap.sailing.gwt.ui.client.shared.racemap.CourseMarkOverlay; +import com.sap.sailing.gwt.ui.client.shared.racemap.FixOverlay; +import com.sap.sailing.gwt.ui.client.shared.racemap.RaceMap; +import com.sap.sailing.gwt.ui.leaderboard.LeaderboardPanel; +import com.sap.sailing.gwt.ui.raceboard.SideBySideComponentViewer; +import com.sap.sailing.gwt.ui.shared.GPSFixDTO; +import com.sap.sailing.gwt.ui.shared.GPSFixDTOWithSpeedWindTackAndLegType; +import com.sap.sailing.gwt.ui.shared.MarkDTO; +import com.sap.sailing.gwt.ui.shared.WindDTO; +import com.sap.sailing.gwt.ui.shared.racemap.CanvasOverlayV3; +import com.sap.sse.common.Util.Pair; +import com.sap.sse.common.settings.AbstractSettings; +import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.async.AsyncActionsExecutor; +import com.sap.sse.gwt.client.player.TimeRangeWithZoomProvider; +import com.sap.sse.gwt.client.player.Timer; +import com.sap.sse.gwt.client.shared.components.Component; +import com.sap.sse.gwt.client.shared.components.SettingsDialog; +import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; + +public class EditMarkPositionPanel extends AbstractRaceChart implements RequiresResize, SelectionChangeEvent.Handler { + protected static final int FIX_OVERLAY_Z_ORDER = 230; + private final RaceMap raceMap; + private final LeaderboardPanel leaderboardPanel; + private final MarksPanel marksPanel; + private Series markSeries; + private LinePlotOptions markSeriesPlotOptions; + private final Label noMarkSelectedLabel; + private MapWidget map; + private boolean visible; + private List courseMarkClickHandlers; + + private Map> marksFromToTimes; + private Date raceFromTime; + private Date raceToTime; + + private Map> marks; + private MarkDTO selectedMark; + private Map polylines; + private final ListDataProvider markDataProvider; + private SideBySideComponentViewer sideBySideComponentViewer; + + private FixPositionChooser currentFixPositionChooser; + private List overlayClickHandlers; + private final MarkPositionService markPositionService; + + private final RaceIdentifierToLeaderboardRaceColumnAndFleetMapper raceIdentifierToLeaderboardRaceColumnAndFleetMapper; + + public EditMarkPositionPanel(Component parent, ComponentContext context, final RaceMap raceMap, + final LeaderboardPanel leaderboardPanel, + RegattaAndRaceIdentifier selectedRaceIdentifier, String leaderboardName, final StringMessages stringMessages, + SailingServiceAsync sailingService, Timer timer, TimeRangeWithZoomProvider timeRangeWithZoomProvider, + AsyncActionsExecutor asyncActionsExecutor, ErrorReporter errorReporter) { + super(parent, context, sailingService, selectedRaceIdentifier, timer, timeRangeWithZoomProvider, stringMessages, + asyncActionsExecutor, errorReporter); + this.markPositionService = new MarkPositionServiceForSailingService(sailingService); + this.raceIdentifierToLeaderboardRaceColumnAndFleetMapper = new RaceIdentifierToLeaderboardRaceColumnAndFleetMapper(); + this.raceMap = raceMap; + this.leaderboardPanel = leaderboardPanel; + this.polylines = new HashMap<>(); + this.markDataProvider = new ListDataProvider<>(); + this.marksPanel = new MarksPanel(this, context, markDataProvider, stringMessages); + this.noMarkSelectedLabel = new Label(stringMessages.pleaseSelectAMark()); + this.noMarkSelectedLabel.setStyleName("abstractChartPanel-importantMessageOfChart"); + this.courseMarkClickHandlers = new ArrayList<>(); + this.marksFromToTimes = new HashMap<>(); + this.getEntryWidget().setTitle(stringMessages.editMarkPositions()); + this.setVisible(false); + this.overlayClickHandlers = new ArrayList<>(); + chart = new Chart() + .setPersistent(true) + .setZoomType(BaseChart.ZoomType.X) + .setMarginLeft(65) + .setMarginRight(65) + .setWidth100() + .setHeight100() + .setBorderWidth(0) + .setBorderRadius(0) + .setBackgroundColor(new Color("#FFFFFF")) + .setPlotBackgroundColor("#f8f8f8") + .setPlotBorderWidth(0) + .setCredits(new Credits().setEnabled(false)) + .setChartTitle(new ChartTitle().setText(stringMessages.markFixes())) + .setChartSubtitle(new ChartSubtitle().setText(stringMessages.clickAndDragToZoomIn())) + .setPersistent(true); + chart.setClickEventHandler(new ChartClickEventHandler() { + @Override + public boolean onClick(ChartClickEvent chartClickEvent) { + return EditMarkPositionPanel.this.onClick(chartClickEvent); + } + }); + chart.setSelectionEventHandler(new ChartSelectionEventHandler() { + @Override + public boolean onSelection(ChartSelectionEvent chartSelectionEvent) { + return EditMarkPositionPanel.this.onXAxisSelectionChange(chartSelectionEvent); + } + }); + chart.getXAxis().setType(Axis.Type.DATE_TIME) + .setMaxZoom(60 * 1000) // 1 minute + .setAxisTitleText(stringMessages.time()); + chart.getXAxis().setLabels(new XAxisLabels().setFormatter(new AxisLabelsFormatter() { + @Override + public String format(AxisLabelsData axisLabelsData) { + return dateFormatHoursMinutes.format(new Date(axisLabelsData.getValueAsLong())); + } + })); + chart.getYAxis().setAxisTitleText(stringMessages.distanceFromAveragePosition() + " (" + stringMessages.metersUnit() + ")").setOption("labels/enabled", false); + timePlotLine = chart.getXAxis().createPlotLine().setColor("#656565").setWidth(1) + .setDashStyle(DashStyle.SOLID); + markSeriesPlotOptions = new LinePlotOptions().setSelected(true).setShowInLegend(false).setLineWidth(1).setColor("#000") + .setHoverStateLineWidth(1).setEnableMouseTracking(true); + markSeries = chart.createSeries().setType(Series.Type.LINE).setYAxis(0) + .setPlotOptions(markSeriesPlotOptions).setName(stringMessages.distanceFromAveragePosition()); + chart.addSeries(markSeries, false, false); + chart.setToolTip(new ToolTip().setEnabled(true).setFormatter(new ToolTipFormatter() { + @Override + public String format(ToolTipData toolTipData) { + final String seriesName = toolTipData.getSeriesName(); + if (seriesName.equals(stringMessages.time())) { + return "" + seriesName + ": " + dateFormat.format(new Date(toolTipData.getXAsLong())) + + "
(" + stringMessages.clickChartToSetTime() + ")"; + } else { + return "" + (dateFormat.format(new Date(toolTipData.getXAsLong()))) + ": " + + NumberFormat.getFormat("0.0").format(toolTipData.getYAsDouble()) + " m"; + } + } + })); + } + + private void canRemoveMarkFix(MarkDTO mark, GPSFixDTO fix, AsyncCallback callback) { + markPositionService.canRemoveMarkFix( + raceIdentifierToLeaderboardRaceColumnAndFleetMapper.getLeaderboardNameAndRaceColumnNameAndFleetName(selectedRaceIdentifier), + mark, fix, callback); + } + + private static class Pixel extends JavaScriptObject { + protected Pixel() { + } + + private native int getX() /*-{ + return this.x; + }-*/; + private native int getY() /*-{ + return this.y; + }-*/; + } + + private class OverlayClickHandler { + private final PopupPanel popup; + private final MenuItem moveMenuItem; + + private final FixOverlay overlay; + private FixOverlay moveOverlay; + + private final MouseDownMapHandler mouseDownHandler; + private HandlerRegistration mouseDownHandlerRegistration; + private final RightClickMapHandler rightClickHandler; + private HandlerRegistration rightClickHandlerRegistration; + + private int mouseDownX; + private int mouseDownY; + private boolean mouseDown; + private boolean dragging; + + //Currently the big problem with the OverlayClickHandler is that you cannot reliably detect if users use a touch device + //and even if they don't at first they could switch from touch to mouse or the other way around. Thus, both input methods + //are always present. Touch users should not accidentally drag fixes, because there is a buffer. + public OverlayClickHandler(final MarkDTO mark, final GPSFixDTO fix, final FixOverlay overlay) { + this.overlay = overlay; + final Polyline polyline = polylines.get(mark); + + map.addResizeHandler(new ResizeMapHandler() { + @Override + public void onEvent(ResizeMapEvent event) { + onResizeMap(event); + } + }); + rightClickHandler = new RightClickMapHandler() { + @Override + public void onEvent(RightClickMapEvent event) { + onRightClick(event); + } + }; + mouseDownHandler = new MouseDownMapHandler() { + @Override + public void onEvent(MouseDownMapEvent event) { + try { + if (event.getProperties().getNumber("which") == 1) { + mouseDownX = event.getProperties().getNumber("pageX").intValue() - map.getAbsoluteLeft(); + mouseDownY = event.getProperties().getNumber("pageY").intValue() - map.getAbsoluteTop(); + mouseDown = true; + dragging = false; + MapOptions options = MapOptions.newInstance(false); + options.setDraggable(false); + map.setOptions(options); + } + } catch (TypeException e) { + GWT.log("Exception trying to obtain pageX or pageY property on event "+event, e); + } + } + }; + map.addMouseMoveHandler(new MouseMoveMapHandler() { + @Override + public void onEvent(MouseMoveMapEvent event) { + if (mouseDown) { + int currentX = 0; + int currentY = 0; + try { + Pixel pixel = event.getProperties().getObject("pixel").cast(); + currentX = pixel.getX(); + currentY = pixel.getY(); + } catch (TypeException e) { + GWT.log("Exception trying to obtain pixel property on event "+event, e); + } + final int index = getIndexOfFixInPolyline(mark, fix); + if (!dragging && Math.sqrt(Math.pow(currentX - mouseDownX, 2) + + Math.pow(currentY - mouseDownY, 2)) > 15) { + dragging = true; + overlay.setVisible(false); + setRedPoint(index); + moveOverlay = new FixOverlay(map, overlay.getZIndex(), overlay.getGPSFixDTO(), overlay.getType(), + "#f00", raceMap.getCoordinateSystem(), stringMessages.dragToChangePosition()); + moveOverlay.addMouseUpHandler(new MouseUpMapHandler() { + @Override + public void onEvent(MouseUpMapEvent event) { + editMarkFix(mark, fix, fix.position); + overlay.setVisible(true); + resetPointColor(index); + moveOverlay.removeFromMap(); + moveOverlay = null; + + MapOptions options = MapOptions.newInstance(false); + options.setDraggable(true); + map.setOptions(options); + mouseDown = false; + } + }); + } + if (dragging) { + fix.position = raceMap.getCoordinateSystem().getPosition(event.getMouseEvent().getLatLng()); + overlay.setGPSFixDTO(fix); + moveOverlay.setGPSFixDTO(fix); + updateRedPoint(index); + polyline.getPath().setAt(index, event.getMouseEvent().getLatLng()); + } + } + } + }); + overlay.addMouseUpHandler(new MouseUpMapHandler() { + @Override + public void onEvent(MouseUpMapEvent event) { + if (mouseDown) { + if (!dragging) { + onClick(event); + } + MapOptions options = MapOptions.newInstance(false); + options.setDraggable(true); + map.setOptions(options); + mouseDown = false; + } + } + }); + map.addMouseOutMoveHandler(new MouseOutMapHandler() { + @Override + public void onEvent(MouseOutMapEvent event) { + if (mouseDown) { + if (dragging) { + editMarkFix(mark, fix, fix.position); + overlay.setVisible(true); + resetPointColor(getIndexOfFixInPolyline(mark, fix)); + moveOverlay.removeFromMap(); + moveOverlay = null; + } + MapOptions options = MapOptions.newInstance(false); + options.setDraggable(true); + map.setOptions(options); + mouseDown = false; + } + } + }); + + popup = new PopupPanel(true); + popup.setStyleName("EditMarkPositionPopup"); + final MenuBar menu = new MenuBar(true); + moveMenuItem = new MenuItem(stringMessages.moveFix(), new ScheduledCommand() { + @Override + public void execute() { + overlay.setVisible(false); + if (currentFixPositionChooser == null) { + OverlayClickHandler.this.unregisterAll(); + currentFixPositionChooser = new FixPositionChooser(EditMarkPositionPanel.this, stringMessages, map, getIndexOfFixInPolyline(mark, fix), polyline.getPath(), overlay, new Callback() { + @Override + public void onFailure(Exception reason) { + overlay.setVisible(true); + OverlayClickHandler.this.registerAll(); + resetCurrentFixPositionChooser(); + } + @Override + public void onSuccess(Position result) { + editMarkFix(mark, fix, result); + overlay.setVisible(true); + OverlayClickHandler.this.registerAll(); + resetCurrentFixPositionChooser(); + } + }); + } + popup.hide(); + } + }); + moveMenuItem.setTitle(stringMessages.useATouchOptimizedUI()); + menu.addItem(moveMenuItem); + canRemoveMarkFix(mark, fix, new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError(stringMessages.errorCommunicatingWithServer()+": "+caught.getMessage(), /* silent */ true); + addDeleteItemAndShowPopup(false); + } + + @Override + public void onSuccess(Boolean result) { + addDeleteItemAndShowPopup(result); + } + + private void addDeleteItemAndShowPopup(boolean enableDelete) { + final MenuItem delete; + if (enableDelete) { + delete = new MenuItem(stringMessages.deleteFix(), new ScheduledCommand() { + @Override + public void execute() { + removeMarkFix(mark, fix); + popup.hide(); + } + }); + } else { + delete = new MenuItem(stringMessages.deleteFix(), new ScheduledCommand() { + @Override + public void execute() { + } + }); + delete.setEnabled(false); + delete.setTitle(stringMessages.theDeletionOfThisFix()); + } + menu.addItem(delete); + popup.setWidget(menu); + } + }); + } + + private int getIndexOfFixInPolyline(MarkDTO mark, GPSFixDTO fix) { + int index; + if (marks == null) { + index = -1; + } else { + index = 0; + for (GPSFixDTO fixToCompare : marks.get(mark).keySet()) { + if (fixToCompare.timepoint.equals(fix.timepoint)) + return index; + index++; + } + } + return index; + } + + public void onClick(MouseUpMapEvent event) { + setPopupPosition(); + moveMenuItem.setVisible(true); + popup.show(); + } + + public OverlayClickHandler register() { + if (mouseDownHandlerRegistration == null) { + mouseDownHandlerRegistration = overlay.addMouseDownHandler(mouseDownHandler); + } + if (rightClickHandlerRegistration == null) { + rightClickHandlerRegistration = overlay.addRightClickHandler(rightClickHandler); + } + return this; + } + + public OverlayClickHandler unregister() { + if (mouseDownHandlerRegistration != null) { + mouseDownHandlerRegistration.removeHandler(); + mouseDownHandlerRegistration = null; + } + if (rightClickHandlerRegistration != null) { + rightClickHandlerRegistration.removeHandler(); + rightClickHandlerRegistration = null; + } + return this; + } + + public void unregisterAll() { + for (OverlayClickHandler handler : overlayClickHandlers) { + handler.unregister(); + } + } + + public void registerAll() { + for (OverlayClickHandler handler : overlayClickHandlers) { + handler.register(); + } + } + + private void setPopupPosition() { + popup.setPopupPosition(overlay.getCanvas().getAbsoluteLeft() + (int) overlay.getFixScaleAndSize().getB().getWidth() / 2 + 5, + overlay.getCanvas().getAbsoluteTop() + (int) overlay.getFixScaleAndSize().getB().getHeight() / 2 + 5); + } + + public void onResizeMap(ResizeMapEvent event) { + if (popup.isVisible()) { + popup.hide(); + } + } + + public void onRightClick(RightClickMapEvent event) { + setPopupPosition(); + moveMenuItem.setVisible(false); + popup.show(); + } + } + + public void setRedPoint(Point[] points, int index) { + points[index].setMarker(new Marker().setFillColor("#f00")); + } + + public void setRedPoint(int index) { + Point[] points = markSeries.getPoints(); + if (points.length > index) { + setRedPoint(points, index); + setSeriesPoints(markSeries, points); + } + } + + public void updateRedPoint(int index) { + if (selectedMark != null && marks != null) { + Point[] points = getSeriesPoints(marks.get(selectedMark).keySet()); + if (points.length > index) { + setRedPoint(points, index); + setSeriesPoints(markSeries, points); + chart.redraw(); + } + } + } + + public void resetPointColor(int index) { + Point[] points = markSeries.getPoints(); + if (points.length > index) { + points[index].setMarker(new Marker().setFillColor(selectedMark.color==null?null:selectedMark.color.getAsHtml())); + setSeriesPoints(markSeries, points); + } + } + + private void createMarkTrackUi(MarkDTO mark, Iterable track) { + Date fromTime = timeRangeWithZoomProvider.getFromTime(); + Date toTime = timeRangeWithZoomProvider.getToTime(); + PolylineOptions options = PolylineOptions.newInstance(); + if (map != null) { + options.setMap(map); + } + options.setStrokeWeight(1); + options.setVisible(false); + final Polyline polyline = Polyline.newInstance(options); // Line can not be dashed at the moment, because line symbols are not supported + polylines.put(mark, polyline); + + SortedMap fixOverlayMap = marks.get(mark); + + for (final GPSFixDTO fix : track) { + final FixOverlay overlay = new FixOverlay(map, FIX_OVERLAY_Z_ORDER, fix, FixType.BUOY, + mark.color==null?null:mark.color.getAsHtml(), raceMap.getCoordinateSystem(), stringMessages.dragToChangePosition()); + fixOverlayMap.put(fix, overlay); + overlay.setVisible(false); + overlayClickHandlers.add(new OverlayClickHandler(mark, fix, overlay).register()); + if (fromTime.after(fix.timepoint)) { + fromTime = fix.timepoint; + } else if (toTime.before(fix.timepoint)) { + toTime = fix.timepoint; + } + } + updatePolylinePoints(mark); + marksFromToTimes.put(mark, new Pair(fromTime, toTime)); + onResize(); + } + + private void loadData() { + if (selectedRaceIdentifier != null && marks == null) { + setWidget(chart); + showLoading(stringMessages.loadingMarkFixes()); + markPositionService.getMarksInTrackedRace(raceIdentifierToLeaderboardRaceColumnAndFleetMapper.getLeaderboardNameAndRaceColumnNameAndFleetName(selectedRaceIdentifier), + new AsyncCallback>() { + @Override + public void onFailure(Throwable caught) { + hideLoading(); + errorReporter.reportError(stringMessages.errorCommunicatingWithServer()+": "+caught.getMessage()); + } + + @Override + public void onSuccess(Iterable marks) { + EditMarkPositionPanel.this.marks = new HashMap<>(); + List markList = new ArrayList<>(); + for (MarkDTO mark : marks) { + SortedMap fixOverlayMap = new TreeMap<>(new Comparator() { + @Override + public int compare(GPSFixDTO o1, GPSFixDTO o2) { + return o1.timepoint.compareTo(o2.timepoint); + } + }); + EditMarkPositionPanel.this.marks.put(mark, fixOverlayMap); + markList.add(mark); + } + markDataProvider.setList(markList); + onSelectionChange(null); + hideLoading(); + } + }); + } + } + + public void addMarkFix(final MarkDTO mark, final Date timepoint, final Position fixPosition) { + final GPSFixDTOWithSpeedWindTackAndLegType fix = new GPSFixDTOWithSpeedWindTackAndLegType(timepoint, fixPosition, null, new WindDTO(), null, null, false); + markPositionService.addMarkFix( + raceIdentifierToLeaderboardRaceColumnAndFleetMapper.getLeaderboardNameAndRaceColumnNameAndFleetName(selectedRaceIdentifier), + mark, fix, new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError(stringMessages.errorCommunicatingWithServer()+": "+caught.getMessage()); + } + + @Override + public void onSuccess(Void result) { + FixOverlay overlay = new FixOverlay(map, FIX_OVERLAY_Z_ORDER, fix, FixType.BUOY, + mark.color==null?null:mark.color.getAsHtml(), + raceMap.getCoordinateSystem(), stringMessages.dragToChangePosition()); + overlayClickHandlers.add(new OverlayClickHandler(mark, fix, overlay).register()); + marks.get(mark).put(fix, overlay); + updatePolylinePoints(mark); + setSeriesPoints(mark); + onResize(); + showNotification(stringMessages.fixSuccessfullyAdded(), NotificationType.SUCCESS); + } + }); + } + + private void editMarkFix(final MarkDTO mark, final GPSFixDTO fix, final Position newPosition) { + markPositionService.editMarkFix( + raceIdentifierToLeaderboardRaceColumnAndFleetMapper.getLeaderboardNameAndRaceColumnNameAndFleetName(selectedRaceIdentifier), + mark, fix, newPosition, new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError(stringMessages.errorCommunicatingWithServer()+": "+caught.getMessage()); + } + + @Override + public void onSuccess(Void result) { + fix.position = newPosition; + marks.get(mark).get(fix).setGPSFixDTO(fix); + updatePolylinePoints(mark); + setSeriesPoints(mark); + onResize(); + showNotification(stringMessages.fixPositionSuccessfullyEdited(), NotificationType.SUCCESS); + timer.setTime(timer.getTime().getTime()+1); + } + }); + } + + private void removeMarkFix(final MarkDTO mark, final GPSFixDTO fix) { + markPositionService.removeMarkFix( + raceIdentifierToLeaderboardRaceColumnAndFleetMapper.getLeaderboardNameAndRaceColumnNameAndFleetName(selectedRaceIdentifier), + mark, fix, new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError(stringMessages.errorCommunicatingWithServer()+": "+caught.getMessage()); + } + + @Override + public void onSuccess(Void result) { + CanvasOverlayV3 overlay = marks.get(mark).remove(fix); + overlay.removeFromMap(); + updatePolylinePoints(mark); + setSeriesPoints(mark); + onResize(); + showNotification(stringMessages.fixSuccessfullyRemoved(), NotificationType.SUCCESS); + } + }); + } + + private void updatePolylinePoints(MarkDTO mark) { + MVCArray path = polylines.get(mark).getPath(); + path.clear(); + for (GPSFixDTO fix : marks.get(mark).keySet()) { + path.push(LatLng.newInstance(fix.position.getLatDeg(), fix.position.getLngDeg())); + } + } + + public Point[] getSeriesPoints(Collection fixes) { + double latAverage = 0; + double lngAverage = 0; + for (GPSFixDTO fix : fixes) { + latAverage += fix.position.getLatDeg(); + lngAverage += fix.position.getLngDeg(); + } + latAverage /= fixes.size(); + lngAverage /= fixes.size(); + Position averagePosition = new DegreePosition(latAverage, lngAverage); + + Point[] points = new Point[fixes.size()]; + int i = 0; + for (GPSFixDTO fix : fixes) { + final double metersFromAverage = fix.position.getDistance(averagePosition).getMeters(); + points[i] = new Point(fix.timepoint.getTime(), metersFromAverage); + i++; + } + return points; + } + + private void setSeriesPoints(MarkDTO mark) { + setSeriesPoints(markSeries, getSeriesPoints(marks.get(mark).keySet())); + } + + public void setSeriesPoints(Point[] points) { + setSeriesPoints(markSeries, points); + } + + public void setVisible(boolean visible) { + this.visible = visible; + if (map == null) { + map = raceMap.getMap(); + if (map != null) { + setMap(map); + } + } + if (this.visible) { + if (sideBySideComponentViewer != null) { + sideBySideComponentViewer.setLeftComponent(marksPanel); + sideBySideComponentViewer.setLeftComponentToggleButtonVisible(false); + } + for (final CourseMarkOverlay overlay : raceMap.getCourseMarkOverlays().values()) { + courseMarkClickHandlers.add(overlay.addClickHandler(new ClickMapHandler() { + @Override + public void onEvent(ClickMapEvent event) { + marksPanel.select(overlay.getMark()); + } + })); + } + raceMap.unregisterAllCourseMarkInfoWindowClickHandlers(); + } else { + if (currentFixPositionChooser != null) { + currentFixPositionChooser.cancel(); + currentFixPositionChooser = null; + } + marksPanel.deselectMark(); + selectedMark = null; + if (sideBySideComponentViewer != null) { + sideBySideComponentViewer.setLeftComponent(leaderboardPanel); + sideBySideComponentViewer.setLeftComponentToggleButtonVisible(true); + } + for (HandlerRegistration registration : courseMarkClickHandlers) { + registration.removeHandler(); + } + raceMap.unregisterAllCourseMarkInfoWindowClickHandlers(); + raceMap.registerAllCourseMarkInfoWindowClickHandlers(); + } + super.setVisible(this.visible); + } + + @Override + public void onResize() { + if (selectedMark != null) { + LatLngBounds bounds = null; + if (marks != null) { + for (GPSFixDTO fix : marks.get(selectedMark).keySet()) { + if (bounds == null) { + bounds = BoundsUtil.getAsBounds(raceMap.getCoordinateSystem().toLatLng(fix.position)); + } else { + bounds = bounds.extend(raceMap.getCoordinateSystem().toLatLng(fix.position)); + } + } + } + } + chart.setSizeToMatchContainer(); + chart.redraw(); + } + + public void redrawChart() { + chart.redraw(); + } + + @Override + public String getLocalizedShortName() { + return null; + } + + @Override + public Widget getEntryWidget() { + return this; + } + + @Override + public boolean hasSettings() { + return false; + } + + @Override + public SettingsDialogComponent getSettingsDialogComponent() { + return null; + } + + @Override + public void updateSettings(AbstractSettings newSettings) { + } + + @Override + public String getDependentCssClassName() { + return null; + } + + public void setMap(final MapWidget map) { + this.map = map; + for (Map.Entry polyline : polylines.entrySet()) { + polyline.getValue().setMap(map); + } + } + + public Date getTimepoint() { + return timer.getTime(); + } + + @Override + protected Button createSettingsButton() { + Button settingsButton = SettingsDialog.createSettingsButton(this, stringMessages); + settingsButton.setVisible(false); + return settingsButton; + } + + @Override + public void timeChanged(Date newTime, Date oldTime) { + if (isVisible()) { + loadData(); + updateTimePlotLine(newTime); + } + } + + public void setComponentViewer(SideBySideComponentViewer sideBySideComponentViewer) { + this.sideBySideComponentViewer = sideBySideComponentViewer; + } + + private void selectMark(MarkDTO mark) { + selectedMark = mark; + if (currentFixPositionChooser != null) { + currentFixPositionChooser.cancel(); + currentFixPositionChooser = null; + } + if (selectedMark != null) { + if (marksFromToTimes.get(selectedMark) != null) { + // For some reason the time slider does not change with this method only if you comment out line 430 and 432 in TimePanel it works + timeRangeWithZoomProvider.setTimeRange(marksFromToTimes.get(selectedMark).getA(), marksFromToTimes.get(selectedMark).getB()); + } + setWidget(chart); + markSeries.remove(); + markSeries.setPlotOptions(markSeriesPlotOptions.setMarker( + new Marker().setFillColor(selectedMark.color != null ? selectedMark.color.getAsHtml() : "#efab00") + .setLineColor("#fff").setLineWidth(2))); + chart.addSeries(markSeries); + setSeriesPoints(selectedMark); + onResize(); // redraw chart + hideAllCourseMarkOverlaysExceptSelected(); + raceMap.hideAllHelplines(); + if (marks != null) { + for (final Entry> e : marks.entrySet()) { + for (CanvasOverlayV3 overlay : e.getValue().values()) { + // show only tail of selected mark, hide all others + overlay.setVisible(selectedMark.equals(e.getKey())); + } + } + } + for (final Entry e : polylines.entrySet()) { + e.getValue().setVisible(selectedMark.equals(e.getKey())); + } + } else { + if (raceFromTime != null && raceToTime != null) { + timeRangeWithZoomProvider.setTimeRange(raceFromTime, raceToTime); + } + setWidget(noMarkSelectedLabel); + showAllCourseMarkOverlays(); + raceMap.showAllHelplinesToShow(); + hideAllFixOverlays(); + hideAllPolylines(); + } + } + + @Override + public void onSelectionChange(SelectionChangeEvent event) { + final MarkDTO mark = marksPanel.getSelectedMark(); + if (mark != null && (marks.get(mark) == null || marks.get(mark).isEmpty())) { + if (mark != null) { + markPositionService.getMarkTrack(raceIdentifierToLeaderboardRaceColumnAndFleetMapper.getLeaderboardNameAndRaceColumnNameAndFleetName(selectedRaceIdentifier), + mark.getIdAsString(), new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError(stringMessages.errorCommunicatingWithServer()+": "+caught.getMessage()); + } + + @Override + public void onSuccess(MarkTrackDTO result) { + createMarkTrackUi(mark, result.getFixes()); + selectMark(mark); + } + }); + } + } else { + selectMark(mark); + } + } + + public void showAllCourseMarkOverlays() { + for (Map.Entry overlay : raceMap.getCourseMarkOverlays().entrySet()) { + overlay.getValue().setVisible(true); + } + } + + public void hideAllCourseMarkOverlaysExceptSelected() { + for (Map.Entry overlay : raceMap.getCourseMarkOverlays().entrySet()) { + overlay.getValue().setVisible(overlay.getKey().equals(selectedMark.getName())); + } + } + + public void hideAllFixOverlays() { + if (marks != null) { + for (Map.Entry> mark : marks.entrySet()) { + for (CanvasOverlayV3 overlay : mark.getValue().values()) { + overlay.setVisible(false); + } + } + } + } + + public void hideAllPolylines() { + for (Map.Entry polyline : polylines.entrySet()) { + polyline.getValue().setVisible(false); + } + } + + /** + * Call {@link #resetCurrentFixPositionChooser()} after the fix position chooser is done. + * + * @throws MultipleFixPositionChooserException + * Is thrown when another fix position chooser is still open + */ + public void createFixPositionChooserToAddFixToMark(MarkDTO mark, Callback callback) { + if (currentFixPositionChooser != null) { + return; + } + int index = 0; + for (GPSFixDTO fix : marks.get(mark).keySet()) { + if (fix.timepoint.after(timer.getTime())) + break; + index++; + } + Polyline polyline = polylines.get(mark); + currentFixPositionChooser = new FixPositionChooser(this, stringMessages, map, index, polyline != null ? polyline.getPath() : null, map.getCenter(), raceMap.getCoordinateSystem(), callback); + } + + public void resetCurrentFixPositionChooser() { + currentFixPositionChooser = null; + } + + private com.google.gwt.user.client.Timer notificationTimer; + + public enum NotificationType { + INFO, + ERROR, + SUCCESS + } + + public void showNotification(String message) { + showNotification(message, NotificationType.INFO); + } + + public void showNotification(String message, NotificationType type) { + if (notificationTimer != null && notificationTimer.isRunning()) { + notificationTimer.run(); + } + final HTMLPanel text = new HTMLPanel("
" + message + "
"); + text.setStyleName("EditMarkPositionNotification"); + map.setControls(ControlPosition.TOP_CENTER, text); + + notificationTimer = new com.google.gwt.user.client.Timer(){ + @Override + public void run() { + text.removeFromParent(); + } + }; + notificationTimer.schedule(5000); + } + + public boolean hasFixAtTimePoint(MarkDTO mark, Date timePoint) { + boolean result = false; + for (GPSFixDTO fix : marks.get(mark).keySet()) { + if (fix.timepoint.equals(timePoint)) { + result = true; + break; + } + } + return result; + } + + public List getMarkFixes() { + if (selectedMark != null) { + List set = new ArrayList<>(); + set.addAll(marks.get(selectedMark).keySet()); + return set; + } else { + return null; + } + } + + public XAxis getXAxis() { + return chart.getXAxis(); + } + + public void setLeaderboard(LeaderboardDTO leaderboard) { + raceIdentifierToLeaderboardRaceColumnAndFleetMapper.setLeaderboard(leaderboard); + } + + @Override + public AbstractSettings getSettings() { + return null; + } + + @Override + public String getId() { + return "EditMarkPositionPanel"; + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MarksPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MarksPanel.java index 24ffd97b6ca..19e62e09bfb 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MarksPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MarksPanel.java @@ -1,179 +1,179 @@ -package com.sap.sailing.gwt.ui.client.shared.charts; - -import java.util.Date; - -import com.google.gwt.cell.client.ButtonCell; -import com.google.gwt.cell.client.FieldUpdater; -import com.google.gwt.cell.client.TextCell; -import com.google.gwt.core.client.Callback; -import com.google.gwt.core.client.GWT; -import com.google.gwt.user.cellview.client.Header; -import com.google.gwt.user.cellview.client.TextHeader; -import com.google.gwt.user.client.ui.Widget; -import com.google.gwt.view.client.ListDataProvider; -import com.google.gwt.view.client.SingleSelectionModel; -import com.sap.sailing.domain.common.InvertibleComparator; -import com.sap.sailing.domain.common.Position; -import com.sap.sailing.domain.common.SortingOrder; -import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sailing.gwt.ui.client.shared.charts.EditMarkPositionPanel.NotificationType; -import com.sap.sailing.gwt.ui.client.shared.controls.FlushableSortedCellTableWithStylableHeaders; -import com.sap.sailing.gwt.ui.client.shared.controls.SortableColumn; -import com.sap.sailing.gwt.ui.leaderboard.LeaderboardTableResources; -import com.sap.sailing.gwt.ui.shared.MarkDTO; -import com.sap.sse.common.settings.AbstractSettings; -import com.sap.sse.gwt.client.celltable.EntityIdentityComparator; -import com.sap.sse.gwt.client.celltable.RefreshableSingleSelectionModel; -import com.sap.sse.gwt.client.shared.components.AbstractCompositeComponent; -import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; - -public class MarksPanel extends AbstractCompositeComponent { - private static final LeaderboardTableResources tableResources = GWT.create(LeaderboardTableResources.class); - - private final ListDataProvider markDataProvider; - private final FlushableSortedCellTableWithStylableHeaders markTable; - - public MarksPanel(final EditMarkPositionPanel parent, ComponentContext context, - final ListDataProvider markDataProvider, final StringMessages stringMessages) { - super(parent, context); - this.markDataProvider = markDataProvider; - markTable = new FlushableSortedCellTableWithStylableHeaders(10000, tableResources); - markTable.setStyleName("EditMarkPositionMarkTable"); - SortableColumn markNameColumn = new SortableColumn(new TextCell(), SortingOrder.NONE) { - @Override - public String getValue(MarkDTO object) { - return object.getName(); - } - - @Override - public InvertibleComparator getComparator() { - return null; - } - - @Override - public Header getHeader() { - return null; - } - }; - markTable.addColumn(markNameColumn, new TextHeader(stringMessages.marks())); - SortableColumn addFixColumn = new SortableColumn(new ButtonCell(), SortingOrder.NONE) { - @Override - public String getValue(MarkDTO object) { - return stringMessages.addNewFix(); - } - - @Override - public InvertibleComparator getComparator() { - return null; - } - - @Override - public Header getHeader() { - return null; - } - }; - - - addFixColumn.setFieldUpdater(new FieldUpdater() { - @Override - public void update(int index, final MarkDTO mark, String value) { - final Date timePoint = parent.timer.getTime(); - select(mark); - if (parent.hasFixAtTimePoint(mark, timePoint)) { - parent.showNotification(stringMessages.pleaseSelectOtherTimepoint(), NotificationType.ERROR); - } else { - parent.createFixPositionChooserToAddFixToMark(mark, new Callback() { - @Override - public void onFailure(Exception reason) { - parent.resetCurrentFixPositionChooser(); - } - @Override - public void onSuccess(Position result) { - parent.addMarkFix(mark, timePoint, result); - parent.resetCurrentFixPositionChooser(); - } - }); - } - } - }); - markTable.addColumn(addFixColumn, new TextHeader("")); - SingleSelectionModel selectionModel = new RefreshableSingleSelectionModel(new EntityIdentityComparator() { - @Override - public boolean representSameEntity(MarkDTO dto1, MarkDTO dto2) { - return dto1.getIdAsString().equals(dto2.getIdAsString()); - } - @Override - public int hashCode(MarkDTO t) { - return t.getIdAsString().hashCode(); - } - },this.markDataProvider); - markTable.setSelectionModel(selectionModel); - markTable.getSelectionModel().addSelectionChangeHandler(parent); - markDataProvider.addDataDisplay(markTable); - initWidget(markTable); - setTitle(stringMessages.marks()); - } - - @Override - public void setVisible(boolean visible) { - super.setVisible(visible); - } - - @Override - public String getLocalizedShortName() { - return null; - } - - @Override - public Widget getEntryWidget() { - return this; - } - - @Override - public boolean hasSettings() { - return false; - } - - @Override - public SettingsDialogComponent getSettingsDialogComponent() { - return null; - } - - @Override - public void updateSettings(AbstractSettings newSettings) { - } - - @Override - public String getDependentCssClassName() { - return null; - } - - public MarkDTO getSelectedMark() { - for (MarkDTO mark : markDataProvider.getList()) { - if (markTable.getSelectionModel().isSelected(mark)) { - return mark; - } - } - return null; - } - - public void deselectMark() { - markTable.getSelectionModel().setSelected(getSelectedMark(), false); - } - - public void select(MarkDTO mark) { - markTable.getSelectionModel().setSelected(mark, true); - } - - @Override - public AbstractSettings getSettings() { - return null; - } - - @Override - public String getId() { - return "MarksPanel"; - } - +package com.sap.sailing.gwt.ui.client.shared.charts; + +import java.util.Date; + +import com.google.gwt.cell.client.ButtonCell; +import com.google.gwt.cell.client.FieldUpdater; +import com.google.gwt.cell.client.TextCell; +import com.google.gwt.core.client.Callback; +import com.google.gwt.core.client.GWT; +import com.google.gwt.user.cellview.client.Header; +import com.google.gwt.user.cellview.client.TextHeader; +import com.google.gwt.user.client.ui.Widget; +import com.google.gwt.view.client.ListDataProvider; +import com.google.gwt.view.client.SingleSelectionModel; +import com.sap.sailing.domain.common.InvertibleComparator; +import com.sap.sailing.domain.common.Position; +import com.sap.sailing.domain.common.SortingOrder; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.client.shared.charts.EditMarkPositionPanel.NotificationType; +import com.sap.sailing.gwt.ui.client.shared.controls.FlushableSortedCellTableWithStylableHeaders; +import com.sap.sailing.gwt.ui.client.shared.controls.SortableColumn; +import com.sap.sailing.gwt.ui.leaderboard.LeaderboardTableResources; +import com.sap.sailing.gwt.ui.shared.MarkDTO; +import com.sap.sse.common.settings.AbstractSettings; +import com.sap.sse.gwt.client.celltable.EntityIdentityComparator; +import com.sap.sse.gwt.client.celltable.RefreshableSingleSelectionModel; +import com.sap.sse.gwt.client.shared.components.AbstractCompositeComponent; +import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; + +public class MarksPanel extends AbstractCompositeComponent { + private static final LeaderboardTableResources tableResources = GWT.create(LeaderboardTableResources.class); + + private final ListDataProvider markDataProvider; + private final FlushableSortedCellTableWithStylableHeaders markTable; + + public MarksPanel(final EditMarkPositionPanel parent, ComponentContext context, + final ListDataProvider markDataProvider, final StringMessages stringMessages) { + super(parent, context); + this.markDataProvider = markDataProvider; + markTable = new FlushableSortedCellTableWithStylableHeaders(10000, tableResources); + markTable.setStyleName("EditMarkPositionMarkTable"); + SortableColumn markNameColumn = new SortableColumn(new TextCell(), SortingOrder.NONE) { + @Override + public String getValue(MarkDTO object) { + return object.getName(); + } + + @Override + public InvertibleComparator getComparator() { + return null; + } + + @Override + public Header getHeader() { + return null; + } + }; + markTable.addColumn(markNameColumn, new TextHeader(stringMessages.marks())); + SortableColumn addFixColumn = new SortableColumn(new ButtonCell(), SortingOrder.NONE) { + @Override + public String getValue(MarkDTO object) { + return stringMessages.addNewFix(); + } + + @Override + public InvertibleComparator getComparator() { + return null; + } + + @Override + public Header getHeader() { + return null; + } + }; + + + addFixColumn.setFieldUpdater(new FieldUpdater() { + @Override + public void update(int index, final MarkDTO mark, String value) { + final Date timePoint = parent.timer.getTime(); + select(mark); + if (parent.hasFixAtTimePoint(mark, timePoint)) { + parent.showNotification(stringMessages.pleaseSelectOtherTimepoint(), NotificationType.ERROR); + } else { + parent.createFixPositionChooserToAddFixToMark(mark, new Callback() { + @Override + public void onFailure(Exception reason) { + parent.resetCurrentFixPositionChooser(); + } + @Override + public void onSuccess(Position result) { + parent.addMarkFix(mark, timePoint, result); + parent.resetCurrentFixPositionChooser(); + } + }); + } + } + }); + markTable.addColumn(addFixColumn, new TextHeader("")); + SingleSelectionModel selectionModel = new RefreshableSingleSelectionModel(new EntityIdentityComparator() { + @Override + public boolean representSameEntity(MarkDTO dto1, MarkDTO dto2) { + return dto1.getIdAsString().equals(dto2.getIdAsString()); + } + @Override + public int hashCode(MarkDTO t) { + return t.getIdAsString().hashCode(); + } + },this.markDataProvider); + markTable.setSelectionModel(selectionModel); + markTable.getSelectionModel().addSelectionChangeHandler(parent); + markDataProvider.addDataDisplay(markTable); + initWidget(markTable); + setTitle(stringMessages.marks()); + } + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + } + + @Override + public String getLocalizedShortName() { + return null; + } + + @Override + public Widget getEntryWidget() { + return this; + } + + @Override + public boolean hasSettings() { + return false; + } + + @Override + public SettingsDialogComponent getSettingsDialogComponent() { + return null; + } + + @Override + public void updateSettings(AbstractSettings newSettings) { + } + + @Override + public String getDependentCssClassName() { + return null; + } + + public MarkDTO getSelectedMark() { + for (MarkDTO mark : markDataProvider.getList()) { + if (markTable.getSelectionModel().isSelected(mark)) { + return mark; + } + } + return null; + } + + public void deselectMark() { + markTable.getSelectionModel().setSelected(getSelectedMark(), false); + } + + public void select(MarkDTO mark) { + markTable.getSelectionModel().setSelected(mark, true); + } + + @Override + public AbstractSettings getSettings() { + return null; + } + + @Override + public String getId() { + return "MarksPanel"; + } + } \ 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/charts/MultiCompetitorLeaderboardChart.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorLeaderboardChart.java index a63acd4e315..fb855227884 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorLeaderboardChart.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorLeaderboardChart.java @@ -13,7 +13,7 @@ import com.sap.sse.gwt.client.async.AsyncActionsExecutor; import com.sap.sse.gwt.client.player.Timer; import com.sap.sse.gwt.client.shared.components.Component; import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; public class MultiCompetitorLeaderboardChart extends AbstractCompetitorLeaderboardChart { private MultiCompetitorLeaderboardChartSettings settings; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java index e197351e8cc..5b978c54bec 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorRaceChart.java @@ -1,118 +1,118 @@ -package com.sap.sailing.gwt.ui.client.shared.charts; - -import com.google.gwt.user.client.rpc.AsyncCallback; -import com.google.gwt.user.client.ui.Button; -import com.sap.sailing.domain.common.DetailType; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sailing.gwt.ui.client.CompetitorSelectionProvider; -import com.sap.sailing.gwt.ui.client.SailingServiceAsync; -import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sailing.gwt.ui.shared.LeaderboardGroupDTO; -import com.sap.sse.gwt.client.ErrorReporter; -import com.sap.sse.gwt.client.async.AsyncActionsExecutor; -import com.sap.sse.gwt.client.player.TimeRangeWithZoomProvider; -import com.sap.sse.gwt.client.player.Timer; -import com.sap.sse.gwt.client.shared.components.Component; -import com.sap.sse.gwt.client.shared.components.SettingsDialog; -import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; - -/** - * MultiCompetitorRaceChart is a GWT panel that can show competitor data (e.g. current speed over ground, windward distance to - * leader) for different races in a chart. The chart type can be selected from the settings. - * - * When calling the constructor a chart is created that creates a final amount of series (so the maximum number of - * competitors cannot be changed in one chart) which are connected to competitors, when the SailingService returns the - * data. So {@code seriesID, competitorID and markSeriesID} are linked with the index. So if you know for example the - * seriesID-index, you can get the competitor by calling competitorID.get(index). - * - * @author Benjamin Ebling (D056866), Axel Uhl (d043530) - * - */ -public class MultiCompetitorRaceChart extends AbstractCompetitorRaceChart implements Component { - - private boolean hasOverallLeaderboard; - - private final MultiCompetitorRaceChartLifecycle lifecycle; - - public MultiCompetitorRaceChart(Component parent, ComponentContext context, - MultiCompetitorRaceChartLifecycle lifecycle, - SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, - CompetitorSelectionProvider competitorSelectionProvider, RegattaAndRaceIdentifier selectedRaceIdentifier, - Timer timer, TimeRangeWithZoomProvider timeRangeWithZoomProvider, final StringMessages stringMessages, - final ErrorReporter errorReporter, boolean compactChart, boolean allowTimeAdjust, - final String leaderboardGroupName, String leaderboardName) { - super(parent, context, sailingService, asyncActionsExecutor, competitorSelectionProvider, - selectedRaceIdentifier, timer, - timeRangeWithZoomProvider, stringMessages, errorReporter, - /* show initially */DetailType.WINDWARD_DISTANCE_TO_COMPETITOR_FARTHEST_AHEAD, null, compactChart, - allowTimeAdjust, leaderboardGroupName, leaderboardName); - this.lifecycle = lifecycle; - if (leaderboardGroupName != null) { - sailingService.getLeaderboardGroupByName(leaderboardGroupName, false, - new AsyncCallback() { - @Override - public void onSuccess(LeaderboardGroupDTO group) { - hasOverallLeaderboard = group != null ? group.hasOverallLeaderboard() : false; - } - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError("Error fetching the leaderboard group '" + leaderboardGroupName - + "': " + caught.getMessage()); - } - }); - } else { - hasOverallLeaderboard = false; - } - } - - @Override - protected Button createSettingsButton() { - Button settingsButton = SettingsDialog.createSettingsButton(this, stringMessages); - return settingsButton; - } - - @Override - public SettingsDialogComponent getSettingsDialogComponent() { - return new MultiCompetitorRaceChartSettingsComponent(new MultiCompetitorRaceChartSettings( - getAbstractSettings(), getSelectedFirstDetailType(), getSelectedSecondDetailType()), - getStringMessages(), hasOverallLeaderboard); - } - - @Override - public void updateSettings(MultiCompetitorRaceChartSettings newSettings) { - boolean settingsChanged = updateSettingsOnly(newSettings); - boolean selectedDetailTypeChanged = setSelectedDetailTypes(newSettings.getFirstDetailType(), - newSettings.getSecondDetailType()); - if (selectedDetailTypeChanged || settingsChanged) { - clearChart(); - timeChanged(timer.getTime(), null); - } - } - - @Override - public MultiCompetitorRaceChartSettings getSettings() { - return new MultiCompetitorRaceChartSettings(getAbstractSettings(), getSelectedFirstDetailType(), getSelectedSecondDetailType()); - } - - @Override - protected Component getComponent() { - return this; - } - - @Override - public String getLocalizedShortName() { - return lifecycle.getLocalizedShortName(); - } - - @Override - public String getDependentCssClassName() { - return "multiCompetitorRaceChart"; - } - - @Override - public String getId() { - return lifecycle.getComponentId(); - } - -} +package com.sap.sailing.gwt.ui.client.shared.charts; + +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.gwt.user.client.ui.Button; +import com.sap.sailing.domain.common.DetailType; +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.gwt.ui.client.CompetitorSelectionProvider; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.shared.LeaderboardGroupDTO; +import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.async.AsyncActionsExecutor; +import com.sap.sse.gwt.client.player.TimeRangeWithZoomProvider; +import com.sap.sse.gwt.client.player.Timer; +import com.sap.sse.gwt.client.shared.components.Component; +import com.sap.sse.gwt.client.shared.components.SettingsDialog; +import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; + +/** + * MultiCompetitorRaceChart is a GWT panel that can show competitor data (e.g. current speed over ground, windward distance to + * leader) for different races in a chart. The chart type can be selected from the settings. + * + * When calling the constructor a chart is created that creates a final amount of series (so the maximum number of + * competitors cannot be changed in one chart) which are connected to competitors, when the SailingService returns the + * data. So {@code seriesID, competitorID and markSeriesID} are linked with the index. So if you know for example the + * seriesID-index, you can get the competitor by calling competitorID.get(index). + * + * @author Benjamin Ebling (D056866), Axel Uhl (d043530) + * + */ +public class MultiCompetitorRaceChart extends AbstractCompetitorRaceChart implements Component { + + private boolean hasOverallLeaderboard; + + private final MultiCompetitorRaceChartLifecycle lifecycle; + + public MultiCompetitorRaceChart(Component parent, ComponentContext context, + MultiCompetitorRaceChartLifecycle lifecycle, + SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, + CompetitorSelectionProvider competitorSelectionProvider, RegattaAndRaceIdentifier selectedRaceIdentifier, + Timer timer, TimeRangeWithZoomProvider timeRangeWithZoomProvider, final StringMessages stringMessages, + final ErrorReporter errorReporter, boolean compactChart, boolean allowTimeAdjust, + final String leaderboardGroupName, String leaderboardName) { + super(parent, context, sailingService, asyncActionsExecutor, competitorSelectionProvider, + selectedRaceIdentifier, timer, + timeRangeWithZoomProvider, stringMessages, errorReporter, + /* show initially */DetailType.WINDWARD_DISTANCE_TO_COMPETITOR_FARTHEST_AHEAD, null, compactChart, + allowTimeAdjust, leaderboardGroupName, leaderboardName); + this.lifecycle = lifecycle; + if (leaderboardGroupName != null) { + sailingService.getLeaderboardGroupByName(leaderboardGroupName, false, + new AsyncCallback() { + @Override + public void onSuccess(LeaderboardGroupDTO group) { + hasOverallLeaderboard = group != null ? group.hasOverallLeaderboard() : false; + } + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError("Error fetching the leaderboard group '" + leaderboardGroupName + + "': " + caught.getMessage()); + } + }); + } else { + hasOverallLeaderboard = false; + } + } + + @Override + protected Button createSettingsButton() { + Button settingsButton = SettingsDialog.createSettingsButton(this, stringMessages); + return settingsButton; + } + + @Override + public SettingsDialogComponent getSettingsDialogComponent() { + return new MultiCompetitorRaceChartSettingsComponent(new MultiCompetitorRaceChartSettings( + getAbstractSettings(), getSelectedFirstDetailType(), getSelectedSecondDetailType()), + getStringMessages(), hasOverallLeaderboard); + } + + @Override + public void updateSettings(MultiCompetitorRaceChartSettings newSettings) { + boolean settingsChanged = updateSettingsOnly(newSettings); + boolean selectedDetailTypeChanged = setSelectedDetailTypes(newSettings.getFirstDetailType(), + newSettings.getSecondDetailType()); + if (selectedDetailTypeChanged || settingsChanged) { + clearChart(); + timeChanged(timer.getTime(), null); + } + } + + @Override + public MultiCompetitorRaceChartSettings getSettings() { + return new MultiCompetitorRaceChartSettings(getAbstractSettings(), getSelectedFirstDetailType(), getSelectedSecondDetailType()); + } + + @Override + protected Component getComponent() { + return this; + } + + @Override + public String getLocalizedShortName() { + return lifecycle.getLocalizedShortName(); + } + + @Override + public String getDependentCssClassName() { + return "multiCompetitorRaceChart"; + } + + @Override + public String getId() { + return lifecycle.getComponentId(); + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/WindChart.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/WindChart.java index 2d01d6dab23..74923e15e6a 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/WindChart.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/WindChart.java @@ -1,663 +1,663 @@ -package com.sap.sailing.gwt.ui.client.shared.charts; - -import java.util.Arrays; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import org.moxieapps.gwt.highcharts.client.Axis; -import org.moxieapps.gwt.highcharts.client.BaseChart; -import org.moxieapps.gwt.highcharts.client.Chart; -import org.moxieapps.gwt.highcharts.client.ChartSubtitle; -import org.moxieapps.gwt.highcharts.client.ChartTitle; -import org.moxieapps.gwt.highcharts.client.Color; -import org.moxieapps.gwt.highcharts.client.Credits; -import org.moxieapps.gwt.highcharts.client.PlotLine; -import org.moxieapps.gwt.highcharts.client.PlotLine.DashStyle; -import org.moxieapps.gwt.highcharts.client.Point; -import org.moxieapps.gwt.highcharts.client.Series; -import org.moxieapps.gwt.highcharts.client.ToolTip; -import org.moxieapps.gwt.highcharts.client.ToolTipData; -import org.moxieapps.gwt.highcharts.client.ToolTipFormatter; -import org.moxieapps.gwt.highcharts.client.events.ChartClickEvent; -import org.moxieapps.gwt.highcharts.client.events.ChartClickEventHandler; -import org.moxieapps.gwt.highcharts.client.events.ChartSelectionEvent; -import org.moxieapps.gwt.highcharts.client.events.ChartSelectionEventHandler; -import org.moxieapps.gwt.highcharts.client.labels.AxisLabelsData; -import org.moxieapps.gwt.highcharts.client.labels.AxisLabelsFormatter; -import org.moxieapps.gwt.highcharts.client.labels.XAxisLabels; -import org.moxieapps.gwt.highcharts.client.labels.YAxisLabels; -import org.moxieapps.gwt.highcharts.client.plotOptions.LinePlotOptions; -import org.moxieapps.gwt.highcharts.client.plotOptions.Marker; - -import com.google.gwt.i18n.client.DateTimeFormat; -import com.google.gwt.i18n.client.NumberFormat; -import com.google.gwt.text.client.DateTimeFormatRenderer; -import com.google.gwt.user.client.rpc.AsyncCallback; -import com.google.gwt.user.client.ui.Button; -import com.google.gwt.user.client.ui.RequiresResize; -import com.google.gwt.user.client.ui.Widget; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sailing.domain.common.WindSource; -import com.sap.sailing.domain.common.WindSourceType; -import com.sap.sailing.domain.common.impl.ColorMapImpl; -import com.sap.sailing.gwt.ui.actions.GetWindInfoAction; -import com.sap.sailing.gwt.ui.client.SailingServiceAsync; -import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sailing.gwt.ui.client.WindSourceTypeFormatter; -import com.sap.sailing.gwt.ui.shared.WindDTO; -import com.sap.sailing.gwt.ui.shared.WindInfoForRaceDTO; -import com.sap.sailing.gwt.ui.shared.WindTrackInfoDTO; -import com.sap.sse.common.Util; -import com.sap.sse.gwt.client.ErrorReporter; -import com.sap.sse.gwt.client.async.AsyncActionsExecutor; -import com.sap.sse.gwt.client.player.TimeRangeWithZoomProvider; -import com.sap.sse.gwt.client.player.Timer; -import com.sap.sse.gwt.client.player.Timer.PlayModes; -import com.sap.sse.gwt.client.shared.components.Component; -import com.sap.sse.gwt.client.shared.components.SettingsDialog; -import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; - -public class WindChart extends AbstractRaceChart implements RequiresResize { - public static final String LOAD_WIND_CHART_DATA_CATEGORY = "loadWindChartData"; - - private static final int LINE_WIDTH = 1; - - private final WindChartSettings settings; - private final WindChartLifecycle windChartLifecycle; - - /** - * Holds one series for each wind source for which data has been received. - */ - private final Map windSourceDirectionSeries; - private final Map windSourceSpeedSeries; - private final Map windSourceDirectionPoints; - private final Map windSourceSpeedPoints; - private Point firstPointOfFirstSeries; - - private Long timeOfEarliestRequestInMillis; - private Long timeOfLatestRequestInMillis; - - private final ColorMapImpl colorMap; - - /** - * @param selectedRaceIdentifier - * if null, this chart won't update its contents automatically upon race selection change; - * otherwise, whenever the selection changes, the wind data of the race selected now is loaded from the - * server and displayed in this chart. If no race is selected, the chart is cleared. The caller of this - * constructor must ensure to trigger {@link RaceSelectionChangeListener#onRaceSelectionChange(List)} at - * least once to ensure that this chart sets its {@link AbstractRaceChart#selectedRaceIdentifier} field. - */ - public WindChart(Component parent, ComponentContext context, WindChartLifecycle windChartLifecycle, - SailingServiceAsync sailingService, - RegattaAndRaceIdentifier selectedRaceIdentifier, Timer timer, - TimeRangeWithZoomProvider timeRangeWithZoomProvider, WindChartSettings settings, final StringMessages stringMessages, - AsyncActionsExecutor asyncActionsExecutor, ErrorReporter errorReporter, boolean compactChart) { - super(parent, context, sailingService, selectedRaceIdentifier, timer, timeRangeWithZoomProvider, stringMessages, - asyncActionsExecutor, errorReporter); - this.windChartLifecycle = windChartLifecycle; - this.settings = settings; - windSourceDirectionSeries = new HashMap(); - windSourceSpeedSeries = new HashMap(); - windSourceDirectionPoints = new HashMap(); - windSourceSpeedPoints = new HashMap(); - firstPointOfFirstSeries = null; - colorMap = new ColorMapImpl(); - chart = new Chart() - .setPersistent(true) - .setReflow(false) - .setZoomType(BaseChart.ZoomType.X) - .setMarginLeft(65) - .setMarginRight(65) - .setWidth100() - .setHeight100() - .setBorderColor(new Color("#CACACA")) - .setBorderWidth(0) - .setBorderRadius(0) - .setBackgroundColor(new Color("#FFFFFF")) - .setPlotBackgroundColor("#f8f8f8") - .setPlotBorderWidth(0) - .setCredits(new Credits().setEnabled(false)) - .setChartTitle(new ChartTitle().setText(stringMessages.wind()).setOption("floating",true)) - .setChartSubtitle(new ChartSubtitle().setText(stringMessages.clickAndDragToZoomIn())) - .setLinePlotOptions(new LinePlotOptions().setLineWidth(LINE_WIDTH).setMarker( - new Marker().setEnabled(false).setHoverState( - new Marker().setEnabled(true).setRadius(4))).setShadow(false) - .setHoverStateLineWidth(LINE_WIDTH)); - chart.setStyleName(chartsCss.chartStyle()); - ChartUtil.useCheckboxesToShowAndHide(chart); - final NumberFormat numberFormat = NumberFormat.getFormat("0"); - chart.setToolTip(new ToolTip().setEnabled(true).setFormatter(new ToolTipFormatter() { - @Override - public String format(ToolTipData toolTipData) { - String seriesName = toolTipData.getSeriesName(); - if (seriesName.equals(WindChart.this.stringMessages.time())) { - return "" + seriesName + ": " + dateFormat.format(new Date(toolTipData.getXAsLong())) - + "
(" + stringMessages.clickChartToSetTime() + ")"; - } else if (seriesName.startsWith(stringMessages.fromDeg()+" ")) { - double value = toolTipData.getYAsDouble() % 360; - return "" + seriesName + (toolTipData.getPointName() != null ? " "+toolTipData.getPointName() : "") - + "
" + - dateFormat.format(new Date(toolTipData.getXAsLong())) + ": " + - numberFormat.format(value < 0 ? value + 360 : value) + stringMessages.degreesShort(); - } else { - return "" + seriesName + (toolTipData.getPointName() != null ? " "+toolTipData.getPointName() : "") - + "
" + - dateFormat.format(new Date(toolTipData.getXAsLong())) + ": " + - numberFormat.format(toolTipData.getYAsDouble()) + stringMessages.knotsUnit(); - } - } - })); - - chart.setClickEventHandler(new ChartClickEventHandler() { - @Override - public boolean onClick(ChartClickEvent chartClickEvent) { - return WindChart.this.onClick(chartClickEvent); - } - }); - - chart.setSelectionEventHandler(new ChartSelectionEventHandler() { - @Override - public boolean onSelection(ChartSelectionEvent chartSelectionEvent) { - return WindChart.this.onXAxisSelectionChange(chartSelectionEvent); - } - }); - - chart.getXAxis().setType(Axis.Type.DATE_TIME) - .setMaxZoom(60 * 1000) // 1 minute - .setAxisTitleText(stringMessages.time()); - chart.getXAxis().setLabels(new XAxisLabels().setFormatter(new AxisLabelsFormatter() { - @Override - public String format(AxisLabelsData axisLabelsData) { - return dateFormatHoursMinutes.format(new Date(axisLabelsData.getValueAsLong())); - } - })); - timePlotLine = chart.getXAxis().createPlotLine().setColor("#656565").setWidth(1.5).setDashStyle(DashStyle.SOLID); - - chart.getYAxis(0).setAxisTitleText(stringMessages.fromDeg()).setStartOnTick(false) - .setLabels(new YAxisLabels().setFormatter(new AxisLabelsFormatter() { - @Override - public String format(AxisLabelsData axisLabelsData) { - long value = axisLabelsData.getValueAsLong() % 360; - return new Long(value < 0 ? value + 360 : value).toString(); - } - })); - chart.getYAxis(1).setOpposite(true).setAxisTitleText(stringMessages.speed()+" ("+stringMessages.knotsUnit()+")").setMin(0) - .setMaxPadding(0.05).setStartOnTick(false).setGridLineWidth(0).setMinorGridLineWidth(0); - if (compactChart) { - chart.setSpacingBottom(10).setSpacingLeft(10).setSpacingRight(10).setSpacingTop(20) - .setOption("legend/margin", 2) - .setOption("title/margin", 5) - .setChartSubtitle(null) - .getXAxis().setAxisTitle(null); - } - setSize("100%", "100%"); - if (selectedRaceIdentifier != null) { - clearCacheAndReload(); - if (isVisible()) { - updateVisibleSeries(); - } - } else { - clearChart(); - } - } - - @Override - protected Button createSettingsButton() { - Button settingsButton = SettingsDialog.createSettingsButton(this, stringMessages); - return settingsButton; - } - - @Override - public String getLocalizedShortName() { - return windChartLifecycle.getLocalizedShortName(); - } - - @Override - public Widget getEntryWidget() { - return this; - } - - private void updateVisibleSeries() { - List currentSeries = Arrays.asList(chart.getSeries()); - Set visibleSeries = new HashSet(currentSeries); - - if (settings.isShowWindDirectionsSeries()) { - for (Map.Entry e : windSourceDirectionSeries.entrySet()) { - Series series = e.getValue(); - if (settings.getWindDirectionSourcesToDisplay().contains(e.getKey().getType())) { - if (!visibleSeries.contains(series)) { - chart.addSeries(series, false, false); - series.select(true); // ensures that the checkbox will be ticked - } - } else { - if (visibleSeries.contains(series)) { - chart.removeSeries(series, false); - } - } - } - } else { - for (Map.Entry e : windSourceDirectionSeries.entrySet()) { - Series series = e.getValue(); - if (visibleSeries.contains(series)) { - chart.removeSeries(series, false); - } - } - } - - if (settings.isShowWindSpeedSeries()) { - for (Map.Entry e : windSourceSpeedSeries.entrySet()) { - Series series = e.getValue(); - if (settings.getWindSpeedSourcesToDisplay().contains(e.getKey().getType())) { - if (!visibleSeries.contains(series)) { - chart.addSeries(series, false, false); - series.select(true); // ensures that the checkbox will be ticked - } - } else { - if (visibleSeries.contains(series)) { - chart.removeSeries(series, false); - } - } - } - } else { - for (Map.Entry e : windSourceSpeedSeries.entrySet()) { - Series series = e.getValue(); - if (visibleSeries.contains(series)) { - chart.removeSeries(series, false); - } - } - } - onResize(); - } - - /** - * Creates the series for the windSource specified. If the series is created and needs to be visible - * based on the {@link #windDirectionSourcesToDisplay}, it is added to the chart. - */ - private Series getOrCreateSpeedSeries(WindSource windSource) { - Series result = windSourceSpeedSeries.get(windSource); - if (result == null) { - result = createSpeedSeries(windSource); - windSourceSpeedSeries.put(windSource, result); - } - return result; - } - - /** - * Creates the series for the windSource specified. If the series is created and needs to be visible - * based on the {@link #windDirectionSourcesToDisplay}, it is added to the chart. - */ - private Series getOrCreateDirectionSeries(WindSource windSource) { - Series result = windSourceDirectionSeries.get(windSource); - if (result == null) { - result = createDirectionSeries(windSource); - windSourceDirectionSeries.put(windSource, result); - } - return result; - } - - - /** - * Only creates the series but doesn't add it to the chart. See also {@link #getOrCreateDirectionSeries(WindSource)} and - * {@link #updateVisibleSeries()} - */ - private Series createDirectionSeries(WindSource windSource) { - Series newSeries = chart - .createSeries() - .setType(Series.Type.LINE) - .setName(stringMessages.fromDeg()+" "+WindSourceTypeFormatter.format(windSource, stringMessages)) - .setYAxis(0) - .setOption("turboThreshold", MAX_SERIES_POINTS) - .setPlotOptions(new LinePlotOptions().setColor(colorMap.getColorByID(windSource).getAsHtml()).setSelected(true)); - return newSeries; - } - - /** - * Only creates the series but doesn't add it to the chart. See also {@link #getOrCreateSpeedSeries(WindSource)} and - * {@link #updateVisibleSeries()} - */ - private Series createSpeedSeries(WindSource windSource) { - Series newSeries = chart - .createSeries() - .setType(Series.Type.LINE) - .setName(stringMessages.windSpeed()+" "+WindSourceTypeFormatter.format(windSource, stringMessages)) - .setYAxis(1) // use the second Y-axis - .setOption("turboThreshold", MAX_SERIES_POINTS) - .setPlotOptions(new LinePlotOptions().setDashStyle(PlotLine.DashStyle.SHORT_DOT) - .setLineWidth(3).setHoverStateLineWidth(3) - .setColor(colorMap.getColorByID(windSource).getAsHtml()).setSelected(true)); // show only the markers, not the connecting lines - return newSeries; - } - - /** - * Updates the wind charts with the wind data from result. If append is true, previously - * existing points in the chart are left unchanged. Otherwise, the existing wind series are replaced. - */ - public void updateChartSeries(WindInfoForRaceDTO result, boolean append) { - final NumberFormat numberFormat = NumberFormat.getFormat("0"); - Long newMinTimepoint = timeOfEarliestRequestInMillis; - Long newMaxTimepoint = timeOfLatestRequestInMillis; - - for (WindSource windSource: result.windTrackInfoByWindSource.keySet()) { - WindTrackInfoDTO windTrackInfo = result.windTrackInfoByWindSource.get(windSource); - Series directionSeries = getOrCreateDirectionSeries(windSource); - Series speedSeries = null; - if (windSource.getType().useSpeed()) { - speedSeries = getOrCreateSpeedSeries(windSource); - } - - Point previousDirectionPoint = null; - if (append && windSourceDirectionPoints.get(windSource) != null - && windSourceDirectionPoints.get(windSource).length != 0) { - previousDirectionPoint = windSourceDirectionPoints.get(windSource)[windSourceDirectionPoints.get(windSource).length - 1]; - } - Point[] directionPoints = new Point[windTrackInfo.windFixes.size()]; - Point[] speedPoints = new Point[windTrackInfo.windFixes.size()]; - int currentPointIndex = 0; - - for (WindDTO wind : windTrackInfo.windFixes) { - if (newMinTimepoint == null || wind.requestTimepoint < newMinTimepoint) { - newMinTimepoint = wind.requestTimepoint; - } - if (newMaxTimepoint == null || wind.requestTimepoint > newMaxTimepoint) { - newMaxTimepoint = wind.requestTimepoint; - } - - if ((timeOfEarliestRequestInMillis == null || wind.requestTimepoint < timeOfEarliestRequestInMillis) || - timeOfLatestRequestInMillis == null || wind.requestTimepoint > timeOfLatestRequestInMillis) { - Point newDirectionPoint = new Point(wind.requestTimepoint, wind.dampenedTrueWindFromDeg); - if (wind.dampenedTrueWindSpeedInKnots != null) { - String name = numberFormat.format(wind.dampenedTrueWindSpeedInKnots)+ stringMessages.knotsUnit(); - // name += " Confidence:" + wind.confidence; - newDirectionPoint.setName(name); - } - - if (previousDirectionPoint != null) { - newDirectionPoint = ChartPointRecalculator.stayClosestToPreviousPoint(previousDirectionPoint, - newDirectionPoint); - } else if (firstPointOfFirstSeries != null && windSourceDirectionPoints.get(windSource) == null) { - //This Point is the first point of a new series - newDirectionPoint = ChartPointRecalculator.stayClosestToPreviousPoint(firstPointOfFirstSeries, newDirectionPoint); - } - directionPoints[currentPointIndex] = newDirectionPoint; - previousDirectionPoint = newDirectionPoint; - - Point newSpeedPoint = new Point(wind.requestTimepoint, wind.dampenedTrueWindSpeedInKnots); - speedPoints[currentPointIndex++] = newSpeedPoint; - } - } - - Point[] newDirectionPoints; - Point[] newSpeedPoints = null; - if (append) { - Point[] oldDirectionPoints = windSourceDirectionPoints.get(windSource) != null ? windSourceDirectionPoints.get(windSource) : new Point[0]; - - newDirectionPoints = new Point[oldDirectionPoints.length + currentPointIndex]; - System.arraycopy(oldDirectionPoints, 0, newDirectionPoints, 0, oldDirectionPoints.length); - System.arraycopy(directionPoints, 0, newDirectionPoints, oldDirectionPoints.length, currentPointIndex); - if (windSource.getType().useSpeed()) { - Point[] oldSpeedPoints = windSourceSpeedPoints.get(windSource) != null ? windSourceSpeedPoints.get(windSource) : new Point[0]; - newSpeedPoints = new Point[oldSpeedPoints.length + currentPointIndex]; - System.arraycopy(oldSpeedPoints, 0, newSpeedPoints, 0, oldSpeedPoints.length); - System.arraycopy(speedPoints, 0, newSpeedPoints, oldSpeedPoints.length, currentPointIndex); - } - } else { - newDirectionPoints = directionPoints; - newSpeedPoints = speedPoints; - } - setSeriesPoints(directionSeries, newDirectionPoints); - windSourceDirectionPoints.put(windSource, newDirectionPoints); - if (windSource.getType().useSpeed()) { - setSeriesPoints(speedSeries, newSpeedPoints); - windSourceSpeedPoints.put(windSource, newSpeedPoints); - } - - if (firstPointOfFirstSeries == null && newDirectionPoints.length != 0) { //If firstPointOfFirstSeries is null, than this series is the first - firstPointOfFirstSeries = newDirectionPoints[0]; - } - } - - timeOfEarliestRequestInMillis = newMinTimepoint; - timeOfLatestRequestInMillis = newMaxTimepoint; - } - - @Override - public boolean hasSettings() { - return windChartLifecycle.hasSettings(); - } - - @Override - public SettingsDialogComponent getSettingsDialogComponent() { - return windChartLifecycle.getSettingsDialogComponent(settings); - } - - /** - * Sets the visibilities of the wind source series based on the new settings. Note that this does not - * re-load any wind data. This has to happen by calling {@link #updateChartSeries(WindInfoForRaceDTO, boolean)}. - */ - @Override - public void updateSettings(WindChartSettings newSettings) { - boolean clearCacheAndReload = false; - final Set oldWindSourceTypesToRequest = getNamesOfWindSourceTypesOfWhichToDisplaySpeedOrDirection(); - if (newSettings.getResolutionInMilliseconds() != settings.getResolutionInMilliseconds()) { - settings.setResolutionInMilliseconds(newSettings.getResolutionInMilliseconds()); - clearCacheAndReload = true; - } - settings.setShowWindDirectionsSeries(newSettings.isShowWindDirectionsSeries()); - settings.setWindDirectionSourcesToDisplay(newSettings.getWindDirectionSourcesToDisplay()); - - settings.setShowWindSpeedSeries(newSettings.isShowWindSpeedSeries()); - settings.setWindSpeedSourcesToDisplay(newSettings.getWindSpeedSourcesToDisplay()); - if (!oldWindSourceTypesToRequest.equals(getNamesOfWindSourceTypesOfWhichToDisplaySpeedOrDirection())) { - clearCacheAndReload = true; - } - if (clearCacheAndReload) { - clearCacheAndReload(); - } - updateVisibleSeries(); - } - - private void clearCacheAndReload() { - timeOfEarliestRequestInMillis = null; - timeOfLatestRequestInMillis = null; - windSourceDirectionPoints.clear(); - windSourceSpeedPoints.clear(); - firstPointOfFirstSeries = null; - loadData(timeRangeWithZoomProvider.getFromTime(), timeRangeWithZoomProvider.getToTime(), /* append */false); - } - - /** - * @param append - * if true, the results retrieved from the server will be appended to the wind chart instead - * of overwriting the existing series. - */ - private void loadData(final Date from, final Date to, final boolean append) { - if (isVisible()) { - if (selectedRaceIdentifier == null) { - clearChart(); - } else if (needsDataLoading() && from != null && to != null) { - setWidget(chart); - // if not playing or empty show loading message - if (shouldShowLoading(timeOfLatestRequestInMillis)) { - showLoading(stringMessages.windChartLoading()); - } - GetWindInfoAction getWindInfoAction = new GetWindInfoAction(sailingService, selectedRaceIdentifier, - from, to, settings.getResolutionInMilliseconds(), getNamesOfWindSourceTypesOfWhichToDisplaySpeedOrDirection(), - /* onlyUpToNewestEvent==true because we don't want - to overshoot the evidence so far */ true); - asyncActionsExecutor.execute(getWindInfoAction, LOAD_WIND_CHART_DATA_CATEGORY, - new AsyncCallback() { - @Override - public void onSuccess(WindInfoForRaceDTO result) { - if (result != null) { - updateChartSeries(result, append); - updateVisibleSeries(); - } else { - if (!append) { - clearChart(); // no wind known for untracked race - } - } - hideLoading(); - } - - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError(stringMessages.errorFetchingWindInformationForRace() + " " - + selectedRaceIdentifier + ": " + caught.getMessage(), timer.getPlayMode() == PlayModes.Live); - hideLoading(); - } - }); - } - } - } - - private Set getNamesOfWindSourceTypesOfWhichToDisplaySpeedOrDirection() { - final Set result = new HashSet<>(); - for (WindSourceType speedType : getSettings().getWindSpeedSourcesToDisplay()) { - result.add(speedType.name()); - } - for (WindSourceType speedType : getSettings().getWindDirectionSourcesToDisplay()) { - result.add(speedType.name()); - } - return result; - } - - private void clearChart() { - chart.removeAllSeries(); - } - - /** - * If in live mode, fetches what's missing since the last fix and date. If nothing has been loaded yet, - * loads from the beginning up to date. If in replay mode, checks if anything has been loaded at all. If not, - * everything for the currently selected race is loaded; otherwise, no-op. - */ - @Override - public void timeChanged(Date newTime, Date oldTime) { - if (isVisible()) { - updateTimePlotLine(newTime); - switch (timer.getPlayMode()) { - case Live: - { - // is date before first cache entry or is cache empty? - if (timeOfEarliestRequestInMillis == null || newTime.getTime() < timeOfEarliestRequestInMillis) { - loadData(timeRangeWithZoomProvider.getFromTime(), newTime, /* append */ true); - } else if (newTime.getTime() > timeOfLatestRequestInMillis) { - loadData(new Date(timeOfLatestRequestInMillis), timeRangeWithZoomProvider.getToTime(), /* append */true); - } - // otherwise the cache spans across date and so we don't need to load anything - break; - } - case Replay: - { - if (timeOfLatestRequestInMillis == null) { - // pure replay mode - loadData(timeRangeWithZoomProvider.getFromTime(), timeRangeWithZoomProvider.getToTime(), /* append */false); - } else { - // replay mode during live play - if (timeOfEarliestRequestInMillis == null || newTime.getTime() < timeOfEarliestRequestInMillis) { - loadData(timeRangeWithZoomProvider.getFromTime(), newTime, /* append */ true); - } else if (newTime.getTime() > timeOfLatestRequestInMillis) { - loadData(new Date(timeOfLatestRequestInMillis), newTime, /* append */true); - } - } - break; - } - } - } - } - - @Override - public void onResize() { - chart.setSizeToMatchContainer(); - // it's important here to recall the redraw method, otherwise the bug fix for wrong checkbox positions (nativeAdjustCheckboxPosition) - // in the BaseChart class would not be called - chart.redraw(); - } - - private boolean needsDataLoading() { - return isVisible(); - } - - /** - * Prints basic data and points of a WindInfoForRaceDTO object to a formatted string. - * Can be used for debugging - */ - @SuppressWarnings("unused") - private String printWindInfoForRace(final Date from, final Date to, WindInfoForRaceDTO result, boolean printFixDetails) { - DateTimeFormatRenderer timeFormatter = new DateTimeFormatRenderer(DateTimeFormat.getFormat("HH:mm:ss:SSS")); - StringBuffer buffer = new StringBuffer(); - buffer.append("\n"); - buffer.append("Loaded wind data..." + "\n"); - buffer.append("From: " + timeFormatter.render(from) + "\n"); - buffer.append("To: " + timeFormatter.render(to) + "\n"); - buffer.append("With resolution: " + settings.getResolutionInMilliseconds() + "\n"); - - for(WindSource windSource: result.windTrackInfoByWindSource.keySet()) { - WindTrackInfoDTO windTrackInfoDTO = result.windTrackInfoByWindSource.get(windSource); - int i = 1; - buffer.append("Data of windsource: " + windSource.name() + "\n"); - if(printFixDetails) { - for(WindDTO windDTO: windTrackInfoDTO.windFixes) { - String windFix = "P" + i++ + ": " + timeFormatter.render(new Date(windDTO.requestTimepoint)); - if(windDTO.measureTimepoint != null) { - windFix += " ," + timeFormatter.render(new Date(windDTO.measureTimepoint)); - } - buffer.append(windFix + "\n"); - } - } else { - buffer.append(Util.size(windTrackInfoDTO.windFixes) + " Fixes" + "\n"); - } - } - buffer.append("\n"); - return buffer.toString(); - } - - /** - * Prints basic data and all points of a windSource to a formatted string. - * Can be used for debugging - */ - @SuppressWarnings("unused") - private String printPoints(WindSource windSource, String whatIsIt, Point[] points, boolean printFixDetails) { - StringBuffer buffer = new StringBuffer(); - DateTimeFormatRenderer timeFormatter = new DateTimeFormatRenderer(DateTimeFormat.getFormat("HH:mm:ss:SSS")); - buffer.append("\n"); - buffer.append("WindSource: " + windSource.name() + ": " + whatIsIt + "\n"); - buffer.append("Resolution in ms: " + settings.getResolutionInMilliseconds() + "\n"); - buffer.append("timeOfEarliestRequest: " + (timeOfEarliestRequestInMillis != null ? timeFormatter.render(new Date(timeOfEarliestRequestInMillis)) : "") + "\n"); - buffer.append("timeOfLatestRequest: " + (timeOfLatestRequestInMillis != null ? timeFormatter.render(new Date(timeOfLatestRequestInMillis)) : "") + "\n"); - if (points == null) { - buffer.append("Points is null" + "\n"); - } else { - buffer.append("Point count: " + points.length + "\n"); - if(printFixDetails) { - Date xAsDate = new Date(); - for (int i = 0; i < points.length; i++) { - Point point = points[i]; - xAsDate.setTime(point.getX().longValue()); - buffer.append("P" + (i + 1) + ": " + timeFormatter.render(xAsDate) + ", V: " + point.getY() + "\n"); - } - } - } - buffer.append("\n"); - return buffer.toString(); - } - - @Override - public String getDependentCssClassName() { - return "windChart"; - } - - @Override - public WindChartSettings getSettings() { - return settings; - } - - @Override - public String getId() { - return windChartLifecycle.getComponentId(); - } -} +package com.sap.sailing.gwt.ui.client.shared.charts; + +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.moxieapps.gwt.highcharts.client.Axis; +import org.moxieapps.gwt.highcharts.client.BaseChart; +import org.moxieapps.gwt.highcharts.client.Chart; +import org.moxieapps.gwt.highcharts.client.ChartSubtitle; +import org.moxieapps.gwt.highcharts.client.ChartTitle; +import org.moxieapps.gwt.highcharts.client.Color; +import org.moxieapps.gwt.highcharts.client.Credits; +import org.moxieapps.gwt.highcharts.client.PlotLine; +import org.moxieapps.gwt.highcharts.client.PlotLine.DashStyle; +import org.moxieapps.gwt.highcharts.client.Point; +import org.moxieapps.gwt.highcharts.client.Series; +import org.moxieapps.gwt.highcharts.client.ToolTip; +import org.moxieapps.gwt.highcharts.client.ToolTipData; +import org.moxieapps.gwt.highcharts.client.ToolTipFormatter; +import org.moxieapps.gwt.highcharts.client.events.ChartClickEvent; +import org.moxieapps.gwt.highcharts.client.events.ChartClickEventHandler; +import org.moxieapps.gwt.highcharts.client.events.ChartSelectionEvent; +import org.moxieapps.gwt.highcharts.client.events.ChartSelectionEventHandler; +import org.moxieapps.gwt.highcharts.client.labels.AxisLabelsData; +import org.moxieapps.gwt.highcharts.client.labels.AxisLabelsFormatter; +import org.moxieapps.gwt.highcharts.client.labels.XAxisLabels; +import org.moxieapps.gwt.highcharts.client.labels.YAxisLabels; +import org.moxieapps.gwt.highcharts.client.plotOptions.LinePlotOptions; +import org.moxieapps.gwt.highcharts.client.plotOptions.Marker; + +import com.google.gwt.i18n.client.DateTimeFormat; +import com.google.gwt.i18n.client.NumberFormat; +import com.google.gwt.text.client.DateTimeFormatRenderer; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.RequiresResize; +import com.google.gwt.user.client.ui.Widget; +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.domain.common.WindSource; +import com.sap.sailing.domain.common.WindSourceType; +import com.sap.sailing.domain.common.impl.ColorMapImpl; +import com.sap.sailing.gwt.ui.actions.GetWindInfoAction; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.client.WindSourceTypeFormatter; +import com.sap.sailing.gwt.ui.shared.WindDTO; +import com.sap.sailing.gwt.ui.shared.WindInfoForRaceDTO; +import com.sap.sailing.gwt.ui.shared.WindTrackInfoDTO; +import com.sap.sse.common.Util; +import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.async.AsyncActionsExecutor; +import com.sap.sse.gwt.client.player.TimeRangeWithZoomProvider; +import com.sap.sse.gwt.client.player.Timer; +import com.sap.sse.gwt.client.player.Timer.PlayModes; +import com.sap.sse.gwt.client.shared.components.Component; +import com.sap.sse.gwt.client.shared.components.SettingsDialog; +import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; + +public class WindChart extends AbstractRaceChart implements RequiresResize { + public static final String LOAD_WIND_CHART_DATA_CATEGORY = "loadWindChartData"; + + private static final int LINE_WIDTH = 1; + + private final WindChartSettings settings; + private final WindChartLifecycle windChartLifecycle; + + /** + * Holds one series for each wind source for which data has been received. + */ + private final Map windSourceDirectionSeries; + private final Map windSourceSpeedSeries; + private final Map windSourceDirectionPoints; + private final Map windSourceSpeedPoints; + private Point firstPointOfFirstSeries; + + private Long timeOfEarliestRequestInMillis; + private Long timeOfLatestRequestInMillis; + + private final ColorMapImpl colorMap; + + /** + * @param selectedRaceIdentifier + * if null, this chart won't update its contents automatically upon race selection change; + * otherwise, whenever the selection changes, the wind data of the race selected now is loaded from the + * server and displayed in this chart. If no race is selected, the chart is cleared. The caller of this + * constructor must ensure to trigger {@link RaceSelectionChangeListener#onRaceSelectionChange(List)} at + * least once to ensure that this chart sets its {@link AbstractRaceChart#selectedRaceIdentifier} field. + */ + public WindChart(Component parent, ComponentContext context, WindChartLifecycle windChartLifecycle, + SailingServiceAsync sailingService, + RegattaAndRaceIdentifier selectedRaceIdentifier, Timer timer, + TimeRangeWithZoomProvider timeRangeWithZoomProvider, WindChartSettings settings, final StringMessages stringMessages, + AsyncActionsExecutor asyncActionsExecutor, ErrorReporter errorReporter, boolean compactChart) { + super(parent, context, sailingService, selectedRaceIdentifier, timer, timeRangeWithZoomProvider, stringMessages, + asyncActionsExecutor, errorReporter); + this.windChartLifecycle = windChartLifecycle; + this.settings = settings; + windSourceDirectionSeries = new HashMap(); + windSourceSpeedSeries = new HashMap(); + windSourceDirectionPoints = new HashMap(); + windSourceSpeedPoints = new HashMap(); + firstPointOfFirstSeries = null; + colorMap = new ColorMapImpl(); + chart = new Chart() + .setPersistent(true) + .setReflow(false) + .setZoomType(BaseChart.ZoomType.X) + .setMarginLeft(65) + .setMarginRight(65) + .setWidth100() + .setHeight100() + .setBorderColor(new Color("#CACACA")) + .setBorderWidth(0) + .setBorderRadius(0) + .setBackgroundColor(new Color("#FFFFFF")) + .setPlotBackgroundColor("#f8f8f8") + .setPlotBorderWidth(0) + .setCredits(new Credits().setEnabled(false)) + .setChartTitle(new ChartTitle().setText(stringMessages.wind()).setOption("floating",true)) + .setChartSubtitle(new ChartSubtitle().setText(stringMessages.clickAndDragToZoomIn())) + .setLinePlotOptions(new LinePlotOptions().setLineWidth(LINE_WIDTH).setMarker( + new Marker().setEnabled(false).setHoverState( + new Marker().setEnabled(true).setRadius(4))).setShadow(false) + .setHoverStateLineWidth(LINE_WIDTH)); + chart.setStyleName(chartsCss.chartStyle()); + ChartUtil.useCheckboxesToShowAndHide(chart); + final NumberFormat numberFormat = NumberFormat.getFormat("0"); + chart.setToolTip(new ToolTip().setEnabled(true).setFormatter(new ToolTipFormatter() { + @Override + public String format(ToolTipData toolTipData) { + String seriesName = toolTipData.getSeriesName(); + if (seriesName.equals(WindChart.this.stringMessages.time())) { + return "" + seriesName + ": " + dateFormat.format(new Date(toolTipData.getXAsLong())) + + "
(" + stringMessages.clickChartToSetTime() + ")"; + } else if (seriesName.startsWith(stringMessages.fromDeg()+" ")) { + double value = toolTipData.getYAsDouble() % 360; + return "" + seriesName + (toolTipData.getPointName() != null ? " "+toolTipData.getPointName() : "") + + "
" + + dateFormat.format(new Date(toolTipData.getXAsLong())) + ": " + + numberFormat.format(value < 0 ? value + 360 : value) + stringMessages.degreesShort(); + } else { + return "" + seriesName + (toolTipData.getPointName() != null ? " "+toolTipData.getPointName() : "") + + "
" + + dateFormat.format(new Date(toolTipData.getXAsLong())) + ": " + + numberFormat.format(toolTipData.getYAsDouble()) + stringMessages.knotsUnit(); + } + } + })); + + chart.setClickEventHandler(new ChartClickEventHandler() { + @Override + public boolean onClick(ChartClickEvent chartClickEvent) { + return WindChart.this.onClick(chartClickEvent); + } + }); + + chart.setSelectionEventHandler(new ChartSelectionEventHandler() { + @Override + public boolean onSelection(ChartSelectionEvent chartSelectionEvent) { + return WindChart.this.onXAxisSelectionChange(chartSelectionEvent); + } + }); + + chart.getXAxis().setType(Axis.Type.DATE_TIME) + .setMaxZoom(60 * 1000) // 1 minute + .setAxisTitleText(stringMessages.time()); + chart.getXAxis().setLabels(new XAxisLabels().setFormatter(new AxisLabelsFormatter() { + @Override + public String format(AxisLabelsData axisLabelsData) { + return dateFormatHoursMinutes.format(new Date(axisLabelsData.getValueAsLong())); + } + })); + timePlotLine = chart.getXAxis().createPlotLine().setColor("#656565").setWidth(1.5).setDashStyle(DashStyle.SOLID); + + chart.getYAxis(0).setAxisTitleText(stringMessages.fromDeg()).setStartOnTick(false) + .setLabels(new YAxisLabels().setFormatter(new AxisLabelsFormatter() { + @Override + public String format(AxisLabelsData axisLabelsData) { + long value = axisLabelsData.getValueAsLong() % 360; + return new Long(value < 0 ? value + 360 : value).toString(); + } + })); + chart.getYAxis(1).setOpposite(true).setAxisTitleText(stringMessages.speed()+" ("+stringMessages.knotsUnit()+")").setMin(0) + .setMaxPadding(0.05).setStartOnTick(false).setGridLineWidth(0).setMinorGridLineWidth(0); + if (compactChart) { + chart.setSpacingBottom(10).setSpacingLeft(10).setSpacingRight(10).setSpacingTop(20) + .setOption("legend/margin", 2) + .setOption("title/margin", 5) + .setChartSubtitle(null) + .getXAxis().setAxisTitle(null); + } + setSize("100%", "100%"); + if (selectedRaceIdentifier != null) { + clearCacheAndReload(); + if (isVisible()) { + updateVisibleSeries(); + } + } else { + clearChart(); + } + } + + @Override + protected Button createSettingsButton() { + Button settingsButton = SettingsDialog.createSettingsButton(this, stringMessages); + return settingsButton; + } + + @Override + public String getLocalizedShortName() { + return windChartLifecycle.getLocalizedShortName(); + } + + @Override + public Widget getEntryWidget() { + return this; + } + + private void updateVisibleSeries() { + List currentSeries = Arrays.asList(chart.getSeries()); + Set visibleSeries = new HashSet(currentSeries); + + if (settings.isShowWindDirectionsSeries()) { + for (Map.Entry e : windSourceDirectionSeries.entrySet()) { + Series series = e.getValue(); + if (settings.getWindDirectionSourcesToDisplay().contains(e.getKey().getType())) { + if (!visibleSeries.contains(series)) { + chart.addSeries(series, false, false); + series.select(true); // ensures that the checkbox will be ticked + } + } else { + if (visibleSeries.contains(series)) { + chart.removeSeries(series, false); + } + } + } + } else { + for (Map.Entry e : windSourceDirectionSeries.entrySet()) { + Series series = e.getValue(); + if (visibleSeries.contains(series)) { + chart.removeSeries(series, false); + } + } + } + + if (settings.isShowWindSpeedSeries()) { + for (Map.Entry e : windSourceSpeedSeries.entrySet()) { + Series series = e.getValue(); + if (settings.getWindSpeedSourcesToDisplay().contains(e.getKey().getType())) { + if (!visibleSeries.contains(series)) { + chart.addSeries(series, false, false); + series.select(true); // ensures that the checkbox will be ticked + } + } else { + if (visibleSeries.contains(series)) { + chart.removeSeries(series, false); + } + } + } + } else { + for (Map.Entry e : windSourceSpeedSeries.entrySet()) { + Series series = e.getValue(); + if (visibleSeries.contains(series)) { + chart.removeSeries(series, false); + } + } + } + onResize(); + } + + /** + * Creates the series for the windSource specified. If the series is created and needs to be visible + * based on the {@link #windDirectionSourcesToDisplay}, it is added to the chart. + */ + private Series getOrCreateSpeedSeries(WindSource windSource) { + Series result = windSourceSpeedSeries.get(windSource); + if (result == null) { + result = createSpeedSeries(windSource); + windSourceSpeedSeries.put(windSource, result); + } + return result; + } + + /** + * Creates the series for the windSource specified. If the series is created and needs to be visible + * based on the {@link #windDirectionSourcesToDisplay}, it is added to the chart. + */ + private Series getOrCreateDirectionSeries(WindSource windSource) { + Series result = windSourceDirectionSeries.get(windSource); + if (result == null) { + result = createDirectionSeries(windSource); + windSourceDirectionSeries.put(windSource, result); + } + return result; + } + + + /** + * Only creates the series but doesn't add it to the chart. See also {@link #getOrCreateDirectionSeries(WindSource)} and + * {@link #updateVisibleSeries()} + */ + private Series createDirectionSeries(WindSource windSource) { + Series newSeries = chart + .createSeries() + .setType(Series.Type.LINE) + .setName(stringMessages.fromDeg()+" "+WindSourceTypeFormatter.format(windSource, stringMessages)) + .setYAxis(0) + .setOption("turboThreshold", MAX_SERIES_POINTS) + .setPlotOptions(new LinePlotOptions().setColor(colorMap.getColorByID(windSource).getAsHtml()).setSelected(true)); + return newSeries; + } + + /** + * Only creates the series but doesn't add it to the chart. See also {@link #getOrCreateSpeedSeries(WindSource)} and + * {@link #updateVisibleSeries()} + */ + private Series createSpeedSeries(WindSource windSource) { + Series newSeries = chart + .createSeries() + .setType(Series.Type.LINE) + .setName(stringMessages.windSpeed()+" "+WindSourceTypeFormatter.format(windSource, stringMessages)) + .setYAxis(1) // use the second Y-axis + .setOption("turboThreshold", MAX_SERIES_POINTS) + .setPlotOptions(new LinePlotOptions().setDashStyle(PlotLine.DashStyle.SHORT_DOT) + .setLineWidth(3).setHoverStateLineWidth(3) + .setColor(colorMap.getColorByID(windSource).getAsHtml()).setSelected(true)); // show only the markers, not the connecting lines + return newSeries; + } + + /** + * Updates the wind charts with the wind data from result. If append is true, previously + * existing points in the chart are left unchanged. Otherwise, the existing wind series are replaced. + */ + public void updateChartSeries(WindInfoForRaceDTO result, boolean append) { + final NumberFormat numberFormat = NumberFormat.getFormat("0"); + Long newMinTimepoint = timeOfEarliestRequestInMillis; + Long newMaxTimepoint = timeOfLatestRequestInMillis; + + for (WindSource windSource: result.windTrackInfoByWindSource.keySet()) { + WindTrackInfoDTO windTrackInfo = result.windTrackInfoByWindSource.get(windSource); + Series directionSeries = getOrCreateDirectionSeries(windSource); + Series speedSeries = null; + if (windSource.getType().useSpeed()) { + speedSeries = getOrCreateSpeedSeries(windSource); + } + + Point previousDirectionPoint = null; + if (append && windSourceDirectionPoints.get(windSource) != null + && windSourceDirectionPoints.get(windSource).length != 0) { + previousDirectionPoint = windSourceDirectionPoints.get(windSource)[windSourceDirectionPoints.get(windSource).length - 1]; + } + Point[] directionPoints = new Point[windTrackInfo.windFixes.size()]; + Point[] speedPoints = new Point[windTrackInfo.windFixes.size()]; + int currentPointIndex = 0; + + for (WindDTO wind : windTrackInfo.windFixes) { + if (newMinTimepoint == null || wind.requestTimepoint < newMinTimepoint) { + newMinTimepoint = wind.requestTimepoint; + } + if (newMaxTimepoint == null || wind.requestTimepoint > newMaxTimepoint) { + newMaxTimepoint = wind.requestTimepoint; + } + + if ((timeOfEarliestRequestInMillis == null || wind.requestTimepoint < timeOfEarliestRequestInMillis) || + timeOfLatestRequestInMillis == null || wind.requestTimepoint > timeOfLatestRequestInMillis) { + Point newDirectionPoint = new Point(wind.requestTimepoint, wind.dampenedTrueWindFromDeg); + if (wind.dampenedTrueWindSpeedInKnots != null) { + String name = numberFormat.format(wind.dampenedTrueWindSpeedInKnots)+ stringMessages.knotsUnit(); + // name += " Confidence:" + wind.confidence; + newDirectionPoint.setName(name); + } + + if (previousDirectionPoint != null) { + newDirectionPoint = ChartPointRecalculator.stayClosestToPreviousPoint(previousDirectionPoint, + newDirectionPoint); + } else if (firstPointOfFirstSeries != null && windSourceDirectionPoints.get(windSource) == null) { + //This Point is the first point of a new series + newDirectionPoint = ChartPointRecalculator.stayClosestToPreviousPoint(firstPointOfFirstSeries, newDirectionPoint); + } + directionPoints[currentPointIndex] = newDirectionPoint; + previousDirectionPoint = newDirectionPoint; + + Point newSpeedPoint = new Point(wind.requestTimepoint, wind.dampenedTrueWindSpeedInKnots); + speedPoints[currentPointIndex++] = newSpeedPoint; + } + } + + Point[] newDirectionPoints; + Point[] newSpeedPoints = null; + if (append) { + Point[] oldDirectionPoints = windSourceDirectionPoints.get(windSource) != null ? windSourceDirectionPoints.get(windSource) : new Point[0]; + + newDirectionPoints = new Point[oldDirectionPoints.length + currentPointIndex]; + System.arraycopy(oldDirectionPoints, 0, newDirectionPoints, 0, oldDirectionPoints.length); + System.arraycopy(directionPoints, 0, newDirectionPoints, oldDirectionPoints.length, currentPointIndex); + if (windSource.getType().useSpeed()) { + Point[] oldSpeedPoints = windSourceSpeedPoints.get(windSource) != null ? windSourceSpeedPoints.get(windSource) : new Point[0]; + newSpeedPoints = new Point[oldSpeedPoints.length + currentPointIndex]; + System.arraycopy(oldSpeedPoints, 0, newSpeedPoints, 0, oldSpeedPoints.length); + System.arraycopy(speedPoints, 0, newSpeedPoints, oldSpeedPoints.length, currentPointIndex); + } + } else { + newDirectionPoints = directionPoints; + newSpeedPoints = speedPoints; + } + setSeriesPoints(directionSeries, newDirectionPoints); + windSourceDirectionPoints.put(windSource, newDirectionPoints); + if (windSource.getType().useSpeed()) { + setSeriesPoints(speedSeries, newSpeedPoints); + windSourceSpeedPoints.put(windSource, newSpeedPoints); + } + + if (firstPointOfFirstSeries == null && newDirectionPoints.length != 0) { //If firstPointOfFirstSeries is null, than this series is the first + firstPointOfFirstSeries = newDirectionPoints[0]; + } + } + + timeOfEarliestRequestInMillis = newMinTimepoint; + timeOfLatestRequestInMillis = newMaxTimepoint; + } + + @Override + public boolean hasSettings() { + return windChartLifecycle.hasSettings(); + } + + @Override + public SettingsDialogComponent getSettingsDialogComponent() { + return windChartLifecycle.getSettingsDialogComponent(settings); + } + + /** + * Sets the visibilities of the wind source series based on the new settings. Note that this does not + * re-load any wind data. This has to happen by calling {@link #updateChartSeries(WindInfoForRaceDTO, boolean)}. + */ + @Override + public void updateSettings(WindChartSettings newSettings) { + boolean clearCacheAndReload = false; + final Set oldWindSourceTypesToRequest = getNamesOfWindSourceTypesOfWhichToDisplaySpeedOrDirection(); + if (newSettings.getResolutionInMilliseconds() != settings.getResolutionInMilliseconds()) { + settings.setResolutionInMilliseconds(newSettings.getResolutionInMilliseconds()); + clearCacheAndReload = true; + } + settings.setShowWindDirectionsSeries(newSettings.isShowWindDirectionsSeries()); + settings.setWindDirectionSourcesToDisplay(newSettings.getWindDirectionSourcesToDisplay()); + + settings.setShowWindSpeedSeries(newSettings.isShowWindSpeedSeries()); + settings.setWindSpeedSourcesToDisplay(newSettings.getWindSpeedSourcesToDisplay()); + if (!oldWindSourceTypesToRequest.equals(getNamesOfWindSourceTypesOfWhichToDisplaySpeedOrDirection())) { + clearCacheAndReload = true; + } + if (clearCacheAndReload) { + clearCacheAndReload(); + } + updateVisibleSeries(); + } + + private void clearCacheAndReload() { + timeOfEarliestRequestInMillis = null; + timeOfLatestRequestInMillis = null; + windSourceDirectionPoints.clear(); + windSourceSpeedPoints.clear(); + firstPointOfFirstSeries = null; + loadData(timeRangeWithZoomProvider.getFromTime(), timeRangeWithZoomProvider.getToTime(), /* append */false); + } + + /** + * @param append + * if true, the results retrieved from the server will be appended to the wind chart instead + * of overwriting the existing series. + */ + private void loadData(final Date from, final Date to, final boolean append) { + if (isVisible()) { + if (selectedRaceIdentifier == null) { + clearChart(); + } else if (needsDataLoading() && from != null && to != null) { + setWidget(chart); + // if not playing or empty show loading message + if (shouldShowLoading(timeOfLatestRequestInMillis)) { + showLoading(stringMessages.windChartLoading()); + } + GetWindInfoAction getWindInfoAction = new GetWindInfoAction(sailingService, selectedRaceIdentifier, + from, to, settings.getResolutionInMilliseconds(), getNamesOfWindSourceTypesOfWhichToDisplaySpeedOrDirection(), + /* onlyUpToNewestEvent==true because we don't want + to overshoot the evidence so far */ true); + asyncActionsExecutor.execute(getWindInfoAction, LOAD_WIND_CHART_DATA_CATEGORY, + new AsyncCallback() { + @Override + public void onSuccess(WindInfoForRaceDTO result) { + if (result != null) { + updateChartSeries(result, append); + updateVisibleSeries(); + } else { + if (!append) { + clearChart(); // no wind known for untracked race + } + } + hideLoading(); + } + + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError(stringMessages.errorFetchingWindInformationForRace() + " " + + selectedRaceIdentifier + ": " + caught.getMessage(), timer.getPlayMode() == PlayModes.Live); + hideLoading(); + } + }); + } + } + } + + private Set getNamesOfWindSourceTypesOfWhichToDisplaySpeedOrDirection() { + final Set result = new HashSet<>(); + for (WindSourceType speedType : getSettings().getWindSpeedSourcesToDisplay()) { + result.add(speedType.name()); + } + for (WindSourceType speedType : getSettings().getWindDirectionSourcesToDisplay()) { + result.add(speedType.name()); + } + return result; + } + + private void clearChart() { + chart.removeAllSeries(); + } + + /** + * If in live mode, fetches what's missing since the last fix and date. If nothing has been loaded yet, + * loads from the beginning up to date. If in replay mode, checks if anything has been loaded at all. If not, + * everything for the currently selected race is loaded; otherwise, no-op. + */ + @Override + public void timeChanged(Date newTime, Date oldTime) { + if (isVisible()) { + updateTimePlotLine(newTime); + switch (timer.getPlayMode()) { + case Live: + { + // is date before first cache entry or is cache empty? + if (timeOfEarliestRequestInMillis == null || newTime.getTime() < timeOfEarliestRequestInMillis) { + loadData(timeRangeWithZoomProvider.getFromTime(), newTime, /* append */ true); + } else if (newTime.getTime() > timeOfLatestRequestInMillis) { + loadData(new Date(timeOfLatestRequestInMillis), timeRangeWithZoomProvider.getToTime(), /* append */true); + } + // otherwise the cache spans across date and so we don't need to load anything + break; + } + case Replay: + { + if (timeOfLatestRequestInMillis == null) { + // pure replay mode + loadData(timeRangeWithZoomProvider.getFromTime(), timeRangeWithZoomProvider.getToTime(), /* append */false); + } else { + // replay mode during live play + if (timeOfEarliestRequestInMillis == null || newTime.getTime() < timeOfEarliestRequestInMillis) { + loadData(timeRangeWithZoomProvider.getFromTime(), newTime, /* append */ true); + } else if (newTime.getTime() > timeOfLatestRequestInMillis) { + loadData(new Date(timeOfLatestRequestInMillis), newTime, /* append */true); + } + } + break; + } + } + } + } + + @Override + public void onResize() { + chart.setSizeToMatchContainer(); + // it's important here to recall the redraw method, otherwise the bug fix for wrong checkbox positions (nativeAdjustCheckboxPosition) + // in the BaseChart class would not be called + chart.redraw(); + } + + private boolean needsDataLoading() { + return isVisible(); + } + + /** + * Prints basic data and points of a WindInfoForRaceDTO object to a formatted string. + * Can be used for debugging + */ + @SuppressWarnings("unused") + private String printWindInfoForRace(final Date from, final Date to, WindInfoForRaceDTO result, boolean printFixDetails) { + DateTimeFormatRenderer timeFormatter = new DateTimeFormatRenderer(DateTimeFormat.getFormat("HH:mm:ss:SSS")); + StringBuffer buffer = new StringBuffer(); + buffer.append("\n"); + buffer.append("Loaded wind data..." + "\n"); + buffer.append("From: " + timeFormatter.render(from) + "\n"); + buffer.append("To: " + timeFormatter.render(to) + "\n"); + buffer.append("With resolution: " + settings.getResolutionInMilliseconds() + "\n"); + + for(WindSource windSource: result.windTrackInfoByWindSource.keySet()) { + WindTrackInfoDTO windTrackInfoDTO = result.windTrackInfoByWindSource.get(windSource); + int i = 1; + buffer.append("Data of windsource: " + windSource.name() + "\n"); + if(printFixDetails) { + for(WindDTO windDTO: windTrackInfoDTO.windFixes) { + String windFix = "P" + i++ + ": " + timeFormatter.render(new Date(windDTO.requestTimepoint)); + if(windDTO.measureTimepoint != null) { + windFix += " ," + timeFormatter.render(new Date(windDTO.measureTimepoint)); + } + buffer.append(windFix + "\n"); + } + } else { + buffer.append(Util.size(windTrackInfoDTO.windFixes) + " Fixes" + "\n"); + } + } + buffer.append("\n"); + return buffer.toString(); + } + + /** + * Prints basic data and all points of a windSource to a formatted string. + * Can be used for debugging + */ + @SuppressWarnings("unused") + private String printPoints(WindSource windSource, String whatIsIt, Point[] points, boolean printFixDetails) { + StringBuffer buffer = new StringBuffer(); + DateTimeFormatRenderer timeFormatter = new DateTimeFormatRenderer(DateTimeFormat.getFormat("HH:mm:ss:SSS")); + buffer.append("\n"); + buffer.append("WindSource: " + windSource.name() + ": " + whatIsIt + "\n"); + buffer.append("Resolution in ms: " + settings.getResolutionInMilliseconds() + "\n"); + buffer.append("timeOfEarliestRequest: " + (timeOfEarliestRequestInMillis != null ? timeFormatter.render(new Date(timeOfEarliestRequestInMillis)) : "") + "\n"); + buffer.append("timeOfLatestRequest: " + (timeOfLatestRequestInMillis != null ? timeFormatter.render(new Date(timeOfLatestRequestInMillis)) : "") + "\n"); + if (points == null) { + buffer.append("Points is null" + "\n"); + } else { + buffer.append("Point count: " + points.length + "\n"); + if(printFixDetails) { + Date xAsDate = new Date(); + for (int i = 0; i < points.length; i++) { + Point point = points[i]; + xAsDate.setTime(point.getX().longValue()); + buffer.append("P" + (i + 1) + ": " + timeFormatter.render(xAsDate) + ", V: " + point.getY() + "\n"); + } + } + } + buffer.append("\n"); + return buffer.toString(); + } + + @Override + public String getDependentCssClassName() { + return "windChart"; + } + + @Override + public WindChartSettings getSettings() { + return settings; + } + + @Override + public String getId() { + return windChartLifecycle.getComponentId(); + } +} 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 406a9b6b0d2..0fa786f0b30 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 @@ -1,2932 +1,2932 @@ -package com.sap.sailing.gwt.ui.client.shared.racemap; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; - -import com.google.gwt.core.client.GWT; -import com.google.gwt.core.client.Scheduler; -import com.google.gwt.core.client.Scheduler.ScheduledCommand; -import com.google.gwt.dom.client.Style; -import com.google.gwt.dom.client.Style.Unit; -import com.google.gwt.event.dom.client.ClickEvent; -import com.google.gwt.event.dom.client.ClickHandler; -import com.google.gwt.event.shared.HandlerRegistration; -import com.google.gwt.i18n.client.DateTimeFormat; -import com.google.gwt.i18n.client.DateTimeFormat.PredefinedFormat; -import com.google.gwt.i18n.client.NumberFormat; -import com.google.gwt.maps.client.LoadApi; -import com.google.gwt.maps.client.LoadApi.LoadLibrary; -import com.google.gwt.maps.client.MapOptions; -import com.google.gwt.maps.client.MapWidget; -import com.google.gwt.maps.client.base.LatLng; -import com.google.gwt.maps.client.base.LatLngBounds; -import com.google.gwt.maps.client.controls.ControlPosition; -import com.google.gwt.maps.client.controls.MapTypeControlOptions; -import com.google.gwt.maps.client.controls.MapTypeStyle; -import com.google.gwt.maps.client.controls.PanControlOptions; -import com.google.gwt.maps.client.controls.ZoomControlOptions; -import com.google.gwt.maps.client.events.bounds.BoundsChangeMapEvent; -import com.google.gwt.maps.client.events.bounds.BoundsChangeMapHandler; -import com.google.gwt.maps.client.events.click.ClickMapEvent; -import com.google.gwt.maps.client.events.click.ClickMapHandler; -import com.google.gwt.maps.client.events.dragend.DragEndMapEvent; -import com.google.gwt.maps.client.events.dragend.DragEndMapHandler; -import com.google.gwt.maps.client.events.idle.IdleMapEvent; -import com.google.gwt.maps.client.events.idle.IdleMapHandler; -import com.google.gwt.maps.client.events.mouseout.MouseOutMapEvent; -import com.google.gwt.maps.client.events.mouseout.MouseOutMapHandler; -import com.google.gwt.maps.client.events.mouseover.MouseOverMapEvent; -import com.google.gwt.maps.client.events.mouseover.MouseOverMapHandler; -import com.google.gwt.maps.client.events.zoom.ZoomChangeMapEvent; -import com.google.gwt.maps.client.events.zoom.ZoomChangeMapHandler; -import com.google.gwt.maps.client.maptypes.MapTypeStyleFeatureType; -import com.google.gwt.maps.client.mvc.MVCArray; -import com.google.gwt.maps.client.overlays.InfoWindow; -import com.google.gwt.maps.client.overlays.InfoWindowOptions; -import com.google.gwt.maps.client.overlays.Marker; -import com.google.gwt.maps.client.overlays.MarkerOptions; -import com.google.gwt.maps.client.overlays.Polygon; -import com.google.gwt.maps.client.overlays.PolygonOptions; -import com.google.gwt.maps.client.overlays.Polyline; -import com.google.gwt.maps.client.overlays.PolylineOptions; -import com.google.gwt.resources.client.ImageResource; -import com.google.gwt.user.client.Window; -import com.google.gwt.user.client.rpc.AsyncCallback; -import com.google.gwt.user.client.ui.AbsolutePanel; -import com.google.gwt.user.client.ui.Button; -import com.google.gwt.user.client.ui.FlowPanel; -import com.google.gwt.user.client.ui.Image; -import com.google.gwt.user.client.ui.Label; -import com.google.gwt.user.client.ui.RequiresResize; -import com.google.gwt.user.client.ui.VerticalPanel; -import com.google.gwt.user.client.ui.Widget; -import com.sap.sailing.domain.common.Bearing; -import com.sap.sailing.domain.common.Bounds; -import com.sap.sailing.domain.common.Distance; -import com.sap.sailing.domain.common.ManeuverType; -import com.sap.sailing.domain.common.Position; -import com.sap.sailing.domain.common.RaceIdentifier; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sailing.domain.common.WindSource; -import com.sap.sailing.domain.common.WindSourceType; -import com.sap.sailing.domain.common.dto.CompetitorDTO; -import com.sap.sailing.domain.common.impl.DegreeBearingImpl; -import com.sap.sailing.domain.common.impl.DegreePosition; -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.gwt.ui.actions.GetBoatPositionsAction; -import com.sap.sailing.gwt.ui.actions.GetPolarAction; -import com.sap.sailing.gwt.ui.actions.GetRaceMapDataAction; -import com.sap.sailing.gwt.ui.actions.GetWindInfoAction; -import com.sap.sailing.gwt.ui.client.ClientResources; -import com.sap.sailing.gwt.ui.client.CompetitorSelectionChangeListener; -import com.sap.sailing.gwt.ui.client.CompetitorSelectionProvider; -import com.sap.sailing.gwt.ui.client.ManeuverTypeFormatter; -import com.sap.sailing.gwt.ui.client.NumberFormatterFactory; -import com.sap.sailing.gwt.ui.client.RaceTimesInfoProviderListener; -import com.sap.sailing.gwt.ui.client.RequiresDataInitialization; -import com.sap.sailing.gwt.ui.client.SailingServiceAsync; -import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sailing.gwt.ui.client.WindSourceTypeFormatter; -import com.sap.sailing.gwt.ui.client.shared.filter.QuickRankProvider; -import com.sap.sailing.gwt.ui.client.shared.racemap.RaceCompetitorSet.CompetitorsForRaceDefinedListener; -import com.sap.sailing.gwt.ui.client.shared.racemap.RaceMapHelpLinesSettings.HelpLineTypes; -import com.sap.sailing.gwt.ui.client.shared.racemap.RaceMapZoomSettings.ZoomTypes; -import com.sap.sailing.gwt.ui.common.client.DateAndTimeFormatterUtil; -import com.sap.sailing.gwt.ui.shared.CompactBoatPositionsDTO; -import com.sap.sailing.gwt.ui.shared.ControlPointDTO; -import com.sap.sailing.gwt.ui.shared.CoursePositionsDTO; -import com.sap.sailing.gwt.ui.shared.GPSFixDTOWithSpeedWindTackAndLegType; -import com.sap.sailing.gwt.ui.shared.LegInfoDTO; -import com.sap.sailing.gwt.ui.shared.ManeuverDTO; -import com.sap.sailing.gwt.ui.shared.MarkDTO; -import com.sap.sailing.gwt.ui.shared.QuickRankDTO; -import com.sap.sailing.gwt.ui.shared.RaceCourseDTO; -import com.sap.sailing.gwt.ui.shared.RaceMapDataDTO; -import com.sap.sailing.gwt.ui.shared.RaceTimesInfoDTO; -import com.sap.sailing.gwt.ui.shared.SidelineDTO; -import com.sap.sailing.gwt.ui.shared.SpeedWithBearingDTO; -import com.sap.sailing.gwt.ui.shared.WaypointDTO; -import com.sap.sailing.gwt.ui.shared.WindDTO; -import com.sap.sailing.gwt.ui.shared.WindInfoForRaceDTO; -import com.sap.sailing.gwt.ui.shared.WindTrackInfoDTO; -import com.sap.sailing.gwt.ui.shared.racemap.CanvasOverlayV3; -import com.sap.sailing.gwt.ui.shared.racemap.GoogleMapAPIKey; -import com.sap.sailing.gwt.ui.shared.racemap.GoogleMapStyleHelper; -import com.sap.sailing.gwt.ui.shared.racemap.RaceSimulationOverlay; -import com.sap.sailing.gwt.ui.shared.racemap.WindStreamletsRaceboardOverlay; -import com.sap.sse.common.Color; -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.filter.Filter; -import com.sap.sse.common.filter.FilterSet; -import com.sap.sse.common.impl.RGBColor; -import com.sap.sse.gwt.client.ErrorReporter; -import com.sap.sse.gwt.client.async.AsyncActionsExecutor; -import com.sap.sse.gwt.client.async.MarkedAsyncCallback; -import com.sap.sse.gwt.client.player.TimeListener; -import com.sap.sse.gwt.client.player.Timer; -import com.sap.sse.gwt.client.player.Timer.PlayModes; -import com.sap.sse.gwt.client.player.Timer.PlayStates; -import com.sap.sse.gwt.client.shared.components.AbstractCompositeComponent; -import com.sap.sse.gwt.client.shared.components.Component; -import com.sap.sse.gwt.client.shared.components.SettingsDialog; -import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; - -public class RaceMap extends AbstractCompositeComponent implements TimeListener, CompetitorSelectionChangeListener, - RaceTimesInfoProviderListener, TailFactory, RequiresDataInitialization, RequiresResize, QuickRankProvider { - private static final Color LOWLIGHTED_TAIL_COLOR = new RGBColor(200, 200, 200); - public static final String GET_RACE_MAP_DATA_CATEGORY = "getRaceMapData"; - public static final String GET_WIND_DATA_CATEGORY = "getWindData"; - - private static final String COMPACT_HEADER_STYLE = "compactHeader"; - public static final Color WATER_COLOR = new RGBColor(0, 67, 125); - - private AbsolutePanel rootPanel = new AbsolutePanel(); - - private MapWidget map; - - /** - * Always valid, non-null. Must be used to map all coordinates, headings, bearings, and directions - * displayed on the map, including the orientations of any canvases such as boat icons, wind displays etc. that are - * embedded in the map. The coordinate systems facilitates the possibility of transformed displays such as - * rotated and translated versions of the map, implementing the "wind-up" view. - */ - private DelegateCoordinateSystem coordinateSystem; - - private FlowPanel headerPanel; - private AbsolutePanel panelForLeftHeaderLabels; - private AbsolutePanel panelForRightHeaderLabels; - - private final SailingServiceAsync sailingService; - private final ErrorReporter errorReporter; - - private final static ClientResources resources = GWT.create(ClientResources.class); - - /** - * Polyline for the start line (connecting two marks representing the start gate). - */ - private Polyline startLine; - - /** - * Polyline for the finish line (connecting two marks representing the finish gate). - */ - private Polyline finishLine; - - /** - * Polyline for the advantage line (the leading line for the boats, orthogonal to the wind direction; touching the leading boat). - */ - private Polyline advantageLine; - - private AdvantageLineAnimator advantageTimer; - - /** - * The windward of two Polylines representing a triangle between startline and first mark. - */ - private Polyline windwardStartLineMarkToFirstMarkLine; - - /** - * The leeward of two Polylines representing a triangle between startline and first mark. - */ - private Polyline leewardStartLineMarkToFirstMarkLine; - - private class AdvantageLineMouseOverMapHandler implements MouseOverMapHandler { - private double trueWindAngle; - private Date date; - - public AdvantageLineMouseOverMapHandler(double trueWindAngle, Date date) { - this.trueWindAngle = trueWindAngle; - this.date = date; - } - - public void setTrueWindBearing(double trueWindAngle) { - this.trueWindAngle = trueWindAngle; - } - - public void setDate(Date date) { - this.date = date; - } - - @Override - public void onEvent(MouseOverMapEvent event) { - map.setTitle(stringMessages.advantageLine()+" (from "+new DegreeBearingImpl(Math.round(trueWindAngle)).reverse().getDegrees()+"deg"+ - (date == null ? ")" : ", "+ date) + ")"); - } - }; - - private AdvantageLineMouseOverMapHandler advantageLineMouseOverHandler; - - /** - * Polylines for the course middle lines; keys are the two control points delimiting the leg for which the - * {@link Polyline} value shows the course middle line. As only one course middle line is shown even if there - * are multiple legs using the same control points in different directions, using a {@link Set} makes this - * independent of the order of the two control points. If no course middle line is currently being shown for - * a pair of control points, the map will not contain a value for this pair. - */ - private final Map, Polyline> courseMiddleLines; - - private final Map courseSidelines; - - /** - * When the {@link HelpLineTypes#COURSEGEOMETRY} option is selected, little markers will be displayed on the - * lines that show the tooltip text in a little info box linked to the line. When the line is removed by - * {@link #showOrRemoveOrUpdateLine(Polyline, boolean, Position, Position, LineInfoProvider, String)}, these - * overlays need to be removed as well. Also, when the {@link HelpLineTypes#COURSEGEOMETRY} setting is - * deactivated, all these overlays need to go. - */ - private final Map infoOverlaysForLinesForCourseGeometry; - - /** - * Wind data used to display the advantage line. Retrieved by a {@link GetWindInfoAction} execution and used in - * {@link #showAdvantageLine(Iterable, Date)}. - */ - private WindInfoForRaceDTO lastCombinedWindTrackInfoDTO; - - /** - * Manages the cached set of {@link GPSFixDTOWithSpeedWindTackAndLegType}s for the boat positions as well as their graphical counterpart in the - * form of {@link Polyline}s. - */ - private final FixesAndTails fixesAndTails; - - /** - * html5 canvases used as boat display on the map - */ - private final Map boatOverlays; - - /** - * html5 canvases used for competitor info display on the map - */ - private final Map competitorInfoOverlays; - - private SmallTransparentInfoOverlay countDownOverlay; - - /** - * Map overlays with html5 canvas used to display wind sensors - */ - private final Map windSensorOverlays; - - /** - * Map overlays with html5 canvas used to display course marks including buoy zones - */ - private final Map courseMarkOverlays; - - private final Map courseMarkClickHandlers; - - private final Map markDTOs; - - /** - * markers displayed in response to - * {@link SailingServiceAsync#getDouglasPoints(String, String, Map, Map, double, AsyncCallback)} - */ - protected Set douglasMarkers; - - /** - * markers displayed in response to - * {@link SailingServiceAsync#getDouglasPoints(String, String, Map, Map, double, AsyncCallback)} - */ - private Set maneuverMarkers; - - private Map> lastManeuverResult; - - private Map> lastDouglasPeuckerResult; - - private final CompetitorSelectionProvider competitorSelection; - - private final RaceCompetitorSet raceCompetitorSet; - - /** - * Used to check if the first initial zoom to the mark markers was already done. - */ - private boolean mapFirstZoomDone = false; - - private final Timer timer; - - private RaceTimesInfoDTO lastRaceTimesInfo; - - private InfoWindow lastInfoWindow = null; - - /** - * RPC calls may receive responses out of order if there are multiple calls in-flight at the same time. If the time - * slider is moved quickly it generates many requests for boat positions quickly after each other. Sometimes, - * responses for requests send later may return before the responses to all earlier requests have been received and - * processed. This counter is used to number the requests. When processing of a response for a later request has - * already begun, responses to earlier requests will be ignored. - */ - private int boatPositionRequestIDCounter; - - /** - * Corresponds to {@link #boatPositionRequestIDCounter}. As soon as the processing of a response for a request ID - * begins, this attribute is set to the ID. A response won't be processed if a later response is already being - * processed. - */ - private int startedProcessingRequestID; - - private RaceMapImageManager raceMapImageManager; - - private RaceMapSettings settings; - private final RaceMapLifecycle raceMapLifecycle; - - private final StringMessages stringMessages; - - private boolean isMapInitialized; - - private Date lastTimeChangeBeforeInitialization; - - private int lastLegNumber; - - /** - * The last quick ranks received from a call to {@link SailingServiceAsync#getQuickRanks(RaceIdentifier, Date, AsyncCallback)} upon - * the last {@link #timeChanged(Date, Date)} event. Therefore, the ranks listed here correspond to the {@link #timer}'s time. - */ - private LinkedHashMap quickRanks; - - /** - * Taken from {@link RaceMapDataDTO#competitorsInOrderOfWindwardDistanceTraveledWithOneBasedLegNumber}; tells the - * windward distances traveled and the leg numbers in which the respective competitor is. - */ - private LinkedHashMap competitorsInOrderOfWindwardDistanceTraveledWithOneBasedLegNumber; - - - private final CombinedWindPanel combinedWindPanel; - private final TrueNorthIndicatorPanel trueNorthIndicatorPanel; - private final FlowPanel topLeftControlsWrapperPanel; - - private final AsyncActionsExecutor asyncActionsExecutor; - - /** - * The map bounds as last received by map callbacks; used to determine whether to suppress the boat animation during zoom/pan - */ - private LatLngBounds currentMapBounds; // bounds to which bounds-changed-handler compares - private int currentZoomLevel; // zoom-level to which bounds-changed-handler compares - - private boolean autoZoomIn = false; // flags auto-zoom-in in progress - private boolean autoZoomOut = false; // flags auto-zoom-out in progress - private int autoZoomLevel; // zoom-level to which auto-zoom-in/-out is zooming - LatLngBounds autoZoomLatLngBounds; // bounds to which auto-zoom-in/-out is panning&zooming - - private RaceSimulationOverlay simulationOverlay; - private WindStreamletsRaceboardOverlay streamletOverlay; - - private final boolean isSimulationEnabled; - - private static final String GET_POLAR_CATEGORY = "getPolar"; - - /** - * Tells about the availability of polar / VPP data for this race. If available, the simulation feature can be - * offered to the user. - */ - private boolean hasPolar; - - private final RegattaAndRaceIdentifier raceIdentifier; - - /** - * When the user requests wind-up display this may happen at a point where no mark positions are known or when - * no wind direction is known yet. In this case, this flag will be set, and when wind information or course mark - * positions are received later, this flag is checked, and if set, a {@link #updateCoordinateSystemFromSettings()} - * call is issued to make sure that the user's request for a new coordinate system is honored. - */ - private boolean requiresCoordinateSystemUpdateWhenCoursePositionAndWindDirectionIsKnown; - - /** - * Tells whether currently an auto-zoom is in progress; this is used particularly to keep the smooth CSS boat transitions - * active while auto-zooming whereas stopping them seems the better option for manual zooms. - */ - private boolean autoZoomInProgress; - - /** - * Tells whether currently an orientation change is in progress; this is required handle map events during the configuration of the map - * during an orientation change. - */ - private boolean orientationChangeInProgress; - - private final NumberFormat numberFormatOneDecimal = NumberFormat.getFormat("0.0"); - - public RaceMap(Component parent, ComponentContext context, RaceMapLifecycle raceMapLifecycle, - RaceMapSettings raceMapSettings, - SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, - ErrorReporter errorReporter, Timer timer, CompetitorSelectionProvider competitorSelection, StringMessages stringMessages, - RegattaAndRaceIdentifier raceIdentifier, RaceMapResources raceMapResources, - boolean showHeaderPanel) { - super(parent, context); - this.raceMapLifecycle = raceMapLifecycle; - this.stringMessages = stringMessages; - this.sailingService = sailingService; - this.raceIdentifier = raceIdentifier; - this.asyncActionsExecutor = asyncActionsExecutor; - this.errorReporter = errorReporter; - this.timer = timer; - this.isSimulationEnabled = true; - timer.addTimeListener(this); - raceMapImageManager = new RaceMapImageManager(raceMapResources); - markDTOs = new HashMap(); - courseSidelines = new HashMap<>(); - courseMiddleLines = new HashMap<>(); - infoOverlaysForLinesForCourseGeometry = new HashMap<>(); - boatOverlays = new HashMap(); - competitorInfoOverlays = new HashMap(); - windSensorOverlays = new HashMap(); - courseMarkOverlays = new HashMap(); - courseMarkClickHandlers = new HashMap(); - this.competitorSelection = competitorSelection; - this.raceCompetitorSet = new RaceCompetitorSet(competitorSelection); - competitorSelection.addCompetitorSelectionChangeListener(this); - settings = raceMapSettings; - coordinateSystem = new DelegateCoordinateSystem(new IdentityCoordinateSystem()); - fixesAndTails = new FixesAndTails(coordinateSystem); - updateCoordinateSystemFromSettings(); - lastTimeChangeBeforeInitialization = null; - isMapInitialized = false; - this.hasPolar = false; - headerPanel = new FlowPanel(); - headerPanel.setStyleName("RaceMap-HeaderPanel"); - panelForLeftHeaderLabels = new AbsolutePanel(); - panelForRightHeaderLabels = new AbsolutePanel(); - initializeData(settings.isShowMapControls(), showHeaderPanel); - RaceMapStyle raceMapStyle = raceMapResources.raceMapStyle(); - raceMapStyle.ensureInjected(); - combinedWindPanel = new CombinedWindPanel(this, raceMapImageManager, raceMapStyle, stringMessages, coordinateSystem); - combinedWindPanel.setVisible(false); - trueNorthIndicatorPanel = new TrueNorthIndicatorPanel(this, raceMapImageManager, raceMapStyle, stringMessages, coordinateSystem); - trueNorthIndicatorPanel.setVisible(true); - topLeftControlsWrapperPanel = new FlowPanel(); - topLeftControlsWrapperPanel.add(combinedWindPanel); - topLeftControlsWrapperPanel.add(trueNorthIndicatorPanel); - orientationChangeInProgress = false; - mapFirstZoomDone = false; - // TODO bug 494: reset zoom settings to user preferences - initWidget(rootPanel); - this.setSize("100%", "100%"); - } - - /** - * The {@link WindDTO#dampenedTrueWindFromDeg} direction if {@link #lastCombinedWindTrackInfoDTO} has a - * {@link WindSourceType#COMBINED} source which has at least one fix recorded; null otherwise. - */ - private Bearing getLastCombinedTrueWindFromDirection() { - if (lastCombinedWindTrackInfoDTO != null) { - for (Entry e : lastCombinedWindTrackInfoDTO.windTrackInfoByWindSource.entrySet()) { - if (e.getKey().getType() == WindSourceType.COMBINED) { - final List windFixes = e.getValue().windFixes; - if (!windFixes.isEmpty()) { - return new DegreeBearingImpl(windFixes.get(0).dampenedTrueWindFromDeg); - } - } - } - } - return null; - } - - /** - * @return {@code true} if the map was redrawn by the call to this method - */ - private boolean updateCoordinateSystemFromSettings() { - boolean redrawn = false; - final MapOptions mapOptions; - orientationChangeInProgress = true; - if (getSettings().isWindUp()) { - final Position centerOfCourse = getCenterOfCourse(); - if (centerOfCourse != null) { - final Bearing lastCombinedTrueWindFromDirection = getLastCombinedTrueWindFromDirection(); - if (lastCombinedTrueWindFromDirection != null) { - // new equator shall point 90deg right of the "from" wind direction to make wind come from top of map - coordinateSystem.setCoordinateSystem(new RotateAndTranslateCoordinateSystem(centerOfCourse, - lastCombinedTrueWindFromDirection.add(new DegreeBearingImpl(90)))); - if (map != null) { - mapOptions = getMapOptions(settings.isShowMapControls(), /* wind-up */ true); - } else { - mapOptions = null; - } - requiresCoordinateSystemUpdateWhenCoursePositionAndWindDirectionIsKnown = false; - } else { - // register callback in case center of course and wind info becomes known - requiresCoordinateSystemUpdateWhenCoursePositionAndWindDirectionIsKnown = true; - mapOptions = null; - } - } else { - // register callback in case center of course and wind info becomes known - requiresCoordinateSystemUpdateWhenCoursePositionAndWindDirectionIsKnown = true; - mapOptions = null; - } - } else { - if (map != null) { - mapOptions = getMapOptions(settings.isShowMapControls(), /* wind-up */ false); - } else { - mapOptions = null; - } - coordinateSystem.setCoordinateSystem(new IdentityCoordinateSystem()); - } - if (mapOptions != null) { // if no coordinate system change happened that affects an existing map, don't redraw - fixesAndTails.clearTails(); - redraw(); - redrawn = true; - // zooming and setting options while the event loop is still working doesn't work reliably; defer until event loop returns - Scheduler.get().scheduleDeferred(new ScheduledCommand() { - @Override - public void execute() { - if (map != null) { - map.setOptions(mapOptions); - // ensure zooming to what the settings tell, or defaults if what the settings tell isn't possible right now - mapFirstZoomDone = false; - trueNorthIndicatorPanel.redraw(); - orientationChangeInProgress = false; - } - } - }); - } - return redrawn; - } - - private void loadMapsAPIV3(final boolean showMapControls, final boolean showHeaderPanel) { - boolean sensor = true; - - // load all the libs for use in the maps - ArrayList loadLibraries = new ArrayList(); - loadLibraries.add(LoadLibrary.DRAWING); - loadLibraries.add(LoadLibrary.GEOMETRY); - - Runnable onLoad = new Runnable() { - @Override - public void run() { - MapOptions mapOptions = getMapOptions(showMapControls, /* wind up */ false); - map = new MapWidget(mapOptions); - rootPanel.add(map, 0, 0); - if (showHeaderPanel) { - Image sapLogo = createSAPLogo(); - rootPanel.add(sapLogo); - } - - map.setControls(ControlPosition.LEFT_TOP, topLeftControlsWrapperPanel); - adjustLeftControlsIndent(); - - RaceMap.this.raceMapImageManager.loadMapIcons(map); - map.setSize("100%", "100%"); - map.addZoomChangeHandler(new ZoomChangeMapHandler() { - @Override - public void onEvent(ZoomChangeMapEvent event) { - if (!autoZoomIn && !autoZoomOut && !orientationChangeInProgress) { - // stop automatic zoom after a manual zoom event; automatic zoom in zoomMapToNewBounds will restore old settings - final List emptyList = Collections.emptyList(); - RaceMapZoomSettings clearedZoomSettings = new RaceMapZoomSettings(emptyList, settings.getZoomSettings().isZoomToSelectedCompetitors()); - settings = new RaceMapSettings(settings, clearedZoomSettings); - } - // TODO bug489 when in wind-up mode, avoid zooming out too far; perhaps zoom back in if zoomed out too far - } - }); - map.addDragEndHandler(new DragEndMapHandler() { - @Override - public void onEvent(DragEndMapEvent event) { - // stop automatic zoom after a manual drag event - autoZoomIn = false; - autoZoomOut = false; - final List emptyList = Collections.emptyList(); - RaceMapZoomSettings clearedZoomSettings = new RaceMapZoomSettings(emptyList, settings.getZoomSettings().isZoomToSelectedCompetitors()); - settings = new RaceMapSettings(settings, clearedZoomSettings); - } - }); - map.addIdleHandler(new IdleMapHandler() { - @Override - public void onEvent(IdleMapEvent event) { - // the "idle"-event is raised at the end of map-animations - if (autoZoomIn) { - // finalize zoom-in that was started with panTo() in zoomMapToNewBounds() - map.setZoom(autoZoomLevel); - autoZoomIn = false; - } - if (autoZoomOut) { - // finalize zoom-out that was started with setZoom() in zoomMapToNewBounds() - map.panTo(autoZoomLatLngBounds.getCenter()); - autoZoomOut = false; - } - } - }); - map.addBoundsChangeHandler(new BoundsChangeMapHandler() { - @Override - public void onEvent(BoundsChangeMapEvent event) { - int newZoomLevel = map.getZoom(); - if (!isAutoZoomInProgress() && (newZoomLevel != currentZoomLevel)) { - removeTransitions(); - } - if ((streamletOverlay != null) && !map.getBounds().equals(currentMapBounds)) { - streamletOverlay.onBoundsChanged(newZoomLevel != currentZoomLevel); - } - if ((simulationOverlay != null) && !map.getBounds().equals(currentMapBounds)) { - simulationOverlay.onBoundsChanged(newZoomLevel != currentZoomLevel); - } - currentMapBounds = map.getBounds(); - currentZoomLevel = newZoomLevel; - headerPanel.getElement().getStyle().setWidth(map.getOffsetWidth(), Unit.PX); - } - }); - - // If there was a time change before the API was loaded, reset the time - if (lastTimeChangeBeforeInitialization != null) { - timeChanged(lastTimeChangeBeforeInitialization, null); - lastTimeChangeBeforeInitialization = null; - } - // Initialize streamlet canvas for wind visualization; it shouldn't be doing anything unless it's visible - streamletOverlay = new WindStreamletsRaceboardOverlay(getMap(), /* zIndex */ 0, - timer, raceIdentifier, sailingService, asyncActionsExecutor, stringMessages, coordinateSystem); - streamletOverlay.addToMap(); - if (settings.isShowWindStreamletOverlay()) { - streamletOverlay.setColors(settings.isShowWindStreamletColors()); - streamletOverlay.setVisible(true); - } - - if (isSimulationEnabled) { - // determine availability of polar diagram - setHasPolar(); - // initialize simulation canvas - simulationOverlay = new RaceSimulationOverlay(getMap(), /* zIndex */ 0, raceIdentifier, sailingService, stringMessages, asyncActionsExecutor, coordinateSystem); - simulationOverlay.addToMap(); - showSimulationOverlay(settings.isShowSimulationOverlay()); - } - if (showHeaderPanel) { - createHeaderPanel(map); - } - if (showMapControls) { - createSettingsButton(map); - } - // Data has been initialized - RaceMap.this.isMapInitialized = true; - RaceMap.this.redraw(); - trueNorthIndicatorPanel.redraw(); - showAdditionalControls(map); - } - }; - LoadApi.go(onLoad, loadLibraries, sensor, "key="+GoogleMapAPIKey.V3_APIKey); - } - - /** - * Subclasses may define additional stuff to be shown on the map. - */ - protected void showAdditionalControls(MapWidget map) { - } - - private void setHasPolar() { - GetPolarAction getPolar = new GetPolarAction(sailingService, raceIdentifier); - asyncActionsExecutor.execute(getPolar, GET_POLAR_CATEGORY, - new MarkedAsyncCallback<>(new AsyncCallback() { - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError(stringMessages.errorDeterminingPolarAvailability( - raceIdentifier.getRaceName(), caught.getMessage()), /* silent */ true); - } - - @Override - public void onSuccess(Boolean result) { - // store results - hasPolar = result.booleanValue(); - } - })); - - } - - /** - * Creates a header panel where additional information can be displayed by using - * {@link #getLeftHeaderPanel()} or {@link #getRightHeaderPanel()}. - * - * This panel is transparent and configured in such a way that it moves other controls - * down by its height. To achieve the goal of not having added widgets transparent - * this widget consists of two parts: First one is the transparent panel and the - * second one is the panel for the controls. The controls then need to moved onto - * the panel by using CSS. - */ - private void createHeaderPanel(MapWidget map) { - // we need a panel that does not have any transparency to have the - // labels shown in the right color. This panel also needs to have - // a higher z-index than other elements on the map - map.setControls(ControlPosition.TOP_LEFT, panelForLeftHeaderLabels); - panelForLeftHeaderLabels.getElement().getParentElement().getStyle().setProperty("zIndex", "1"); - panelForLeftHeaderLabels.getElement().getStyle().setProperty("overflow", "visible"); - rootPanel.add(panelForRightHeaderLabels); - panelForRightHeaderLabels.getElement().getStyle().setProperty("zIndex", "1"); - panelForRightHeaderLabels.getElement().getStyle().setProperty("overflow", "visible"); - // need to initialize size before css kicks in to make sure - // that controls get positioned right - headerPanel.getElement().getStyle().setHeight(60, Unit.PX); - headerPanel.getElement().getStyle().setWidth(map.getOffsetWidth(), Unit.PX); - // some sort of hack: not positioning TOP_LEFT because then the - // controls at RIGHT would not get the correct top setting - map.setControls(ControlPosition.TOP_RIGHT, headerPanel); - } - - private void createSettingsButton(MapWidget map) { - final Component component = this; - Button settingsButton = new Button(); - settingsButton.setStyleName("gwt-MapSettingsButton"); - settingsButton.ensureDebugId("raceMapSettingsButton"); - settingsButton.setTitle(stringMessages.settings()); - settingsButton.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - SettingsDialog dialog = new SettingsDialog(component, stringMessages); - dialog.ensureDebugId("raceMapSettings"); - dialog.show(); - } - }); - map.setControls(ControlPosition.RIGHT_TOP, settingsButton); - } - - private void removeTransitions() { - // remove the canvas animations for boats - for (CanvasOverlayV3 boatOverlay : RaceMap.this.getBoatOverlays().values()) { - boatOverlay.removeCanvasPositionAndRotationTransition(); - } - // remove the canvas animations for the info overlays of the selected boats - for (CompetitorInfoOverlay infoOverlay : competitorInfoOverlays.values()) { - infoOverlay.removeCanvasPositionAndRotationTransition(); - } - for (CourseMarkOverlay markOverlay : courseMarkOverlays.values()) { - markOverlay.removeCanvasPositionAndRotationTransition(); - } - // remove the advantage line animation - if (advantageTimer != null) { - advantageTimer.removeAnimation(); - } - } - - public void redraw() { - timeChanged(timer.getTime(), null); - } - - Map getBoatOverlays() { - return Collections.unmodifiableMap(boatOverlays); - } - - public MapWidget getMap() { - return map; - } - - public RaceSimulationOverlay getSimulationOverlay() { - return simulationOverlay; - } - - /** - * @return the Panel where labels or other controls for the header can be positioned - */ - public AbsolutePanel getLeftHeaderPanel() { - return panelForLeftHeaderLabels; - } - - public AbsolutePanel getRightHeaderPanel() { - return panelForRightHeaderLabels; - } - - @Override - public void raceTimesInfosReceived(Map raceTimesInfos, long clientTimeWhenRequestWasSent, Date serverTimeDuringRequest, long clientTimeWhenResponseWasReceived) { - timer.adjustClientServerOffset(clientTimeWhenRequestWasSent, serverTimeDuringRequest, clientTimeWhenResponseWasReceived); - this.lastRaceTimesInfo = raceTimesInfos.get(raceIdentifier); - } - - /** - * In {@link PlayModes#Live live mode}, when {@link #loadCompleteLeaderboard(Date) loading the leaderboard contents}, null - * is used as time point. The condition for this is encapsulated in this method so others can find out. For example, when a time change - * is signaled due to local offset / delay adjustments, no additional call to {@link #loadCompleteLeaderboard(Date)} would be required - * as null will be passed in any case, not being affected by local time offsets. - */ - private boolean useNullAsTimePoint() { - return timer.getPlayMode() == PlayModes.Live; - } - - @Override - public void timeChanged(final Date newTime, final Date oldTime) { - if (newTime != null && isMapInitialized) { - if (raceIdentifier != null) { - RegattaAndRaceIdentifier race = raceIdentifier; - final Iterable competitorsToShow = getCompetitorsToShow(); - if (race != null) { - final long transitionTimeInMillis = calculateTimeForPositionTransitionInMillis(newTime, oldTime); - final com.sap.sse.common.Util.Triple, Map, Map> fromAndToAndOverlap = - fixesAndTails.computeFromAndTo(newTime, competitorsToShow, settings.getEffectiveTailLengthInMilliseconds()); - // Request map data update, possibly in two calls; see method details - callGetRaceMapDataForAllOverlappingAndTipsOfNonOverlappingAndGetBoatPositionsForAllOthers(fromAndToAndOverlap, race, newTime, transitionTimeInMillis, competitorsToShow); - // draw the wind into the map, get the combined wind - List windSourceTypeNames = new ArrayList(); - windSourceTypeNames.add(WindSourceType.EXPEDITION.name()); - windSourceTypeNames.add(WindSourceType.COMBINED.name()); - GetWindInfoAction getWindInfoAction = new GetWindInfoAction(sailingService, race, 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) { - List> windSourcesToShow = new ArrayList>(); - if (windInfo != null) { - lastCombinedWindTrackInfoDTO = windInfo; - showAdvantageLine(competitorsToShow, newTime, transitionTimeInMillis); - for (WindSource windSource : windInfo.windTrackInfoByWindSource.keySet()) { - WindTrackInfoDTO windTrackInfoDTO = windInfo.windTrackInfoByWindSource.get(windSource); - switch (windSource.getType()) { - case EXPEDITION: - // we filter out measured wind sources with vary low confidence - if (windTrackInfoDTO.minWindConfidence > 0.0001) { - windSourcesToShow.add(new com.sap.sse.common.Util.Pair(windSource, windTrackInfoDTO)); - } - break; - case COMBINED: - showCombinedWindOnMap(windSource, windTrackInfoDTO); - if (requiresCoordinateSystemUpdateWhenCoursePositionAndWindDirectionIsKnown) { - updateCoordinateSystemFromSettings(); - } - break; - default: - // Which wind sources are requested is defined in a list above this - // action. So we throw here an exception to notice a missing source. - throw new UnsupportedOperationException( - "There is currently no support for the enum value '" - + windSource.getType() + "' in this method."); - } - } - } - showWindSensorsOnMap(windSourcesToShow); - } - }); - } - } - } - } - - /** - * Requests updates for map data and, when received, updates the map structures accordingly. - *

- * - * The update can happen in one or two round trips. We assume that overlapping segments usually don't require a lot - * of loading time as the most typical case will be to update a longer tail with a few new fixes that were received - * since the last time tick. These supposedly fast requests are handled by a {@link GetRaceMapDataAction} which also - * requests updates for mark positions, sidelines and quick ranks. The same request also loads boat positions for - * the zero-length interval at newTime for the non-overlapping tails assuming that this will work - * fairly fast and in particular in O(1) time regardless of tail length, compared to fetching the entire tail for - * all competitors. This will at least provide quick feedback about those competitors' positions even if loading - * their entire tail may take a little longer. The {@link GetRaceMapDataAction} therefore is intended to be a rather - * quick call. - *

- * - * Non-overlapping position requests typically occur for the first request when no fix at all is known for the - * competitor yet, or when the user has radically moved the time slider to some other time such that given the - * current tail length setting the new tail segment does not overlap with the old one, requiring a full load of the - * entire tail data for that competitor. For these non-overlapping requests, this method creates a - * {@link GetBoatPositionsAction} request loading the entire tail required, but not quick ranks, sidelines and mark - * positions. Updating the results of this call is done in {@link #updateBoatPositions(Date, long, Map, Iterable, Map, boolean)}. - *

- */ - private void callGetRaceMapDataForAllOverlappingAndTipsOfNonOverlappingAndGetBoatPositionsForAllOthers( - final Triple, Map, Map> fromAndToAndOverlap, - RegattaAndRaceIdentifier race, final Date newTime, final long transitionTimeInMillis, final Iterable competitorsToShow) { - 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 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, - (settings.isShowSimulationOverlay() ? simulationOverlay.getLegIdentifier() : null), - raceCompetitorSet.getMd5OfIdsAsStringOfCompetitorParticipatingInRaceInAlphanumericOrderOfTheirID()), - GET_RACE_MAP_DATA_CATEGORY, - getRaceMapDataCallback(newTime, transitionTimeInMillis, fromAndToAndOverlap.getC(), competitorsToShow, ++boatPositionRequestIDCounter)); - // 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()) { - asyncActionsExecutor.execute(new GetBoatPositionsAction(sailingService, race, fromTimesForNonOverlappingTailsCall, toTimesForNonOverlappingTailsCall, - /* extrapolate */ true), GET_RACE_MAP_DATA_CATEGORY, - new MarkedAsyncCallback<>(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); - } - })); - } - } - - private AsyncCallback getRaceMapDataCallback( - final Date newTime, - final long transitionTimeInMillis, - final Map hasTailOverlapForCompetitor, - final Iterable competitorsToShow, final int requestID) { - return new MarkedAsyncCallback<>(new AsyncCallback() { - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError("Error obtaining racemap data: " + caught.getMessage(), true /*silentMode */); - } - - @Override - public void onSuccess(RaceMapDataDTO raceMapDataDTO) { - if (map != null && raceMapDataDTO != null) { - // process response only if not received out of order - if (startedProcessingRequestID < requestID) { - startedProcessingRequestID = requestID; - if (raceMapDataDTO.raceCompetitorIdsAsStrings != null) { - try { - raceCompetitorSet.setIdsAsStringsOfCompetitorsInRace(raceMapDataDTO.raceCompetitorIdsAsStrings); - } catch (Exception e) { - GWT.log("Error trying to update competitor set for race "+raceIdentifier.getRaceName()+ - " in regatta "+raceIdentifier.getRegattaName(), e); - } - } - quickRanks = raceMapDataDTO.quickRanks; - competitorsInOrderOfWindwardDistanceTraveledWithOneBasedLegNumber = - raceMapDataDTO.competitorsInOrderOfWindwardDistanceTraveledWithOneBasedLegNumber; - if (isSimulationEnabled && settings.isShowSimulationOverlay()) { - lastLegNumber = raceMapDataDTO.coursePositions.currentLegNumber; - simulationOverlay.updateLeg(Math.max(lastLegNumber, 1), /* clearCanvas */ false, raceMapDataDTO.simulationResultVersion); - } - // Do boat specific actions - Map> boatData = raceMapDataDTO.boatPositions; - updateBoatPositions(newTime, transitionTimeInMillis, hasTailOverlapForCompetitor, competitorsToShow, boatData, /* updateTailsOnly */ false); - if (douglasMarkers != null) { - removeAllMarkDouglasPeuckerpoints(); - } - if (maneuverMarkers != null) { - removeAllManeuverMarkers(); - } - - if (requiresCoordinateSystemUpdateWhenCoursePositionAndWindDirectionIsKnown) { - updateCoordinateSystemFromSettings(); - } - // Do mark specific actions - showCourseMarksOnMap(raceMapDataDTO.coursePositions, transitionTimeInMillis); - showCourseSidelinesOnMap(raceMapDataDTO.courseSidelines); - showStartAndFinishAndCourseMiddleLines(raceMapDataDTO.coursePositions); - showStartLineToFirstMarkTriangle(raceMapDataDTO.coursePositions); - - // Rezoom the map - LatLngBounds zoomToBounds = null; - if (!settings.getZoomSettings().containsZoomType(ZoomTypes.NONE)) { // Auto zoom if setting is not manual - zoomToBounds = settings.getZoomSettings().getNewBounds(RaceMap.this); - if (zoomToBounds == null && !mapFirstZoomDone) { - zoomToBounds = getDefaultZoomBounds(); // the user-specified zoom couldn't find what it was looking for; try defaults once - } - } else if (!mapFirstZoomDone) { // Zoom once to the marks if marks exist - zoomToBounds = new CourseMarksBoundsCalculator().calculateNewBounds(RaceMap.this); - if (zoomToBounds == null) { - zoomToBounds = getDefaultZoomBounds(); // use default zoom, e.g., - } - /* - * Reset the mapZoomedOrPannedSinceLastRaceSelection: In spite of the fact that - * the map was just zoomed to the bounds of the marks, it was not a zoom or pan - * triggered by the user. As a consequence the - * mapZoomedOrPannedSinceLastRaceSelection option has to reset again. - */ - } - zoomMapToNewBounds(zoomToBounds); - mapFirstZoomDone = true; - } - } else { - lastTimeChangeBeforeInitialization = newTime; - } - } - }); - } - - /** - * @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(CompetitorDTO)}); 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(CompetitorDTO, 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)}. - * @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 - * competitor info bubbles moved. This assumes that the tips of these tails are loaded in a separate call - * 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. - */ - private void updateBoatPositions(final Date newTime, final long transitionTimeInMillis, - final Map hasTailOverlapForCompetitor, - final Iterable competitorsToShow, Map> boatData, boolean updateTailsOnly) { - fixesAndTails.updateFixes(boatData, hasTailOverlapForCompetitor, RaceMap.this, transitionTimeInMillis); - showBoatsOnMap(newTime, transitionTimeInMillis, - /* re-calculate; it could have changed since the asynchronous request was made: */ getCompetitorsToShow(), - updateTailsOnly); - 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); - } - } - - private void showCourseSidelinesOnMap(List sidelinesDTOs) { - if (map != null && sidelinesDTOs != null ) { - Map toRemoveSidelines = new HashMap(courseSidelines); - for (SidelineDTO sidelineDTO : sidelinesDTOs) { - if (sidelineDTO.getMarks().size() == 2) { // right now we only support sidelines with 2 marks - Polygon sideline = courseSidelines.get(sidelineDTO); - LatLng[] sidelinePoints = new LatLng[sidelineDTO.getMarks().size()]; - int i=0; - for (MarkDTO sidelineMark : sidelineDTO.getMarks()) { - sidelinePoints[i] = coordinateSystem.toLatLng(sidelineMark.position); - i++; - } - if (sideline == null) { - PolygonOptions options = PolygonOptions.newInstance(); - options.setClickable(true); - options.setStrokeColor("#0000FF"); - options.setStrokeWeight(1); - options.setStrokeOpacity(1.0); - options.setFillColor(null); - options.setFillOpacity(1.0); - - sideline = Polygon.newInstance(options); - MVCArray pointsAsArray = MVCArray.newInstance(sidelinePoints); - sideline.setPath(pointsAsArray); - - sideline.addMouseOverHandler(new MouseOverMapHandler() { - @Override - public void onEvent(MouseOverMapEvent event) { - map.setTitle(stringMessages.sideline()); - } - }); - sideline.addMouseOutMoveHandler(new MouseOutMapHandler() { - @Override - public void onEvent(MouseOutMapEvent event) { - map.setTitle(""); - } - }); - courseSidelines.put(sidelineDTO, sideline); - sideline.setMap(map); - } else { - sideline.getPath().removeAt(1); - sideline.getPath().removeAt(0); - sideline.getPath().insertAt(0, sidelinePoints[0]); - sideline.getPath().insertAt(1, sidelinePoints[1]); - toRemoveSidelines.remove(sidelineDTO); - } - } - } - for (SidelineDTO toRemoveSideline : toRemoveSidelines.keySet()) { - Polygon sideline = courseSidelines.remove(toRemoveSideline); - sideline.setMap(null); - } - } - } - - private void showCourseMarksOnMap(CoursePositionsDTO courseDTO, long transitionTimeInMillis) { - 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.getName()); - 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.getName(), courseMarkOverlay); - markDTOs.put(markDTO.getName(), markDTO); - registerCourseMarkInfoWindowClickHandler(markDTO.getName()); - 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.getName()); - } - } - } - for (String toRemoveMarkName : toRemoveCourseMarks.keySet()) { - CourseMarkOverlay removedOverlay = courseMarkOverlays.remove(toRemoveMarkName); - if(removedOverlay != null) { - removedOverlay.removeFromMap(); - } - } - } - } - - /** - * Based on the mark positions in {@link #courseMarkOverlays}' values this method determines the center of gravity of these marks' - * {@link CourseMarkOverlay#getPosition() positions}. - */ - private Position getCenterOfCourse() { - ScalablePosition center = null; - int count = 0; - for (CourseMarkOverlay markOverlay : courseMarkOverlays.values()) { - ScalablePosition markPosition = new ScalablePosition(markOverlay.getPosition()); - if (center == null) { - center = markPosition; - } else { - center.add(markPosition); - } - count++; - } - return center == null ? null : center.divide(count); - } - - private void showCombinedWindOnMap(WindSource windSource, WindTrackInfoDTO windTrackInfoDTO) { - if (map != null) { - combinedWindPanel.setWindInfo(windTrackInfoDTO, windSource); - combinedWindPanel.redraw(); - } - } - - private void showWindSensorsOnMap(List> windSensorsList) { - if (map != null) { - Set toRemoveWindSources = new HashSet(windSensorOverlays.keySet()); - for (com.sap.sse.common.Util.Pair windSourcePair : windSensorsList) { - WindSource windSource = windSourcePair.getA(); - WindTrackInfoDTO windTrackInfoDTO = windSourcePair.getB(); - - WindSensorOverlay windSensorOverlay = windSensorOverlays.get(windSource); - if (windSensorOverlay == null) { - windSensorOverlay = createWindSensorOverlay(RaceMapOverlaysZIndexes.WINDSENSOR_ZINDEX, windSource, windTrackInfoDTO); - windSensorOverlays.put(windSource, windSensorOverlay); - windSensorOverlay.addToMap(); - } else { - windSensorOverlay.setWindInfo(windTrackInfoDTO, windSource); - windSensorOverlay.draw(); - toRemoveWindSources.remove(windSource); - } - } - for (WindSource toRemoveWindSource : toRemoveWindSources) { - WindSensorOverlay removedWindSensorOverlay = windSensorOverlays.remove(toRemoveWindSource); - if (removedWindSensorOverlay != null) { - removedWindSensorOverlay.removeFromMap(); - } - } - } - } - - private void showCompetitorInfoOnMap(final Date newTime, final long timeForPositionTransitionMillis, final Iterable competitorsToShow) { - if (map != null) { - if (settings.isShowSelectedCompetitorsInfo()) { - Set toRemoveCompetorInfoOverlays = new HashSet( - competitorInfoOverlays.keySet()); - for (CompetitorDTO competitorDTO : competitorsToShow) { - if (fixesAndTails.hasFixesFor(competitorDTO)) { - GPSFixDTOWithSpeedWindTackAndLegType lastBoatFix = getBoatFix(competitorDTO, newTime); - if (lastBoatFix != null) { - CompetitorInfoOverlay competitorInfoOverlay = competitorInfoOverlays.get(competitorDTO); - if (competitorInfoOverlay == null) { - competitorInfoOverlay = createCompetitorInfoOverlay(RaceMapOverlaysZIndexes.INFO_OVERLAY_ZINDEX, competitorDTO); - competitorInfoOverlays.put(competitorDTO, competitorInfoOverlay); - competitorInfoOverlay.setInfoText(createInfoText(competitorDTO, lastBoatFix)); - competitorInfoOverlay.setPosition(lastBoatFix.position, timeForPositionTransitionMillis); - competitorInfoOverlay.addToMap(); - } else { - competitorInfoOverlay.setInfoText(createInfoText(competitorDTO, lastBoatFix)); - competitorInfoOverlay.setPosition(lastBoatFix.position, timeForPositionTransitionMillis); - competitorInfoOverlay.draw(); - } - toRemoveCompetorInfoOverlays.remove(competitorDTO); - } - } - } - for (CompetitorDTO toRemoveCompetorDTO : toRemoveCompetorInfoOverlays) { - CompetitorInfoOverlay competitorInfoOverlay = competitorInfoOverlays.get(toRemoveCompetorDTO); - competitorInfoOverlay.removeFromMap(); - competitorInfoOverlays.remove(toRemoveCompetorDTO); - } - } else { - // remove all overlays - for (CompetitorInfoOverlay competitorInfoOverlay : competitorInfoOverlays.values()) { - competitorInfoOverlay.removeFromMap(); - } - competitorInfoOverlays.clear(); - } - } - } - - private long calculateTimeForPositionTransitionInMillis(final Date newTime, final Date oldTime) { - final long timeForPositionTransitionMillis; - boolean hasTimeJumped = oldTime != null && Math.abs(oldTime.getTime() - newTime.getTime()) > 3*timer.getRefreshInterval(); - if (timer.getPlayState() == PlayStates.Playing && !hasTimeJumped) { - // choose 130% of the refresh interval as transition period to make it unlikely that the transition - // stops before the next update has been received - timeForPositionTransitionMillis = 1300 * timer.getRefreshInterval() / 1000; - } else { - timeForPositionTransitionMillis = -1; // -1 means 'no transition - } - return timeForPositionTransitionMillis; - } - - /** - * @param updateTailsOnly - * if false, tails of competitors not in competitorsToShow are removed from the - * map - */ - private void showBoatsOnMap(final Date newTime, final long timeForPositionTransitionMillis, - final Iterable competitorsToShow, boolean updateTailsOnly) { - if (map != null) { - Date tailsFromTime = new Date(newTime.getTime() - settings.getEffectiveTailLengthInMilliseconds()); - Date tailsToTime = newTime; - Set competitorDTOsOfUnusedTails = new HashSet(); - Set competitorDTOsOfUnusedBoatCanvases = new HashSet(); - if (!updateTailsOnly) { - competitorDTOsOfUnusedTails.addAll(fixesAndTails.getCompetitorsWithTails()); - competitorDTOsOfUnusedBoatCanvases.addAll(boatOverlays.keySet()); - } - for (CompetitorDTO competitorDTO : competitorsToShow) { - if (fixesAndTails.hasFixesFor(competitorDTO)) { - if (!fixesAndTails.hasTail(competitorDTO)) { - fixesAndTails.createTailAndUpdateIndices(competitorDTO, tailsFromTime, tailsToTime, this); - } else { - fixesAndTails.updateTail(competitorDTO, tailsFromTime, tailsToTime, (int) (timeForPositionTransitionMillis==-1?-1:timeForPositionTransitionMillis/2)); - if (!updateTailsOnly) { - competitorDTOsOfUnusedTails.remove(competitorDTO); - } - PolylineOptions newOptions = createTailStyle(competitorDTO, displayHighlighted(competitorDTO)); - fixesAndTails.getTail(competitorDTO).setOptions(newOptions); - } - boolean usedExistingBoatCanvas = updateBoatCanvasForCompetitor(competitorDTO, newTime, timeForPositionTransitionMillis); - if (usedExistingBoatCanvas && !updateTailsOnly) { - competitorDTOsOfUnusedBoatCanvases.remove(competitorDTO); - } - } - } - if (!updateTailsOnly) { - for (CompetitorDTO unusedBoatCanvasCompetitorDTO : competitorDTOsOfUnusedBoatCanvases) { - CanvasOverlayV3 boatCanvas = boatOverlays.get(unusedBoatCanvasCompetitorDTO); - boatCanvas.removeFromMap(); - boatOverlays.remove(unusedBoatCanvasCompetitorDTO); - } - for (CompetitorDTO unusedTailCompetitorDTO : competitorDTOsOfUnusedTails) { - fixesAndTails.removeTail(unusedTailCompetitorDTO); - } - } - } - } - - /** - * This algorithm is limited to distances such that dlon < pi/2, i.e., those that extend around less than one - * quarter of the circumference of the earth in longitude. A completely general, but more complicated algorithm is - * necessary if greater distances are allowed. - */ - public LatLng calculatePositionAlongRhumbline(LatLng position, double bearingDeg, Distance distance) { - double distanceRad = distance.getCentralAngleRad(); - double lat1 = position.getLatitude() / 180. * Math.PI; - double lon1 = position.getLongitude() / 180. * Math.PI; - double bearingRad = bearingDeg / 180. * Math.PI; - double lat2 = Math.asin(Math.sin(lat1) * Math.cos(distanceRad) + - Math.cos(lat1) * Math.sin(distanceRad) * Math.cos(bearingRad)); - double lon2 = lon1 + Math.atan2(Math.sin(bearingRad)*Math.sin(distanceRad)*Math.cos(lat1), - Math.cos(distanceRad)-Math.sin(lat1)*Math.sin(lat2)); - lon2 = (lon2+3*Math.PI) % (2*Math.PI) - Math.PI; // normalize to -180..+180� - // position is already in LatLng space, so no mapping through coordinateSystem is required here - return LatLng.newInstance(lat2 / Math.PI * 180., lon2 / Math.PI * 180.); - } - - /** - * Returns a pair whose first component is the leg number (one-based) of the competitor returned as the second component. - */ - private com.sap.sse.common.Util.Pair getFarthestAheadVisibleCompetitorWithOneBasedLegNumber( - Iterable competitorsToShow) { - CompetitorDTO leadingCompetitorDTO = null; - int legOfLeaderCompetitor = -1; - // this only works because the quickRanks are sorted - for (Entry competitorsByWindwardDistanceTraveledAndOneBasedLegNumber : - competitorsInOrderOfWindwardDistanceTraveledWithOneBasedLegNumber.entrySet()) { - if (Util.contains(competitorsToShow, competitorsByWindwardDistanceTraveledAndOneBasedLegNumber.getKey()) && - competitorsByWindwardDistanceTraveledAndOneBasedLegNumber.getValue() != null) { - leadingCompetitorDTO = competitorsByWindwardDistanceTraveledAndOneBasedLegNumber.getKey(); - legOfLeaderCompetitor = competitorsByWindwardDistanceTraveledAndOneBasedLegNumber.getValue(); - return new com.sap.sse.common.Util.Pair(legOfLeaderCompetitor, leadingCompetitorDTO); - } - } - return null; - } - - final static Distance advantageLineLength = new MeterDistance(1000); // TODO this should probably rather scale with the visible area of the map; bug 616 - private void showAdvantageLine(Iterable competitorsToShow, Date date, long timeForPositionTransitionMillis) { - if (map != null && lastRaceTimesInfo != null && quickRanks != null && lastCombinedWindTrackInfoDTO != null) { - boolean drawAdvantageLine = false; - if (settings.getHelpLinesSettings().isVisible(HelpLineTypes.ADVANTAGELINE)) { - // find competitor with highest rank - com.sap.sse.common.Util.Pair visibleLeaderInfo = getFarthestAheadVisibleCompetitorWithOneBasedLegNumber(competitorsToShow); - // the boat fix may be null; may mean that no positions were loaded yet for the leading visible boat; - // don't show anything - GPSFixDTOWithSpeedWindTackAndLegType lastBoatFix = null; - boolean isVisibleLeaderInfoComplete = false; - boolean isLegTypeKnown = false; - WindTrackInfoDTO windDataForLegMiddle = null; - LegInfoDTO legInfoDTO = null; - if (visibleLeaderInfo != null - && visibleLeaderInfo.getA() > 0 - && visibleLeaderInfo.getA() <= lastRaceTimesInfo.getLegInfos().size() - // get wind at middle of leg for leading visible competitor - && (windDataForLegMiddle = lastCombinedWindTrackInfoDTO - .getCombinedWindOnLegMiddle(visibleLeaderInfo.getA() - 1)) != null - && !windDataForLegMiddle.windFixes.isEmpty()) { - isVisibleLeaderInfoComplete = true; - legInfoDTO = lastRaceTimesInfo.getLegInfos().get(visibleLeaderInfo.getA() - 1); - if (legInfoDTO.legType != null) { - isLegTypeKnown = true; - } - lastBoatFix = getBoatFix(visibleLeaderInfo.getB(), date); - } - if (isVisibleLeaderInfoComplete && isLegTypeKnown && lastBoatFix != null && lastBoatFix.speedWithBearing != null) { - Distance distanceFromBoatPositionInKm = visibleLeaderInfo.getB().getBoatClass().getHullLength(); // one hull length - // implement and use Position.translateRhumb() - double bearingOfBoatInDeg = lastBoatFix.speedWithBearing.bearingInDegrees; - LatLng boatPosition = coordinateSystem.toLatLng(lastBoatFix.position); - LatLng posAheadOfFirstBoat = calculatePositionAlongRhumbline(boatPosition, - coordinateSystem.mapDegreeBearing(bearingOfBoatInDeg), distanceFromBoatPositionInKm); - final WindDTO windFix = windDataForLegMiddle.windFixes.get(0); - double bearingOfCombinedWindInDeg = windFix.trueWindBearingDeg; - double rotatedBearingDeg1 = 0.0; - double rotatedBearingDeg2 = 0.0; - switch (lastBoatFix.legType) { - case UPWIND: - case DOWNWIND: { - rotatedBearingDeg1 = bearingOfCombinedWindInDeg + 90.0; - if (rotatedBearingDeg1 >= 360.0) { - rotatedBearingDeg1 -= 360.0; - } - rotatedBearingDeg2 = bearingOfCombinedWindInDeg - 90.0; - if (rotatedBearingDeg2 < 0.0) { - rotatedBearingDeg2 += 360.0; - } - } - break; - case REACHING: { - rotatedBearingDeg1 = legInfoDTO.legBearingInDegrees + 90.0; - if (rotatedBearingDeg1 >= 360.0) { - rotatedBearingDeg1 -= 360.0; - } - rotatedBearingDeg2 = legInfoDTO.legBearingInDegrees - 90.0; - if (rotatedBearingDeg2 < 0.0) { - rotatedBearingDeg2 += 360.0; - } - } - break; - } - MVCArray nextPath = MVCArray.newInstance(); - LatLng advantageLinePos1 = calculatePositionAlongRhumbline(posAheadOfFirstBoat, - coordinateSystem.mapDegreeBearing(rotatedBearingDeg1), advantageLineLength.scale(0.5)); - LatLng advantageLinePos2 = calculatePositionAlongRhumbline(posAheadOfFirstBoat, - coordinateSystem.mapDegreeBearing(rotatedBearingDeg2), advantageLineLength.scale(0.5)); - if (advantageLine == null) { - PolylineOptions options = PolylineOptions.newInstance(); - options.setClickable(true); - options.setGeodesic(true); - options.setStrokeColor("#000000"); - options.setStrokeWeight(1); - options.setStrokeOpacity(0.5); - - advantageLine = Polyline.newInstance(options); - advantageTimer = new AdvantageLineAnimator(advantageLine); - MVCArray pointsAsArray = MVCArray.newInstance(); - pointsAsArray.insertAt(0, advantageLinePos1); - pointsAsArray.insertAt(1, advantageLinePos2); - advantageLine.setPath(pointsAsArray); - advantageLine.setMap(map); - Hoverline advantageHoverline = new Hoverline(advantageLine, options, this); - - advantageLineMouseOverHandler = new AdvantageLineMouseOverMapHandler( - bearingOfCombinedWindInDeg, new Date(windFix.measureTimepoint)); - advantageLine.addMouseOverHandler(advantageLineMouseOverHandler); - advantageHoverline.addMouseOutMoveHandler(new MouseOutMapHandler() { - @Override - public void onEvent(MouseOutMapEvent event) { - map.setTitle(""); - } - }); - } else { - nextPath.push(advantageLinePos1); - nextPath.push(advantageLinePos2); - advantageTimer.setNextPositionAndTransitionMillis(nextPath, timeForPositionTransitionMillis); - if (advantageLineMouseOverHandler != null) { - advantageLineMouseOverHandler.setTrueWindBearing(bearingOfCombinedWindInDeg); - advantageLineMouseOverHandler.setDate(new Date(windFix.measureTimepoint)); - } - } - drawAdvantageLine = true; - } - } - if (!drawAdvantageLine) { - if (advantageLine != null) { - advantageLine.setMap(null); - advantageLine = null; - advantageTimer = null; - } - } - } - } - - private final StringBuilder windwardStartLineMarkToFirstMarkLineText = new StringBuilder(); - private final StringBuilder leewardStartLineMarkToFirstMarkLineText = new StringBuilder(); - - private void showStartLineToFirstMarkTriangle(final CoursePositionsDTO courseDTO){ - final List startMarkPositions = courseDTO.getStartMarkPositions(); - if (startMarkPositions.size() > 1 && courseDTO.waypointPositions.size() > 1) { - final Position windwardStartLinePosition = startMarkPositions.get(0); - final Position leewardStartLinePosition = startMarkPositions.get(1); - final Position firstMarkPosition = courseDTO.waypointPositions.get(1); - windwardStartLineMarkToFirstMarkLineText.replace(0, windwardStartLineMarkToFirstMarkLineText.length(), - stringMessages.startLineToFirstMarkTriangle(numberFormatOneDecimal - .format(windwardStartLinePosition.getDistance(firstMarkPosition) - .getMeters()))); - leewardStartLineMarkToFirstMarkLineText.replace(0, leewardStartLineMarkToFirstMarkLineText.length(), - stringMessages.startLineToFirstMarkTriangle(numberFormatOneDecimal - .format(leewardStartLinePosition.getDistance(firstMarkPosition) - .getMeters()))); - final LineInfoProvider windwardStartLineMarkToFirstMarkLineInfoProvider = new LineInfoProvider() { - @Override - public String getLineInfo() { - return windwardStartLineMarkToFirstMarkLineText.toString(); - } - }; - final LineInfoProvider leewardStartLineMarkToFirstMarkLineInfoProvider = new LineInfoProvider() { - @Override - public String getLineInfo() { - return leewardStartLineMarkToFirstMarkLineText.toString(); - } - }; - windwardStartLineMarkToFirstMarkLine = showOrRemoveOrUpdateLine(windwardStartLineMarkToFirstMarkLine, /* showLine */ - (settings.getHelpLinesSettings().isVisible(HelpLineTypes.STARTLINETOFIRSTMARKTRIANGLE)) - && startMarkPositions.size() > 1 && courseDTO.waypointPositions.size() > 1, - windwardStartLinePosition, firstMarkPosition, windwardStartLineMarkToFirstMarkLineInfoProvider, - "grey"); - leewardStartLineMarkToFirstMarkLine = showOrRemoveOrUpdateLine(leewardStartLineMarkToFirstMarkLine, /* showLine */ - (settings.getHelpLinesSettings().isVisible(HelpLineTypes.STARTLINETOFIRSTMARKTRIANGLE)) - && startMarkPositions.size() > 1 && courseDTO.waypointPositions.size() > 1, - leewardStartLinePosition, firstMarkPosition, leewardStartLineMarkToFirstMarkLineInfoProvider, - "grey"); - } - } - - private final StringBuilder startLineAdvantageText = new StringBuilder(); - private final StringBuilder finishLineAdvantageText = new StringBuilder(); - final LineInfoProvider startLineInfoProvider = new LineInfoProvider() { - @Override - public String getLineInfo() { - return stringMessages.startLine()+startLineAdvantageText; - } - }; - final LineInfoProvider finishLineInfoProvider = new LineInfoProvider() { - @Override - public String getLineInfo() { - return stringMessages.finishLine()+finishLineAdvantageText; - } - }; - - private void showStartAndFinishAndCourseMiddleLines(final CoursePositionsDTO courseDTO) { - if (map != null && courseDTO != null && courseDTO.course != null && courseDTO.course.waypoints != null && - !courseDTO.course.waypoints.isEmpty()) { - // draw the start line - final WaypointDTO startWaypoint = courseDTO.course.waypoints.get(0); - updateCountdownCanvas(startWaypoint); - final int numberOfStartWaypointMarks = courseDTO.getStartMarkPositions() == null ? 0 : courseDTO.getStartMarkPositions().size(); - final int numberOfFinishWaypointMarks = courseDTO.getFinishMarkPositions() == null ? 0 : courseDTO.getFinishMarkPositions().size(); - final Position startLineLeftPosition = numberOfStartWaypointMarks == 0 ? null : courseDTO.getStartMarkPositions().get(0); - final Position startLineRightPosition = numberOfStartWaypointMarks < 2 ? null : courseDTO.getStartMarkPositions().get(1); - if (courseDTO.startLineAngleFromPortToStarboardWhenApproachingLineToCombinedWind != null) { - startLineAdvantageText.replace(0, startLineAdvantageText.length(), " "+stringMessages.lineAngleToWindAndAdvantage( - NumberFormat.getFormat("0.0").format(courseDTO.startLineLengthInMeters), - NumberFormat.getFormat("0.0").format(Math.abs(courseDTO.startLineAngleFromPortToStarboardWhenApproachingLineToCombinedWind)), - courseDTO.startLineAdvantageousSide.name().charAt(0)+courseDTO.startLineAdvantageousSide.name().substring(1).toLowerCase(), - NumberFormat.getFormat("0.0").format(courseDTO.startLineAdvantageInMeters))); - } else { - startLineAdvantageText.delete(0, startLineAdvantageText.length()); - } - final boolean showStartLineBasedOnCurrentLeg = numberOfStartWaypointMarks == 2 && courseDTO.currentLegNumber <= 1; - final boolean showFinishLineBasedOnCurrentLeg = numberOfFinishWaypointMarks == 2 && courseDTO.currentLegNumber == courseDTO.totalLegsCount; - // show the line when STARTLINE is selected and the current leg is around the start leg, - // or when COURSEGEOMETRY is selected and the finish line isn't equal and wouldn't be shown at the same time based on the current leg. - // With this, if COURSEGEOMETRY is selected and start and finish line are equal, the start line will not be displayed if - // based on the race progress the finish line is to be preferred, so only the finish line will be shown. - final boolean reallyShowStartLine = - (settings.getHelpLinesSettings().isVisible(HelpLineTypes.STARTLINE) && showStartLineBasedOnCurrentLeg) || - (settings.getHelpLinesSettings().isVisible(HelpLineTypes.COURSEGEOMETRY) && - (!showFinishLineBasedOnCurrentLeg || !startLineEqualsFinishLine(courseDTO))); - // show the line when FINISHLINE is selected and the current leg is the last leg, - // or when COURSEGEOMETRY is selected and the start line isn't equal or the current leg is the last leg. - // With this, if COURSEGEOMETRY is selected and start and finish line are equal, the start line will be displayed unless - // the finish line should take precedence based on race progress. - final boolean reallyShowFinishLine = showFinishLineBasedOnCurrentLeg && - (!showStartLineBasedOnCurrentLeg || !startLineEqualsFinishLine(courseDTO)) && - (settings.getHelpLinesSettings().isVisible(HelpLineTypes.FINISHLINE) && showFinishLineBasedOnCurrentLeg) || - (settings.getHelpLinesSettings().isVisible(HelpLineTypes.COURSEGEOMETRY) && - (!startLineEqualsFinishLine(courseDTO) || showFinishLineBasedOnCurrentLeg)); - startLine = showOrRemoveOrUpdateLine(startLine, reallyShowStartLine, - startLineLeftPosition, startLineRightPosition, startLineInfoProvider, "#ffffff"); - // draw the finish line - final Position finishLineLeftPosition = numberOfFinishWaypointMarks == 0 ? null : courseDTO.getFinishMarkPositions().get(0); - final Position finishLineRightPosition = numberOfFinishWaypointMarks < 2 ? null : courseDTO.getFinishMarkPositions().get(1); - if (courseDTO.finishLineAngleFromPortToStarboardWhenApproachingLineToCombinedWind != null) { - finishLineAdvantageText.replace(0, finishLineAdvantageText.length(), " "+stringMessages.lineAngleToWindAndAdvantage( - NumberFormat.getFormat("0.0").format(courseDTO.finishLineLengthInMeters), - NumberFormat.getFormat("0.0").format(Math.abs(courseDTO.finishLineAngleFromPortToStarboardWhenApproachingLineToCombinedWind)), - courseDTO.finishLineAdvantageousSide.name().charAt(0)+courseDTO.finishLineAdvantageousSide.name().substring(1).toLowerCase(), - NumberFormat.getFormat("0.0").format(courseDTO.finishLineAdvantageInMeters))); - } else { - finishLineAdvantageText.delete(0, finishLineAdvantageText.length()); - } - finishLine = showOrRemoveOrUpdateLine(finishLine, reallyShowFinishLine, - finishLineLeftPosition, finishLineRightPosition, finishLineInfoProvider, "#000000"); - // the control point pairs for which we already decided whether or not - // to show a course middle line for; values tell whether to show the line and for which zero-based - // start waypoint index to do so; when for an equal control point pair multiple decisions with different - // outcome are made, a decision to show the line overrules the decision to not show it (OR-semantics) - final Map, Pair> keysAlreadyHandled = new HashMap<>(); - for (int zeroBasedIndexOfStartWaypoint = 0; zeroBasedIndexOfStartWaypoint key = getCourseMiddleLinesKey(courseDTO, zeroBasedIndexOfStartWaypoint); - boolean showCourseMiddleLine = keysAlreadyHandled.containsKey(key) && keysAlreadyHandled.get(key).getA() || - settings.getHelpLinesSettings().isVisible(HelpLineTypes.COURSEGEOMETRY) || - (settings.getHelpLinesSettings().isVisible(HelpLineTypes.COURSEMIDDLELINE) - && courseDTO.currentLegNumber > 0 - && courseDTO.currentLegNumber-1 == zeroBasedIndexOfStartWaypoint); - keysAlreadyHandled.put(key, new Pair<>(showCourseMiddleLine, zeroBasedIndexOfStartWaypoint)); - } - Set> keysToConsider = new HashSet<>(keysAlreadyHandled.keySet()); - keysToConsider.addAll(courseMiddleLines.keySet()); - for (final Set key : keysToConsider) { - final int zeroBasedIndexOfStartWaypoint = keysAlreadyHandled.containsKey(key) ? - keysAlreadyHandled.get(key).getB() : 0; // if not handled, the line will be removed, so the waypoint index doesn't matter - final Pair showLineAndZeroBasedIndexOfStartWaypoint = keysAlreadyHandled.get(key); - final boolean showCourseMiddleLine = showLineAndZeroBasedIndexOfStartWaypoint != null && showLineAndZeroBasedIndexOfStartWaypoint.getA(); - courseMiddleLines.put(key, showOrRemoveCourseMiddleLine(courseDTO, courseMiddleLines.get(key), zeroBasedIndexOfStartWaypoint, showCourseMiddleLine)); - } - } - } - - private boolean startLineEqualsFinishLine(CoursePositionsDTO courseDTO) { - final List waypoints; - return courseDTO != null && courseDTO.course != null && - (waypoints = courseDTO.course.waypoints) != null && - waypoints.get(0).controlPoint.equals(waypoints.get(waypoints.size()-1).controlPoint); - } - - /** - * Given a zero-based index into courseDTO's {@link RaceCourseDTO#waypoints waypoints list} that denotes the start - * waypoint of the leg in question, returns a key that can be used for the {@link #courseMiddleLines} map, consisting of a set - * that holds the two {@link ControlPointDTO}s representing the start and finish control point of that leg. - */ - private Set getCourseMiddleLinesKey(final CoursePositionsDTO courseDTO, - final int zeroBasedIndexOfStartWaypoint) { - ControlPointDTO startControlPoint = courseDTO.course.waypoints.get(zeroBasedIndexOfStartWaypoint).controlPoint; - ControlPointDTO endControlPoint = courseDTO.course.waypoints.get(zeroBasedIndexOfStartWaypoint+1).controlPoint; - final Set key = new HashSet<>(); - key.add(startControlPoint); - key.add(endControlPoint); - return key; - } - - /** - * @param showLine - * tells whether or not to show the line; if the lineToShowOrRemoveOrUpdate references a - * line but the line shall not be shown, the line is removed from the map; conversely, if the line is not - * yet shown but shall be, a new line is created, added to the map and returned. If the line is shown and - * shall continue to be shown, the line is returned after updating its vertex coordinates. - * @return null if the line is not shown; the polyline object representing the line being displayed - * otherwise - */ - private Polyline showOrRemoveCourseMiddleLine(final CoursePositionsDTO courseDTO, Polyline lineToShowOrRemoveOrUpdate, - final int zeroBasedIndexOfStartWaypoint, final boolean showLine) { - final Position position1DTO = courseDTO.waypointPositions.get(zeroBasedIndexOfStartWaypoint); - final Position position2DTO = courseDTO.waypointPositions.get(zeroBasedIndexOfStartWaypoint+1); - final LineInfoProvider lineInfoProvider = new LineInfoProvider() { - @Override - public String getLineInfo() { - final StringBuilder sb = new StringBuilder(); - sb.append(stringMessages.courseMiddleLine()); - sb.append('\n'); - sb.append(NumberFormat.getFormat("0").format( - Math.abs(position1DTO.getDistance(position2DTO).getMeters()))+stringMessages.metersUnit()); - if (lastCombinedWindTrackInfoDTO != null) { - final WindTrackInfoDTO windTrackAtLegMiddle = lastCombinedWindTrackInfoDTO.getCombinedWindOnLegMiddle(zeroBasedIndexOfStartWaypoint); - if (windTrackAtLegMiddle != null && windTrackAtLegMiddle.windFixes != null && !windTrackAtLegMiddle.windFixes.isEmpty()) { - WindDTO windAtLegMiddle = windTrackAtLegMiddle.windFixes.get(0); - final double legBearingDeg = position1DTO.getBearingGreatCircle(position2DTO).getDegrees(); - final String diff = NumberFormat.getFormat("0.0").format( - Math.min(Math.abs(windAtLegMiddle.dampenedTrueWindBearingDeg-legBearingDeg), - Math.abs(windAtLegMiddle.dampenedTrueWindFromDeg-legBearingDeg))); - sb.append(", "); - sb.append(stringMessages.degreesToWind(diff)); - } - } - return sb.toString(); - } - }; - return showOrRemoveOrUpdateLine(lineToShowOrRemoveOrUpdate, showLine, position1DTO, position2DTO, lineInfoProvider, "#2268a0"); - } - - private interface LineInfoProvider { - String getLineInfo(); - } - - /** - * @param showLine - * tells whether or not to show the line; if the lineToShowOrRemoveOrUpdate references a - * line but the line shall not be shown, the line is removed from the map; conversely, if the line is not - * yet shown but shall be, a new line is created, added to the map and returned. If the line is shown and - * shall continue to be shown, the line is returned after updating its vertex coordinates. - * @return null if the line is not shown; the polyline object representing the line being displayed - * otherwise - */ - private Polyline showOrRemoveOrUpdateLine(Polyline lineToShowOrRemoveOrUpdate, final boolean showLine, - final Position position1DTO, final Position position2DTO, final LineInfoProvider lineInfoProvider, String lineColorRGB) { - if (position1DTO != null && position2DTO != null) { - if (showLine) { - LatLng courseMiddleLinePoint1 = coordinateSystem.toLatLng(position1DTO); - LatLng courseMiddleLinePoint2 = coordinateSystem.toLatLng(position2DTO); - final MVCArray pointsAsArray; - if (lineToShowOrRemoveOrUpdate == null) { - PolylineOptions options = PolylineOptions.newInstance(); - options.setClickable(true); - options.setGeodesic(true); - options.setStrokeColor(lineColorRGB); - options.setStrokeWeight(1); - options.setStrokeOpacity(1.0); - pointsAsArray = MVCArray.newInstance(); - lineToShowOrRemoveOrUpdate = Polyline.newInstance(options); - lineToShowOrRemoveOrUpdate.setPath(pointsAsArray); - lineToShowOrRemoveOrUpdate.setMap(map); - Hoverline lineToShowOrRemoveOrUpdateHoverline = new Hoverline(lineToShowOrRemoveOrUpdate, options, this); - lineToShowOrRemoveOrUpdate.addMouseOverHandler(new MouseOverMapHandler() { - @Override - public void onEvent(MouseOverMapEvent event) { - map.setTitle(lineInfoProvider.getLineInfo()); - } - }); - lineToShowOrRemoveOrUpdateHoverline.addMouseOutMoveHandler(new MouseOutMapHandler() { - @Override - public void onEvent(MouseOutMapEvent event) { - map.setTitle(""); - } - }); - } else { - pointsAsArray = lineToShowOrRemoveOrUpdate.getPath(); - pointsAsArray.removeAt(1); - pointsAsArray.removeAt(0); - } - adjustInfoOverlayForVisibleLine(lineToShowOrRemoveOrUpdate, position1DTO, position2DTO, lineInfoProvider); - pointsAsArray.insertAt(0, courseMiddleLinePoint1); - pointsAsArray.insertAt(1, courseMiddleLinePoint2); - } else { - if (lineToShowOrRemoveOrUpdate != null) { - lineToShowOrRemoveOrUpdate.setMap(null); - adjustInfoOverlayForRemovedLine(lineToShowOrRemoveOrUpdate); - lineToShowOrRemoveOrUpdate = null; - } - } - } - return lineToShowOrRemoveOrUpdate; - } - - private void adjustInfoOverlayForRemovedLine(Polyline lineToShowOrRemoveOrUpdate) { - SmallTransparentInfoOverlay infoOverlay = infoOverlaysForLinesForCourseGeometry.remove(lineToShowOrRemoveOrUpdate); - if (infoOverlay != null) { - infoOverlay.removeFromMap(); - } - } - - private void adjustInfoOverlayForVisibleLine(Polyline lineToShowOrRemoveOrUpdate, final Position position1DTO, - final Position position2DTO, final LineInfoProvider lineInfoProvider) { - SmallTransparentInfoOverlay infoOverlay = infoOverlaysForLinesForCourseGeometry.get(lineToShowOrRemoveOrUpdate); - if (getSettings().getHelpLinesSettings().isVisible(HelpLineTypes.COURSEGEOMETRY)) { - if (infoOverlay == null) { - infoOverlay = new SmallTransparentInfoOverlay(map, RaceMapOverlaysZIndexes.INFO_OVERLAY_ZINDEX, lineInfoProvider.getLineInfo(), coordinateSystem); - infoOverlaysForLinesForCourseGeometry.put(lineToShowOrRemoveOrUpdate, infoOverlay); - infoOverlay.addToMap(); - } else { - infoOverlay.setInfoText(lineInfoProvider.getLineInfo()); - } - infoOverlay.setPosition(position1DTO.translateGreatCircle(position1DTO.getBearingGreatCircle(position2DTO), - position1DTO.getDistance(position2DTO).scale(0.5)), /* transition time */ -1); - infoOverlay.draw(); - } else { - if (infoOverlay != null) { - infoOverlay.removeFromMap(); - infoOverlaysForLinesForCourseGeometry.remove(lineToShowOrRemoveOrUpdate); - } - } - } - - /** - * If, according to {@link #lastRaceTimesInfo} and {@link #timer} the race is - * still in the pre-start phase, show a {@link SmallTransparentInfoOverlay} at the - * start line that shows the count down. - */ - private void updateCountdownCanvas(WaypointDTO startWaypoint) { - if (!settings.isShowSelectedCompetitorsInfo() || startWaypoint == null || Util.isEmpty(startWaypoint.controlPoint.getMarks()) - || lastRaceTimesInfo == null || lastRaceTimesInfo.startOfRace == null || timer.getTime().after(lastRaceTimesInfo.startOfRace)) { - if (countDownOverlay != null) { - countDownOverlay.removeFromMap(); - countDownOverlay = null; - } - } else { - long timeToStartInMs = lastRaceTimesInfo.startOfRace.getTime() - timer.getTime().getTime(); - String countDownText = timeToStartInMs > 1000 ? stringMessages.timeToStart(DateAndTimeFormatterUtil - .formatElapsedTime(timeToStartInMs)) : stringMessages.start(); - if (countDownOverlay == null) { - countDownOverlay = new SmallTransparentInfoOverlay(map, RaceMapOverlaysZIndexes.INFO_OVERLAY_ZINDEX, - countDownText, coordinateSystem); - countDownOverlay.addToMap(); - } else { - countDownOverlay.setInfoText(countDownText); - } - countDownOverlay.setPosition(startWaypoint.controlPoint.getMarks().iterator().next().position, /* transition time */ -1); - countDownOverlay.draw(); - } - } - - // Google scales coordinates so that the globe-tile has mercator-latitude [-pi, +pi], i.e. tile height of 2*pi - // mercator-latitude pi corresponds to geo-latitude of approx. 85.0998 (where Google cuts off the map visualization) - // official documentation: http://developers.google.com/maps/documentation/javascript/maptypes#TileCoordinates - private double getMercatorLatitude(double lat) { - // cutting-off for latitudes close to +-90 degrees is recommended (to avoid division by zero) - double sine = Math.max(-0.9999, Math.min(0.9999, Math.sin(Math.PI * lat / 180))); - return Math.log((1 + sine) / (1 - sine)) / 2; - } - - public int getZoomLevel(LatLngBounds bounds) { - int GLOBE_PXSIZE = 256; // a constant in Google's map projection - int MAX_ZOOM = 20; // maximum zoom-level that should be automatically selected - double LOG2 = Math.log(2.0); - double deltaLng = bounds.getNorthEast().getLongitude() - bounds.getSouthWest().getLongitude(); - double deltaLat = getMercatorLatitude(bounds.getNorthEast().getLatitude()) - getMercatorLatitude(bounds.getSouthWest().getLatitude()); - if ((deltaLng == 0) && (deltaLat == 0)) { - return MAX_ZOOM; - } - if (deltaLng < 0) { - deltaLng += 360; - } - int zoomLng = (int) Math.floor(Math.log(map.getDiv().getClientWidth() * 360 / deltaLng / GLOBE_PXSIZE) / LOG2); - int zoomLat = (int) Math.floor(Math.log(map.getDiv().getClientHeight() * 2 * Math.PI / deltaLat / GLOBE_PXSIZE) / LOG2); - return Math.min(Math.min(zoomLat, zoomLng), MAX_ZOOM); - } - - private void zoomMapToNewBounds(LatLngBounds newBounds) { - if (newBounds != null) { - LatLngBounds currentMapBounds; - if (map.getBounds() == null - || !BoundsUtil.contains((currentMapBounds = map.getBounds()), newBounds) - || graticuleAreaRatio(currentMapBounds, newBounds) > 10) { - // only change bounds if the new bounds don't fit into the current map zoom - Iterable oldZoomTypesToConsiderSettings = settings.getZoomSettings().getTypesToConsiderOnZoom(); - setAutoZoomInProgress(true); - autoZoomLatLngBounds = newBounds; - int newZoomLevel = getZoomLevel(autoZoomLatLngBounds); - if (newZoomLevel != map.getZoom()) { - // distinguish between zoom-in and zoom-out, because the sequence of panTo() and setZoom() - // appears different on the screen due to map-animations - // following sequences keep the selected boats allways visible: - // zoom-in : 1. panTo(), 2. setZoom() - // zoom-out: 1. setZoom(), 2. panTo() - autoZoomIn = newZoomLevel > map.getZoom(); - autoZoomOut = !autoZoomIn; - autoZoomLevel = newZoomLevel; - removeTransitions(); - if (autoZoomIn) { - map.panTo(autoZoomLatLngBounds.getCenter()); - } else { - map.setZoom(autoZoomLevel); - } - } else { - map.panTo(autoZoomLatLngBounds.getCenter()); - } - RaceMapZoomSettings restoredZoomSettings = new RaceMapZoomSettings(oldZoomTypesToConsiderSettings, settings.getZoomSettings().isZoomToSelectedCompetitors()); - settings = new RaceMapSettings(settings, restoredZoomSettings); - setAutoZoomInProgress(false); - } - } - } - - private double graticuleAreaRatio(LatLngBounds containing, LatLngBounds contained) { - assert BoundsUtil.contains(containing, contained); - double containingAreaRatio = getGraticuleArea(containing) / getGraticuleArea(contained); - return containingAreaRatio; - } - - /** - * A much simplified "area" calculation for a {@link Bounds} object, multiplying the differences in latitude and longitude degrees. - * The result therefore is in the order of magnitude of 60*60 square nautical miles. - */ - private double getGraticuleArea(LatLngBounds bounds) { - return ((BoundsUtil.isCrossesDateLine(bounds) ? bounds.getNorthEast().getLongitude()+360 : bounds.getNorthEast().getLongitude())-bounds.getSouthWest().getLongitude()) * - (bounds.getNorthEast().getLatitude() - bounds.getSouthWest().getLatitude()); - } - - private void setAutoZoomInProgress(boolean autoZoomInProgress) { - this.autoZoomInProgress = autoZoomInProgress; - } - - boolean isAutoZoomInProgress() { - return autoZoomInProgress; - } - - /** - * @param timeForPositionTransitionMillis use -1 to not animate the position transition, e.g., during map zoom or non-play - */ - private boolean updateBoatCanvasForCompetitor(CompetitorDTO competitorDTO, Date date, long timeForPositionTransitionMillis) { - boolean usedExistingCanvas = false; - GPSFixDTOWithSpeedWindTackAndLegType lastBoatFix = getBoatFix(competitorDTO, date); - if (lastBoatFix != null) { - BoatOverlay boatOverlay = boatOverlays.get(competitorDTO); - if (boatOverlay == null) { - boatOverlay = createBoatOverlay(RaceMapOverlaysZIndexes.BOATS_ZINDEX, competitorDTO, displayHighlighted(competitorDTO)); - boatOverlays.put(competitorDTO, boatOverlay); - boatOverlay.setSelected(displayHighlighted(competitorDTO)); - boatOverlay.setBoatFix(lastBoatFix, timeForPositionTransitionMillis); - boatOverlay.addToMap(); - } else { - usedExistingCanvas = true; - boatOverlay.setSelected(displayHighlighted(competitorDTO)); - boatOverlay.setBoatFix(lastBoatFix, timeForPositionTransitionMillis); - boatOverlay.draw(); - } - } - - return usedExistingCanvas; - } - - private boolean displayHighlighted(CompetitorDTO competitorDTO) { - return !settings.isShowOnlySelectedCompetitors() && competitorSelection.isSelected(competitorDTO); - } - - private class CourseMarkInfoWindowClickHandler implements ClickMapHandler { - private final MarkDTO markDTO; - private final CourseMarkOverlay courseMarkOverlay; - - public CourseMarkInfoWindowClickHandler(MarkDTO markDTO, CourseMarkOverlay courseMarkOverlay) { - this.markDTO = markDTO; - this.courseMarkOverlay = courseMarkOverlay; - } - - @Override - public void onEvent(ClickMapEvent event) { - LatLng latlng = courseMarkOverlay.getMarkLatLngPosition(); - showMarkInfoWindow(markDTO, latlng); - } - } - - private void registerCourseMarkInfoWindowClickHandler(final String markDTOName) { - final CourseMarkOverlay courseMarkOverlay = courseMarkOverlays.get(markDTOName); - courseMarkClickHandlers.put(markDTOName, - courseMarkOverlay.addClickHandler(new CourseMarkInfoWindowClickHandler(markDTOs.get(markDTOName), courseMarkOverlay))); - } - - public void registerAllCourseMarkInfoWindowClickHandlers() { - for (String markDTOName : markDTOs.keySet()) { - registerCourseMarkInfoWindowClickHandler(markDTOName); - } - } - - public void unregisterAllCourseMarkInfoWindowClickHandlers() { - Iterator> iterator = courseMarkClickHandlers.entrySet().iterator(); - while(iterator.hasNext()) { - Entry handler = iterator.next(); - handler.getValue().removeHandler(); - iterator.remove(); - } - } - - private CompetitorInfoOverlay createCompetitorInfoOverlay(int zIndex, final CompetitorDTO competitorDTO) { - GPSFixDTOWithSpeedWindTackAndLegType gpsFixDTO = getBoatFix(competitorDTO, timer.getTime()); - return new CompetitorInfoOverlay(map, zIndex, competitorSelection.getColor(competitorDTO, raceIdentifier), createInfoText(competitorDTO, gpsFixDTO), coordinateSystem); - } - - private String createInfoText(CompetitorDTO competitorDTO, GPSFixDTOWithSpeedWindTackAndLegType gpsFixDTO) { - StringBuilder infoText = new StringBuilder(); - infoText.append(competitorDTO.getSailID()).append("\n"); - infoText.append(NumberFormatterFactory.getDecimalFormat(1).format(gpsFixDTO.speedWithBearing.speedInKnots)) - .append(" ").append(stringMessages.knotsUnit()).append("\n"); - QuickRankDTO quickRankDTO = quickRanks.get(competitorDTO); - if (quickRankDTO != null) { - infoText.append(stringMessages.rank()).append(" : ").append(quickRanks.get(competitorDTO).rank); - } - return infoText.toString(); - } - - private BoatOverlay createBoatOverlay(int zIndex, final CompetitorDTO competitorDTO, boolean highlighted) { - final BoatOverlay boatCanvas = new BoatOverlay(map, zIndex, competitorDTO, competitorSelection.getColor(competitorDTO, raceIdentifier), coordinateSystem); - boatCanvas.setSelected(highlighted); - boatCanvas.addClickHandler(new ClickMapHandler() { - @Override - public void onEvent(ClickMapEvent event) { - if (lastInfoWindow != null) { - lastInfoWindow.close(); - } - GPSFixDTOWithSpeedWindTackAndLegType latestFixForCompetitor = getBoatFix(competitorDTO, timer.getTime()); - LatLng where = coordinateSystem.toLatLng(latestFixForCompetitor.position); - InfoWindowOptions options = InfoWindowOptions.newInstance(); - InfoWindow infoWindow = InfoWindow.newInstance(options); - infoWindow.setContent(getInfoWindowContent(competitorDTO, latestFixForCompetitor)); - infoWindow.setPosition(where); - lastInfoWindow = infoWindow; - infoWindow.open(map); - } - }); - - boatCanvas.addMouseOverHandler(new MouseOverMapHandler() { - @Override - public void onEvent(MouseOverMapEvent event) { - map.setTitle(competitorDTO.getSailID()); - } - }); - boatCanvas.addMouseOutMoveHandler(new MouseOutMapHandler() { - @Override - public void onEvent(MouseOutMapEvent event) { - map.setTitle(""); - } - }); - - return boatCanvas; - } - - protected WindSensorOverlay createWindSensorOverlay(int zIndex, final WindSource windSource, final WindTrackInfoDTO windTrackInfoDTO) { - final WindSensorOverlay windSensorOverlay = new WindSensorOverlay(map, zIndex, raceMapImageManager, stringMessages, coordinateSystem); - windSensorOverlay.setWindInfo(windTrackInfoDTO, windSource); - windSensorOverlay.addClickHandler(new ClickMapHandler() { - @Override - public void onEvent(ClickMapEvent event) { - showWindSensorInfoWindow(windSensorOverlay); - } - }); - return windSensorOverlay; - } - - private void showMarkInfoWindow(MarkDTO markDTO, LatLng position) { - if(lastInfoWindow != null) { - lastInfoWindow.close(); - } - InfoWindowOptions options = InfoWindowOptions.newInstance(); - InfoWindow infoWindow = InfoWindow.newInstance(options); - infoWindow.setContent(getInfoWindowContent(markDTO)); - infoWindow.setPosition(position); - lastInfoWindow = infoWindow; - infoWindow.open(map); - } - - private void showCompetitorInfoWindow(final CompetitorDTO competitorDTO, LatLng where) { - if(lastInfoWindow != null) { - lastInfoWindow.close(); - } - GPSFixDTOWithSpeedWindTackAndLegType latestFixForCompetitor = getBoatFix(competitorDTO, timer.getTime()); - // TODO find close fix where the mouse was; see BUG 470 - InfoWindowOptions options = InfoWindowOptions.newInstance(); - InfoWindow infoWindow = InfoWindow.newInstance(options); - infoWindow.setContent(getInfoWindowContent(competitorDTO, latestFixForCompetitor)); - infoWindow.setPosition(where); - lastInfoWindow = infoWindow; - infoWindow.open(map); - } - - private String formatPosition(double lat, double lng) { - NumberFormat numberFormat = NumberFormat.getFormat("0.00000"); - String result = numberFormat.format(lat) + " lat, " + numberFormat.format(lng) + " lng"; - return result; - } - - private void showWindSensorInfoWindow(final WindSensorOverlay windSensorOverlay) { - WindSource windSource = windSensorOverlay.getWindSource(); - WindTrackInfoDTO windTrackInfoDTO = windSensorOverlay.getWindTrackInfoDTO(); - WindDTO windDTO = windTrackInfoDTO.windFixes.get(0); - if(windDTO != null && windDTO.position != null) { - if(lastInfoWindow != null) { - lastInfoWindow.close(); - } - LatLng where = coordinateSystem.toLatLng(windDTO.position); - InfoWindowOptions options = InfoWindowOptions.newInstance(); - InfoWindow infoWindow = InfoWindow.newInstance(options); - infoWindow.setContent(getInfoWindowContent(windSource, windTrackInfoDTO)); - infoWindow.setPosition(where); - lastInfoWindow = infoWindow; - infoWindow.open(map); - } - } - - private Widget createInfoWindowLabelAndValue(String labelName, String value) { - FlowPanel flowPanel = new FlowPanel(); - Label label = new Label(labelName + ":"); - label.setWordWrap(false); - label.getElement().getStyle().setFloat(Style.Float.LEFT); - label.getElement().getStyle().setPadding(3, Style.Unit.PX); - label.getElement().getStyle().setFontWeight(Style.FontWeight.BOLD); - flowPanel.add(label); - - Label valueLabel = new Label(value); - valueLabel.setWordWrap(false); - valueLabel.getElement().getStyle().setFloat(Style.Float.LEFT); - valueLabel.getElement().getStyle().setPadding(3, Style.Unit.PX); - flowPanel.add(valueLabel); - - return flowPanel; - } - - private Widget getInfoWindowContent(MarkDTO markDTO) { - VerticalPanel vPanel = new VerticalPanel(); - vPanel.add(createInfoWindowLabelAndValue(stringMessages.mark(), markDTO.getName())); - vPanel.add(createInfoWindowLabelAndValue(stringMessages.position(), formatPosition(markDTO.position.getLatDeg(), markDTO.position.getLngDeg()))); - return vPanel; - } - - public Widget getInfoWindowContent(ManeuverDTO maneuver) { - SpeedWithBearingDTO before = maneuver.speedWithBearingBefore; - SpeedWithBearingDTO after = maneuver.speedWithBearingAfter; - VerticalPanel vPanel = new VerticalPanel(); - vPanel.add(createInfoWindowLabelAndValue(stringMessages.maneuverType(), - ManeuverTypeFormatter.format(maneuver.type, stringMessages))); - vPanel.add(createInfoWindowLabelAndValue(stringMessages.time(), - DateTimeFormat.getFormat(PredefinedFormat.TIME_FULL).format(maneuver.timepoint))); - vPanel.add(createInfoWindowLabelAndValue(stringMessages.directionChange(), - ((int) maneuver.directionChangeInDegrees) + " " + stringMessages.degreesShort() + " (" - + ((int) before.bearingInDegrees) + " " + stringMessages.degreesShort() + " -> " - + ((int) after.bearingInDegrees) + " " + stringMessages.degreesShort() + ")")); - vPanel.add(createInfoWindowLabelAndValue(stringMessages.speedChange(), - NumberFormat.getDecimalFormat().format(after.speedInKnots - before.speedInKnots) + " " - + stringMessages.knotsUnit() + " (" - + NumberFormat.getDecimalFormat().format(before.speedInKnots) + " " + stringMessages.knotsUnit() - + " -> " + NumberFormat.getDecimalFormat().format(after.speedInKnots) + " " - + stringMessages.knotsUnit() + ")")); - if (maneuver.maneuverLossInMeters != null) { - vPanel.add(createInfoWindowLabelAndValue(stringMessages.maneuverLoss(), NumberFormat.getFormat("0.0").format(maneuver.maneuverLossInMeters)+"m")); - } - return vPanel; - } - - private Widget getInfoWindowContent(WindSource windSource, WindTrackInfoDTO windTrackInfoDTO) { - WindDTO windDTO = windTrackInfoDTO.windFixes.get(0); - NumberFormat numberFormat = NumberFormat.getFormat("0.0"); - VerticalPanel vPanel = new VerticalPanel(); - vPanel.add(createInfoWindowLabelAndValue(stringMessages.windSource(), WindSourceTypeFormatter.format(windSource, stringMessages))); - vPanel.add(createInfoWindowLabelAndValue(stringMessages.wind(), Math.round(windDTO.dampenedTrueWindFromDeg) + " " + stringMessages.degreesShort())); - vPanel.add(createInfoWindowLabelAndValue(stringMessages.windSpeed(), numberFormat.format(windDTO.dampenedTrueWindSpeedInKnots))); - vPanel.add(createInfoWindowLabelAndValue(stringMessages.position(), formatPosition(windDTO.position.getLatDeg(), windDTO.position.getLngDeg()))); - return vPanel; - } - - private Widget getInfoWindowContent(CompetitorDTO competitorDTO, GPSFixDTOWithSpeedWindTackAndLegType lastFix) { - final VerticalPanel vPanel = new VerticalPanel(); - vPanel.add(createInfoWindowLabelAndValue(stringMessages.competitor(), competitorDTO.getName())); - vPanel.add(createInfoWindowLabelAndValue(stringMessages.sailNumber(), competitorDTO.getSailID())); - Integer rank = null; - if (quickRanks != null) { - QuickRankDTO quickRank = quickRanks.get(competitorDTO); - if (quickRank != null) { - rank = quickRank.rank; - } - } - if (rank != null) { - vPanel.add(createInfoWindowLabelAndValue(stringMessages.rank(), String.valueOf(rank))); - } - SpeedWithBearingDTO speedWithBearing = lastFix.speedWithBearing; - if (speedWithBearing == null) { - // TODO should we show the boat at all? - speedWithBearing = new SpeedWithBearingDTO(0, 0); - } - vPanel.add(createInfoWindowLabelAndValue(stringMessages.speed(), - NumberFormatterFactory.getDecimalFormat(1).format(speedWithBearing.speedInKnots) + " "+stringMessages.knotsUnit())); - vPanel.add(createInfoWindowLabelAndValue(stringMessages.bearing(), (int) speedWithBearing.bearingInDegrees + " "+stringMessages.degreesShort())); - if (lastFix.degreesBoatToTheWind != null) { - vPanel.add(createInfoWindowLabelAndValue(stringMessages.degreesBoatToTheWind(), - (int) Math.abs(lastFix.degreesBoatToTheWind) + " " + stringMessages.degreesShort())); - } - if (raceIdentifier != null) { - RegattaAndRaceIdentifier race = raceIdentifier; - if (race != null) { - Map from = new HashMap(); - from.put(competitorDTO, fixesAndTails.getFixes(competitorDTO).get(fixesAndTails.getFirstShownFix(competitorDTO)).timepoint); - Map to = new HashMap(); - to.put(competitorDTO, getBoatFix(competitorDTO, timer.getTime()).timepoint); - sailingService.getDouglasPoints(race, from, to, 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(); - } - if (!(timer.getPlayState() == PlayStates.Playing)) { - if (settings.isShowDouglasPeuckerPoints()) { - showMarkDouglasPeuckerPoints(result); - } - } - } - }); - sailingService.getManeuvers(race, from, to, - new AsyncCallback>>() { - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError("Error obtaining maneuvers: " + caught.getMessage(), true /*silentMode */); - } - - @Override - public void onSuccess(Map> result) { - lastManeuverResult = result; - if (maneuverMarkers != null) { - removeAllManeuverMarkers(); - } - if (!(timer.getPlayState() == PlayStates.Playing)) { - showManeuvers(result); - } - } - }); - - } - } - return vPanel; - } - - /** - * @return the {@link CompetitorSelectionProvider#getSelectedCompetitors()} if - * {@link RaceMapSettings#isShowOnlySelectedCompetitors() only selected competitors are to be shown}, the - * {@link CompetitorSelectionProvider#getFilteredCompetitors() filtered competitors} otherwise. In both cases, - * if we have {@link RaceCompetitorSet information about the competitors participating} in this map's race, - * the result set is reduced to those, no matter if other regatta participants would otherwise have been in - * the result set - */ - private Iterable getCompetitorsToShow() { - final Set result = new HashSet<>(); - 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); - } - } - } else { - for (final CompetitorDTO selectedCompetitor : selection) { - if (raceCompetitorIdsAsString == null || raceCompetitorIdsAsString.contains(selectedCompetitor.getIdAsString())) { - result.add(selectedCompetitor); - } - } - } - return result; - } - - protected void removeAllMarkDouglasPeuckerpoints() { - if (douglasMarkers != null) { - for (Marker marker : douglasMarkers) { - marker.setMap((MapWidget) null); - } - } - douglasMarkers = null; - } - - protected void removeAllManeuverMarkers() { - if (maneuverMarkers != null) { - for (Marker marker : maneuverMarkers) { - marker.setMap((MapWidget) null); - } - maneuverMarkers = null; - } - } - - private void showMarkDouglasPeuckerPoints(Map> gpsFixPointMapForCompetitors) { - douglasMarkers = new HashSet(); - if (map != null && gpsFixPointMapForCompetitors != null) { - Set keySet = gpsFixPointMapForCompetitors.keySet(); - Iterator iter = keySet.iterator(); - while (iter.hasNext()) { - CompetitorDTO competitorDTO = iter.next(); - List gpsFix = gpsFixPointMapForCompetitors.get(competitorDTO); - for (GPSFixDTOWithSpeedWindTackAndLegType fix : gpsFix) { - LatLng latLng = coordinateSystem.toLatLng(fix.position); - MarkerOptions options = MarkerOptions.newInstance(); - options.setTitle(fix.timepoint+": "+fix.position+", "+fix.speedWithBearing.toString()); - Marker marker = Marker.newInstance(options); - marker.setPosition(latLng); - douglasMarkers.add(marker); - marker.setMap(map); - } - } - } - } - - private void showManeuvers(Map> maneuvers) { - maneuverMarkers = new HashSet(); - if (map != null && maneuvers != null) { - Set keySet = maneuvers.keySet(); - Iterator iter = keySet.iterator(); - while (iter.hasNext()) { - CompetitorDTO competitorDTO = iter.next(); - List maneuversForCompetitor = maneuvers.get(competitorDTO); - for (ManeuverDTO maneuver : maneuversForCompetitor) { - if (settings.isShowManeuverType(maneuver.type)) { - createAndAddMarkerOfManeuver(maneuver); - } - } - } - } - } - - private void createAndAddMarkerOfManeuver(ManeuverDTO maneuver) { - LatLng latLng = coordinateSystem.toLatLng(maneuver.position); - Marker maneuverMarker = raceMapImageManager.maneuverIconsForTypeAndDirectionIndicatingColor - .get(new Pair(maneuver.type, ManeuverColor.getManeuverColor(maneuver))); - MarkerOptions options = MarkerOptions.newInstance(); - options.setIcon(maneuverMarker.getIcon_MarkerImage()); - Marker marker = Marker.newInstance(options); - marker.setPosition(latLng); - marker.setTitle(ManeuverTypeFormatter.format(maneuver.type, stringMessages)); - marker.addClickHandler(new ManeuverMarkerClickedListener(this, maneuver)); - maneuverMarkers.add(marker); - marker.setMap(map); - } - - /** - * @param date - * the point in time for which to determine the competitor's boat position; approximated by using the fix - * from {@link #fixes} whose time point comes closest to this date - * - * @return The GPS fix for the given competitor from {@link #fixes} that is closest to date, or - * null if no fix is available - */ - private GPSFixDTOWithSpeedWindTackAndLegType getBoatFix(CompetitorDTO competitorDTO, Date date) { - GPSFixDTOWithSpeedWindTackAndLegType result = null; - 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() { - @Override - public int compare(GPSFixDTOWithSpeedWindTackAndLegType o1, GPSFixDTOWithSpeedWindTackAndLegType o2) { - return o1.timepoint.compareTo(o2.timepoint); - } - }); - if (i<0) { - i = -i-1; // no perfect match; i is now the insertion point - // if the insertion point is at the end, use last fix - if (i >= competitorFixes.size()) { - result = competitorFixes.get(competitorFixes.size()-1); - } else if (i == 0) { - // if the insertion point is at the beginning, use first fix - result = competitorFixes.get(0); - } else { - // competitorFixes must have at least two elements, and i points neither to the end nor the beginning; - // get the fix from i and i+1 whose timepoint is closer to date - final GPSFixDTOWithSpeedWindTackAndLegType fixBefore = competitorFixes.get(i-1); - final GPSFixDTOWithSpeedWindTackAndLegType fixAfter = competitorFixes.get(i); - final GPSFixDTOWithSpeedWindTackAndLegType closer; - if (date.getTime() - fixBefore.timepoint.getTime() < fixAfter.timepoint.getTime() - date.getTime()) { - closer = fixBefore; - } else { - closer = fixAfter; - } - // 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(), - factorForBefore*fixBefore.position.getLngDeg() + factorForAfter*fixAfter.position.getLngDeg()); - final double betweenBearing; - if (fixBefore.speedWithBearing == null) { - if (fixAfter.speedWithBearing == null) { - betweenBearing = 0; - } else { - betweenBearing = fixAfter.speedWithBearing.bearingInDegrees; - } - } else if (fixAfter.speedWithBearing == null) { - betweenBearing = fixBefore.speedWithBearing.bearingInDegrees; - } else { - betweenBearing = new ScalableBearing(new DegreeBearingImpl(fixBefore.speedWithBearing.bearingInDegrees)). - multiply(factorForBefore).add(new ScalableBearing(new DegreeBearingImpl(fixAfter.speedWithBearing.bearingInDegrees)). - multiply(factorForAfter)).divide(1).getDegrees(); - } - SpeedWithBearingDTO betweenSpeed = new SpeedWithBearingDTO( - factorForBefore*(fixBefore.speedWithBearing==null?0:fixBefore.speedWithBearing.speedInKnots) + - factorForAfter*(fixAfter.speedWithBearing==null?0:fixAfter.speedWithBearing.speedInKnots), - betweenBearing); - result = new GPSFixDTOWithSpeedWindTackAndLegType(date, betweenPosition, betweenSpeed, closer.degreesBoatToTheWind, - closer.tack, closer.legType, fixBefore.extrapolated || fixAfter.extrapolated); - } - } else { - // perfect match - final GPSFixDTOWithSpeedWindTackAndLegType fixAfter = competitorFixes.get(i); - result = fixAfter; - } - } - return result; - } - - public RaceMapSettings getSettings() { - return settings; - } - - @Override - public void addedToSelection(CompetitorDTO competitor) { - if (settings.isShowOnlySelectedCompetitors()) { - if (Util.size(competitorSelection.getSelectedCompetitors()) == 1) { - // first competitors selected; remove all others from map - Iterator> i = boatOverlays.entrySet().iterator(); - while (i.hasNext()) { - Entry next = i.next(); - if (!next.getKey().equals(competitor)) { - CanvasOverlayV3 boatOverlay = next.getValue(); - boatOverlay.removeFromMap(); - fixesAndTails.removeTail(next.getKey()); - i.remove(); // only this way a ConcurrentModificationException while looping can be avoided - } - } - showCompetitorInfoOnMap(timer.getTime(), -1, competitorSelection.getSelectedFilteredCompetitors()); - } else { - // adding a single competitor; may need to re-load data, so refresh: - timeChanged(timer.getTime(), null); - } - } else { - // only change highlighting - BoatOverlay boatCanvas = boatOverlays.get(competitor); - if (boatCanvas != null) { - boatCanvas.setSelected(displayHighlighted(competitor)); - boatCanvas.draw(); - showCompetitorInfoOnMap(timer.getTime(), -1, competitorSelection.getSelectedFilteredCompetitors()); - } else { - // seems like an internal error not to find the lowlighted marker; but maybe the - // competitor was added late to the race; - // data for newly selected competitor supposedly missing; refresh - timeChanged(timer.getTime(), null); - } - } - // Now update tails for all competitors because selection change may also affect all unselected competitors - for (CompetitorDTO oneOfAllCompetitors : competitorSelection.getAllCompetitors()) { - Polyline tail = fixesAndTails.getTail(oneOfAllCompetitors); - if (tail != null) { - PolylineOptions newOptions = createTailStyle(oneOfAllCompetitors, displayHighlighted(oneOfAllCompetitors)); - tail.setOptions(newOptions); - } - } - // Trigger auto-zoom if needed - RaceMapZoomSettings zoomSettings = settings.getZoomSettings(); - if (!zoomSettings.containsZoomType(ZoomTypes.NONE) && zoomSettings.isZoomToSelectedCompetitors()) { - zoomMapToNewBounds(zoomSettings.getNewBounds(this)); - } - } - - @Override - public void removedFromSelection(CompetitorDTO competitor) { - if (isShowAnyHelperLines()) { - // helper lines depend on which competitor is visible, because the *visible* leader is used for - // deciding which helper lines to show: - timeChanged(timer.getTime(), null); - } else { - // try a more incremental update otherwise - if (settings.isShowOnlySelectedCompetitors()) { - // if selection is now empty, show all competitors - if (Util.isEmpty(competitorSelection.getSelectedCompetitors())) { - timeChanged(timer.getTime(), null); - } else { - // otherwise remove only deselected competitor's boat images and tail - BoatOverlay removedBoatOverlay = boatOverlays.remove(competitor); - if (removedBoatOverlay != null) { - removedBoatOverlay.removeFromMap(); - } - fixesAndTails.removeTail(competitor); - showCompetitorInfoOnMap(timer.getTime(), -1, competitorSelection.getSelectedFilteredCompetitors()); - } - } else { - // "lowlight" currently selected competitor - BoatOverlay boatCanvas = boatOverlays.get(competitor); - if (boatCanvas != null) { - boatCanvas.setSelected(displayHighlighted(competitor)); - boatCanvas.draw(); - } - showCompetitorInfoOnMap(timer.getTime(), -1, competitorSelection.getSelectedFilteredCompetitors()); - } - } - //Trigger auto-zoom if needed - RaceMapZoomSettings zoomSettings = settings.getZoomSettings(); - if (!zoomSettings.containsZoomType(ZoomTypes.NONE) && zoomSettings.isZoomToSelectedCompetitors()) { - zoomMapToNewBounds(zoomSettings.getNewBounds(this)); - } - } - - private boolean isShowAnyHelperLines() { - return settings.getHelpLinesSettings().isShowAnyHelperLines(); - } - - @Override - public String getLocalizedShortName() { - return raceMapLifecycle.getLocalizedShortName(); - } - - @Override - public Widget getEntryWidget() { - return this; - } - - @Override - public boolean hasSettings() { - return raceMapLifecycle.hasSettings(); - } - - @Override - public SettingsDialogComponent getSettingsDialogComponent() { - return new RaceMapSettingsDialogComponent(settings, stringMessages, this.isSimulationEnabled && this.hasPolar); - } - - @Override - public void updateSettings(RaceMapSettings newSettings) { - boolean maneuverTypeSelectionChanged = false; - boolean requiresRedraw = false; - boolean requiresUpdateCoordinateSystem = false; - - for (ManeuverType maneuverType : ManeuverType.values()) { - if (newSettings.isShowManeuverType(maneuverType) != settings.isShowManeuverType(maneuverType)) { - maneuverTypeSelectionChanged = true; - } - } - if (maneuverTypeSelectionChanged) { - if (!(timer.getPlayState() == PlayStates.Playing) && lastManeuverResult != null) { - removeAllManeuverMarkers(); - showManeuvers(lastManeuverResult); - } - } - if (newSettings.isShowDouglasPeuckerPoints() != settings.isShowDouglasPeuckerPoints()) { - if (!(timer.getPlayState() == PlayStates.Playing) && lastDouglasPeuckerResult != null && newSettings.isShowDouglasPeuckerPoints()) { - removeAllMarkDouglasPeuckerpoints(); - showMarkDouglasPeuckerPoints(lastDouglasPeuckerResult); - } else if (!newSettings.isShowDouglasPeuckerPoints()) { - removeAllMarkDouglasPeuckerpoints(); - } - } - if (newSettings.getTailLengthInMilliseconds() != settings.getTailLengthInMilliseconds()) { - requiresRedraw = true; - } - if (newSettings.getBuoyZoneRadius() != settings.getBuoyZoneRadius()) { - requiresRedraw = true; - } - if (newSettings.isShowOnlySelectedCompetitors() != settings.isShowOnlySelectedCompetitors()) { - requiresRedraw = true; - } - if (newSettings.isShowSelectedCompetitorsInfo() != settings.isShowSelectedCompetitorsInfo()) { - requiresRedraw = true; - } - if (!newSettings.getZoomSettings().equals(settings.getZoomSettings())) { - if (!newSettings.getZoomSettings().containsZoomType(ZoomTypes.NONE)) { - removeTransitions(); - zoomMapToNewBounds(newSettings.getZoomSettings().getNewBounds(this)); - } - } - if (!newSettings.getHelpLinesSettings().equals(settings.getHelpLinesSettings())) { - requiresRedraw = true; - } - if (newSettings.isShowWindStreamletOverlay() != settings.isShowWindStreamletOverlay()) { - streamletOverlay.setVisible(newSettings.isShowWindStreamletOverlay()); - } - if (newSettings.isShowWindStreamletColors() != settings.isShowWindStreamletColors()) { - streamletOverlay.setColors(newSettings.isShowWindStreamletColors()); - } - if (newSettings.isShowSimulationOverlay() != settings.isShowSimulationOverlay()) { - showSimulationOverlay(newSettings.isShowSimulationOverlay()); - } - if (newSettings.isWindUp() != settings.isWindUp()) { - requiresUpdateCoordinateSystem = true; - requiresRedraw = true; - } - this.settings = newSettings; - - if (requiresUpdateCoordinateSystem) { - updateCoordinateSystemFromSettings(); - } - if (requiresRedraw) { - redraw(); - } - } - - private void showSimulationOverlay(boolean visible) { - simulationOverlay.setVisible(visible); - if (visible) { - simulationOverlay.updateLeg(Math.max(lastLegNumber,1), true, -1 /* ensure ui-update */); - } - } - - public static class BoatsBoundsCalculator extends LatLngBoundsCalculatorForSelected { - @Override - public LatLngBounds calculateNewBounds(RaceMap forMap) { - LatLngBounds newBounds = null; - Iterable selectedCompetitors = forMap.competitorSelection.getSelectedCompetitors(); - Iterable competitors = new ArrayList(); - if (selectedCompetitors == null || !selectedCompetitors.iterator().hasNext()) { - competitors = forMap.getCompetitorsToShow(); - } else { - competitors = isZoomOnlyToSelectedCompetitors(forMap) ? selectedCompetitors : forMap.getCompetitorsToShow(); - } - for (CompetitorDTO competitor : competitors) { - try { - GPSFixDTOWithSpeedWindTackAndLegType competitorFix = forMap.getBoatFix(competitor, forMap.timer.getTime()); - Position competitorPosition = competitorFix != null ? competitorFix.position : null; - if (competitorPosition != null) { - if (newBounds == null) { - newBounds = BoundsUtil.getAsBounds(forMap.coordinateSystem.toLatLng(competitorPosition)); - } else { - newBounds = newBounds.extend(forMap.coordinateSystem.toLatLng(competitorPosition)); - } - } - } catch (IndexOutOfBoundsException e) { - // TODO can't this be predicted and the exception be avoided in the first place? - // Catch this in case the competitor has no GPS fixes at the current time (e.g. in race 'Finale 2' of STG) - } - } - return newBounds; - } - - } - - public static class TailsBoundsCalculator extends LatLngBoundsCalculatorForSelected { - @Override - public LatLngBounds calculateNewBounds(RaceMap racemap) { - LatLngBounds newBounds = null; - Iterable competitors = isZoomOnlyToSelectedCompetitors(racemap) ? racemap.competitorSelection.getSelectedCompetitors() : racemap.getCompetitorsToShow(); - for (CompetitorDTO competitor : competitors) { - Polyline tail = racemap.fixesAndTails.getTail(competitor); - LatLngBounds bounds = null; - // TODO: Find a replacement for missing Polyline function getBounds() from v2 - // see also http://stackoverflow.com/questions/3284808/getting-the-bounds-of-a-polyine-in-google-maps-api-v3; - // optionally, consider providing a bounds cache with two sorted sets that organize the LatLng objects for O(1) bounds calculation and logarithmic add, ideally O(1) remove - if (tail != null && tail.getPath().getLength() >= 1) { - bounds = BoundsUtil.getAsBounds(tail.getPath().get(0)); - for (int i = 1; i < tail.getPath().getLength(); i++) { - bounds = bounds.extend(tail.getPath().get(i)); - } - } - if (bounds != null) { - if (newBounds == null) { - newBounds = bounds; - } else { - newBounds = newBounds.union(bounds); - } - } - } - return newBounds; - } - } - - public static class CourseMarksBoundsCalculator implements LatLngBoundsCalculator { - @Override - public LatLngBounds calculateNewBounds(RaceMap forMap) { - LatLngBounds newBounds = null; - Iterable marksToZoom = forMap.markDTOs.values(); - if (marksToZoom != null) { - for (MarkDTO markDTO : marksToZoom) { - if (newBounds == null) { - newBounds = BoundsUtil.getAsBounds(forMap.coordinateSystem.toLatLng(markDTO.position)); - } else { - newBounds = newBounds.extend(forMap.coordinateSystem.toLatLng(markDTO.position)); - } - } - } - return newBounds; - } - } - - public static class WindSensorsBoundsCalculator implements LatLngBoundsCalculator { - @Override - public LatLngBounds calculateNewBounds(RaceMap forMap) { - LatLngBounds newBounds = null; - Collection marksToZoom = forMap.windSensorOverlays.values(); - if (marksToZoom != null) { - for (WindSensorOverlay windSensorOverlay : marksToZoom) { - if (BoundsUtil.getAsPosition(windSensorOverlay.getLatLngPosition()) != null) { - LatLngBounds bounds = BoundsUtil.getAsBounds(windSensorOverlay.getLatLngPosition()); - if (newBounds == null) { - newBounds = bounds; - } else { - newBounds = newBounds.extend(windSensorOverlay.getLatLngPosition()); - } - } - } - } - return newBounds; - } - } - - @Override - public void initializeData(boolean showMapControls, boolean showHeaderPanel) { - loadMapsAPIV3(showMapControls, showHeaderPanel); - } - - @Override - public boolean isDataInitialized() { - return isMapInitialized; - } - - @Override - public void onResize() { - if (map != null) { - map.triggerResize(); - zoomMapToNewBounds(settings.getZoomSettings().getNewBounds(RaceMap.this)); - } - // Adjust RaceMap headers to avoid overlapping based on the RaceMap width - boolean isCompactHeader = this.getOffsetWidth() <= 600; - getLeftHeaderPanel().setStyleName(COMPACT_HEADER_STYLE, isCompactHeader); - getRightHeaderPanel().setStyleName(COMPACT_HEADER_STYLE, isCompactHeader); - - // Adjust combined wind and true north indicator panel indent, based on the RaceMap height - if (topLeftControlsWrapperPanel.getParent() != null) { - this.adjustLeftControlsIndent(); - } - } - - private void adjustLeftControlsIndent() { - topLeftControlsWrapperPanel.getParent().setStyleName("TopLeftControlsWrapperPanelParentDiv"); - String leftControlsIndentStyle = getLeftControlsIndentStyle(); - if (leftControlsIndentStyle != null) { - topLeftControlsWrapperPanel.getParent().addStyleName(leftControlsIndentStyle); - } - } - - @Override - public void competitorsListChanged(Iterable competitors) { - timeChanged(timer.getTime(), null); - } - - @Override - public void filteredCompetitorsListChanged(Iterable filteredCompetitors) { - timeChanged(timer.getTime(), null); - } - - @Override - public void filterChanged(FilterSet> oldFilterSet, - FilterSet> newFilterSet) { - // nothing to do; if the list of filtered competitors has changed, a separate call to filteredCompetitorsListChanged will occur - } - - @Override - public PolylineOptions createTailStyle(CompetitorDTO competitor, boolean isHighlighted) { - PolylineOptions options = PolylineOptions.newInstance(); - options.setClickable(true); - options.setGeodesic(true); - options.setStrokeOpacity(1.0); - boolean noCompetitorSelected = Util.isEmpty(competitorSelection.getSelectedCompetitors()); - if (isHighlighted || noCompetitorSelected || getSettings().isShowOnlySelectedCompetitors()) { - options.setStrokeColor(competitorSelection.getColor(competitor, raceIdentifier).getAsHtml()); - } else { - options.setStrokeColor(LOWLIGHTED_TAIL_COLOR.getAsHtml()); - } - if (isHighlighted) { - options.setStrokeWeight(2); - } else { - options.setStrokeWeight(1); - } - options.setZindex(RaceMapOverlaysZIndexes.BOATTAILS_ZINDEX); - return options; - } - - @Override - public Polyline createTail(final CompetitorDTO competitor, List points) { - PolylineOptions options = createTailStyle(competitor, displayHighlighted(competitor)); - Polyline result = Polyline.newInstance(options); - MVCArray pointsAsArray = MVCArray.newInstance(points.toArray(new LatLng[0])); - result.setPath(pointsAsArray); - result.setMap(map); - Hoverline resultHoverline = new Hoverline(result, options, this); - final ClickMapHandler clickHandler = new ClickMapHandler() { - @Override - public void onEvent(ClickMapEvent event) { - showCompetitorInfoWindow(competitor, event.getMouseEvent().getLatLng()); - } - }; - result.addClickHandler(clickHandler); - resultHoverline.addClickHandler(clickHandler); - result.addMouseOverHandler(new MouseOverMapHandler() { - @Override - public void onEvent(MouseOverMapEvent event) { - map.setTitle(competitor.getSailID() + ", " + competitor.getName()); - } - }); - resultHoverline.addMouseOutMoveHandler(new MouseOutMapHandler() { - @Override - public void onEvent(MouseOutMapEvent event) { - map.setTitle(""); - } - }); - return result; - } - - @Override - public Integer getRank(CompetitorDTO competitor) { - final Integer result; - QuickRankDTO quickRank = quickRanks.get(competitor); - if (quickRank != null) { - result = quickRank.rank; - } else { - result = null; - } - return result; - } - - private Image createSAPLogo() { - ImageResource sapLogoResource = resources.sapLogoOverlay(); - Image sapLogo = new Image(sapLogoResource); - sapLogo.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - Window.open("https://www.sap.com/sponsorships", "_blank", null); - } - }); - sapLogo.setStyleName("raceBoard-Logo"); - return sapLogo; - } - - @Override - public String getDependentCssClassName() { - return "raceMap"; - } - - /** - * The default zoom bounds are defined by the boats - */ - private LatLngBounds getDefaultZoomBounds() { - return new BoatsBoundsCalculator().calculateNewBounds(RaceMap.this); - } - - private MapOptions getMapOptions(final boolean showMapControls, boolean windUp) { - MapOptions mapOptions = MapOptions.newInstance(); - mapOptions.setScrollWheel(true); - mapOptions.setMapTypeControl(showMapControls && !windUp); - mapOptions.setPanControl(showMapControls); - mapOptions.setZoomControl(showMapControls); - mapOptions.setScaleControl(true); - if (windUp) { - mapOptions.setMinZoom(8); - } else { - mapOptions.setMinZoom(0); - } - MapTypeStyle[] mapTypeStyles = new MapTypeStyle[4]; - - // hide all transit lines including ferry lines - mapTypeStyles[0] = GoogleMapStyleHelper.createHiddenStyle(MapTypeStyleFeatureType.TRANSIT); - // hide points of interest - mapTypeStyles[1] = GoogleMapStyleHelper.createHiddenStyle(MapTypeStyleFeatureType.POI); - // simplify road display - mapTypeStyles[2] = GoogleMapStyleHelper.createSimplifiedStyle(MapTypeStyleFeatureType.ROAD); - // set water color - mapTypeStyles[3] = GoogleMapStyleHelper.createColorStyle(MapTypeStyleFeatureType.WATER, WATER_COLOR); - - MapTypeControlOptions mapTypeControlOptions = MapTypeControlOptions.newInstance(); - mapTypeControlOptions.setPosition(ControlPosition.BOTTOM_RIGHT); - mapOptions.setMapTypeControlOptions(mapTypeControlOptions); - - mapOptions.setMapTypeStyles(mapTypeStyles); - // no need to try to position the scale control; it always ends up at the right bottom corner - mapOptions.setStreetViewControl(false); - if (showMapControls) { - ZoomControlOptions zoomControlOptions = ZoomControlOptions.newInstance(); - zoomControlOptions.setPosition(ControlPosition.RIGHT_TOP); - mapOptions.setZoomControlOptions(zoomControlOptions); - PanControlOptions panControlOptions = PanControlOptions.newInstance(); - panControlOptions.setPosition(ControlPosition.RIGHT_TOP); - mapOptions.setPanControlOptions(panControlOptions); - } - return mapOptions; - } - - /** - * @return CSS style name to adjust the indent of left controls (combined wind and true north indicator). - */ - protected String getLeftControlsIndentStyle() { - return null; - } - - public Map getCourseMarkOverlays() { - return courseMarkOverlays; - } - - public CoordinateSystem getCoordinateSystem() { - return coordinateSystem; - } - - public void hideAllHelplines() { - if (startLine != null) { - startLine.setVisible(false); - } - if (finishLine != null) { - finishLine.setVisible(false); - } - if (advantageLine != null) { - advantageLine.setVisible(false); - } - if (windwardStartLineMarkToFirstMarkLine != null && leewardStartLineMarkToFirstMarkLine != null) { - windwardStartLineMarkToFirstMarkLine.setVisible(false); - leewardStartLineMarkToFirstMarkLine.setVisible(false); - } - for (Polyline courseMiddleline : courseMiddleLines.values()) { - if (courseMiddleline != null) { - courseMiddleline.setVisible(false); - } - } - for (Polygon courseSideline : courseSidelines.values()) { - if (courseSideline != null) { - courseSideline.setVisible(false); - } - } - } - - public void showAllHelplinesToShow() { - if (settings.getHelpLinesSettings().isShowAnyHelperLines()) { - if (startLine != null && settings.getHelpLinesSettings().isVisible(HelpLineTypes.STARTLINE)) - startLine.setVisible(true); - if (finishLine != null && settings.getHelpLinesSettings().isVisible(HelpLineTypes.FINISHLINE)) - finishLine.setVisible(true); - if (advantageLine != null && settings.getHelpLinesSettings().isVisible(HelpLineTypes.ADVANTAGELINE)) - advantageLine.setVisible(true); - if (windwardStartLineMarkToFirstMarkLine != null && leewardStartLineMarkToFirstMarkLine != null && - settings.getHelpLinesSettings().isVisible(HelpLineTypes.STARTLINETOFIRSTMARKTRIANGLE)) { - windwardStartLineMarkToFirstMarkLine.setVisible(true); - leewardStartLineMarkToFirstMarkLine.setVisible(true); - } - if (settings.getHelpLinesSettings().isVisible(HelpLineTypes.COURSEMIDDLELINE)) { - for (Polyline courseMiddleline : courseMiddleLines.values()) { - courseMiddleline.setVisible(true); - } - } - if (settings.getHelpLinesSettings().isVisible(HelpLineTypes.COURSEGEOMETRY)) { - for (Polygon courseSideline : courseSidelines.values()) { - courseSideline.setVisible(true); - } - } - } - } - - void setLastInfoWindow(InfoWindow infoWindow) { - lastInfoWindow = infoWindow; - } - - InfoWindow getLastInfoWindow() { - return lastInfoWindow; - } - - public void addCompetitorsForRaceDefinedListener(CompetitorsForRaceDefinedListener listener) { - raceCompetitorSet.addCompetitorsForRaceDefinedListener(listener); - } - - public void removeCompetitorsForRaceDefinedListener(CompetitorsForRaceDefinedListener listener) { - raceCompetitorSet.removeCompetitorsForRaceDefinedListener(listener); - } - - @Override - public String getId() { - return raceMapLifecycle.getComponentId(); - } - - public RaceMapLifecycle getLifecycle() { - return raceMapLifecycle; - } - -} +package com.sap.sailing.gwt.ui.client.shared.racemap; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.google.gwt.dom.client.Style; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.i18n.client.DateTimeFormat; +import com.google.gwt.i18n.client.DateTimeFormat.PredefinedFormat; +import com.google.gwt.i18n.client.NumberFormat; +import com.google.gwt.maps.client.LoadApi; +import com.google.gwt.maps.client.LoadApi.LoadLibrary; +import com.google.gwt.maps.client.MapOptions; +import com.google.gwt.maps.client.MapWidget; +import com.google.gwt.maps.client.base.LatLng; +import com.google.gwt.maps.client.base.LatLngBounds; +import com.google.gwt.maps.client.controls.ControlPosition; +import com.google.gwt.maps.client.controls.MapTypeControlOptions; +import com.google.gwt.maps.client.controls.MapTypeStyle; +import com.google.gwt.maps.client.controls.PanControlOptions; +import com.google.gwt.maps.client.controls.ZoomControlOptions; +import com.google.gwt.maps.client.events.bounds.BoundsChangeMapEvent; +import com.google.gwt.maps.client.events.bounds.BoundsChangeMapHandler; +import com.google.gwt.maps.client.events.click.ClickMapEvent; +import com.google.gwt.maps.client.events.click.ClickMapHandler; +import com.google.gwt.maps.client.events.dragend.DragEndMapEvent; +import com.google.gwt.maps.client.events.dragend.DragEndMapHandler; +import com.google.gwt.maps.client.events.idle.IdleMapEvent; +import com.google.gwt.maps.client.events.idle.IdleMapHandler; +import com.google.gwt.maps.client.events.mouseout.MouseOutMapEvent; +import com.google.gwt.maps.client.events.mouseout.MouseOutMapHandler; +import com.google.gwt.maps.client.events.mouseover.MouseOverMapEvent; +import com.google.gwt.maps.client.events.mouseover.MouseOverMapHandler; +import com.google.gwt.maps.client.events.zoom.ZoomChangeMapEvent; +import com.google.gwt.maps.client.events.zoom.ZoomChangeMapHandler; +import com.google.gwt.maps.client.maptypes.MapTypeStyleFeatureType; +import com.google.gwt.maps.client.mvc.MVCArray; +import com.google.gwt.maps.client.overlays.InfoWindow; +import com.google.gwt.maps.client.overlays.InfoWindowOptions; +import com.google.gwt.maps.client.overlays.Marker; +import com.google.gwt.maps.client.overlays.MarkerOptions; +import com.google.gwt.maps.client.overlays.Polygon; +import com.google.gwt.maps.client.overlays.PolygonOptions; +import com.google.gwt.maps.client.overlays.Polyline; +import com.google.gwt.maps.client.overlays.PolylineOptions; +import com.google.gwt.resources.client.ImageResource; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.gwt.user.client.ui.AbsolutePanel; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.Image; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.RequiresResize; +import com.google.gwt.user.client.ui.VerticalPanel; +import com.google.gwt.user.client.ui.Widget; +import com.sap.sailing.domain.common.Bearing; +import com.sap.sailing.domain.common.Bounds; +import com.sap.sailing.domain.common.Distance; +import com.sap.sailing.domain.common.ManeuverType; +import com.sap.sailing.domain.common.Position; +import com.sap.sailing.domain.common.RaceIdentifier; +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.domain.common.WindSource; +import com.sap.sailing.domain.common.WindSourceType; +import com.sap.sailing.domain.common.dto.CompetitorDTO; +import com.sap.sailing.domain.common.impl.DegreeBearingImpl; +import com.sap.sailing.domain.common.impl.DegreePosition; +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.gwt.ui.actions.GetBoatPositionsAction; +import com.sap.sailing.gwt.ui.actions.GetPolarAction; +import com.sap.sailing.gwt.ui.actions.GetRaceMapDataAction; +import com.sap.sailing.gwt.ui.actions.GetWindInfoAction; +import com.sap.sailing.gwt.ui.client.ClientResources; +import com.sap.sailing.gwt.ui.client.CompetitorSelectionChangeListener; +import com.sap.sailing.gwt.ui.client.CompetitorSelectionProvider; +import com.sap.sailing.gwt.ui.client.ManeuverTypeFormatter; +import com.sap.sailing.gwt.ui.client.NumberFormatterFactory; +import com.sap.sailing.gwt.ui.client.RaceTimesInfoProviderListener; +import com.sap.sailing.gwt.ui.client.RequiresDataInitialization; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.client.WindSourceTypeFormatter; +import com.sap.sailing.gwt.ui.client.shared.filter.QuickRankProvider; +import com.sap.sailing.gwt.ui.client.shared.racemap.RaceCompetitorSet.CompetitorsForRaceDefinedListener; +import com.sap.sailing.gwt.ui.client.shared.racemap.RaceMapHelpLinesSettings.HelpLineTypes; +import com.sap.sailing.gwt.ui.client.shared.racemap.RaceMapZoomSettings.ZoomTypes; +import com.sap.sailing.gwt.ui.common.client.DateAndTimeFormatterUtil; +import com.sap.sailing.gwt.ui.shared.CompactBoatPositionsDTO; +import com.sap.sailing.gwt.ui.shared.ControlPointDTO; +import com.sap.sailing.gwt.ui.shared.CoursePositionsDTO; +import com.sap.sailing.gwt.ui.shared.GPSFixDTOWithSpeedWindTackAndLegType; +import com.sap.sailing.gwt.ui.shared.LegInfoDTO; +import com.sap.sailing.gwt.ui.shared.ManeuverDTO; +import com.sap.sailing.gwt.ui.shared.MarkDTO; +import com.sap.sailing.gwt.ui.shared.QuickRankDTO; +import com.sap.sailing.gwt.ui.shared.RaceCourseDTO; +import com.sap.sailing.gwt.ui.shared.RaceMapDataDTO; +import com.sap.sailing.gwt.ui.shared.RaceTimesInfoDTO; +import com.sap.sailing.gwt.ui.shared.SidelineDTO; +import com.sap.sailing.gwt.ui.shared.SpeedWithBearingDTO; +import com.sap.sailing.gwt.ui.shared.WaypointDTO; +import com.sap.sailing.gwt.ui.shared.WindDTO; +import com.sap.sailing.gwt.ui.shared.WindInfoForRaceDTO; +import com.sap.sailing.gwt.ui.shared.WindTrackInfoDTO; +import com.sap.sailing.gwt.ui.shared.racemap.CanvasOverlayV3; +import com.sap.sailing.gwt.ui.shared.racemap.GoogleMapAPIKey; +import com.sap.sailing.gwt.ui.shared.racemap.GoogleMapStyleHelper; +import com.sap.sailing.gwt.ui.shared.racemap.RaceSimulationOverlay; +import com.sap.sailing.gwt.ui.shared.racemap.WindStreamletsRaceboardOverlay; +import com.sap.sse.common.Color; +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.filter.Filter; +import com.sap.sse.common.filter.FilterSet; +import com.sap.sse.common.impl.RGBColor; +import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.async.AsyncActionsExecutor; +import com.sap.sse.gwt.client.async.MarkedAsyncCallback; +import com.sap.sse.gwt.client.player.TimeListener; +import com.sap.sse.gwt.client.player.Timer; +import com.sap.sse.gwt.client.player.Timer.PlayModes; +import com.sap.sse.gwt.client.player.Timer.PlayStates; +import com.sap.sse.gwt.client.shared.components.AbstractCompositeComponent; +import com.sap.sse.gwt.client.shared.components.Component; +import com.sap.sse.gwt.client.shared.components.SettingsDialog; +import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; + +public class RaceMap extends AbstractCompositeComponent implements TimeListener, CompetitorSelectionChangeListener, + RaceTimesInfoProviderListener, TailFactory, RequiresDataInitialization, RequiresResize, QuickRankProvider { + private static final Color LOWLIGHTED_TAIL_COLOR = new RGBColor(200, 200, 200); + public static final String GET_RACE_MAP_DATA_CATEGORY = "getRaceMapData"; + public static final String GET_WIND_DATA_CATEGORY = "getWindData"; + + private static final String COMPACT_HEADER_STYLE = "compactHeader"; + public static final Color WATER_COLOR = new RGBColor(0, 67, 125); + + private AbsolutePanel rootPanel = new AbsolutePanel(); + + private MapWidget map; + + /** + * Always valid, non-null. Must be used to map all coordinates, headings, bearings, and directions + * displayed on the map, including the orientations of any canvases such as boat icons, wind displays etc. that are + * embedded in the map. The coordinate systems facilitates the possibility of transformed displays such as + * rotated and translated versions of the map, implementing the "wind-up" view. + */ + private DelegateCoordinateSystem coordinateSystem; + + private FlowPanel headerPanel; + private AbsolutePanel panelForLeftHeaderLabels; + private AbsolutePanel panelForRightHeaderLabels; + + private final SailingServiceAsync sailingService; + private final ErrorReporter errorReporter; + + private final static ClientResources resources = GWT.create(ClientResources.class); + + /** + * Polyline for the start line (connecting two marks representing the start gate). + */ + private Polyline startLine; + + /** + * Polyline for the finish line (connecting two marks representing the finish gate). + */ + private Polyline finishLine; + + /** + * Polyline for the advantage line (the leading line for the boats, orthogonal to the wind direction; touching the leading boat). + */ + private Polyline advantageLine; + + private AdvantageLineAnimator advantageTimer; + + /** + * The windward of two Polylines representing a triangle between startline and first mark. + */ + private Polyline windwardStartLineMarkToFirstMarkLine; + + /** + * The leeward of two Polylines representing a triangle between startline and first mark. + */ + private Polyline leewardStartLineMarkToFirstMarkLine; + + private class AdvantageLineMouseOverMapHandler implements MouseOverMapHandler { + private double trueWindAngle; + private Date date; + + public AdvantageLineMouseOverMapHandler(double trueWindAngle, Date date) { + this.trueWindAngle = trueWindAngle; + this.date = date; + } + + public void setTrueWindBearing(double trueWindAngle) { + this.trueWindAngle = trueWindAngle; + } + + public void setDate(Date date) { + this.date = date; + } + + @Override + public void onEvent(MouseOverMapEvent event) { + map.setTitle(stringMessages.advantageLine()+" (from "+new DegreeBearingImpl(Math.round(trueWindAngle)).reverse().getDegrees()+"deg"+ + (date == null ? ")" : ", "+ date) + ")"); + } + }; + + private AdvantageLineMouseOverMapHandler advantageLineMouseOverHandler; + + /** + * Polylines for the course middle lines; keys are the two control points delimiting the leg for which the + * {@link Polyline} value shows the course middle line. As only one course middle line is shown even if there + * are multiple legs using the same control points in different directions, using a {@link Set} makes this + * independent of the order of the two control points. If no course middle line is currently being shown for + * a pair of control points, the map will not contain a value for this pair. + */ + private final Map, Polyline> courseMiddleLines; + + private final Map courseSidelines; + + /** + * When the {@link HelpLineTypes#COURSEGEOMETRY} option is selected, little markers will be displayed on the + * lines that show the tooltip text in a little info box linked to the line. When the line is removed by + * {@link #showOrRemoveOrUpdateLine(Polyline, boolean, Position, Position, LineInfoProvider, String)}, these + * overlays need to be removed as well. Also, when the {@link HelpLineTypes#COURSEGEOMETRY} setting is + * deactivated, all these overlays need to go. + */ + private final Map infoOverlaysForLinesForCourseGeometry; + + /** + * Wind data used to display the advantage line. Retrieved by a {@link GetWindInfoAction} execution and used in + * {@link #showAdvantageLine(Iterable, Date)}. + */ + private WindInfoForRaceDTO lastCombinedWindTrackInfoDTO; + + /** + * Manages the cached set of {@link GPSFixDTOWithSpeedWindTackAndLegType}s for the boat positions as well as their graphical counterpart in the + * form of {@link Polyline}s. + */ + private final FixesAndTails fixesAndTails; + + /** + * html5 canvases used as boat display on the map + */ + private final Map boatOverlays; + + /** + * html5 canvases used for competitor info display on the map + */ + private final Map competitorInfoOverlays; + + private SmallTransparentInfoOverlay countDownOverlay; + + /** + * Map overlays with html5 canvas used to display wind sensors + */ + private final Map windSensorOverlays; + + /** + * Map overlays with html5 canvas used to display course marks including buoy zones + */ + private final Map courseMarkOverlays; + + private final Map courseMarkClickHandlers; + + private final Map markDTOs; + + /** + * markers displayed in response to + * {@link SailingServiceAsync#getDouglasPoints(String, String, Map, Map, double, AsyncCallback)} + */ + protected Set douglasMarkers; + + /** + * markers displayed in response to + * {@link SailingServiceAsync#getDouglasPoints(String, String, Map, Map, double, AsyncCallback)} + */ + private Set maneuverMarkers; + + private Map> lastManeuverResult; + + private Map> lastDouglasPeuckerResult; + + private final CompetitorSelectionProvider competitorSelection; + + private final RaceCompetitorSet raceCompetitorSet; + + /** + * Used to check if the first initial zoom to the mark markers was already done. + */ + private boolean mapFirstZoomDone = false; + + private final Timer timer; + + private RaceTimesInfoDTO lastRaceTimesInfo; + + private InfoWindow lastInfoWindow = null; + + /** + * RPC calls may receive responses out of order if there are multiple calls in-flight at the same time. If the time + * slider is moved quickly it generates many requests for boat positions quickly after each other. Sometimes, + * responses for requests send later may return before the responses to all earlier requests have been received and + * processed. This counter is used to number the requests. When processing of a response for a later request has + * already begun, responses to earlier requests will be ignored. + */ + private int boatPositionRequestIDCounter; + + /** + * Corresponds to {@link #boatPositionRequestIDCounter}. As soon as the processing of a response for a request ID + * begins, this attribute is set to the ID. A response won't be processed if a later response is already being + * processed. + */ + private int startedProcessingRequestID; + + private RaceMapImageManager raceMapImageManager; + + private RaceMapSettings settings; + private final RaceMapLifecycle raceMapLifecycle; + + private final StringMessages stringMessages; + + private boolean isMapInitialized; + + private Date lastTimeChangeBeforeInitialization; + + private int lastLegNumber; + + /** + * The last quick ranks received from a call to {@link SailingServiceAsync#getQuickRanks(RaceIdentifier, Date, AsyncCallback)} upon + * the last {@link #timeChanged(Date, Date)} event. Therefore, the ranks listed here correspond to the {@link #timer}'s time. + */ + private LinkedHashMap quickRanks; + + /** + * Taken from {@link RaceMapDataDTO#competitorsInOrderOfWindwardDistanceTraveledWithOneBasedLegNumber}; tells the + * windward distances traveled and the leg numbers in which the respective competitor is. + */ + private LinkedHashMap competitorsInOrderOfWindwardDistanceTraveledWithOneBasedLegNumber; + + + private final CombinedWindPanel combinedWindPanel; + private final TrueNorthIndicatorPanel trueNorthIndicatorPanel; + private final FlowPanel topLeftControlsWrapperPanel; + + private final AsyncActionsExecutor asyncActionsExecutor; + + /** + * The map bounds as last received by map callbacks; used to determine whether to suppress the boat animation during zoom/pan + */ + private LatLngBounds currentMapBounds; // bounds to which bounds-changed-handler compares + private int currentZoomLevel; // zoom-level to which bounds-changed-handler compares + + private boolean autoZoomIn = false; // flags auto-zoom-in in progress + private boolean autoZoomOut = false; // flags auto-zoom-out in progress + private int autoZoomLevel; // zoom-level to which auto-zoom-in/-out is zooming + LatLngBounds autoZoomLatLngBounds; // bounds to which auto-zoom-in/-out is panning&zooming + + private RaceSimulationOverlay simulationOverlay; + private WindStreamletsRaceboardOverlay streamletOverlay; + + private final boolean isSimulationEnabled; + + private static final String GET_POLAR_CATEGORY = "getPolar"; + + /** + * Tells about the availability of polar / VPP data for this race. If available, the simulation feature can be + * offered to the user. + */ + private boolean hasPolar; + + private final RegattaAndRaceIdentifier raceIdentifier; + + /** + * When the user requests wind-up display this may happen at a point where no mark positions are known or when + * no wind direction is known yet. In this case, this flag will be set, and when wind information or course mark + * positions are received later, this flag is checked, and if set, a {@link #updateCoordinateSystemFromSettings()} + * call is issued to make sure that the user's request for a new coordinate system is honored. + */ + private boolean requiresCoordinateSystemUpdateWhenCoursePositionAndWindDirectionIsKnown; + + /** + * Tells whether currently an auto-zoom is in progress; this is used particularly to keep the smooth CSS boat transitions + * active while auto-zooming whereas stopping them seems the better option for manual zooms. + */ + private boolean autoZoomInProgress; + + /** + * Tells whether currently an orientation change is in progress; this is required handle map events during the configuration of the map + * during an orientation change. + */ + private boolean orientationChangeInProgress; + + private final NumberFormat numberFormatOneDecimal = NumberFormat.getFormat("0.0"); + + public RaceMap(Component parent, ComponentContext context, RaceMapLifecycle raceMapLifecycle, + RaceMapSettings raceMapSettings, + SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, + ErrorReporter errorReporter, Timer timer, CompetitorSelectionProvider competitorSelection, StringMessages stringMessages, + RegattaAndRaceIdentifier raceIdentifier, RaceMapResources raceMapResources, + boolean showHeaderPanel) { + super(parent, context); + this.raceMapLifecycle = raceMapLifecycle; + this.stringMessages = stringMessages; + this.sailingService = sailingService; + this.raceIdentifier = raceIdentifier; + this.asyncActionsExecutor = asyncActionsExecutor; + this.errorReporter = errorReporter; + this.timer = timer; + this.isSimulationEnabled = true; + timer.addTimeListener(this); + raceMapImageManager = new RaceMapImageManager(raceMapResources); + markDTOs = new HashMap(); + courseSidelines = new HashMap<>(); + courseMiddleLines = new HashMap<>(); + infoOverlaysForLinesForCourseGeometry = new HashMap<>(); + boatOverlays = new HashMap(); + competitorInfoOverlays = new HashMap(); + windSensorOverlays = new HashMap(); + courseMarkOverlays = new HashMap(); + courseMarkClickHandlers = new HashMap(); + this.competitorSelection = competitorSelection; + this.raceCompetitorSet = new RaceCompetitorSet(competitorSelection); + competitorSelection.addCompetitorSelectionChangeListener(this); + settings = raceMapSettings; + coordinateSystem = new DelegateCoordinateSystem(new IdentityCoordinateSystem()); + fixesAndTails = new FixesAndTails(coordinateSystem); + updateCoordinateSystemFromSettings(); + lastTimeChangeBeforeInitialization = null; + isMapInitialized = false; + this.hasPolar = false; + headerPanel = new FlowPanel(); + headerPanel.setStyleName("RaceMap-HeaderPanel"); + panelForLeftHeaderLabels = new AbsolutePanel(); + panelForRightHeaderLabels = new AbsolutePanel(); + initializeData(settings.isShowMapControls(), showHeaderPanel); + RaceMapStyle raceMapStyle = raceMapResources.raceMapStyle(); + raceMapStyle.ensureInjected(); + combinedWindPanel = new CombinedWindPanel(this, raceMapImageManager, raceMapStyle, stringMessages, coordinateSystem); + combinedWindPanel.setVisible(false); + trueNorthIndicatorPanel = new TrueNorthIndicatorPanel(this, raceMapImageManager, raceMapStyle, stringMessages, coordinateSystem); + trueNorthIndicatorPanel.setVisible(true); + topLeftControlsWrapperPanel = new FlowPanel(); + topLeftControlsWrapperPanel.add(combinedWindPanel); + topLeftControlsWrapperPanel.add(trueNorthIndicatorPanel); + orientationChangeInProgress = false; + mapFirstZoomDone = false; + // TODO bug 494: reset zoom settings to user preferences + initWidget(rootPanel); + this.setSize("100%", "100%"); + } + + /** + * The {@link WindDTO#dampenedTrueWindFromDeg} direction if {@link #lastCombinedWindTrackInfoDTO} has a + * {@link WindSourceType#COMBINED} source which has at least one fix recorded; null otherwise. + */ + private Bearing getLastCombinedTrueWindFromDirection() { + if (lastCombinedWindTrackInfoDTO != null) { + for (Entry e : lastCombinedWindTrackInfoDTO.windTrackInfoByWindSource.entrySet()) { + if (e.getKey().getType() == WindSourceType.COMBINED) { + final List windFixes = e.getValue().windFixes; + if (!windFixes.isEmpty()) { + return new DegreeBearingImpl(windFixes.get(0).dampenedTrueWindFromDeg); + } + } + } + } + return null; + } + + /** + * @return {@code true} if the map was redrawn by the call to this method + */ + private boolean updateCoordinateSystemFromSettings() { + boolean redrawn = false; + final MapOptions mapOptions; + orientationChangeInProgress = true; + if (getSettings().isWindUp()) { + final Position centerOfCourse = getCenterOfCourse(); + if (centerOfCourse != null) { + final Bearing lastCombinedTrueWindFromDirection = getLastCombinedTrueWindFromDirection(); + if (lastCombinedTrueWindFromDirection != null) { + // new equator shall point 90deg right of the "from" wind direction to make wind come from top of map + coordinateSystem.setCoordinateSystem(new RotateAndTranslateCoordinateSystem(centerOfCourse, + lastCombinedTrueWindFromDirection.add(new DegreeBearingImpl(90)))); + if (map != null) { + mapOptions = getMapOptions(settings.isShowMapControls(), /* wind-up */ true); + } else { + mapOptions = null; + } + requiresCoordinateSystemUpdateWhenCoursePositionAndWindDirectionIsKnown = false; + } else { + // register callback in case center of course and wind info becomes known + requiresCoordinateSystemUpdateWhenCoursePositionAndWindDirectionIsKnown = true; + mapOptions = null; + } + } else { + // register callback in case center of course and wind info becomes known + requiresCoordinateSystemUpdateWhenCoursePositionAndWindDirectionIsKnown = true; + mapOptions = null; + } + } else { + if (map != null) { + mapOptions = getMapOptions(settings.isShowMapControls(), /* wind-up */ false); + } else { + mapOptions = null; + } + coordinateSystem.setCoordinateSystem(new IdentityCoordinateSystem()); + } + if (mapOptions != null) { // if no coordinate system change happened that affects an existing map, don't redraw + fixesAndTails.clearTails(); + redraw(); + redrawn = true; + // zooming and setting options while the event loop is still working doesn't work reliably; defer until event loop returns + Scheduler.get().scheduleDeferred(new ScheduledCommand() { + @Override + public void execute() { + if (map != null) { + map.setOptions(mapOptions); + // ensure zooming to what the settings tell, or defaults if what the settings tell isn't possible right now + mapFirstZoomDone = false; + trueNorthIndicatorPanel.redraw(); + orientationChangeInProgress = false; + } + } + }); + } + return redrawn; + } + + private void loadMapsAPIV3(final boolean showMapControls, final boolean showHeaderPanel) { + boolean sensor = true; + + // load all the libs for use in the maps + ArrayList loadLibraries = new ArrayList(); + loadLibraries.add(LoadLibrary.DRAWING); + loadLibraries.add(LoadLibrary.GEOMETRY); + + Runnable onLoad = new Runnable() { + @Override + public void run() { + MapOptions mapOptions = getMapOptions(showMapControls, /* wind up */ false); + map = new MapWidget(mapOptions); + rootPanel.add(map, 0, 0); + if (showHeaderPanel) { + Image sapLogo = createSAPLogo(); + rootPanel.add(sapLogo); + } + + map.setControls(ControlPosition.LEFT_TOP, topLeftControlsWrapperPanel); + adjustLeftControlsIndent(); + + RaceMap.this.raceMapImageManager.loadMapIcons(map); + map.setSize("100%", "100%"); + map.addZoomChangeHandler(new ZoomChangeMapHandler() { + @Override + public void onEvent(ZoomChangeMapEvent event) { + if (!autoZoomIn && !autoZoomOut && !orientationChangeInProgress) { + // stop automatic zoom after a manual zoom event; automatic zoom in zoomMapToNewBounds will restore old settings + final List emptyList = Collections.emptyList(); + RaceMapZoomSettings clearedZoomSettings = new RaceMapZoomSettings(emptyList, settings.getZoomSettings().isZoomToSelectedCompetitors()); + settings = new RaceMapSettings(settings, clearedZoomSettings); + } + // TODO bug489 when in wind-up mode, avoid zooming out too far; perhaps zoom back in if zoomed out too far + } + }); + map.addDragEndHandler(new DragEndMapHandler() { + @Override + public void onEvent(DragEndMapEvent event) { + // stop automatic zoom after a manual drag event + autoZoomIn = false; + autoZoomOut = false; + final List emptyList = Collections.emptyList(); + RaceMapZoomSettings clearedZoomSettings = new RaceMapZoomSettings(emptyList, settings.getZoomSettings().isZoomToSelectedCompetitors()); + settings = new RaceMapSettings(settings, clearedZoomSettings); + } + }); + map.addIdleHandler(new IdleMapHandler() { + @Override + public void onEvent(IdleMapEvent event) { + // the "idle"-event is raised at the end of map-animations + if (autoZoomIn) { + // finalize zoom-in that was started with panTo() in zoomMapToNewBounds() + map.setZoom(autoZoomLevel); + autoZoomIn = false; + } + if (autoZoomOut) { + // finalize zoom-out that was started with setZoom() in zoomMapToNewBounds() + map.panTo(autoZoomLatLngBounds.getCenter()); + autoZoomOut = false; + } + } + }); + map.addBoundsChangeHandler(new BoundsChangeMapHandler() { + @Override + public void onEvent(BoundsChangeMapEvent event) { + int newZoomLevel = map.getZoom(); + if (!isAutoZoomInProgress() && (newZoomLevel != currentZoomLevel)) { + removeTransitions(); + } + if ((streamletOverlay != null) && !map.getBounds().equals(currentMapBounds)) { + streamletOverlay.onBoundsChanged(newZoomLevel != currentZoomLevel); + } + if ((simulationOverlay != null) && !map.getBounds().equals(currentMapBounds)) { + simulationOverlay.onBoundsChanged(newZoomLevel != currentZoomLevel); + } + currentMapBounds = map.getBounds(); + currentZoomLevel = newZoomLevel; + headerPanel.getElement().getStyle().setWidth(map.getOffsetWidth(), Unit.PX); + } + }); + + // If there was a time change before the API was loaded, reset the time + if (lastTimeChangeBeforeInitialization != null) { + timeChanged(lastTimeChangeBeforeInitialization, null); + lastTimeChangeBeforeInitialization = null; + } + // Initialize streamlet canvas for wind visualization; it shouldn't be doing anything unless it's visible + streamletOverlay = new WindStreamletsRaceboardOverlay(getMap(), /* zIndex */ 0, + timer, raceIdentifier, sailingService, asyncActionsExecutor, stringMessages, coordinateSystem); + streamletOverlay.addToMap(); + if (settings.isShowWindStreamletOverlay()) { + streamletOverlay.setColors(settings.isShowWindStreamletColors()); + streamletOverlay.setVisible(true); + } + + if (isSimulationEnabled) { + // determine availability of polar diagram + setHasPolar(); + // initialize simulation canvas + simulationOverlay = new RaceSimulationOverlay(getMap(), /* zIndex */ 0, raceIdentifier, sailingService, stringMessages, asyncActionsExecutor, coordinateSystem); + simulationOverlay.addToMap(); + showSimulationOverlay(settings.isShowSimulationOverlay()); + } + if (showHeaderPanel) { + createHeaderPanel(map); + } + if (showMapControls) { + createSettingsButton(map); + } + // Data has been initialized + RaceMap.this.isMapInitialized = true; + RaceMap.this.redraw(); + trueNorthIndicatorPanel.redraw(); + showAdditionalControls(map); + } + }; + LoadApi.go(onLoad, loadLibraries, sensor, "key="+GoogleMapAPIKey.V3_APIKey); + } + + /** + * Subclasses may define additional stuff to be shown on the map. + */ + protected void showAdditionalControls(MapWidget map) { + } + + private void setHasPolar() { + GetPolarAction getPolar = new GetPolarAction(sailingService, raceIdentifier); + asyncActionsExecutor.execute(getPolar, GET_POLAR_CATEGORY, + new MarkedAsyncCallback<>(new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError(stringMessages.errorDeterminingPolarAvailability( + raceIdentifier.getRaceName(), caught.getMessage()), /* silent */ true); + } + + @Override + public void onSuccess(Boolean result) { + // store results + hasPolar = result.booleanValue(); + } + })); + + } + + /** + * Creates a header panel where additional information can be displayed by using + * {@link #getLeftHeaderPanel()} or {@link #getRightHeaderPanel()}. + * + * This panel is transparent and configured in such a way that it moves other controls + * down by its height. To achieve the goal of not having added widgets transparent + * this widget consists of two parts: First one is the transparent panel and the + * second one is the panel for the controls. The controls then need to moved onto + * the panel by using CSS. + */ + private void createHeaderPanel(MapWidget map) { + // we need a panel that does not have any transparency to have the + // labels shown in the right color. This panel also needs to have + // a higher z-index than other elements on the map + map.setControls(ControlPosition.TOP_LEFT, panelForLeftHeaderLabels); + panelForLeftHeaderLabels.getElement().getParentElement().getStyle().setProperty("zIndex", "1"); + panelForLeftHeaderLabels.getElement().getStyle().setProperty("overflow", "visible"); + rootPanel.add(panelForRightHeaderLabels); + panelForRightHeaderLabels.getElement().getStyle().setProperty("zIndex", "1"); + panelForRightHeaderLabels.getElement().getStyle().setProperty("overflow", "visible"); + // need to initialize size before css kicks in to make sure + // that controls get positioned right + headerPanel.getElement().getStyle().setHeight(60, Unit.PX); + headerPanel.getElement().getStyle().setWidth(map.getOffsetWidth(), Unit.PX); + // some sort of hack: not positioning TOP_LEFT because then the + // controls at RIGHT would not get the correct top setting + map.setControls(ControlPosition.TOP_RIGHT, headerPanel); + } + + private void createSettingsButton(MapWidget map) { + final Component component = this; + Button settingsButton = new Button(); + settingsButton.setStyleName("gwt-MapSettingsButton"); + settingsButton.ensureDebugId("raceMapSettingsButton"); + settingsButton.setTitle(stringMessages.settings()); + settingsButton.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + SettingsDialog dialog = new SettingsDialog(component, stringMessages); + dialog.ensureDebugId("raceMapSettings"); + dialog.show(); + } + }); + map.setControls(ControlPosition.RIGHT_TOP, settingsButton); + } + + private void removeTransitions() { + // remove the canvas animations for boats + for (CanvasOverlayV3 boatOverlay : RaceMap.this.getBoatOverlays().values()) { + boatOverlay.removeCanvasPositionAndRotationTransition(); + } + // remove the canvas animations for the info overlays of the selected boats + for (CompetitorInfoOverlay infoOverlay : competitorInfoOverlays.values()) { + infoOverlay.removeCanvasPositionAndRotationTransition(); + } + for (CourseMarkOverlay markOverlay : courseMarkOverlays.values()) { + markOverlay.removeCanvasPositionAndRotationTransition(); + } + // remove the advantage line animation + if (advantageTimer != null) { + advantageTimer.removeAnimation(); + } + } + + public void redraw() { + timeChanged(timer.getTime(), null); + } + + Map getBoatOverlays() { + return Collections.unmodifiableMap(boatOverlays); + } + + public MapWidget getMap() { + return map; + } + + public RaceSimulationOverlay getSimulationOverlay() { + return simulationOverlay; + } + + /** + * @return the Panel where labels or other controls for the header can be positioned + */ + public AbsolutePanel getLeftHeaderPanel() { + return panelForLeftHeaderLabels; + } + + public AbsolutePanel getRightHeaderPanel() { + return panelForRightHeaderLabels; + } + + @Override + public void raceTimesInfosReceived(Map raceTimesInfos, long clientTimeWhenRequestWasSent, Date serverTimeDuringRequest, long clientTimeWhenResponseWasReceived) { + timer.adjustClientServerOffset(clientTimeWhenRequestWasSent, serverTimeDuringRequest, clientTimeWhenResponseWasReceived); + this.lastRaceTimesInfo = raceTimesInfos.get(raceIdentifier); + } + + /** + * In {@link PlayModes#Live live mode}, when {@link #loadCompleteLeaderboard(Date) loading the leaderboard contents}, null + * is used as time point. The condition for this is encapsulated in this method so others can find out. For example, when a time change + * is signaled due to local offset / delay adjustments, no additional call to {@link #loadCompleteLeaderboard(Date)} would be required + * as null will be passed in any case, not being affected by local time offsets. + */ + private boolean useNullAsTimePoint() { + return timer.getPlayMode() == PlayModes.Live; + } + + @Override + public void timeChanged(final Date newTime, final Date oldTime) { + if (newTime != null && isMapInitialized) { + if (raceIdentifier != null) { + RegattaAndRaceIdentifier race = raceIdentifier; + final Iterable competitorsToShow = getCompetitorsToShow(); + if (race != null) { + final long transitionTimeInMillis = calculateTimeForPositionTransitionInMillis(newTime, oldTime); + final com.sap.sse.common.Util.Triple, Map, Map> fromAndToAndOverlap = + fixesAndTails.computeFromAndTo(newTime, competitorsToShow, settings.getEffectiveTailLengthInMilliseconds()); + // Request map data update, possibly in two calls; see method details + callGetRaceMapDataForAllOverlappingAndTipsOfNonOverlappingAndGetBoatPositionsForAllOthers(fromAndToAndOverlap, race, newTime, transitionTimeInMillis, competitorsToShow); + // draw the wind into the map, get the combined wind + List windSourceTypeNames = new ArrayList(); + windSourceTypeNames.add(WindSourceType.EXPEDITION.name()); + windSourceTypeNames.add(WindSourceType.COMBINED.name()); + GetWindInfoAction getWindInfoAction = new GetWindInfoAction(sailingService, race, 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) { + List> windSourcesToShow = new ArrayList>(); + if (windInfo != null) { + lastCombinedWindTrackInfoDTO = windInfo; + showAdvantageLine(competitorsToShow, newTime, transitionTimeInMillis); + for (WindSource windSource : windInfo.windTrackInfoByWindSource.keySet()) { + WindTrackInfoDTO windTrackInfoDTO = windInfo.windTrackInfoByWindSource.get(windSource); + switch (windSource.getType()) { + case EXPEDITION: + // we filter out measured wind sources with vary low confidence + if (windTrackInfoDTO.minWindConfidence > 0.0001) { + windSourcesToShow.add(new com.sap.sse.common.Util.Pair(windSource, windTrackInfoDTO)); + } + break; + case COMBINED: + showCombinedWindOnMap(windSource, windTrackInfoDTO); + if (requiresCoordinateSystemUpdateWhenCoursePositionAndWindDirectionIsKnown) { + updateCoordinateSystemFromSettings(); + } + break; + default: + // Which wind sources are requested is defined in a list above this + // action. So we throw here an exception to notice a missing source. + throw new UnsupportedOperationException( + "There is currently no support for the enum value '" + + windSource.getType() + "' in this method."); + } + } + } + showWindSensorsOnMap(windSourcesToShow); + } + }); + } + } + } + } + + /** + * Requests updates for map data and, when received, updates the map structures accordingly. + *

+ * + * The update can happen in one or two round trips. We assume that overlapping segments usually don't require a lot + * of loading time as the most typical case will be to update a longer tail with a few new fixes that were received + * since the last time tick. These supposedly fast requests are handled by a {@link GetRaceMapDataAction} which also + * requests updates for mark positions, sidelines and quick ranks. The same request also loads boat positions for + * the zero-length interval at newTime for the non-overlapping tails assuming that this will work + * fairly fast and in particular in O(1) time regardless of tail length, compared to fetching the entire tail for + * all competitors. This will at least provide quick feedback about those competitors' positions even if loading + * their entire tail may take a little longer. The {@link GetRaceMapDataAction} therefore is intended to be a rather + * quick call. + *

+ * + * Non-overlapping position requests typically occur for the first request when no fix at all is known for the + * competitor yet, or when the user has radically moved the time slider to some other time such that given the + * current tail length setting the new tail segment does not overlap with the old one, requiring a full load of the + * entire tail data for that competitor. For these non-overlapping requests, this method creates a + * {@link GetBoatPositionsAction} request loading the entire tail required, but not quick ranks, sidelines and mark + * positions. Updating the results of this call is done in {@link #updateBoatPositions(Date, long, Map, Iterable, Map, boolean)}. + *

+ */ + private void callGetRaceMapDataForAllOverlappingAndTipsOfNonOverlappingAndGetBoatPositionsForAllOthers( + final Triple, Map, Map> fromAndToAndOverlap, + RegattaAndRaceIdentifier race, final Date newTime, final long transitionTimeInMillis, final Iterable competitorsToShow) { + 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 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, + (settings.isShowSimulationOverlay() ? simulationOverlay.getLegIdentifier() : null), + raceCompetitorSet.getMd5OfIdsAsStringOfCompetitorParticipatingInRaceInAlphanumericOrderOfTheirID()), + GET_RACE_MAP_DATA_CATEGORY, + getRaceMapDataCallback(newTime, transitionTimeInMillis, fromAndToAndOverlap.getC(), competitorsToShow, ++boatPositionRequestIDCounter)); + // 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()) { + asyncActionsExecutor.execute(new GetBoatPositionsAction(sailingService, race, fromTimesForNonOverlappingTailsCall, toTimesForNonOverlappingTailsCall, + /* extrapolate */ true), GET_RACE_MAP_DATA_CATEGORY, + new MarkedAsyncCallback<>(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); + } + })); + } + } + + private AsyncCallback getRaceMapDataCallback( + final Date newTime, + final long transitionTimeInMillis, + final Map hasTailOverlapForCompetitor, + final Iterable competitorsToShow, final int requestID) { + return new MarkedAsyncCallback<>(new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError("Error obtaining racemap data: " + caught.getMessage(), true /*silentMode */); + } + + @Override + public void onSuccess(RaceMapDataDTO raceMapDataDTO) { + if (map != null && raceMapDataDTO != null) { + // process response only if not received out of order + if (startedProcessingRequestID < requestID) { + startedProcessingRequestID = requestID; + if (raceMapDataDTO.raceCompetitorIdsAsStrings != null) { + try { + raceCompetitorSet.setIdsAsStringsOfCompetitorsInRace(raceMapDataDTO.raceCompetitorIdsAsStrings); + } catch (Exception e) { + GWT.log("Error trying to update competitor set for race "+raceIdentifier.getRaceName()+ + " in regatta "+raceIdentifier.getRegattaName(), e); + } + } + quickRanks = raceMapDataDTO.quickRanks; + competitorsInOrderOfWindwardDistanceTraveledWithOneBasedLegNumber = + raceMapDataDTO.competitorsInOrderOfWindwardDistanceTraveledWithOneBasedLegNumber; + if (isSimulationEnabled && settings.isShowSimulationOverlay()) { + lastLegNumber = raceMapDataDTO.coursePositions.currentLegNumber; + simulationOverlay.updateLeg(Math.max(lastLegNumber, 1), /* clearCanvas */ false, raceMapDataDTO.simulationResultVersion); + } + // Do boat specific actions + Map> boatData = raceMapDataDTO.boatPositions; + updateBoatPositions(newTime, transitionTimeInMillis, hasTailOverlapForCompetitor, competitorsToShow, boatData, /* updateTailsOnly */ false); + if (douglasMarkers != null) { + removeAllMarkDouglasPeuckerpoints(); + } + if (maneuverMarkers != null) { + removeAllManeuverMarkers(); + } + + if (requiresCoordinateSystemUpdateWhenCoursePositionAndWindDirectionIsKnown) { + updateCoordinateSystemFromSettings(); + } + // Do mark specific actions + showCourseMarksOnMap(raceMapDataDTO.coursePositions, transitionTimeInMillis); + showCourseSidelinesOnMap(raceMapDataDTO.courseSidelines); + showStartAndFinishAndCourseMiddleLines(raceMapDataDTO.coursePositions); + showStartLineToFirstMarkTriangle(raceMapDataDTO.coursePositions); + + // Rezoom the map + LatLngBounds zoomToBounds = null; + if (!settings.getZoomSettings().containsZoomType(ZoomTypes.NONE)) { // Auto zoom if setting is not manual + zoomToBounds = settings.getZoomSettings().getNewBounds(RaceMap.this); + if (zoomToBounds == null && !mapFirstZoomDone) { + zoomToBounds = getDefaultZoomBounds(); // the user-specified zoom couldn't find what it was looking for; try defaults once + } + } else if (!mapFirstZoomDone) { // Zoom once to the marks if marks exist + zoomToBounds = new CourseMarksBoundsCalculator().calculateNewBounds(RaceMap.this); + if (zoomToBounds == null) { + zoomToBounds = getDefaultZoomBounds(); // use default zoom, e.g., + } + /* + * Reset the mapZoomedOrPannedSinceLastRaceSelection: In spite of the fact that + * the map was just zoomed to the bounds of the marks, it was not a zoom or pan + * triggered by the user. As a consequence the + * mapZoomedOrPannedSinceLastRaceSelection option has to reset again. + */ + } + zoomMapToNewBounds(zoomToBounds); + mapFirstZoomDone = true; + } + } else { + lastTimeChangeBeforeInitialization = newTime; + } + } + }); + } + + /** + * @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(CompetitorDTO)}); 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(CompetitorDTO, 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)}. + * @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 + * competitor info bubbles moved. This assumes that the tips of these tails are loaded in a separate call + * 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. + */ + private void updateBoatPositions(final Date newTime, final long transitionTimeInMillis, + final Map hasTailOverlapForCompetitor, + final Iterable competitorsToShow, Map> boatData, boolean updateTailsOnly) { + fixesAndTails.updateFixes(boatData, hasTailOverlapForCompetitor, RaceMap.this, transitionTimeInMillis); + showBoatsOnMap(newTime, transitionTimeInMillis, + /* re-calculate; it could have changed since the asynchronous request was made: */ getCompetitorsToShow(), + updateTailsOnly); + 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); + } + } + + private void showCourseSidelinesOnMap(List sidelinesDTOs) { + if (map != null && sidelinesDTOs != null ) { + Map toRemoveSidelines = new HashMap(courseSidelines); + for (SidelineDTO sidelineDTO : sidelinesDTOs) { + if (sidelineDTO.getMarks().size() == 2) { // right now we only support sidelines with 2 marks + Polygon sideline = courseSidelines.get(sidelineDTO); + LatLng[] sidelinePoints = new LatLng[sidelineDTO.getMarks().size()]; + int i=0; + for (MarkDTO sidelineMark : sidelineDTO.getMarks()) { + sidelinePoints[i] = coordinateSystem.toLatLng(sidelineMark.position); + i++; + } + if (sideline == null) { + PolygonOptions options = PolygonOptions.newInstance(); + options.setClickable(true); + options.setStrokeColor("#0000FF"); + options.setStrokeWeight(1); + options.setStrokeOpacity(1.0); + options.setFillColor(null); + options.setFillOpacity(1.0); + + sideline = Polygon.newInstance(options); + MVCArray pointsAsArray = MVCArray.newInstance(sidelinePoints); + sideline.setPath(pointsAsArray); + + sideline.addMouseOverHandler(new MouseOverMapHandler() { + @Override + public void onEvent(MouseOverMapEvent event) { + map.setTitle(stringMessages.sideline()); + } + }); + sideline.addMouseOutMoveHandler(new MouseOutMapHandler() { + @Override + public void onEvent(MouseOutMapEvent event) { + map.setTitle(""); + } + }); + courseSidelines.put(sidelineDTO, sideline); + sideline.setMap(map); + } else { + sideline.getPath().removeAt(1); + sideline.getPath().removeAt(0); + sideline.getPath().insertAt(0, sidelinePoints[0]); + sideline.getPath().insertAt(1, sidelinePoints[1]); + toRemoveSidelines.remove(sidelineDTO); + } + } + } + for (SidelineDTO toRemoveSideline : toRemoveSidelines.keySet()) { + Polygon sideline = courseSidelines.remove(toRemoveSideline); + sideline.setMap(null); + } + } + } + + private void showCourseMarksOnMap(CoursePositionsDTO courseDTO, long transitionTimeInMillis) { + 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.getName()); + 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.getName(), courseMarkOverlay); + markDTOs.put(markDTO.getName(), markDTO); + registerCourseMarkInfoWindowClickHandler(markDTO.getName()); + 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.getName()); + } + } + } + for (String toRemoveMarkName : toRemoveCourseMarks.keySet()) { + CourseMarkOverlay removedOverlay = courseMarkOverlays.remove(toRemoveMarkName); + if(removedOverlay != null) { + removedOverlay.removeFromMap(); + } + } + } + } + + /** + * Based on the mark positions in {@link #courseMarkOverlays}' values this method determines the center of gravity of these marks' + * {@link CourseMarkOverlay#getPosition() positions}. + */ + private Position getCenterOfCourse() { + ScalablePosition center = null; + int count = 0; + for (CourseMarkOverlay markOverlay : courseMarkOverlays.values()) { + ScalablePosition markPosition = new ScalablePosition(markOverlay.getPosition()); + if (center == null) { + center = markPosition; + } else { + center.add(markPosition); + } + count++; + } + return center == null ? null : center.divide(count); + } + + private void showCombinedWindOnMap(WindSource windSource, WindTrackInfoDTO windTrackInfoDTO) { + if (map != null) { + combinedWindPanel.setWindInfo(windTrackInfoDTO, windSource); + combinedWindPanel.redraw(); + } + } + + private void showWindSensorsOnMap(List> windSensorsList) { + if (map != null) { + Set toRemoveWindSources = new HashSet(windSensorOverlays.keySet()); + for (com.sap.sse.common.Util.Pair windSourcePair : windSensorsList) { + WindSource windSource = windSourcePair.getA(); + WindTrackInfoDTO windTrackInfoDTO = windSourcePair.getB(); + + WindSensorOverlay windSensorOverlay = windSensorOverlays.get(windSource); + if (windSensorOverlay == null) { + windSensorOverlay = createWindSensorOverlay(RaceMapOverlaysZIndexes.WINDSENSOR_ZINDEX, windSource, windTrackInfoDTO); + windSensorOverlays.put(windSource, windSensorOverlay); + windSensorOverlay.addToMap(); + } else { + windSensorOverlay.setWindInfo(windTrackInfoDTO, windSource); + windSensorOverlay.draw(); + toRemoveWindSources.remove(windSource); + } + } + for (WindSource toRemoveWindSource : toRemoveWindSources) { + WindSensorOverlay removedWindSensorOverlay = windSensorOverlays.remove(toRemoveWindSource); + if (removedWindSensorOverlay != null) { + removedWindSensorOverlay.removeFromMap(); + } + } + } + } + + private void showCompetitorInfoOnMap(final Date newTime, final long timeForPositionTransitionMillis, final Iterable competitorsToShow) { + if (map != null) { + if (settings.isShowSelectedCompetitorsInfo()) { + Set toRemoveCompetorInfoOverlays = new HashSet( + competitorInfoOverlays.keySet()); + for (CompetitorDTO competitorDTO : competitorsToShow) { + if (fixesAndTails.hasFixesFor(competitorDTO)) { + GPSFixDTOWithSpeedWindTackAndLegType lastBoatFix = getBoatFix(competitorDTO, newTime); + if (lastBoatFix != null) { + CompetitorInfoOverlay competitorInfoOverlay = competitorInfoOverlays.get(competitorDTO); + if (competitorInfoOverlay == null) { + competitorInfoOverlay = createCompetitorInfoOverlay(RaceMapOverlaysZIndexes.INFO_OVERLAY_ZINDEX, competitorDTO); + competitorInfoOverlays.put(competitorDTO, competitorInfoOverlay); + competitorInfoOverlay.setInfoText(createInfoText(competitorDTO, lastBoatFix)); + competitorInfoOverlay.setPosition(lastBoatFix.position, timeForPositionTransitionMillis); + competitorInfoOverlay.addToMap(); + } else { + competitorInfoOverlay.setInfoText(createInfoText(competitorDTO, lastBoatFix)); + competitorInfoOverlay.setPosition(lastBoatFix.position, timeForPositionTransitionMillis); + competitorInfoOverlay.draw(); + } + toRemoveCompetorInfoOverlays.remove(competitorDTO); + } + } + } + for (CompetitorDTO toRemoveCompetorDTO : toRemoveCompetorInfoOverlays) { + CompetitorInfoOverlay competitorInfoOverlay = competitorInfoOverlays.get(toRemoveCompetorDTO); + competitorInfoOverlay.removeFromMap(); + competitorInfoOverlays.remove(toRemoveCompetorDTO); + } + } else { + // remove all overlays + for (CompetitorInfoOverlay competitorInfoOverlay : competitorInfoOverlays.values()) { + competitorInfoOverlay.removeFromMap(); + } + competitorInfoOverlays.clear(); + } + } + } + + private long calculateTimeForPositionTransitionInMillis(final Date newTime, final Date oldTime) { + final long timeForPositionTransitionMillis; + boolean hasTimeJumped = oldTime != null && Math.abs(oldTime.getTime() - newTime.getTime()) > 3*timer.getRefreshInterval(); + if (timer.getPlayState() == PlayStates.Playing && !hasTimeJumped) { + // choose 130% of the refresh interval as transition period to make it unlikely that the transition + // stops before the next update has been received + timeForPositionTransitionMillis = 1300 * timer.getRefreshInterval() / 1000; + } else { + timeForPositionTransitionMillis = -1; // -1 means 'no transition + } + return timeForPositionTransitionMillis; + } + + /** + * @param updateTailsOnly + * if false, tails of competitors not in competitorsToShow are removed from the + * map + */ + private void showBoatsOnMap(final Date newTime, final long timeForPositionTransitionMillis, + final Iterable competitorsToShow, boolean updateTailsOnly) { + if (map != null) { + Date tailsFromTime = new Date(newTime.getTime() - settings.getEffectiveTailLengthInMilliseconds()); + Date tailsToTime = newTime; + Set competitorDTOsOfUnusedTails = new HashSet(); + Set competitorDTOsOfUnusedBoatCanvases = new HashSet(); + if (!updateTailsOnly) { + competitorDTOsOfUnusedTails.addAll(fixesAndTails.getCompetitorsWithTails()); + competitorDTOsOfUnusedBoatCanvases.addAll(boatOverlays.keySet()); + } + for (CompetitorDTO competitorDTO : competitorsToShow) { + if (fixesAndTails.hasFixesFor(competitorDTO)) { + if (!fixesAndTails.hasTail(competitorDTO)) { + fixesAndTails.createTailAndUpdateIndices(competitorDTO, tailsFromTime, tailsToTime, this); + } else { + fixesAndTails.updateTail(competitorDTO, tailsFromTime, tailsToTime, (int) (timeForPositionTransitionMillis==-1?-1:timeForPositionTransitionMillis/2)); + if (!updateTailsOnly) { + competitorDTOsOfUnusedTails.remove(competitorDTO); + } + PolylineOptions newOptions = createTailStyle(competitorDTO, displayHighlighted(competitorDTO)); + fixesAndTails.getTail(competitorDTO).setOptions(newOptions); + } + boolean usedExistingBoatCanvas = updateBoatCanvasForCompetitor(competitorDTO, newTime, timeForPositionTransitionMillis); + if (usedExistingBoatCanvas && !updateTailsOnly) { + competitorDTOsOfUnusedBoatCanvases.remove(competitorDTO); + } + } + } + if (!updateTailsOnly) { + for (CompetitorDTO unusedBoatCanvasCompetitorDTO : competitorDTOsOfUnusedBoatCanvases) { + CanvasOverlayV3 boatCanvas = boatOverlays.get(unusedBoatCanvasCompetitorDTO); + boatCanvas.removeFromMap(); + boatOverlays.remove(unusedBoatCanvasCompetitorDTO); + } + for (CompetitorDTO unusedTailCompetitorDTO : competitorDTOsOfUnusedTails) { + fixesAndTails.removeTail(unusedTailCompetitorDTO); + } + } + } + } + + /** + * This algorithm is limited to distances such that dlon < pi/2, i.e., those that extend around less than one + * quarter of the circumference of the earth in longitude. A completely general, but more complicated algorithm is + * necessary if greater distances are allowed. + */ + public LatLng calculatePositionAlongRhumbline(LatLng position, double bearingDeg, Distance distance) { + double distanceRad = distance.getCentralAngleRad(); + double lat1 = position.getLatitude() / 180. * Math.PI; + double lon1 = position.getLongitude() / 180. * Math.PI; + double bearingRad = bearingDeg / 180. * Math.PI; + double lat2 = Math.asin(Math.sin(lat1) * Math.cos(distanceRad) + + Math.cos(lat1) * Math.sin(distanceRad) * Math.cos(bearingRad)); + double lon2 = lon1 + Math.atan2(Math.sin(bearingRad)*Math.sin(distanceRad)*Math.cos(lat1), + Math.cos(distanceRad)-Math.sin(lat1)*Math.sin(lat2)); + lon2 = (lon2+3*Math.PI) % (2*Math.PI) - Math.PI; // normalize to -180..+180� + // position is already in LatLng space, so no mapping through coordinateSystem is required here + return LatLng.newInstance(lat2 / Math.PI * 180., lon2 / Math.PI * 180.); + } + + /** + * Returns a pair whose first component is the leg number (one-based) of the competitor returned as the second component. + */ + private com.sap.sse.common.Util.Pair getFarthestAheadVisibleCompetitorWithOneBasedLegNumber( + Iterable competitorsToShow) { + CompetitorDTO leadingCompetitorDTO = null; + int legOfLeaderCompetitor = -1; + // this only works because the quickRanks are sorted + for (Entry competitorsByWindwardDistanceTraveledAndOneBasedLegNumber : + competitorsInOrderOfWindwardDistanceTraveledWithOneBasedLegNumber.entrySet()) { + if (Util.contains(competitorsToShow, competitorsByWindwardDistanceTraveledAndOneBasedLegNumber.getKey()) && + competitorsByWindwardDistanceTraveledAndOneBasedLegNumber.getValue() != null) { + leadingCompetitorDTO = competitorsByWindwardDistanceTraveledAndOneBasedLegNumber.getKey(); + legOfLeaderCompetitor = competitorsByWindwardDistanceTraveledAndOneBasedLegNumber.getValue(); + return new com.sap.sse.common.Util.Pair(legOfLeaderCompetitor, leadingCompetitorDTO); + } + } + return null; + } + + final static Distance advantageLineLength = new MeterDistance(1000); // TODO this should probably rather scale with the visible area of the map; bug 616 + private void showAdvantageLine(Iterable competitorsToShow, Date date, long timeForPositionTransitionMillis) { + if (map != null && lastRaceTimesInfo != null && quickRanks != null && lastCombinedWindTrackInfoDTO != null) { + boolean drawAdvantageLine = false; + if (settings.getHelpLinesSettings().isVisible(HelpLineTypes.ADVANTAGELINE)) { + // find competitor with highest rank + com.sap.sse.common.Util.Pair visibleLeaderInfo = getFarthestAheadVisibleCompetitorWithOneBasedLegNumber(competitorsToShow); + // the boat fix may be null; may mean that no positions were loaded yet for the leading visible boat; + // don't show anything + GPSFixDTOWithSpeedWindTackAndLegType lastBoatFix = null; + boolean isVisibleLeaderInfoComplete = false; + boolean isLegTypeKnown = false; + WindTrackInfoDTO windDataForLegMiddle = null; + LegInfoDTO legInfoDTO = null; + if (visibleLeaderInfo != null + && visibleLeaderInfo.getA() > 0 + && visibleLeaderInfo.getA() <= lastRaceTimesInfo.getLegInfos().size() + // get wind at middle of leg for leading visible competitor + && (windDataForLegMiddle = lastCombinedWindTrackInfoDTO + .getCombinedWindOnLegMiddle(visibleLeaderInfo.getA() - 1)) != null + && !windDataForLegMiddle.windFixes.isEmpty()) { + isVisibleLeaderInfoComplete = true; + legInfoDTO = lastRaceTimesInfo.getLegInfos().get(visibleLeaderInfo.getA() - 1); + if (legInfoDTO.legType != null) { + isLegTypeKnown = true; + } + lastBoatFix = getBoatFix(visibleLeaderInfo.getB(), date); + } + if (isVisibleLeaderInfoComplete && isLegTypeKnown && lastBoatFix != null && lastBoatFix.speedWithBearing != null) { + Distance distanceFromBoatPositionInKm = visibleLeaderInfo.getB().getBoatClass().getHullLength(); // one hull length + // implement and use Position.translateRhumb() + double bearingOfBoatInDeg = lastBoatFix.speedWithBearing.bearingInDegrees; + LatLng boatPosition = coordinateSystem.toLatLng(lastBoatFix.position); + LatLng posAheadOfFirstBoat = calculatePositionAlongRhumbline(boatPosition, + coordinateSystem.mapDegreeBearing(bearingOfBoatInDeg), distanceFromBoatPositionInKm); + final WindDTO windFix = windDataForLegMiddle.windFixes.get(0); + double bearingOfCombinedWindInDeg = windFix.trueWindBearingDeg; + double rotatedBearingDeg1 = 0.0; + double rotatedBearingDeg2 = 0.0; + switch (lastBoatFix.legType) { + case UPWIND: + case DOWNWIND: { + rotatedBearingDeg1 = bearingOfCombinedWindInDeg + 90.0; + if (rotatedBearingDeg1 >= 360.0) { + rotatedBearingDeg1 -= 360.0; + } + rotatedBearingDeg2 = bearingOfCombinedWindInDeg - 90.0; + if (rotatedBearingDeg2 < 0.0) { + rotatedBearingDeg2 += 360.0; + } + } + break; + case REACHING: { + rotatedBearingDeg1 = legInfoDTO.legBearingInDegrees + 90.0; + if (rotatedBearingDeg1 >= 360.0) { + rotatedBearingDeg1 -= 360.0; + } + rotatedBearingDeg2 = legInfoDTO.legBearingInDegrees - 90.0; + if (rotatedBearingDeg2 < 0.0) { + rotatedBearingDeg2 += 360.0; + } + } + break; + } + MVCArray nextPath = MVCArray.newInstance(); + LatLng advantageLinePos1 = calculatePositionAlongRhumbline(posAheadOfFirstBoat, + coordinateSystem.mapDegreeBearing(rotatedBearingDeg1), advantageLineLength.scale(0.5)); + LatLng advantageLinePos2 = calculatePositionAlongRhumbline(posAheadOfFirstBoat, + coordinateSystem.mapDegreeBearing(rotatedBearingDeg2), advantageLineLength.scale(0.5)); + if (advantageLine == null) { + PolylineOptions options = PolylineOptions.newInstance(); + options.setClickable(true); + options.setGeodesic(true); + options.setStrokeColor("#000000"); + options.setStrokeWeight(1); + options.setStrokeOpacity(0.5); + + advantageLine = Polyline.newInstance(options); + advantageTimer = new AdvantageLineAnimator(advantageLine); + MVCArray pointsAsArray = MVCArray.newInstance(); + pointsAsArray.insertAt(0, advantageLinePos1); + pointsAsArray.insertAt(1, advantageLinePos2); + advantageLine.setPath(pointsAsArray); + advantageLine.setMap(map); + Hoverline advantageHoverline = new Hoverline(advantageLine, options, this); + + advantageLineMouseOverHandler = new AdvantageLineMouseOverMapHandler( + bearingOfCombinedWindInDeg, new Date(windFix.measureTimepoint)); + advantageLine.addMouseOverHandler(advantageLineMouseOverHandler); + advantageHoverline.addMouseOutMoveHandler(new MouseOutMapHandler() { + @Override + public void onEvent(MouseOutMapEvent event) { + map.setTitle(""); + } + }); + } else { + nextPath.push(advantageLinePos1); + nextPath.push(advantageLinePos2); + advantageTimer.setNextPositionAndTransitionMillis(nextPath, timeForPositionTransitionMillis); + if (advantageLineMouseOverHandler != null) { + advantageLineMouseOverHandler.setTrueWindBearing(bearingOfCombinedWindInDeg); + advantageLineMouseOverHandler.setDate(new Date(windFix.measureTimepoint)); + } + } + drawAdvantageLine = true; + } + } + if (!drawAdvantageLine) { + if (advantageLine != null) { + advantageLine.setMap(null); + advantageLine = null; + advantageTimer = null; + } + } + } + } + + private final StringBuilder windwardStartLineMarkToFirstMarkLineText = new StringBuilder(); + private final StringBuilder leewardStartLineMarkToFirstMarkLineText = new StringBuilder(); + + private void showStartLineToFirstMarkTriangle(final CoursePositionsDTO courseDTO){ + final List startMarkPositions = courseDTO.getStartMarkPositions(); + if (startMarkPositions.size() > 1 && courseDTO.waypointPositions.size() > 1) { + final Position windwardStartLinePosition = startMarkPositions.get(0); + final Position leewardStartLinePosition = startMarkPositions.get(1); + final Position firstMarkPosition = courseDTO.waypointPositions.get(1); + windwardStartLineMarkToFirstMarkLineText.replace(0, windwardStartLineMarkToFirstMarkLineText.length(), + stringMessages.startLineToFirstMarkTriangle(numberFormatOneDecimal + .format(windwardStartLinePosition.getDistance(firstMarkPosition) + .getMeters()))); + leewardStartLineMarkToFirstMarkLineText.replace(0, leewardStartLineMarkToFirstMarkLineText.length(), + stringMessages.startLineToFirstMarkTriangle(numberFormatOneDecimal + .format(leewardStartLinePosition.getDistance(firstMarkPosition) + .getMeters()))); + final LineInfoProvider windwardStartLineMarkToFirstMarkLineInfoProvider = new LineInfoProvider() { + @Override + public String getLineInfo() { + return windwardStartLineMarkToFirstMarkLineText.toString(); + } + }; + final LineInfoProvider leewardStartLineMarkToFirstMarkLineInfoProvider = new LineInfoProvider() { + @Override + public String getLineInfo() { + return leewardStartLineMarkToFirstMarkLineText.toString(); + } + }; + windwardStartLineMarkToFirstMarkLine = showOrRemoveOrUpdateLine(windwardStartLineMarkToFirstMarkLine, /* showLine */ + (settings.getHelpLinesSettings().isVisible(HelpLineTypes.STARTLINETOFIRSTMARKTRIANGLE)) + && startMarkPositions.size() > 1 && courseDTO.waypointPositions.size() > 1, + windwardStartLinePosition, firstMarkPosition, windwardStartLineMarkToFirstMarkLineInfoProvider, + "grey"); + leewardStartLineMarkToFirstMarkLine = showOrRemoveOrUpdateLine(leewardStartLineMarkToFirstMarkLine, /* showLine */ + (settings.getHelpLinesSettings().isVisible(HelpLineTypes.STARTLINETOFIRSTMARKTRIANGLE)) + && startMarkPositions.size() > 1 && courseDTO.waypointPositions.size() > 1, + leewardStartLinePosition, firstMarkPosition, leewardStartLineMarkToFirstMarkLineInfoProvider, + "grey"); + } + } + + private final StringBuilder startLineAdvantageText = new StringBuilder(); + private final StringBuilder finishLineAdvantageText = new StringBuilder(); + final LineInfoProvider startLineInfoProvider = new LineInfoProvider() { + @Override + public String getLineInfo() { + return stringMessages.startLine()+startLineAdvantageText; + } + }; + final LineInfoProvider finishLineInfoProvider = new LineInfoProvider() { + @Override + public String getLineInfo() { + return stringMessages.finishLine()+finishLineAdvantageText; + } + }; + + private void showStartAndFinishAndCourseMiddleLines(final CoursePositionsDTO courseDTO) { + if (map != null && courseDTO != null && courseDTO.course != null && courseDTO.course.waypoints != null && + !courseDTO.course.waypoints.isEmpty()) { + // draw the start line + final WaypointDTO startWaypoint = courseDTO.course.waypoints.get(0); + updateCountdownCanvas(startWaypoint); + final int numberOfStartWaypointMarks = courseDTO.getStartMarkPositions() == null ? 0 : courseDTO.getStartMarkPositions().size(); + final int numberOfFinishWaypointMarks = courseDTO.getFinishMarkPositions() == null ? 0 : courseDTO.getFinishMarkPositions().size(); + final Position startLineLeftPosition = numberOfStartWaypointMarks == 0 ? null : courseDTO.getStartMarkPositions().get(0); + final Position startLineRightPosition = numberOfStartWaypointMarks < 2 ? null : courseDTO.getStartMarkPositions().get(1); + if (courseDTO.startLineAngleFromPortToStarboardWhenApproachingLineToCombinedWind != null) { + startLineAdvantageText.replace(0, startLineAdvantageText.length(), " "+stringMessages.lineAngleToWindAndAdvantage( + NumberFormat.getFormat("0.0").format(courseDTO.startLineLengthInMeters), + NumberFormat.getFormat("0.0").format(Math.abs(courseDTO.startLineAngleFromPortToStarboardWhenApproachingLineToCombinedWind)), + courseDTO.startLineAdvantageousSide.name().charAt(0)+courseDTO.startLineAdvantageousSide.name().substring(1).toLowerCase(), + NumberFormat.getFormat("0.0").format(courseDTO.startLineAdvantageInMeters))); + } else { + startLineAdvantageText.delete(0, startLineAdvantageText.length()); + } + final boolean showStartLineBasedOnCurrentLeg = numberOfStartWaypointMarks == 2 && courseDTO.currentLegNumber <= 1; + final boolean showFinishLineBasedOnCurrentLeg = numberOfFinishWaypointMarks == 2 && courseDTO.currentLegNumber == courseDTO.totalLegsCount; + // show the line when STARTLINE is selected and the current leg is around the start leg, + // or when COURSEGEOMETRY is selected and the finish line isn't equal and wouldn't be shown at the same time based on the current leg. + // With this, if COURSEGEOMETRY is selected and start and finish line are equal, the start line will not be displayed if + // based on the race progress the finish line is to be preferred, so only the finish line will be shown. + final boolean reallyShowStartLine = + (settings.getHelpLinesSettings().isVisible(HelpLineTypes.STARTLINE) && showStartLineBasedOnCurrentLeg) || + (settings.getHelpLinesSettings().isVisible(HelpLineTypes.COURSEGEOMETRY) && + (!showFinishLineBasedOnCurrentLeg || !startLineEqualsFinishLine(courseDTO))); + // show the line when FINISHLINE is selected and the current leg is the last leg, + // or when COURSEGEOMETRY is selected and the start line isn't equal or the current leg is the last leg. + // With this, if COURSEGEOMETRY is selected and start and finish line are equal, the start line will be displayed unless + // the finish line should take precedence based on race progress. + final boolean reallyShowFinishLine = showFinishLineBasedOnCurrentLeg && + (!showStartLineBasedOnCurrentLeg || !startLineEqualsFinishLine(courseDTO)) && + (settings.getHelpLinesSettings().isVisible(HelpLineTypes.FINISHLINE) && showFinishLineBasedOnCurrentLeg) || + (settings.getHelpLinesSettings().isVisible(HelpLineTypes.COURSEGEOMETRY) && + (!startLineEqualsFinishLine(courseDTO) || showFinishLineBasedOnCurrentLeg)); + startLine = showOrRemoveOrUpdateLine(startLine, reallyShowStartLine, + startLineLeftPosition, startLineRightPosition, startLineInfoProvider, "#ffffff"); + // draw the finish line + final Position finishLineLeftPosition = numberOfFinishWaypointMarks == 0 ? null : courseDTO.getFinishMarkPositions().get(0); + final Position finishLineRightPosition = numberOfFinishWaypointMarks < 2 ? null : courseDTO.getFinishMarkPositions().get(1); + if (courseDTO.finishLineAngleFromPortToStarboardWhenApproachingLineToCombinedWind != null) { + finishLineAdvantageText.replace(0, finishLineAdvantageText.length(), " "+stringMessages.lineAngleToWindAndAdvantage( + NumberFormat.getFormat("0.0").format(courseDTO.finishLineLengthInMeters), + NumberFormat.getFormat("0.0").format(Math.abs(courseDTO.finishLineAngleFromPortToStarboardWhenApproachingLineToCombinedWind)), + courseDTO.finishLineAdvantageousSide.name().charAt(0)+courseDTO.finishLineAdvantageousSide.name().substring(1).toLowerCase(), + NumberFormat.getFormat("0.0").format(courseDTO.finishLineAdvantageInMeters))); + } else { + finishLineAdvantageText.delete(0, finishLineAdvantageText.length()); + } + finishLine = showOrRemoveOrUpdateLine(finishLine, reallyShowFinishLine, + finishLineLeftPosition, finishLineRightPosition, finishLineInfoProvider, "#000000"); + // the control point pairs for which we already decided whether or not + // to show a course middle line for; values tell whether to show the line and for which zero-based + // start waypoint index to do so; when for an equal control point pair multiple decisions with different + // outcome are made, a decision to show the line overrules the decision to not show it (OR-semantics) + final Map, Pair> keysAlreadyHandled = new HashMap<>(); + for (int zeroBasedIndexOfStartWaypoint = 0; zeroBasedIndexOfStartWaypoint key = getCourseMiddleLinesKey(courseDTO, zeroBasedIndexOfStartWaypoint); + boolean showCourseMiddleLine = keysAlreadyHandled.containsKey(key) && keysAlreadyHandled.get(key).getA() || + settings.getHelpLinesSettings().isVisible(HelpLineTypes.COURSEGEOMETRY) || + (settings.getHelpLinesSettings().isVisible(HelpLineTypes.COURSEMIDDLELINE) + && courseDTO.currentLegNumber > 0 + && courseDTO.currentLegNumber-1 == zeroBasedIndexOfStartWaypoint); + keysAlreadyHandled.put(key, new Pair<>(showCourseMiddleLine, zeroBasedIndexOfStartWaypoint)); + } + Set> keysToConsider = new HashSet<>(keysAlreadyHandled.keySet()); + keysToConsider.addAll(courseMiddleLines.keySet()); + for (final Set key : keysToConsider) { + final int zeroBasedIndexOfStartWaypoint = keysAlreadyHandled.containsKey(key) ? + keysAlreadyHandled.get(key).getB() : 0; // if not handled, the line will be removed, so the waypoint index doesn't matter + final Pair showLineAndZeroBasedIndexOfStartWaypoint = keysAlreadyHandled.get(key); + final boolean showCourseMiddleLine = showLineAndZeroBasedIndexOfStartWaypoint != null && showLineAndZeroBasedIndexOfStartWaypoint.getA(); + courseMiddleLines.put(key, showOrRemoveCourseMiddleLine(courseDTO, courseMiddleLines.get(key), zeroBasedIndexOfStartWaypoint, showCourseMiddleLine)); + } + } + } + + private boolean startLineEqualsFinishLine(CoursePositionsDTO courseDTO) { + final List waypoints; + return courseDTO != null && courseDTO.course != null && + (waypoints = courseDTO.course.waypoints) != null && + waypoints.get(0).controlPoint.equals(waypoints.get(waypoints.size()-1).controlPoint); + } + + /** + * Given a zero-based index into courseDTO's {@link RaceCourseDTO#waypoints waypoints list} that denotes the start + * waypoint of the leg in question, returns a key that can be used for the {@link #courseMiddleLines} map, consisting of a set + * that holds the two {@link ControlPointDTO}s representing the start and finish control point of that leg. + */ + private Set getCourseMiddleLinesKey(final CoursePositionsDTO courseDTO, + final int zeroBasedIndexOfStartWaypoint) { + ControlPointDTO startControlPoint = courseDTO.course.waypoints.get(zeroBasedIndexOfStartWaypoint).controlPoint; + ControlPointDTO endControlPoint = courseDTO.course.waypoints.get(zeroBasedIndexOfStartWaypoint+1).controlPoint; + final Set key = new HashSet<>(); + key.add(startControlPoint); + key.add(endControlPoint); + return key; + } + + /** + * @param showLine + * tells whether or not to show the line; if the lineToShowOrRemoveOrUpdate references a + * line but the line shall not be shown, the line is removed from the map; conversely, if the line is not + * yet shown but shall be, a new line is created, added to the map and returned. If the line is shown and + * shall continue to be shown, the line is returned after updating its vertex coordinates. + * @return null if the line is not shown; the polyline object representing the line being displayed + * otherwise + */ + private Polyline showOrRemoveCourseMiddleLine(final CoursePositionsDTO courseDTO, Polyline lineToShowOrRemoveOrUpdate, + final int zeroBasedIndexOfStartWaypoint, final boolean showLine) { + final Position position1DTO = courseDTO.waypointPositions.get(zeroBasedIndexOfStartWaypoint); + final Position position2DTO = courseDTO.waypointPositions.get(zeroBasedIndexOfStartWaypoint+1); + final LineInfoProvider lineInfoProvider = new LineInfoProvider() { + @Override + public String getLineInfo() { + final StringBuilder sb = new StringBuilder(); + sb.append(stringMessages.courseMiddleLine()); + sb.append('\n'); + sb.append(NumberFormat.getFormat("0").format( + Math.abs(position1DTO.getDistance(position2DTO).getMeters()))+stringMessages.metersUnit()); + if (lastCombinedWindTrackInfoDTO != null) { + final WindTrackInfoDTO windTrackAtLegMiddle = lastCombinedWindTrackInfoDTO.getCombinedWindOnLegMiddle(zeroBasedIndexOfStartWaypoint); + if (windTrackAtLegMiddle != null && windTrackAtLegMiddle.windFixes != null && !windTrackAtLegMiddle.windFixes.isEmpty()) { + WindDTO windAtLegMiddle = windTrackAtLegMiddle.windFixes.get(0); + final double legBearingDeg = position1DTO.getBearingGreatCircle(position2DTO).getDegrees(); + final String diff = NumberFormat.getFormat("0.0").format( + Math.min(Math.abs(windAtLegMiddle.dampenedTrueWindBearingDeg-legBearingDeg), + Math.abs(windAtLegMiddle.dampenedTrueWindFromDeg-legBearingDeg))); + sb.append(", "); + sb.append(stringMessages.degreesToWind(diff)); + } + } + return sb.toString(); + } + }; + return showOrRemoveOrUpdateLine(lineToShowOrRemoveOrUpdate, showLine, position1DTO, position2DTO, lineInfoProvider, "#2268a0"); + } + + private interface LineInfoProvider { + String getLineInfo(); + } + + /** + * @param showLine + * tells whether or not to show the line; if the lineToShowOrRemoveOrUpdate references a + * line but the line shall not be shown, the line is removed from the map; conversely, if the line is not + * yet shown but shall be, a new line is created, added to the map and returned. If the line is shown and + * shall continue to be shown, the line is returned after updating its vertex coordinates. + * @return null if the line is not shown; the polyline object representing the line being displayed + * otherwise + */ + private Polyline showOrRemoveOrUpdateLine(Polyline lineToShowOrRemoveOrUpdate, final boolean showLine, + final Position position1DTO, final Position position2DTO, final LineInfoProvider lineInfoProvider, String lineColorRGB) { + if (position1DTO != null && position2DTO != null) { + if (showLine) { + LatLng courseMiddleLinePoint1 = coordinateSystem.toLatLng(position1DTO); + LatLng courseMiddleLinePoint2 = coordinateSystem.toLatLng(position2DTO); + final MVCArray pointsAsArray; + if (lineToShowOrRemoveOrUpdate == null) { + PolylineOptions options = PolylineOptions.newInstance(); + options.setClickable(true); + options.setGeodesic(true); + options.setStrokeColor(lineColorRGB); + options.setStrokeWeight(1); + options.setStrokeOpacity(1.0); + pointsAsArray = MVCArray.newInstance(); + lineToShowOrRemoveOrUpdate = Polyline.newInstance(options); + lineToShowOrRemoveOrUpdate.setPath(pointsAsArray); + lineToShowOrRemoveOrUpdate.setMap(map); + Hoverline lineToShowOrRemoveOrUpdateHoverline = new Hoverline(lineToShowOrRemoveOrUpdate, options, this); + lineToShowOrRemoveOrUpdate.addMouseOverHandler(new MouseOverMapHandler() { + @Override + public void onEvent(MouseOverMapEvent event) { + map.setTitle(lineInfoProvider.getLineInfo()); + } + }); + lineToShowOrRemoveOrUpdateHoverline.addMouseOutMoveHandler(new MouseOutMapHandler() { + @Override + public void onEvent(MouseOutMapEvent event) { + map.setTitle(""); + } + }); + } else { + pointsAsArray = lineToShowOrRemoveOrUpdate.getPath(); + pointsAsArray.removeAt(1); + pointsAsArray.removeAt(0); + } + adjustInfoOverlayForVisibleLine(lineToShowOrRemoveOrUpdate, position1DTO, position2DTO, lineInfoProvider); + pointsAsArray.insertAt(0, courseMiddleLinePoint1); + pointsAsArray.insertAt(1, courseMiddleLinePoint2); + } else { + if (lineToShowOrRemoveOrUpdate != null) { + lineToShowOrRemoveOrUpdate.setMap(null); + adjustInfoOverlayForRemovedLine(lineToShowOrRemoveOrUpdate); + lineToShowOrRemoveOrUpdate = null; + } + } + } + return lineToShowOrRemoveOrUpdate; + } + + private void adjustInfoOverlayForRemovedLine(Polyline lineToShowOrRemoveOrUpdate) { + SmallTransparentInfoOverlay infoOverlay = infoOverlaysForLinesForCourseGeometry.remove(lineToShowOrRemoveOrUpdate); + if (infoOverlay != null) { + infoOverlay.removeFromMap(); + } + } + + private void adjustInfoOverlayForVisibleLine(Polyline lineToShowOrRemoveOrUpdate, final Position position1DTO, + final Position position2DTO, final LineInfoProvider lineInfoProvider) { + SmallTransparentInfoOverlay infoOverlay = infoOverlaysForLinesForCourseGeometry.get(lineToShowOrRemoveOrUpdate); + if (getSettings().getHelpLinesSettings().isVisible(HelpLineTypes.COURSEGEOMETRY)) { + if (infoOverlay == null) { + infoOverlay = new SmallTransparentInfoOverlay(map, RaceMapOverlaysZIndexes.INFO_OVERLAY_ZINDEX, lineInfoProvider.getLineInfo(), coordinateSystem); + infoOverlaysForLinesForCourseGeometry.put(lineToShowOrRemoveOrUpdate, infoOverlay); + infoOverlay.addToMap(); + } else { + infoOverlay.setInfoText(lineInfoProvider.getLineInfo()); + } + infoOverlay.setPosition(position1DTO.translateGreatCircle(position1DTO.getBearingGreatCircle(position2DTO), + position1DTO.getDistance(position2DTO).scale(0.5)), /* transition time */ -1); + infoOverlay.draw(); + } else { + if (infoOverlay != null) { + infoOverlay.removeFromMap(); + infoOverlaysForLinesForCourseGeometry.remove(lineToShowOrRemoveOrUpdate); + } + } + } + + /** + * If, according to {@link #lastRaceTimesInfo} and {@link #timer} the race is + * still in the pre-start phase, show a {@link SmallTransparentInfoOverlay} at the + * start line that shows the count down. + */ + private void updateCountdownCanvas(WaypointDTO startWaypoint) { + if (!settings.isShowSelectedCompetitorsInfo() || startWaypoint == null || Util.isEmpty(startWaypoint.controlPoint.getMarks()) + || lastRaceTimesInfo == null || lastRaceTimesInfo.startOfRace == null || timer.getTime().after(lastRaceTimesInfo.startOfRace)) { + if (countDownOverlay != null) { + countDownOverlay.removeFromMap(); + countDownOverlay = null; + } + } else { + long timeToStartInMs = lastRaceTimesInfo.startOfRace.getTime() - timer.getTime().getTime(); + String countDownText = timeToStartInMs > 1000 ? stringMessages.timeToStart(DateAndTimeFormatterUtil + .formatElapsedTime(timeToStartInMs)) : stringMessages.start(); + if (countDownOverlay == null) { + countDownOverlay = new SmallTransparentInfoOverlay(map, RaceMapOverlaysZIndexes.INFO_OVERLAY_ZINDEX, + countDownText, coordinateSystem); + countDownOverlay.addToMap(); + } else { + countDownOverlay.setInfoText(countDownText); + } + countDownOverlay.setPosition(startWaypoint.controlPoint.getMarks().iterator().next().position, /* transition time */ -1); + countDownOverlay.draw(); + } + } + + // Google scales coordinates so that the globe-tile has mercator-latitude [-pi, +pi], i.e. tile height of 2*pi + // mercator-latitude pi corresponds to geo-latitude of approx. 85.0998 (where Google cuts off the map visualization) + // official documentation: http://developers.google.com/maps/documentation/javascript/maptypes#TileCoordinates + private double getMercatorLatitude(double lat) { + // cutting-off for latitudes close to +-90 degrees is recommended (to avoid division by zero) + double sine = Math.max(-0.9999, Math.min(0.9999, Math.sin(Math.PI * lat / 180))); + return Math.log((1 + sine) / (1 - sine)) / 2; + } + + public int getZoomLevel(LatLngBounds bounds) { + int GLOBE_PXSIZE = 256; // a constant in Google's map projection + int MAX_ZOOM = 20; // maximum zoom-level that should be automatically selected + double LOG2 = Math.log(2.0); + double deltaLng = bounds.getNorthEast().getLongitude() - bounds.getSouthWest().getLongitude(); + double deltaLat = getMercatorLatitude(bounds.getNorthEast().getLatitude()) - getMercatorLatitude(bounds.getSouthWest().getLatitude()); + if ((deltaLng == 0) && (deltaLat == 0)) { + return MAX_ZOOM; + } + if (deltaLng < 0) { + deltaLng += 360; + } + int zoomLng = (int) Math.floor(Math.log(map.getDiv().getClientWidth() * 360 / deltaLng / GLOBE_PXSIZE) / LOG2); + int zoomLat = (int) Math.floor(Math.log(map.getDiv().getClientHeight() * 2 * Math.PI / deltaLat / GLOBE_PXSIZE) / LOG2); + return Math.min(Math.min(zoomLat, zoomLng), MAX_ZOOM); + } + + private void zoomMapToNewBounds(LatLngBounds newBounds) { + if (newBounds != null) { + LatLngBounds currentMapBounds; + if (map.getBounds() == null + || !BoundsUtil.contains((currentMapBounds = map.getBounds()), newBounds) + || graticuleAreaRatio(currentMapBounds, newBounds) > 10) { + // only change bounds if the new bounds don't fit into the current map zoom + Iterable oldZoomTypesToConsiderSettings = settings.getZoomSettings().getTypesToConsiderOnZoom(); + setAutoZoomInProgress(true); + autoZoomLatLngBounds = newBounds; + int newZoomLevel = getZoomLevel(autoZoomLatLngBounds); + if (newZoomLevel != map.getZoom()) { + // distinguish between zoom-in and zoom-out, because the sequence of panTo() and setZoom() + // appears different on the screen due to map-animations + // following sequences keep the selected boats allways visible: + // zoom-in : 1. panTo(), 2. setZoom() + // zoom-out: 1. setZoom(), 2. panTo() + autoZoomIn = newZoomLevel > map.getZoom(); + autoZoomOut = !autoZoomIn; + autoZoomLevel = newZoomLevel; + removeTransitions(); + if (autoZoomIn) { + map.panTo(autoZoomLatLngBounds.getCenter()); + } else { + map.setZoom(autoZoomLevel); + } + } else { + map.panTo(autoZoomLatLngBounds.getCenter()); + } + RaceMapZoomSettings restoredZoomSettings = new RaceMapZoomSettings(oldZoomTypesToConsiderSettings, settings.getZoomSettings().isZoomToSelectedCompetitors()); + settings = new RaceMapSettings(settings, restoredZoomSettings); + setAutoZoomInProgress(false); + } + } + } + + private double graticuleAreaRatio(LatLngBounds containing, LatLngBounds contained) { + assert BoundsUtil.contains(containing, contained); + double containingAreaRatio = getGraticuleArea(containing) / getGraticuleArea(contained); + return containingAreaRatio; + } + + /** + * A much simplified "area" calculation for a {@link Bounds} object, multiplying the differences in latitude and longitude degrees. + * The result therefore is in the order of magnitude of 60*60 square nautical miles. + */ + private double getGraticuleArea(LatLngBounds bounds) { + return ((BoundsUtil.isCrossesDateLine(bounds) ? bounds.getNorthEast().getLongitude()+360 : bounds.getNorthEast().getLongitude())-bounds.getSouthWest().getLongitude()) * + (bounds.getNorthEast().getLatitude() - bounds.getSouthWest().getLatitude()); + } + + private void setAutoZoomInProgress(boolean autoZoomInProgress) { + this.autoZoomInProgress = autoZoomInProgress; + } + + boolean isAutoZoomInProgress() { + return autoZoomInProgress; + } + + /** + * @param timeForPositionTransitionMillis use -1 to not animate the position transition, e.g., during map zoom or non-play + */ + private boolean updateBoatCanvasForCompetitor(CompetitorDTO competitorDTO, Date date, long timeForPositionTransitionMillis) { + boolean usedExistingCanvas = false; + GPSFixDTOWithSpeedWindTackAndLegType lastBoatFix = getBoatFix(competitorDTO, date); + if (lastBoatFix != null) { + BoatOverlay boatOverlay = boatOverlays.get(competitorDTO); + if (boatOverlay == null) { + boatOverlay = createBoatOverlay(RaceMapOverlaysZIndexes.BOATS_ZINDEX, competitorDTO, displayHighlighted(competitorDTO)); + boatOverlays.put(competitorDTO, boatOverlay); + boatOverlay.setSelected(displayHighlighted(competitorDTO)); + boatOverlay.setBoatFix(lastBoatFix, timeForPositionTransitionMillis); + boatOverlay.addToMap(); + } else { + usedExistingCanvas = true; + boatOverlay.setSelected(displayHighlighted(competitorDTO)); + boatOverlay.setBoatFix(lastBoatFix, timeForPositionTransitionMillis); + boatOverlay.draw(); + } + } + + return usedExistingCanvas; + } + + private boolean displayHighlighted(CompetitorDTO competitorDTO) { + return !settings.isShowOnlySelectedCompetitors() && competitorSelection.isSelected(competitorDTO); + } + + private class CourseMarkInfoWindowClickHandler implements ClickMapHandler { + private final MarkDTO markDTO; + private final CourseMarkOverlay courseMarkOverlay; + + public CourseMarkInfoWindowClickHandler(MarkDTO markDTO, CourseMarkOverlay courseMarkOverlay) { + this.markDTO = markDTO; + this.courseMarkOverlay = courseMarkOverlay; + } + + @Override + public void onEvent(ClickMapEvent event) { + LatLng latlng = courseMarkOverlay.getMarkLatLngPosition(); + showMarkInfoWindow(markDTO, latlng); + } + } + + private void registerCourseMarkInfoWindowClickHandler(final String markDTOName) { + final CourseMarkOverlay courseMarkOverlay = courseMarkOverlays.get(markDTOName); + courseMarkClickHandlers.put(markDTOName, + courseMarkOverlay.addClickHandler(new CourseMarkInfoWindowClickHandler(markDTOs.get(markDTOName), courseMarkOverlay))); + } + + public void registerAllCourseMarkInfoWindowClickHandlers() { + for (String markDTOName : markDTOs.keySet()) { + registerCourseMarkInfoWindowClickHandler(markDTOName); + } + } + + public void unregisterAllCourseMarkInfoWindowClickHandlers() { + Iterator> iterator = courseMarkClickHandlers.entrySet().iterator(); + while(iterator.hasNext()) { + Entry handler = iterator.next(); + handler.getValue().removeHandler(); + iterator.remove(); + } + } + + private CompetitorInfoOverlay createCompetitorInfoOverlay(int zIndex, final CompetitorDTO competitorDTO) { + GPSFixDTOWithSpeedWindTackAndLegType gpsFixDTO = getBoatFix(competitorDTO, timer.getTime()); + return new CompetitorInfoOverlay(map, zIndex, competitorSelection.getColor(competitorDTO, raceIdentifier), createInfoText(competitorDTO, gpsFixDTO), coordinateSystem); + } + + private String createInfoText(CompetitorDTO competitorDTO, GPSFixDTOWithSpeedWindTackAndLegType gpsFixDTO) { + StringBuilder infoText = new StringBuilder(); + infoText.append(competitorDTO.getSailID()).append("\n"); + infoText.append(NumberFormatterFactory.getDecimalFormat(1).format(gpsFixDTO.speedWithBearing.speedInKnots)) + .append(" ").append(stringMessages.knotsUnit()).append("\n"); + QuickRankDTO quickRankDTO = quickRanks.get(competitorDTO); + if (quickRankDTO != null) { + infoText.append(stringMessages.rank()).append(" : ").append(quickRanks.get(competitorDTO).rank); + } + return infoText.toString(); + } + + private BoatOverlay createBoatOverlay(int zIndex, final CompetitorDTO competitorDTO, boolean highlighted) { + final BoatOverlay boatCanvas = new BoatOverlay(map, zIndex, competitorDTO, competitorSelection.getColor(competitorDTO, raceIdentifier), coordinateSystem); + boatCanvas.setSelected(highlighted); + boatCanvas.addClickHandler(new ClickMapHandler() { + @Override + public void onEvent(ClickMapEvent event) { + if (lastInfoWindow != null) { + lastInfoWindow.close(); + } + GPSFixDTOWithSpeedWindTackAndLegType latestFixForCompetitor = getBoatFix(competitorDTO, timer.getTime()); + LatLng where = coordinateSystem.toLatLng(latestFixForCompetitor.position); + InfoWindowOptions options = InfoWindowOptions.newInstance(); + InfoWindow infoWindow = InfoWindow.newInstance(options); + infoWindow.setContent(getInfoWindowContent(competitorDTO, latestFixForCompetitor)); + infoWindow.setPosition(where); + lastInfoWindow = infoWindow; + infoWindow.open(map); + } + }); + + boatCanvas.addMouseOverHandler(new MouseOverMapHandler() { + @Override + public void onEvent(MouseOverMapEvent event) { + map.setTitle(competitorDTO.getSailID()); + } + }); + boatCanvas.addMouseOutMoveHandler(new MouseOutMapHandler() { + @Override + public void onEvent(MouseOutMapEvent event) { + map.setTitle(""); + } + }); + + return boatCanvas; + } + + protected WindSensorOverlay createWindSensorOverlay(int zIndex, final WindSource windSource, final WindTrackInfoDTO windTrackInfoDTO) { + final WindSensorOverlay windSensorOverlay = new WindSensorOverlay(map, zIndex, raceMapImageManager, stringMessages, coordinateSystem); + windSensorOverlay.setWindInfo(windTrackInfoDTO, windSource); + windSensorOverlay.addClickHandler(new ClickMapHandler() { + @Override + public void onEvent(ClickMapEvent event) { + showWindSensorInfoWindow(windSensorOverlay); + } + }); + return windSensorOverlay; + } + + private void showMarkInfoWindow(MarkDTO markDTO, LatLng position) { + if(lastInfoWindow != null) { + lastInfoWindow.close(); + } + InfoWindowOptions options = InfoWindowOptions.newInstance(); + InfoWindow infoWindow = InfoWindow.newInstance(options); + infoWindow.setContent(getInfoWindowContent(markDTO)); + infoWindow.setPosition(position); + lastInfoWindow = infoWindow; + infoWindow.open(map); + } + + private void showCompetitorInfoWindow(final CompetitorDTO competitorDTO, LatLng where) { + if(lastInfoWindow != null) { + lastInfoWindow.close(); + } + GPSFixDTOWithSpeedWindTackAndLegType latestFixForCompetitor = getBoatFix(competitorDTO, timer.getTime()); + // TODO find close fix where the mouse was; see BUG 470 + InfoWindowOptions options = InfoWindowOptions.newInstance(); + InfoWindow infoWindow = InfoWindow.newInstance(options); + infoWindow.setContent(getInfoWindowContent(competitorDTO, latestFixForCompetitor)); + infoWindow.setPosition(where); + lastInfoWindow = infoWindow; + infoWindow.open(map); + } + + private String formatPosition(double lat, double lng) { + NumberFormat numberFormat = NumberFormat.getFormat("0.00000"); + String result = numberFormat.format(lat) + " lat, " + numberFormat.format(lng) + " lng"; + return result; + } + + private void showWindSensorInfoWindow(final WindSensorOverlay windSensorOverlay) { + WindSource windSource = windSensorOverlay.getWindSource(); + WindTrackInfoDTO windTrackInfoDTO = windSensorOverlay.getWindTrackInfoDTO(); + WindDTO windDTO = windTrackInfoDTO.windFixes.get(0); + if(windDTO != null && windDTO.position != null) { + if(lastInfoWindow != null) { + lastInfoWindow.close(); + } + LatLng where = coordinateSystem.toLatLng(windDTO.position); + InfoWindowOptions options = InfoWindowOptions.newInstance(); + InfoWindow infoWindow = InfoWindow.newInstance(options); + infoWindow.setContent(getInfoWindowContent(windSource, windTrackInfoDTO)); + infoWindow.setPosition(where); + lastInfoWindow = infoWindow; + infoWindow.open(map); + } + } + + private Widget createInfoWindowLabelAndValue(String labelName, String value) { + FlowPanel flowPanel = new FlowPanel(); + Label label = new Label(labelName + ":"); + label.setWordWrap(false); + label.getElement().getStyle().setFloat(Style.Float.LEFT); + label.getElement().getStyle().setPadding(3, Style.Unit.PX); + label.getElement().getStyle().setFontWeight(Style.FontWeight.BOLD); + flowPanel.add(label); + + Label valueLabel = new Label(value); + valueLabel.setWordWrap(false); + valueLabel.getElement().getStyle().setFloat(Style.Float.LEFT); + valueLabel.getElement().getStyle().setPadding(3, Style.Unit.PX); + flowPanel.add(valueLabel); + + return flowPanel; + } + + private Widget getInfoWindowContent(MarkDTO markDTO) { + VerticalPanel vPanel = new VerticalPanel(); + vPanel.add(createInfoWindowLabelAndValue(stringMessages.mark(), markDTO.getName())); + vPanel.add(createInfoWindowLabelAndValue(stringMessages.position(), formatPosition(markDTO.position.getLatDeg(), markDTO.position.getLngDeg()))); + return vPanel; + } + + public Widget getInfoWindowContent(ManeuverDTO maneuver) { + SpeedWithBearingDTO before = maneuver.speedWithBearingBefore; + SpeedWithBearingDTO after = maneuver.speedWithBearingAfter; + VerticalPanel vPanel = new VerticalPanel(); + vPanel.add(createInfoWindowLabelAndValue(stringMessages.maneuverType(), + ManeuverTypeFormatter.format(maneuver.type, stringMessages))); + vPanel.add(createInfoWindowLabelAndValue(stringMessages.time(), + DateTimeFormat.getFormat(PredefinedFormat.TIME_FULL).format(maneuver.timepoint))); + vPanel.add(createInfoWindowLabelAndValue(stringMessages.directionChange(), + ((int) maneuver.directionChangeInDegrees) + " " + stringMessages.degreesShort() + " (" + + ((int) before.bearingInDegrees) + " " + stringMessages.degreesShort() + " -> " + + ((int) after.bearingInDegrees) + " " + stringMessages.degreesShort() + ")")); + vPanel.add(createInfoWindowLabelAndValue(stringMessages.speedChange(), + NumberFormat.getDecimalFormat().format(after.speedInKnots - before.speedInKnots) + " " + + stringMessages.knotsUnit() + " (" + + NumberFormat.getDecimalFormat().format(before.speedInKnots) + " " + stringMessages.knotsUnit() + + " -> " + NumberFormat.getDecimalFormat().format(after.speedInKnots) + " " + + stringMessages.knotsUnit() + ")")); + if (maneuver.maneuverLossInMeters != null) { + vPanel.add(createInfoWindowLabelAndValue(stringMessages.maneuverLoss(), NumberFormat.getFormat("0.0").format(maneuver.maneuverLossInMeters)+"m")); + } + return vPanel; + } + + private Widget getInfoWindowContent(WindSource windSource, WindTrackInfoDTO windTrackInfoDTO) { + WindDTO windDTO = windTrackInfoDTO.windFixes.get(0); + NumberFormat numberFormat = NumberFormat.getFormat("0.0"); + VerticalPanel vPanel = new VerticalPanel(); + vPanel.add(createInfoWindowLabelAndValue(stringMessages.windSource(), WindSourceTypeFormatter.format(windSource, stringMessages))); + vPanel.add(createInfoWindowLabelAndValue(stringMessages.wind(), Math.round(windDTO.dampenedTrueWindFromDeg) + " " + stringMessages.degreesShort())); + vPanel.add(createInfoWindowLabelAndValue(stringMessages.windSpeed(), numberFormat.format(windDTO.dampenedTrueWindSpeedInKnots))); + vPanel.add(createInfoWindowLabelAndValue(stringMessages.position(), formatPosition(windDTO.position.getLatDeg(), windDTO.position.getLngDeg()))); + return vPanel; + } + + private Widget getInfoWindowContent(CompetitorDTO competitorDTO, GPSFixDTOWithSpeedWindTackAndLegType lastFix) { + final VerticalPanel vPanel = new VerticalPanel(); + vPanel.add(createInfoWindowLabelAndValue(stringMessages.competitor(), competitorDTO.getName())); + vPanel.add(createInfoWindowLabelAndValue(stringMessages.sailNumber(), competitorDTO.getSailID())); + Integer rank = null; + if (quickRanks != null) { + QuickRankDTO quickRank = quickRanks.get(competitorDTO); + if (quickRank != null) { + rank = quickRank.rank; + } + } + if (rank != null) { + vPanel.add(createInfoWindowLabelAndValue(stringMessages.rank(), String.valueOf(rank))); + } + SpeedWithBearingDTO speedWithBearing = lastFix.speedWithBearing; + if (speedWithBearing == null) { + // TODO should we show the boat at all? + speedWithBearing = new SpeedWithBearingDTO(0, 0); + } + vPanel.add(createInfoWindowLabelAndValue(stringMessages.speed(), + NumberFormatterFactory.getDecimalFormat(1).format(speedWithBearing.speedInKnots) + " "+stringMessages.knotsUnit())); + vPanel.add(createInfoWindowLabelAndValue(stringMessages.bearing(), (int) speedWithBearing.bearingInDegrees + " "+stringMessages.degreesShort())); + if (lastFix.degreesBoatToTheWind != null) { + vPanel.add(createInfoWindowLabelAndValue(stringMessages.degreesBoatToTheWind(), + (int) Math.abs(lastFix.degreesBoatToTheWind) + " " + stringMessages.degreesShort())); + } + if (raceIdentifier != null) { + RegattaAndRaceIdentifier race = raceIdentifier; + if (race != null) { + Map from = new HashMap(); + from.put(competitorDTO, fixesAndTails.getFixes(competitorDTO).get(fixesAndTails.getFirstShownFix(competitorDTO)).timepoint); + Map to = new HashMap(); + to.put(competitorDTO, getBoatFix(competitorDTO, timer.getTime()).timepoint); + sailingService.getDouglasPoints(race, from, to, 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(); + } + if (!(timer.getPlayState() == PlayStates.Playing)) { + if (settings.isShowDouglasPeuckerPoints()) { + showMarkDouglasPeuckerPoints(result); + } + } + } + }); + sailingService.getManeuvers(race, from, to, + new AsyncCallback>>() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError("Error obtaining maneuvers: " + caught.getMessage(), true /*silentMode */); + } + + @Override + public void onSuccess(Map> result) { + lastManeuverResult = result; + if (maneuverMarkers != null) { + removeAllManeuverMarkers(); + } + if (!(timer.getPlayState() == PlayStates.Playing)) { + showManeuvers(result); + } + } + }); + + } + } + return vPanel; + } + + /** + * @return the {@link CompetitorSelectionProvider#getSelectedCompetitors()} if + * {@link RaceMapSettings#isShowOnlySelectedCompetitors() only selected competitors are to be shown}, the + * {@link CompetitorSelectionProvider#getFilteredCompetitors() filtered competitors} otherwise. In both cases, + * if we have {@link RaceCompetitorSet information about the competitors participating} in this map's race, + * the result set is reduced to those, no matter if other regatta participants would otherwise have been in + * the result set + */ + private Iterable getCompetitorsToShow() { + final Set result = new HashSet<>(); + 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); + } + } + } else { + for (final CompetitorDTO selectedCompetitor : selection) { + if (raceCompetitorIdsAsString == null || raceCompetitorIdsAsString.contains(selectedCompetitor.getIdAsString())) { + result.add(selectedCompetitor); + } + } + } + return result; + } + + protected void removeAllMarkDouglasPeuckerpoints() { + if (douglasMarkers != null) { + for (Marker marker : douglasMarkers) { + marker.setMap((MapWidget) null); + } + } + douglasMarkers = null; + } + + protected void removeAllManeuverMarkers() { + if (maneuverMarkers != null) { + for (Marker marker : maneuverMarkers) { + marker.setMap((MapWidget) null); + } + maneuverMarkers = null; + } + } + + private void showMarkDouglasPeuckerPoints(Map> gpsFixPointMapForCompetitors) { + douglasMarkers = new HashSet(); + if (map != null && gpsFixPointMapForCompetitors != null) { + Set keySet = gpsFixPointMapForCompetitors.keySet(); + Iterator iter = keySet.iterator(); + while (iter.hasNext()) { + CompetitorDTO competitorDTO = iter.next(); + List gpsFix = gpsFixPointMapForCompetitors.get(competitorDTO); + for (GPSFixDTOWithSpeedWindTackAndLegType fix : gpsFix) { + LatLng latLng = coordinateSystem.toLatLng(fix.position); + MarkerOptions options = MarkerOptions.newInstance(); + options.setTitle(fix.timepoint+": "+fix.position+", "+fix.speedWithBearing.toString()); + Marker marker = Marker.newInstance(options); + marker.setPosition(latLng); + douglasMarkers.add(marker); + marker.setMap(map); + } + } + } + } + + private void showManeuvers(Map> maneuvers) { + maneuverMarkers = new HashSet(); + if (map != null && maneuvers != null) { + Set keySet = maneuvers.keySet(); + Iterator iter = keySet.iterator(); + while (iter.hasNext()) { + CompetitorDTO competitorDTO = iter.next(); + List maneuversForCompetitor = maneuvers.get(competitorDTO); + for (ManeuverDTO maneuver : maneuversForCompetitor) { + if (settings.isShowManeuverType(maneuver.type)) { + createAndAddMarkerOfManeuver(maneuver); + } + } + } + } + } + + private void createAndAddMarkerOfManeuver(ManeuverDTO maneuver) { + LatLng latLng = coordinateSystem.toLatLng(maneuver.position); + Marker maneuverMarker = raceMapImageManager.maneuverIconsForTypeAndDirectionIndicatingColor + .get(new Pair(maneuver.type, ManeuverColor.getManeuverColor(maneuver))); + MarkerOptions options = MarkerOptions.newInstance(); + options.setIcon(maneuverMarker.getIcon_MarkerImage()); + Marker marker = Marker.newInstance(options); + marker.setPosition(latLng); + marker.setTitle(ManeuverTypeFormatter.format(maneuver.type, stringMessages)); + marker.addClickHandler(new ManeuverMarkerClickedListener(this, maneuver)); + maneuverMarkers.add(marker); + marker.setMap(map); + } + + /** + * @param date + * the point in time for which to determine the competitor's boat position; approximated by using the fix + * from {@link #fixes} whose time point comes closest to this date + * + * @return The GPS fix for the given competitor from {@link #fixes} that is closest to date, or + * null if no fix is available + */ + private GPSFixDTOWithSpeedWindTackAndLegType getBoatFix(CompetitorDTO competitorDTO, Date date) { + GPSFixDTOWithSpeedWindTackAndLegType result = null; + 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() { + @Override + public int compare(GPSFixDTOWithSpeedWindTackAndLegType o1, GPSFixDTOWithSpeedWindTackAndLegType o2) { + return o1.timepoint.compareTo(o2.timepoint); + } + }); + if (i<0) { + i = -i-1; // no perfect match; i is now the insertion point + // if the insertion point is at the end, use last fix + if (i >= competitorFixes.size()) { + result = competitorFixes.get(competitorFixes.size()-1); + } else if (i == 0) { + // if the insertion point is at the beginning, use first fix + result = competitorFixes.get(0); + } else { + // competitorFixes must have at least two elements, and i points neither to the end nor the beginning; + // get the fix from i and i+1 whose timepoint is closer to date + final GPSFixDTOWithSpeedWindTackAndLegType fixBefore = competitorFixes.get(i-1); + final GPSFixDTOWithSpeedWindTackAndLegType fixAfter = competitorFixes.get(i); + final GPSFixDTOWithSpeedWindTackAndLegType closer; + if (date.getTime() - fixBefore.timepoint.getTime() < fixAfter.timepoint.getTime() - date.getTime()) { + closer = fixBefore; + } else { + closer = fixAfter; + } + // 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(), + factorForBefore*fixBefore.position.getLngDeg() + factorForAfter*fixAfter.position.getLngDeg()); + final double betweenBearing; + if (fixBefore.speedWithBearing == null) { + if (fixAfter.speedWithBearing == null) { + betweenBearing = 0; + } else { + betweenBearing = fixAfter.speedWithBearing.bearingInDegrees; + } + } else if (fixAfter.speedWithBearing == null) { + betweenBearing = fixBefore.speedWithBearing.bearingInDegrees; + } else { + betweenBearing = new ScalableBearing(new DegreeBearingImpl(fixBefore.speedWithBearing.bearingInDegrees)). + multiply(factorForBefore).add(new ScalableBearing(new DegreeBearingImpl(fixAfter.speedWithBearing.bearingInDegrees)). + multiply(factorForAfter)).divide(1).getDegrees(); + } + SpeedWithBearingDTO betweenSpeed = new SpeedWithBearingDTO( + factorForBefore*(fixBefore.speedWithBearing==null?0:fixBefore.speedWithBearing.speedInKnots) + + factorForAfter*(fixAfter.speedWithBearing==null?0:fixAfter.speedWithBearing.speedInKnots), + betweenBearing); + result = new GPSFixDTOWithSpeedWindTackAndLegType(date, betweenPosition, betweenSpeed, closer.degreesBoatToTheWind, + closer.tack, closer.legType, fixBefore.extrapolated || fixAfter.extrapolated); + } + } else { + // perfect match + final GPSFixDTOWithSpeedWindTackAndLegType fixAfter = competitorFixes.get(i); + result = fixAfter; + } + } + return result; + } + + public RaceMapSettings getSettings() { + return settings; + } + + @Override + public void addedToSelection(CompetitorDTO competitor) { + if (settings.isShowOnlySelectedCompetitors()) { + if (Util.size(competitorSelection.getSelectedCompetitors()) == 1) { + // first competitors selected; remove all others from map + Iterator> i = boatOverlays.entrySet().iterator(); + while (i.hasNext()) { + Entry next = i.next(); + if (!next.getKey().equals(competitor)) { + CanvasOverlayV3 boatOverlay = next.getValue(); + boatOverlay.removeFromMap(); + fixesAndTails.removeTail(next.getKey()); + i.remove(); // only this way a ConcurrentModificationException while looping can be avoided + } + } + showCompetitorInfoOnMap(timer.getTime(), -1, competitorSelection.getSelectedFilteredCompetitors()); + } else { + // adding a single competitor; may need to re-load data, so refresh: + timeChanged(timer.getTime(), null); + } + } else { + // only change highlighting + BoatOverlay boatCanvas = boatOverlays.get(competitor); + if (boatCanvas != null) { + boatCanvas.setSelected(displayHighlighted(competitor)); + boatCanvas.draw(); + showCompetitorInfoOnMap(timer.getTime(), -1, competitorSelection.getSelectedFilteredCompetitors()); + } else { + // seems like an internal error not to find the lowlighted marker; but maybe the + // competitor was added late to the race; + // data for newly selected competitor supposedly missing; refresh + timeChanged(timer.getTime(), null); + } + } + // Now update tails for all competitors because selection change may also affect all unselected competitors + for (CompetitorDTO oneOfAllCompetitors : competitorSelection.getAllCompetitors()) { + Polyline tail = fixesAndTails.getTail(oneOfAllCompetitors); + if (tail != null) { + PolylineOptions newOptions = createTailStyle(oneOfAllCompetitors, displayHighlighted(oneOfAllCompetitors)); + tail.setOptions(newOptions); + } + } + // Trigger auto-zoom if needed + RaceMapZoomSettings zoomSettings = settings.getZoomSettings(); + if (!zoomSettings.containsZoomType(ZoomTypes.NONE) && zoomSettings.isZoomToSelectedCompetitors()) { + zoomMapToNewBounds(zoomSettings.getNewBounds(this)); + } + } + + @Override + public void removedFromSelection(CompetitorDTO competitor) { + if (isShowAnyHelperLines()) { + // helper lines depend on which competitor is visible, because the *visible* leader is used for + // deciding which helper lines to show: + timeChanged(timer.getTime(), null); + } else { + // try a more incremental update otherwise + if (settings.isShowOnlySelectedCompetitors()) { + // if selection is now empty, show all competitors + if (Util.isEmpty(competitorSelection.getSelectedCompetitors())) { + timeChanged(timer.getTime(), null); + } else { + // otherwise remove only deselected competitor's boat images and tail + BoatOverlay removedBoatOverlay = boatOverlays.remove(competitor); + if (removedBoatOverlay != null) { + removedBoatOverlay.removeFromMap(); + } + fixesAndTails.removeTail(competitor); + showCompetitorInfoOnMap(timer.getTime(), -1, competitorSelection.getSelectedFilteredCompetitors()); + } + } else { + // "lowlight" currently selected competitor + BoatOverlay boatCanvas = boatOverlays.get(competitor); + if (boatCanvas != null) { + boatCanvas.setSelected(displayHighlighted(competitor)); + boatCanvas.draw(); + } + showCompetitorInfoOnMap(timer.getTime(), -1, competitorSelection.getSelectedFilteredCompetitors()); + } + } + //Trigger auto-zoom if needed + RaceMapZoomSettings zoomSettings = settings.getZoomSettings(); + if (!zoomSettings.containsZoomType(ZoomTypes.NONE) && zoomSettings.isZoomToSelectedCompetitors()) { + zoomMapToNewBounds(zoomSettings.getNewBounds(this)); + } + } + + private boolean isShowAnyHelperLines() { + return settings.getHelpLinesSettings().isShowAnyHelperLines(); + } + + @Override + public String getLocalizedShortName() { + return raceMapLifecycle.getLocalizedShortName(); + } + + @Override + public Widget getEntryWidget() { + return this; + } + + @Override + public boolean hasSettings() { + return raceMapLifecycle.hasSettings(); + } + + @Override + public SettingsDialogComponent getSettingsDialogComponent() { + return new RaceMapSettingsDialogComponent(settings, stringMessages, this.isSimulationEnabled && this.hasPolar); + } + + @Override + public void updateSettings(RaceMapSettings newSettings) { + boolean maneuverTypeSelectionChanged = false; + boolean requiresRedraw = false; + boolean requiresUpdateCoordinateSystem = false; + + for (ManeuverType maneuverType : ManeuverType.values()) { + if (newSettings.isShowManeuverType(maneuverType) != settings.isShowManeuverType(maneuverType)) { + maneuverTypeSelectionChanged = true; + } + } + if (maneuverTypeSelectionChanged) { + if (!(timer.getPlayState() == PlayStates.Playing) && lastManeuverResult != null) { + removeAllManeuverMarkers(); + showManeuvers(lastManeuverResult); + } + } + if (newSettings.isShowDouglasPeuckerPoints() != settings.isShowDouglasPeuckerPoints()) { + if (!(timer.getPlayState() == PlayStates.Playing) && lastDouglasPeuckerResult != null && newSettings.isShowDouglasPeuckerPoints()) { + removeAllMarkDouglasPeuckerpoints(); + showMarkDouglasPeuckerPoints(lastDouglasPeuckerResult); + } else if (!newSettings.isShowDouglasPeuckerPoints()) { + removeAllMarkDouglasPeuckerpoints(); + } + } + if (newSettings.getTailLengthInMilliseconds() != settings.getTailLengthInMilliseconds()) { + requiresRedraw = true; + } + if (newSettings.getBuoyZoneRadius() != settings.getBuoyZoneRadius()) { + requiresRedraw = true; + } + if (newSettings.isShowOnlySelectedCompetitors() != settings.isShowOnlySelectedCompetitors()) { + requiresRedraw = true; + } + if (newSettings.isShowSelectedCompetitorsInfo() != settings.isShowSelectedCompetitorsInfo()) { + requiresRedraw = true; + } + if (!newSettings.getZoomSettings().equals(settings.getZoomSettings())) { + if (!newSettings.getZoomSettings().containsZoomType(ZoomTypes.NONE)) { + removeTransitions(); + zoomMapToNewBounds(newSettings.getZoomSettings().getNewBounds(this)); + } + } + if (!newSettings.getHelpLinesSettings().equals(settings.getHelpLinesSettings())) { + requiresRedraw = true; + } + if (newSettings.isShowWindStreamletOverlay() != settings.isShowWindStreamletOverlay()) { + streamletOverlay.setVisible(newSettings.isShowWindStreamletOverlay()); + } + if (newSettings.isShowWindStreamletColors() != settings.isShowWindStreamletColors()) { + streamletOverlay.setColors(newSettings.isShowWindStreamletColors()); + } + if (newSettings.isShowSimulationOverlay() != settings.isShowSimulationOverlay()) { + showSimulationOverlay(newSettings.isShowSimulationOverlay()); + } + if (newSettings.isWindUp() != settings.isWindUp()) { + requiresUpdateCoordinateSystem = true; + requiresRedraw = true; + } + this.settings = newSettings; + + if (requiresUpdateCoordinateSystem) { + updateCoordinateSystemFromSettings(); + } + if (requiresRedraw) { + redraw(); + } + } + + private void showSimulationOverlay(boolean visible) { + simulationOverlay.setVisible(visible); + if (visible) { + simulationOverlay.updateLeg(Math.max(lastLegNumber,1), true, -1 /* ensure ui-update */); + } + } + + public static class BoatsBoundsCalculator extends LatLngBoundsCalculatorForSelected { + @Override + public LatLngBounds calculateNewBounds(RaceMap forMap) { + LatLngBounds newBounds = null; + Iterable selectedCompetitors = forMap.competitorSelection.getSelectedCompetitors(); + Iterable competitors = new ArrayList(); + if (selectedCompetitors == null || !selectedCompetitors.iterator().hasNext()) { + competitors = forMap.getCompetitorsToShow(); + } else { + competitors = isZoomOnlyToSelectedCompetitors(forMap) ? selectedCompetitors : forMap.getCompetitorsToShow(); + } + for (CompetitorDTO competitor : competitors) { + try { + GPSFixDTOWithSpeedWindTackAndLegType competitorFix = forMap.getBoatFix(competitor, forMap.timer.getTime()); + Position competitorPosition = competitorFix != null ? competitorFix.position : null; + if (competitorPosition != null) { + if (newBounds == null) { + newBounds = BoundsUtil.getAsBounds(forMap.coordinateSystem.toLatLng(competitorPosition)); + } else { + newBounds = newBounds.extend(forMap.coordinateSystem.toLatLng(competitorPosition)); + } + } + } catch (IndexOutOfBoundsException e) { + // TODO can't this be predicted and the exception be avoided in the first place? + // Catch this in case the competitor has no GPS fixes at the current time (e.g. in race 'Finale 2' of STG) + } + } + return newBounds; + } + + } + + public static class TailsBoundsCalculator extends LatLngBoundsCalculatorForSelected { + @Override + public LatLngBounds calculateNewBounds(RaceMap racemap) { + LatLngBounds newBounds = null; + Iterable competitors = isZoomOnlyToSelectedCompetitors(racemap) ? racemap.competitorSelection.getSelectedCompetitors() : racemap.getCompetitorsToShow(); + for (CompetitorDTO competitor : competitors) { + Polyline tail = racemap.fixesAndTails.getTail(competitor); + LatLngBounds bounds = null; + // TODO: Find a replacement for missing Polyline function getBounds() from v2 + // see also http://stackoverflow.com/questions/3284808/getting-the-bounds-of-a-polyine-in-google-maps-api-v3; + // optionally, consider providing a bounds cache with two sorted sets that organize the LatLng objects for O(1) bounds calculation and logarithmic add, ideally O(1) remove + if (tail != null && tail.getPath().getLength() >= 1) { + bounds = BoundsUtil.getAsBounds(tail.getPath().get(0)); + for (int i = 1; i < tail.getPath().getLength(); i++) { + bounds = bounds.extend(tail.getPath().get(i)); + } + } + if (bounds != null) { + if (newBounds == null) { + newBounds = bounds; + } else { + newBounds = newBounds.union(bounds); + } + } + } + return newBounds; + } + } + + public static class CourseMarksBoundsCalculator implements LatLngBoundsCalculator { + @Override + public LatLngBounds calculateNewBounds(RaceMap forMap) { + LatLngBounds newBounds = null; + Iterable marksToZoom = forMap.markDTOs.values(); + if (marksToZoom != null) { + for (MarkDTO markDTO : marksToZoom) { + if (newBounds == null) { + newBounds = BoundsUtil.getAsBounds(forMap.coordinateSystem.toLatLng(markDTO.position)); + } else { + newBounds = newBounds.extend(forMap.coordinateSystem.toLatLng(markDTO.position)); + } + } + } + return newBounds; + } + } + + public static class WindSensorsBoundsCalculator implements LatLngBoundsCalculator { + @Override + public LatLngBounds calculateNewBounds(RaceMap forMap) { + LatLngBounds newBounds = null; + Collection marksToZoom = forMap.windSensorOverlays.values(); + if (marksToZoom != null) { + for (WindSensorOverlay windSensorOverlay : marksToZoom) { + if (BoundsUtil.getAsPosition(windSensorOverlay.getLatLngPosition()) != null) { + LatLngBounds bounds = BoundsUtil.getAsBounds(windSensorOverlay.getLatLngPosition()); + if (newBounds == null) { + newBounds = bounds; + } else { + newBounds = newBounds.extend(windSensorOverlay.getLatLngPosition()); + } + } + } + } + return newBounds; + } + } + + @Override + public void initializeData(boolean showMapControls, boolean showHeaderPanel) { + loadMapsAPIV3(showMapControls, showHeaderPanel); + } + + @Override + public boolean isDataInitialized() { + return isMapInitialized; + } + + @Override + public void onResize() { + if (map != null) { + map.triggerResize(); + zoomMapToNewBounds(settings.getZoomSettings().getNewBounds(RaceMap.this)); + } + // Adjust RaceMap headers to avoid overlapping based on the RaceMap width + boolean isCompactHeader = this.getOffsetWidth() <= 600; + getLeftHeaderPanel().setStyleName(COMPACT_HEADER_STYLE, isCompactHeader); + getRightHeaderPanel().setStyleName(COMPACT_HEADER_STYLE, isCompactHeader); + + // Adjust combined wind and true north indicator panel indent, based on the RaceMap height + if (topLeftControlsWrapperPanel.getParent() != null) { + this.adjustLeftControlsIndent(); + } + } + + private void adjustLeftControlsIndent() { + topLeftControlsWrapperPanel.getParent().setStyleName("TopLeftControlsWrapperPanelParentDiv"); + String leftControlsIndentStyle = getLeftControlsIndentStyle(); + if (leftControlsIndentStyle != null) { + topLeftControlsWrapperPanel.getParent().addStyleName(leftControlsIndentStyle); + } + } + + @Override + public void competitorsListChanged(Iterable competitors) { + timeChanged(timer.getTime(), null); + } + + @Override + public void filteredCompetitorsListChanged(Iterable filteredCompetitors) { + timeChanged(timer.getTime(), null); + } + + @Override + public void filterChanged(FilterSet> oldFilterSet, + FilterSet> newFilterSet) { + // nothing to do; if the list of filtered competitors has changed, a separate call to filteredCompetitorsListChanged will occur + } + + @Override + public PolylineOptions createTailStyle(CompetitorDTO competitor, boolean isHighlighted) { + PolylineOptions options = PolylineOptions.newInstance(); + options.setClickable(true); + options.setGeodesic(true); + options.setStrokeOpacity(1.0); + boolean noCompetitorSelected = Util.isEmpty(competitorSelection.getSelectedCompetitors()); + if (isHighlighted || noCompetitorSelected || getSettings().isShowOnlySelectedCompetitors()) { + options.setStrokeColor(competitorSelection.getColor(competitor, raceIdentifier).getAsHtml()); + } else { + options.setStrokeColor(LOWLIGHTED_TAIL_COLOR.getAsHtml()); + } + if (isHighlighted) { + options.setStrokeWeight(2); + } else { + options.setStrokeWeight(1); + } + options.setZindex(RaceMapOverlaysZIndexes.BOATTAILS_ZINDEX); + return options; + } + + @Override + public Polyline createTail(final CompetitorDTO competitor, List points) { + PolylineOptions options = createTailStyle(competitor, displayHighlighted(competitor)); + Polyline result = Polyline.newInstance(options); + MVCArray pointsAsArray = MVCArray.newInstance(points.toArray(new LatLng[0])); + result.setPath(pointsAsArray); + result.setMap(map); + Hoverline resultHoverline = new Hoverline(result, options, this); + final ClickMapHandler clickHandler = new ClickMapHandler() { + @Override + public void onEvent(ClickMapEvent event) { + showCompetitorInfoWindow(competitor, event.getMouseEvent().getLatLng()); + } + }; + result.addClickHandler(clickHandler); + resultHoverline.addClickHandler(clickHandler); + result.addMouseOverHandler(new MouseOverMapHandler() { + @Override + public void onEvent(MouseOverMapEvent event) { + map.setTitle(competitor.getSailID() + ", " + competitor.getName()); + } + }); + resultHoverline.addMouseOutMoveHandler(new MouseOutMapHandler() { + @Override + public void onEvent(MouseOutMapEvent event) { + map.setTitle(""); + } + }); + return result; + } + + @Override + public Integer getRank(CompetitorDTO competitor) { + final Integer result; + QuickRankDTO quickRank = quickRanks.get(competitor); + if (quickRank != null) { + result = quickRank.rank; + } else { + result = null; + } + return result; + } + + private Image createSAPLogo() { + ImageResource sapLogoResource = resources.sapLogoOverlay(); + Image sapLogo = new Image(sapLogoResource); + sapLogo.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + Window.open("https://www.sap.com/sponsorships", "_blank", null); + } + }); + sapLogo.setStyleName("raceBoard-Logo"); + return sapLogo; + } + + @Override + public String getDependentCssClassName() { + return "raceMap"; + } + + /** + * The default zoom bounds are defined by the boats + */ + private LatLngBounds getDefaultZoomBounds() { + return new BoatsBoundsCalculator().calculateNewBounds(RaceMap.this); + } + + private MapOptions getMapOptions(final boolean showMapControls, boolean windUp) { + MapOptions mapOptions = MapOptions.newInstance(); + mapOptions.setScrollWheel(true); + mapOptions.setMapTypeControl(showMapControls && !windUp); + mapOptions.setPanControl(showMapControls); + mapOptions.setZoomControl(showMapControls); + mapOptions.setScaleControl(true); + if (windUp) { + mapOptions.setMinZoom(8); + } else { + mapOptions.setMinZoom(0); + } + MapTypeStyle[] mapTypeStyles = new MapTypeStyle[4]; + + // hide all transit lines including ferry lines + mapTypeStyles[0] = GoogleMapStyleHelper.createHiddenStyle(MapTypeStyleFeatureType.TRANSIT); + // hide points of interest + mapTypeStyles[1] = GoogleMapStyleHelper.createHiddenStyle(MapTypeStyleFeatureType.POI); + // simplify road display + mapTypeStyles[2] = GoogleMapStyleHelper.createSimplifiedStyle(MapTypeStyleFeatureType.ROAD); + // set water color + mapTypeStyles[3] = GoogleMapStyleHelper.createColorStyle(MapTypeStyleFeatureType.WATER, WATER_COLOR); + + MapTypeControlOptions mapTypeControlOptions = MapTypeControlOptions.newInstance(); + mapTypeControlOptions.setPosition(ControlPosition.BOTTOM_RIGHT); + mapOptions.setMapTypeControlOptions(mapTypeControlOptions); + + mapOptions.setMapTypeStyles(mapTypeStyles); + // no need to try to position the scale control; it always ends up at the right bottom corner + mapOptions.setStreetViewControl(false); + if (showMapControls) { + ZoomControlOptions zoomControlOptions = ZoomControlOptions.newInstance(); + zoomControlOptions.setPosition(ControlPosition.RIGHT_TOP); + mapOptions.setZoomControlOptions(zoomControlOptions); + PanControlOptions panControlOptions = PanControlOptions.newInstance(); + panControlOptions.setPosition(ControlPosition.RIGHT_TOP); + mapOptions.setPanControlOptions(panControlOptions); + } + return mapOptions; + } + + /** + * @return CSS style name to adjust the indent of left controls (combined wind and true north indicator). + */ + protected String getLeftControlsIndentStyle() { + return null; + } + + public Map getCourseMarkOverlays() { + return courseMarkOverlays; + } + + public CoordinateSystem getCoordinateSystem() { + return coordinateSystem; + } + + public void hideAllHelplines() { + if (startLine != null) { + startLine.setVisible(false); + } + if (finishLine != null) { + finishLine.setVisible(false); + } + if (advantageLine != null) { + advantageLine.setVisible(false); + } + if (windwardStartLineMarkToFirstMarkLine != null && leewardStartLineMarkToFirstMarkLine != null) { + windwardStartLineMarkToFirstMarkLine.setVisible(false); + leewardStartLineMarkToFirstMarkLine.setVisible(false); + } + for (Polyline courseMiddleline : courseMiddleLines.values()) { + if (courseMiddleline != null) { + courseMiddleline.setVisible(false); + } + } + for (Polygon courseSideline : courseSidelines.values()) { + if (courseSideline != null) { + courseSideline.setVisible(false); + } + } + } + + public void showAllHelplinesToShow() { + if (settings.getHelpLinesSettings().isShowAnyHelperLines()) { + if (startLine != null && settings.getHelpLinesSettings().isVisible(HelpLineTypes.STARTLINE)) + startLine.setVisible(true); + if (finishLine != null && settings.getHelpLinesSettings().isVisible(HelpLineTypes.FINISHLINE)) + finishLine.setVisible(true); + if (advantageLine != null && settings.getHelpLinesSettings().isVisible(HelpLineTypes.ADVANTAGELINE)) + advantageLine.setVisible(true); + if (windwardStartLineMarkToFirstMarkLine != null && leewardStartLineMarkToFirstMarkLine != null && + settings.getHelpLinesSettings().isVisible(HelpLineTypes.STARTLINETOFIRSTMARKTRIANGLE)) { + windwardStartLineMarkToFirstMarkLine.setVisible(true); + leewardStartLineMarkToFirstMarkLine.setVisible(true); + } + if (settings.getHelpLinesSettings().isVisible(HelpLineTypes.COURSEMIDDLELINE)) { + for (Polyline courseMiddleline : courseMiddleLines.values()) { + courseMiddleline.setVisible(true); + } + } + if (settings.getHelpLinesSettings().isVisible(HelpLineTypes.COURSEGEOMETRY)) { + for (Polygon courseSideline : courseSidelines.values()) { + courseSideline.setVisible(true); + } + } + } + } + + void setLastInfoWindow(InfoWindow infoWindow) { + lastInfoWindow = infoWindow; + } + + InfoWindow getLastInfoWindow() { + return lastInfoWindow; + } + + public void addCompetitorsForRaceDefinedListener(CompetitorsForRaceDefinedListener listener) { + raceCompetitorSet.addCompetitorsForRaceDefinedListener(listener); + } + + public void removeCompetitorsForRaceDefinedListener(CompetitorsForRaceDefinedListener listener) { + raceCompetitorSet.removeCompetitorsForRaceDefinedListener(listener); + } + + @Override + public String getId() { + return raceMapLifecycle.getComponentId(); + } + + public RaceMapLifecycle getLifecycle() { + return raceMapLifecycle; + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/AnchorDataMiningSettingsControl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/AnchorDataMiningSettingsControl.java index 3d3d19a73d2..acad39806dd 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/AnchorDataMiningSettingsControl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/AnchorDataMiningSettingsControl.java @@ -1,140 +1,140 @@ -package com.sap.sailing.gwt.ui.datamining; - -import java.util.Collection; -import java.util.HashMap; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.Map.Entry; - -import com.google.gwt.core.client.GWT; -import com.google.gwt.event.dom.client.ClickEvent; -import com.google.gwt.event.dom.client.ClickHandler; -import com.google.gwt.user.client.ui.AbstractImagePrototype; -import com.google.gwt.user.client.ui.Anchor; -import com.google.gwt.user.client.ui.Widget; -import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sse.common.settings.Settings; -import com.sap.sse.gwt.client.shared.components.AbstractComponent; -import com.sap.sse.gwt.client.shared.components.Component; -import com.sap.sse.gwt.client.shared.components.ComponentResources; -import com.sap.sse.gwt.client.shared.components.CompositeSettings; -import com.sap.sse.gwt.client.shared.components.CompositeTabbedSettingsDialogComponent; -import com.sap.sse.gwt.client.shared.components.SettingsDialog; -import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; - -public class AnchorDataMiningSettingsControl extends AbstractComponent implements DataMiningSettingsControl { - public static final ComponentResources resources = GWT.create(ComponentResources.class); - - private final StringMessages stringMessages; - private final Collection> components; - private final Anchor anchor; - - public AnchorDataMiningSettingsControl(Component parent, ComponentContext context, - final StringMessages stringMessages) { - super(parent, context); - this.stringMessages = stringMessages; - components = new LinkedHashSet<>(); - - anchor = new Anchor(AbstractImagePrototype.create(resources.darkSettingsIcon()).getSafeHtml()); - anchor.addStyleName("settingsAnchor"); - anchor.setTitle(stringMessages.dataMiningSettings()); - anchor.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - new SettingsDialog<>(AnchorDataMiningSettingsControl.this, stringMessages).show(); - } - }); - } - - @Override - public void addSettingsComponent(Component component) { - components.add(component); - } - - @Override - public void removeSettingsComponent(Component component) { - components.remove(component); - } - - @Override - public String getLocalizedShortName() { - return stringMessages.dataMiningSettings(); - } - - @Override - public Widget getEntryWidget() { - return anchor; - } - - @Override - public boolean isVisible() { - return anchor.isVisible(); - } - - @Override - public void setVisible(boolean visibility) { - anchor.setVisible(visibility); - } - - @Override - public boolean hasSettings() { - for (Component component : components) { - if (component.hasSettings()) { - return true; - } - } - return false; - } - - @Override - public SettingsDialogComponent getSettingsDialogComponent() { - return new CompositeTabbedSettingsDialogComponent(components); - } - - @Override - public void updateSettings(CompositeSettings newSettings) { - for (Entry componentAndSettings : newSettings.getSettingsPerComponentId().entrySet()) { - updateSettings(componentAndSettings); - } - } - - @Override - public CompositeSettings getSettings() { - Map settings = new HashMap<>(); - for (Component component : components) { - settings.put(component.getId(), component.hasSettings() ? component.getSettings() : null); - } - return new CompositeSettings(settings); - } - - private void updateSettings(Entry componentIdAndSettings) { - // we assume that the component to which the ID resolves matches with the settings type provided - @SuppressWarnings("unchecked") - Component component = (Component) findComponentById(componentIdAndSettings.getKey()); - if (component != null) { - final S settings = componentIdAndSettings.getValue(); - component.updateSettings(settings); - } - } - - private Component findComponentById(String componentId) { - for (Component component : components) { - if (component.getId().equals(componentId)) { - return component; - } - } - return null; - } - - @Override - public String getDependentCssClassName() { - return "AnchorDataMiningSettingsControl"; - } - - @Override - public String getId() { - return "AnchorDataMiningSettingsControl"; - } - -} +package com.sap.sailing.gwt.ui.datamining; + +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Map.Entry; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.user.client.ui.AbstractImagePrototype; +import com.google.gwt.user.client.ui.Anchor; +import com.google.gwt.user.client.ui.Widget; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sse.common.settings.Settings; +import com.sap.sse.gwt.client.shared.components.AbstractComponent; +import com.sap.sse.gwt.client.shared.components.Component; +import com.sap.sse.gwt.client.shared.components.ComponentResources; +import com.sap.sse.gwt.client.shared.components.CompositeSettings; +import com.sap.sse.gwt.client.shared.components.CompositeTabbedSettingsDialogComponent; +import com.sap.sse.gwt.client.shared.components.SettingsDialog; +import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; + +public class AnchorDataMiningSettingsControl extends AbstractComponent implements DataMiningSettingsControl { + public static final ComponentResources resources = GWT.create(ComponentResources.class); + + private final StringMessages stringMessages; + private final Collection> components; + private final Anchor anchor; + + public AnchorDataMiningSettingsControl(Component parent, ComponentContext context, + final StringMessages stringMessages) { + super(parent, context); + this.stringMessages = stringMessages; + components = new LinkedHashSet<>(); + + anchor = new Anchor(AbstractImagePrototype.create(resources.darkSettingsIcon()).getSafeHtml()); + anchor.addStyleName("settingsAnchor"); + anchor.setTitle(stringMessages.dataMiningSettings()); + anchor.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + new SettingsDialog<>(AnchorDataMiningSettingsControl.this, stringMessages).show(); + } + }); + } + + @Override + public void addSettingsComponent(Component component) { + components.add(component); + } + + @Override + public void removeSettingsComponent(Component component) { + components.remove(component); + } + + @Override + public String getLocalizedShortName() { + return stringMessages.dataMiningSettings(); + } + + @Override + public Widget getEntryWidget() { + return anchor; + } + + @Override + public boolean isVisible() { + return anchor.isVisible(); + } + + @Override + public void setVisible(boolean visibility) { + anchor.setVisible(visibility); + } + + @Override + public boolean hasSettings() { + for (Component component : components) { + if (component.hasSettings()) { + return true; + } + } + return false; + } + + @Override + public SettingsDialogComponent getSettingsDialogComponent() { + return new CompositeTabbedSettingsDialogComponent(components); + } + + @Override + public void updateSettings(CompositeSettings newSettings) { + for (Entry componentAndSettings : newSettings.getSettingsPerComponentId().entrySet()) { + updateSettings(componentAndSettings); + } + } + + @Override + public CompositeSettings getSettings() { + Map settings = new HashMap<>(); + for (Component component : components) { + settings.put(component.getId(), component.hasSettings() ? component.getSettings() : null); + } + return new CompositeSettings(settings); + } + + private void updateSettings(Entry componentIdAndSettings) { + // we assume that the component to which the ID resolves matches with the settings type provided + @SuppressWarnings("unchecked") + Component component = (Component) findComponentById(componentIdAndSettings.getKey()); + if (component != null) { + final S settings = componentIdAndSettings.getValue(); + component.updateSettings(settings); + } + } + + private Component findComponentById(String componentId) { + for (Component component : components) { + if (component.getId().equals(componentId)) { + return component; + } + } + return null; + } + + @Override + public String getDependentCssClassName() { + return "AnchorDataMiningSettingsControl"; + } + + @Override + public String getId() { + return "AnchorDataMiningSettingsControl"; + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/developer/PredefinedQueryRunner.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/developer/PredefinedQueryRunner.java index 16e9f1b94f2..2cf724e2ff0 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/developer/PredefinedQueryRunner.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/developer/PredefinedQueryRunner.java @@ -28,7 +28,7 @@ import com.sap.sse.gwt.client.ErrorReporter; import com.sap.sse.gwt.client.controls.busyindicator.SimpleBusyIndicator; import com.sap.sse.gwt.client.shared.components.Component; import com.sap.sse.gwt.client.shared.components.ComponentWithoutSettings; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; public class PredefinedQueryRunner extends ComponentWithoutSettings { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/developer/QueryDefinitionViewer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/developer/QueryDefinitionViewer.java index 0ea267e6c21..4a9fd3d562b 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/developer/QueryDefinitionViewer.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/developer/QueryDefinitionViewer.java @@ -19,7 +19,7 @@ import com.sap.sailing.gwt.ui.datamining.developer.QueryDefinitionParser.TypeToC import com.sap.sse.datamining.shared.dto.StatisticQueryDefinitionDTO; import com.sap.sse.gwt.client.shared.components.Component; import com.sap.sse.gwt.client.shared.components.ComponentWithoutSettings; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; public class QueryDefinitionViewer extends ComponentWithoutSettings implements QueryDefinitionChangedListener { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/execution/SimpleQueryRunner.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/execution/SimpleQueryRunner.java index 32f3faa86c9..a47a729cb06 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/execution/SimpleQueryRunner.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/execution/SimpleQueryRunner.java @@ -23,7 +23,7 @@ import com.sap.sse.gwt.client.ErrorReporter; import com.sap.sse.gwt.client.shared.components.AbstractComponent; import com.sap.sse.gwt.client.shared.components.Component; import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; public class SimpleQueryRunner extends AbstractComponent implements QueryRunner { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/AbstractNumericResultsPresenter.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/AbstractNumericResultsPresenter.java index c65ee5d433d..807436e9ffb 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/AbstractNumericResultsPresenter.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/AbstractNumericResultsPresenter.java @@ -24,7 +24,7 @@ import com.sap.sse.common.settings.Settings; import com.sap.sse.datamining.shared.GroupKey; import com.sap.sse.datamining.shared.impl.dto.QueryResultDTO; import com.sap.sse.gwt.client.shared.components.Component; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; public abstract class AbstractNumericResultsPresenter extends AbstractResultsPresenter { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/AbstractResultsPresenter.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/AbstractResultsPresenter.java index 2bdf8c2e881..94a806f5c7a 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/AbstractResultsPresenter.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/AbstractResultsPresenter.java @@ -17,7 +17,7 @@ import com.sap.sse.datamining.shared.GroupKey; import com.sap.sse.datamining.shared.impl.dto.QueryResultDTO; import com.sap.sse.gwt.client.shared.components.AbstractComponent; import com.sap.sse.gwt.client.shared.components.Component; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; public abstract class AbstractResultsPresenter extends AbstractComponent implements ResultsPresenterWithControls { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/MultiResultsPresenter.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/MultiResultsPresenter.java index e0e47ce505b..1a64ffa70b8 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/MultiResultsPresenter.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/MultiResultsPresenter.java @@ -1,218 +1,218 @@ -package com.sap.sailing.gwt.ui.datamining.presentation; - -import java.util.ArrayList; -import java.util.List; - -import com.google.gwt.core.client.Scheduler; -import com.google.gwt.core.client.Scheduler.ScheduledCommand; -import com.google.gwt.event.logical.shared.ValueChangeEvent; -import com.google.gwt.event.logical.shared.ValueChangeHandler; -import com.google.gwt.user.client.ui.DeckLayoutPanel; -import com.google.gwt.user.client.ui.HorizontalPanel; -import com.google.gwt.user.client.ui.Label; -import com.google.gwt.user.client.ui.ValueListBox; -import com.google.gwt.user.client.ui.Widget; -import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sailing.gwt.ui.client.shared.controls.AbstractObjectRenderer; -import com.sap.sailing.gwt.ui.datamining.ResultsPresenter; -import com.sap.sse.common.settings.Settings; -import com.sap.sse.datamining.shared.impl.dto.QueryResultDTO; -import com.sap.sse.gwt.client.shared.components.AbstractComponent; -import com.sap.sse.gwt.client.shared.components.Component; -import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; - -public class MultiResultsPresenter extends AbstractComponent implements ResultsPresenter { - - private final StringMessages stringMessages; - - private final DeckLayoutPanel presenterPanel; - private final HorizontalPanel controlsPanel; +package com.sap.sailing.gwt.ui.datamining.presentation; + +import java.util.ArrayList; +import java.util.List; + +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.google.gwt.event.logical.shared.ValueChangeEvent; +import com.google.gwt.event.logical.shared.ValueChangeHandler; +import com.google.gwt.user.client.ui.DeckLayoutPanel; +import com.google.gwt.user.client.ui.HorizontalPanel; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.ValueListBox; +import com.google.gwt.user.client.ui.Widget; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.client.shared.controls.AbstractObjectRenderer; +import com.sap.sailing.gwt.ui.datamining.ResultsPresenter; +import com.sap.sse.common.settings.Settings; +import com.sap.sse.datamining.shared.impl.dto.QueryResultDTO; +import com.sap.sse.gwt.client.shared.components.AbstractComponent; +import com.sap.sse.gwt.client.shared.components.Component; +import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; + +public class MultiResultsPresenter extends AbstractComponent implements ResultsPresenter { + + private final StringMessages stringMessages; + + private final DeckLayoutPanel presenterPanel; + private final HorizontalPanel controlsPanel; private final ValueListBox> presentersListBox; private AbstractResultsPresenter currentPresenter; - + private List> availablePresenters; - - public MultiResultsPresenter(Component parent, ComponentContext context, StringMessages stringMessages) { - super(parent, context); - this.stringMessages = stringMessages; + + public MultiResultsPresenter(Component parent, ComponentContext context, StringMessages stringMessages) { + super(parent, context); + this.stringMessages = stringMessages; availablePresenters = new ArrayList<>(); availablePresenters.add(new ColumnChartDescriptor()); availablePresenters.add(new PlainDescriptor()); - - presenterPanel = new DeckLayoutPanel(); - - controlsPanel = new HorizontalPanel(); - controlsPanel.setSpacing(5); - controlsPanel.add(new Label(stringMessages.choosePresentation() + ":")); + + presenterPanel = new DeckLayoutPanel(); + + controlsPanel = new HorizontalPanel(); + controlsPanel.setSpacing(5); + controlsPanel.add(new Label(stringMessages.choosePresentation() + ":")); presentersListBox = new ValueListBox<>(new AbstractObjectRenderer>() { - @Override + @Override protected String convertObjectToString(PresenterDescriptor nonNullObject) { - return nonNullObject.getName(); - } - }); + return nonNullObject.getName(); + } + }); presentersListBox.addValueChangeHandler(new ValueChangeHandler>() { - @Override + @Override public void onValueChange(ValueChangeEvent> event) { - setCurrentPresenter(event.getValue().getPresenter()); - } - }); - controlsPanel.add(presentersListBox); + setCurrentPresenter(event.getValue().getPresenter()); + } + }); + controlsPanel.add(presentersListBox); presentersListBox.setValue(availablePresenters.get(0), false); presentersListBox.setAcceptableValues(availablePresenters); - + setCurrentPresenter(availablePresenters.get(0).getPresenter()); - } - + } + private void setCurrentPresenter(AbstractResultsPresenter presenter) { - controlsPanel.removeFromParent(); - presenter.addControl(controlsPanel); - + controlsPanel.removeFromParent(); + presenter.addControl(controlsPanel); + if (presenter instanceof AbstractNumericResultsPresenter && currentPresenter instanceof AbstractNumericResultsPresenter) { String dataKey = ((AbstractNumericResultsPresenter) currentPresenter).getSelectedDataKey(); ((AbstractNumericResultsPresenter) presenter).setSelectedDataKey(dataKey); } - currentPresenter = presenter; - presenterPanel.setWidget(currentPresenter.getEntryWidget()); - Scheduler.get().scheduleDeferred(new ScheduledCommand() { - @Override - public void execute() { - presenterPanel.onResize(); - } - }); - } - - @Override - public QueryResultDTO getCurrentResult() { - return currentPresenter.getCurrentResult(); - } - - @Override - public void showResult(QueryResultDTO result) { + currentPresenter = presenter; + presenterPanel.setWidget(currentPresenter.getEntryWidget()); + Scheduler.get().scheduleDeferred(new ScheduledCommand() { + @Override + public void execute() { + presenterPanel.onResize(); + } + }); + } + + @Override + public QueryResultDTO getCurrentResult() { + return currentPresenter.getCurrentResult(); + } + + @Override + public void showResult(QueryResultDTO result) { for (PresenterDescriptor descriptor : availablePresenters) { - descriptor.getPresenter().showResult(result); - } - } - - @Override - public void showError(String error) { + descriptor.getPresenter().showResult(result); + } + } + + @Override + public void showError(String error) { for (PresenterDescriptor descriptor : availablePresenters) { - descriptor.getPresenter().showError(error); - } - } - - @Override - public void showError(String mainError, Iterable detailedErrors) { + descriptor.getPresenter().showError(error); + } + } + + @Override + public void showError(String mainError, Iterable detailedErrors) { for (PresenterDescriptor descriptor : availablePresenters) { - descriptor.getPresenter().showError(mainError, detailedErrors); - } - } - - @Override - public void showBusyIndicator() { + descriptor.getPresenter().showError(mainError, detailedErrors); + } + } + + @Override + public void showBusyIndicator() { for (PresenterDescriptor descriptor : availablePresenters) { - descriptor.getPresenter().showBusyIndicator(); - } - } - - @Override - public String getLocalizedShortName() { - return stringMessages.multiResultsPresenter(); - } - - @Override - public Widget getEntryWidget() { - return presenterPanel; - } - - @Override - public boolean isVisible() { - return presenterPanel.isVisible(); - } - - @Override - public void setVisible(boolean visibility) { - presenterPanel.setVisible(visibility); - } - - @Override - public boolean hasSettings() { - return false; - } - - @Override - public SettingsDialogComponent getSettingsDialogComponent() { - return null; - } - - @Override - public void updateSettings(Settings newSettings) { - // no-op - } - - @Override - public Settings getSettings() { - return null; - } - - @Override - public String getDependentCssClassName() { - return "multiResultsPresenter"; - } - + descriptor.getPresenter().showBusyIndicator(); + } + } + + @Override + public String getLocalizedShortName() { + return stringMessages.multiResultsPresenter(); + } + + @Override + public Widget getEntryWidget() { + return presenterPanel; + } + + @Override + public boolean isVisible() { + return presenterPanel.isVisible(); + } + + @Override + public void setVisible(boolean visibility) { + presenterPanel.setVisible(visibility); + } + + @Override + public boolean hasSettings() { + return false; + } + + @Override + public SettingsDialogComponent getSettingsDialogComponent() { + return null; + } + + @Override + public void updateSettings(Settings newSettings) { + // no-op + } + + @Override + public Settings getSettings() { + return null; + } + + @Override + public String getDependentCssClassName() { + return "multiResultsPresenter"; + } + private interface PresenterDescriptor { - - public String getName(); - + + public String getName(); + public AbstractResultsPresenter getPresenter(); - - } - + + } + private class PlainDescriptor implements PresenterDescriptor { - - private final AbstractResultsPresenter presenter; - - public PlainDescriptor() { - presenter = new PlainResultsPresenter(MultiResultsPresenter.this, getComponentContext(), stringMessages); - } - - @Override - public String getName() { - return stringMessages.plainText(); - } - - @Override - public AbstractResultsPresenter getPresenter() { - return presenter; - } - - } - + + private final AbstractResultsPresenter presenter; + + public PlainDescriptor() { + presenter = new PlainResultsPresenter(MultiResultsPresenter.this, getComponentContext(), stringMessages); + } + + @Override + public String getName() { + return stringMessages.plainText(); + } + + @Override + public AbstractResultsPresenter getPresenter() { + return presenter; + } + + } + private class ColumnChartDescriptor implements PresenterDescriptor { - - private final ResultsChart presenter; - - public ColumnChartDescriptor() { - presenter = new ResultsChart(MultiResultsPresenter.this, getComponentContext(), stringMessages); - } - - @Override - public String getName() { - return stringMessages.columnChart(); - } - - @Override - public AbstractResultsPresenter getPresenter() { - return presenter; - } - - } - - @Override - public String getId() { - return "MultiResultsPresenter"; - } - -} + + private final ResultsChart presenter; + + public ColumnChartDescriptor() { + presenter = new ResultsChart(MultiResultsPresenter.this, getComponentContext(), stringMessages); + } + + @Override + public String getName() { + return stringMessages.columnChart(); + } + + @Override + public AbstractResultsPresenter getPresenter() { + return presenter; + } + + } + + @Override + public String getId() { + return "MultiResultsPresenter"; + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/PlainFilterSelectionPresenter.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/PlainFilterSelectionPresenter.java index 45e44bbc473..456d18531af 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/PlainFilterSelectionPresenter.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/PlainFilterSelectionPresenter.java @@ -29,7 +29,7 @@ import com.sap.sse.datamining.shared.impl.dto.FunctionDTO; import com.sap.sse.gwt.client.shared.components.AbstractComponent; import com.sap.sse.gwt.client.shared.components.Component; import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; public class PlainFilterSelectionPresenter extends AbstractComponent implements FilterSelectionPresenter, FilterSelectionChangedListener, DataRetrieverChainDefinitionChangedListener { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/PlainResultsPresenter.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/PlainResultsPresenter.java index 47c2767cb6f..cec553a26fd 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/PlainResultsPresenter.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/PlainResultsPresenter.java @@ -19,7 +19,7 @@ import com.sap.sse.datamining.shared.GroupKey; import com.sap.sse.datamining.shared.impl.dto.QueryResultDTO; import com.sap.sse.gwt.client.shared.components.Component; import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; public class PlainResultsPresenter extends AbstractNumericResultsPresenter { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/ResultsChart.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/ResultsChart.java index 644c4cf04f6..f0a7e6bad2b 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/ResultsChart.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/ResultsChart.java @@ -40,7 +40,7 @@ import com.sap.sse.datamining.shared.impl.CompoundGroupKey; import com.sap.sse.datamining.shared.impl.GenericGroupKey; import com.sap.sse.gwt.client.shared.components.Component; import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; public class ResultsChart extends AbstractNumericResultsPresenter { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/TabbedResultsPresenter.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/TabbedResultsPresenter.java index d1dac98b8f7..b483bfa308d 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/TabbedResultsPresenter.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/TabbedResultsPresenter.java @@ -25,7 +25,7 @@ import com.sap.sse.datamining.shared.impl.dto.QueryResultDTO; import com.sap.sse.gwt.client.shared.components.AbstractComponent; import com.sap.sse.gwt.client.shared.components.Component; import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; public class TabbedResultsPresenter extends AbstractComponent implements ResultsPresenter { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/selection/AbstractQueryDefinitionProvider.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/selection/AbstractQueryDefinitionProvider.java index 9315931effd..805caa81c5e 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/selection/AbstractQueryDefinitionProvider.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/selection/AbstractQueryDefinitionProvider.java @@ -26,7 +26,7 @@ import com.sap.sse.datamining.shared.impl.dto.FunctionDTO; import com.sap.sse.gwt.client.ErrorReporter; import com.sap.sse.gwt.client.shared.components.AbstractComponent; import com.sap.sse.gwt.client.shared.components.Component; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; public abstract class AbstractQueryDefinitionProvider extends AbstractComponent implements QueryDefinitionProvider { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/selection/MultiDimensionalGroupingProvider.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/selection/MultiDimensionalGroupingProvider.java index 0800ced8ad6..9cd913e1417 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/selection/MultiDimensionalGroupingProvider.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/selection/MultiDimensionalGroupingProvider.java @@ -30,7 +30,7 @@ import com.sap.sse.gwt.client.ErrorReporter; import com.sap.sse.gwt.client.shared.components.AbstractComponent; import com.sap.sse.gwt.client.shared.components.Component; import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; public class MultiDimensionalGroupingProvider extends AbstractComponent implements GroupingProvider { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/selection/QueryDefinitionProviderWithControls.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/selection/QueryDefinitionProviderWithControls.java index 61b1d28d205..d5d3a1bbe25 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/selection/QueryDefinitionProviderWithControls.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/selection/QueryDefinitionProviderWithControls.java @@ -48,7 +48,7 @@ import com.sap.sse.datamining.shared.impl.dto.ModifiableStatisticQueryDefinition import com.sap.sse.gwt.client.ErrorReporter; import com.sap.sse.gwt.client.shared.components.Component; import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; public class QueryDefinitionProviderWithControls extends AbstractQueryDefinitionProvider implements WithControls { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/selection/RetrieverLevelSettingsComponent.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/selection/RetrieverLevelSettingsComponent.java index ac9ec0e1f28..50f4e7bad53 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/selection/RetrieverLevelSettingsComponent.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/selection/RetrieverLevelSettingsComponent.java @@ -5,7 +5,7 @@ import com.sap.sse.common.settings.SerializableSettings; import com.sap.sse.datamining.shared.impl.dto.DataRetrieverLevelDTO; import com.sap.sse.gwt.client.shared.components.AbstractComponent; import com.sap.sse.gwt.client.shared.components.Component; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; public abstract class RetrieverLevelSettingsComponent extends AbstractComponent { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/selection/SimpleDataRetrieverChainDefinitionProvider.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/selection/SimpleDataRetrieverChainDefinitionProvider.java index 9f663e0b358..1991ef8485e 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/selection/SimpleDataRetrieverChainDefinitionProvider.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/selection/SimpleDataRetrieverChainDefinitionProvider.java @@ -36,7 +36,7 @@ import com.sap.sse.gwt.client.shared.components.Component; import com.sap.sse.gwt.client.shared.components.CompositeSettings; import com.sap.sse.gwt.client.shared.components.CompositeTabbedSettingsDialogComponent; import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; public class SimpleDataRetrieverChainDefinitionProvider extends AbstractComponent implements DataRetrieverChainDefinitionProvider { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/selection/SimpleStatisticProvider.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/selection/SimpleStatisticProvider.java index 56b9dc8e01c..fe6386ac197 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/selection/SimpleStatisticProvider.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/selection/SimpleStatisticProvider.java @@ -32,7 +32,7 @@ import com.sap.sse.gwt.client.ErrorReporter; import com.sap.sse.gwt.client.shared.components.AbstractComponent; import com.sap.sse.gwt.client.shared.components.Component; import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; public class SimpleStatisticProvider extends AbstractComponent implements StatisticProvider { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/selection/filter/ListRetrieverChainFilterSelectionProvider.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/selection/filter/ListRetrieverChainFilterSelectionProvider.java index 0360e5fb2e9..06e4e8d8d1d 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/selection/filter/ListRetrieverChainFilterSelectionProvider.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/selection/filter/ListRetrieverChainFilterSelectionProvider.java @@ -40,7 +40,7 @@ import com.sap.sse.gwt.client.ErrorReporter; import com.sap.sse.gwt.client.shared.components.AbstractComponent; import com.sap.sse.gwt.client.shared.components.Component; import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; public class ListRetrieverChainFilterSelectionProvider extends AbstractComponent implements FilterSelectionProvider { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/selection/filter/RetrieverLevelFilterSelectionProvider.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/selection/filter/RetrieverLevelFilterSelectionProvider.java index 7502d7b7e6f..b9b2b94f54a 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/selection/filter/RetrieverLevelFilterSelectionProvider.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/selection/filter/RetrieverLevelFilterSelectionProvider.java @@ -24,7 +24,7 @@ import com.sap.sse.gwt.client.ErrorReporter; import com.sap.sse.gwt.client.shared.components.AbstractComponent; import com.sap.sse.gwt.client.shared.components.Component; import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; public class RetrieverLevelFilterSelectionProvider extends AbstractComponent { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/AbstractLeaderboardViewer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/AbstractLeaderboardViewer.java index d0d67a831a8..0f9dba3dd70 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/AbstractLeaderboardViewer.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/AbstractLeaderboardViewer.java @@ -23,8 +23,8 @@ import com.sap.sse.gwt.client.shared.components.Component; import com.sap.sse.gwt.client.shared.components.SettingsDialog; import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; import com.sap.sse.gwt.client.shared.perspective.AbstractPerspectiveComposite; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; import com.sap.sse.gwt.client.shared.perspective.PerspectiveCompositeSettings; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; /** * A base class for a leaderboard viewer. diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardEntryPoint.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardEntryPoint.java index a7da478a3c1..2608e26efcd 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardEntryPoint.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardEntryPoint.java @@ -31,10 +31,10 @@ import com.sap.sse.gwt.client.async.MarkedAsyncCallback; import com.sap.sse.gwt.client.player.Timer; import com.sap.sse.gwt.client.player.Timer.PlayModes; import com.sap.sse.gwt.client.player.Timer.PlayStates; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; -import com.sap.sse.gwt.client.shared.perspective.ComponentContextWithSettingsStorage; -import com.sap.sse.gwt.client.shared.perspective.OnSettingsLoadedCallback; import com.sap.sse.gwt.client.shared.perspective.PerspectiveCompositeSettings; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; +import com.sap.sse.gwt.client.shared.settings.ComponentContextWithSettingsStorage; +import com.sap.sse.gwt.client.shared.settings.OnSettingsLoadedCallback; import com.sap.sse.gwt.settings.SettingsToUrlSerializer; import com.sap.sse.security.ui.settings.StorageDefinitionId; import com.sap.sse.security.ui.settings.UserSettingsStorageManager; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardPanel.java index 7208c4a4ead..a1cf96cbf9b 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardPanel.java @@ -1,3500 +1,3500 @@ -package com.sap.sailing.gwt.ui.leaderboard; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; - -import com.google.gwt.cell.client.AbstractSafeHtmlCell; -import com.google.gwt.cell.client.Cell; -import com.google.gwt.cell.client.Cell.Context; -import com.google.gwt.cell.client.CompositeCell; -import com.google.gwt.cell.client.TextCell; -import com.google.gwt.core.client.GWT; -import com.google.gwt.core.client.Scheduler; -import com.google.gwt.core.client.Scheduler.ScheduledCommand; -import com.google.gwt.dom.client.BrowserEvents; -import com.google.gwt.dom.client.Element; -import com.google.gwt.dom.client.Style.FontWeight; -import com.google.gwt.dom.client.Style.Unit; -import com.google.gwt.event.dom.client.ClickEvent; -import com.google.gwt.event.dom.client.ClickHandler; -import com.google.gwt.event.shared.HandlerRegistration; -import com.google.gwt.i18n.client.NumberFormat; -import com.google.gwt.resources.client.ImageResource; -import com.google.gwt.safecss.shared.SafeStyles; -import com.google.gwt.safecss.shared.SafeStylesBuilder; -import com.google.gwt.safehtml.client.SafeHtmlTemplates; -import com.google.gwt.safehtml.shared.SafeHtml; -import com.google.gwt.safehtml.shared.SafeHtmlBuilder; -import com.google.gwt.safehtml.shared.SafeHtmlUtils; -import com.google.gwt.text.shared.AbstractSafeHtmlRenderer; -import com.google.gwt.user.cellview.client.Column; -import com.google.gwt.user.cellview.client.Header; -import com.google.gwt.user.cellview.client.SafeHtmlHeader; -import com.google.gwt.user.client.rpc.AsyncCallback; -import com.google.gwt.user.client.ui.AbstractImagePrototype; -import com.google.gwt.user.client.ui.Anchor; -import com.google.gwt.user.client.ui.Button; -import com.google.gwt.user.client.ui.DockPanel; -import com.google.gwt.user.client.ui.FlowPanel; -import com.google.gwt.user.client.ui.HasHorizontalAlignment; -import com.google.gwt.user.client.ui.HasVerticalAlignment; -import com.google.gwt.user.client.ui.HorizontalPanel; -import com.google.gwt.user.client.ui.ImageResourceRenderer; -import com.google.gwt.user.client.ui.Label; -import com.google.gwt.user.client.ui.SimplePanel; -import com.google.gwt.user.client.ui.Widget; -import com.google.gwt.view.client.CellPreviewEvent; -import com.google.gwt.view.client.ListDataProvider; -import com.google.gwt.view.client.MultiSelectionModel; -import com.google.gwt.view.client.SelectionChangeEvent; -import com.google.gwt.view.client.SelectionChangeEvent.Handler; -import com.sap.sailing.domain.common.DetailType; -import com.sap.sailing.domain.common.Distance; -import com.sap.sailing.domain.common.InvertibleComparator; -import com.sap.sailing.domain.common.LeaderboardNameConstants; -import com.sap.sailing.domain.common.MaxPointsReason; -import com.sap.sailing.domain.common.Mile; -import com.sap.sailing.domain.common.RaceIdentifier; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sailing.domain.common.SortingOrder; -import com.sap.sailing.domain.common.Tack; -import com.sap.sailing.domain.common.dto.AbstractLeaderboardDTO; -import com.sap.sailing.domain.common.dto.CompetitorDTO; -import com.sap.sailing.domain.common.dto.FleetDTO; -import com.sap.sailing.domain.common.dto.IncrementalOrFullLeaderboardDTO; -import com.sap.sailing.domain.common.dto.LeaderboardDTO; -import com.sap.sailing.domain.common.dto.LeaderboardEntryDTO; -import com.sap.sailing.domain.common.dto.LeaderboardRowDTO; -import com.sap.sailing.domain.common.dto.LegEntryDTO; -import com.sap.sailing.domain.common.dto.RaceColumnDTO; -import com.sap.sailing.domain.common.impl.InvertibleComparatorAdapter; -import com.sap.sailing.gwt.settings.client.leaderboard.LeaderboardPanelLifecycle; -import com.sap.sailing.gwt.settings.client.leaderboard.LeaderboardSettings; -import com.sap.sailing.gwt.settings.client.leaderboard.LeaderboardSettings.RaceColumnSelectionStrategies; -import com.sap.sailing.gwt.settings.client.leaderboard.LeaderboardSettingsDialogComponent; -import com.sap.sailing.gwt.ui.actions.GetLeaderboardByNameAction; -import com.sap.sailing.gwt.ui.client.Collator; -import com.sap.sailing.gwt.ui.client.CompetitorSelectionChangeListener; -import com.sap.sailing.gwt.ui.client.CompetitorSelectionProvider; -import com.sap.sailing.gwt.ui.client.FlagImageResolver; -import com.sap.sailing.gwt.ui.client.LeaderboardUpdateListener; -import com.sap.sailing.gwt.ui.client.LeaderboardUpdateProvider; -import com.sap.sailing.gwt.ui.client.RaceTimesInfoProvider; -import com.sap.sailing.gwt.ui.client.RaceTimesInfoProviderListener; -import com.sap.sailing.gwt.ui.client.SailingServiceAsync; -import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sailing.gwt.ui.client.shared.controls.AbstractSortableColumnWithMinMax; -import com.sap.sailing.gwt.ui.client.shared.controls.FlushableSortedCellTableWithStylableHeaders; -import com.sap.sailing.gwt.ui.client.shared.controls.SelectionCheckboxColumn; -import com.sap.sailing.gwt.ui.client.shared.filter.CompetitorRaceRankFilter; -import com.sap.sailing.gwt.ui.client.shared.filter.FilterWithUI; -import com.sap.sailing.gwt.ui.client.shared.filter.LeaderboardFetcher; -import com.sap.sailing.gwt.ui.common.client.DateAndTimeFormatterUtil; -import com.sap.sailing.gwt.ui.leaderboard.DetailTypeColumn.LegDetailField; -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.Util.Pair; -import com.sap.sse.common.filter.BinaryOperator; -import com.sap.sse.common.filter.Filter; -import com.sap.sse.common.filter.FilterSet; -import com.sap.sse.common.settings.util.SettingsDefaultValuesUtils; -import com.sap.sse.gwt.client.ErrorReporter; -import com.sap.sse.gwt.client.async.AsyncActionsExecutor; -import com.sap.sse.gwt.client.async.MarkedAsyncCallback; -import com.sap.sse.gwt.client.celltable.EntityIdentityComparator; -import com.sap.sse.gwt.client.controls.busyindicator.BusyIndicator; -import com.sap.sse.gwt.client.controls.busyindicator.BusyStateChangeListener; -import com.sap.sse.gwt.client.controls.busyindicator.BusyStateProvider; -import com.sap.sse.gwt.client.controls.busyindicator.SimpleBusyIndicator; -import com.sap.sse.gwt.client.panels.OverlayAssistantScrollPanel; -import com.sap.sse.gwt.client.player.PlayStateListener; -import com.sap.sse.gwt.client.player.TimeListener; -import com.sap.sse.gwt.client.player.Timer; -import com.sap.sse.gwt.client.player.Timer.PlayModes; -import com.sap.sse.gwt.client.player.Timer.PlayStates; -import com.sap.sse.gwt.client.shared.components.AbstractCompositeComponent; -import com.sap.sse.gwt.client.shared.components.Component; -import com.sap.sse.gwt.client.shared.components.ComponentResources; -import com.sap.sse.gwt.client.shared.components.IsEmbeddableComponent; -import com.sap.sse.gwt.client.shared.components.SettingsDialog; -import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; - -/** - * A leaderboard essentially consists of a table widget that in its columns displays the entries. - * - * @author Axel Uhl (D043530) - * - */ -public class LeaderboardPanel extends AbstractCompositeComponent - implements TimeListener, PlayStateListener, DisplayedLeaderboardRowsProvider, IsEmbeddableComponent, - CompetitorSelectionChangeListener, LeaderboardFetcher, BusyStateProvider, LeaderboardUpdateProvider { - private static final LeaderboardResources RESOURCES = GWT.create(LeaderboardResources.class); - private static final ComponentResources COMPONENT_RESOURCES = GWT.create(ComponentResources.class); - private static final LeaderboardTableResources TABLE_RESOURCES = GWT.create(LeaderboardTableResources.class); - - public static final String LOAD_LEADERBOARD_DATA_CATEGORY = "loadLeaderboardData"; - - protected static final NumberFormat scoreFormat = NumberFormat.getFormat("0.##"); - - private final SailingServiceAsync sailingService; - - private static String IS_LIVE_TEXT_COLOR = "#FF0000"; - private static String DEFAULT_TEXT_COLOR = "#000000"; - - private static final String STYLE_LEADERBOARD_CONTENT = "leaderboardContent"; - private static final String STYLE_LEADERBOARD_INFO = "leaderboardInfo"; - private static final String STYLE_LEADERBOARD_TOOLBAR = "leaderboardContent-toolbar"; - private static final String STYLE_LEADERBOARD_LIVE_RACE = "leaderboardContent-liverace"; - private static RaceColumnTemplate raceColumnTemplate = new RaceColumnTemplate(); - - static class RaceColumnTemplate { - private MyTemplate template = GWT.create(MyTemplate.class); - - interface MyTemplate extends SafeHtmlTemplates { - @SafeHtmlTemplates.Template("
") - SafeHtml styledDiv(SafeStyles style); - } - - /** - * Originally in the safehtml template: color:{0}; border-bottom: 3px solid {1}; - * - * @param textColor - * @param borderColor - * @return - */ - public SafeHtml cellFrameWithTextColorAndFleetBorder(String textColor, String borderColor) { - SafeStylesBuilder sb = new SafeStylesBuilder(); - sb.trustedColor(textColor); - sb.trustedNameAndValue("border-bottom", "3px solid " + borderColor); - return template.styledDiv(sb.toSafeStyles()); - } - - /** - * - * Originally in the safehtml template: color:{0}; - * - * @param textColor - * @return - */ - public SafeHtml cellFrameWithTextColor(String textColor) { - SafeStylesBuilder sb = new SafeStylesBuilder(); - sb.trustedColor(textColor); - return template.styledDiv(sb.toSafeStyles()); - } - } - - /** - * The leaderboard name is used to - * {@link SailingServiceAsync#getLeaderboardByName(String, java.util.Date, String[], boolean, String, com.google.gwt.user.client.rpc.AsyncCallback) - * obtain the leaderboard contents} from the server. It may change in case the leaderboard is renamed. - */ - - private LeaderboardSettings currentSettings; - - private String leaderboardName; - - private final ErrorReporter errorReporter; - - public final StringMessages stringMessages; - - private final FlushableSortedCellTableWithStylableHeaders leaderboardTable; - - private final MultiSelectionModel leaderboardSelectionModel; - - private LeaderboardDTO leaderboard; - - private final TotalRankColumn totalRankColumn; - - private final SelectionCheckboxColumn selectionCheckboxColumn; - - /** - * Passed to the {@link ManeuverCountRaceColumn}. Modifications to this list will modify the column's children list - * when updated the next time. - */ - private final List selectedManeuverDetails = new ArrayList();; - - /** - * Passed to the {@link LegColumn}. Modifications to this list will modify the column's children list when updated - * the next time. - */ - private final List selectedLegDetails = new ArrayList(); - - /** - * Passed to the {@link TextRaceColumn}. Modifications to this list will modify the column's children list when - * updated the next time. - */ - private final List selectedRaceDetails = new ArrayList(); - - private final List selectedOverallDetailColumns = new ArrayList();; - - private final Map> overallDetailColumnMap; - - private RaceColumnSelection raceColumnSelection; - - protected final String RACE_COLUMN_HEADER_STYLE = TABLE_RESOURCES.cellTableStyle().cellTableRaceColumnHeader(); - - protected final String LEG_DETAIL_COLUMN_HEADER_STYLE = TABLE_RESOURCES.cellTableStyle() - .cellTableLegDetailColumnHeader();; - - protected final String LEG_DETAIL_COLUMN_STYLE = TABLE_RESOURCES.cellTableStyle().cellTableLegDetailColumn(); - - protected final String LEG_COLUMN_HEADER_STYLE = TABLE_RESOURCES.cellTableStyle().cellTableLegColumnHeader(); - - protected final String RACE_COLUMN_STYLE = TABLE_RESOURCES.cellTableStyle().cellTableRaceColumn(); - - protected final String LEG_COLUMN_STYLE = TABLE_RESOURCES.cellTableStyle().cellTableLegColumn(); - - protected final String TOTAL_COLUMN_STYLE = TABLE_RESOURCES.cellTableStyle().cellTableTotalColumn(); - - private final Timer timer; - - /** - * A {@link LeaderboardDTO} tells something about the live delay through its - * {@link LeaderboardDTO#getDelayToLiveInMillisForLatestRace()} method. If this flag is true, the live - * delay in the {@link #timer} will be adjusted each time the {@link #updateLeaderboard(LeaderboardDTO)} method is - * invoked. Otherwise, the timer's delay will be left alone which is helpful, e.g., if the leaderboard panel is - * embedded in a race board panel that focuses on one particular race which may not be the same as the one - * controlling the leaderboard's overall live delay. - */ - private final boolean adjustTimerDelay; - - private boolean autoExpandPreSelectedRace; - - private boolean autoExpandLastRaceColumn; - - /** - * When true, the race columns don't display the competitors' scores in the race represented by the - * column but the cumulative score up to that race. - */ - private boolean showAddedScores; - - private boolean showCompetitorSailId; - private boolean showCompetitorFullName; - - /** - * When true then an additional column just before the overall points is displayed that sums up the - * number of races sailed per competitor. - */ - private boolean showOverallColumnWithNumberOfRacesCompletedPerCompetitor; - - /** - * Remembers whether the auto-expand of the pre-selected race (see {@link #autoExpandPreSelectedRace}) or last - * selected race {@link #autoExpandLastRaceColumn} has been performed once. It must not be performed another time. - */ - private boolean autoExpandPerformedOnce; - - /** - * This anchor's HTML holds the image tag for the play/pause button that needs to be updated when the {@link #timer} - * changes its playing state - */ - private Anchor playPause; - - private final CompetitorSelectionProvider competitorSelectionProvider; - private final HorizontalPanel filterControlPanel; - private Label filterStatusLabel; - private Button filterClearButton; - - /** - * The handler for changes in the leaderboard table's selection; its registration is kept in - * {@link #leaderboardAsTableSelectionModelRegistration} while it is registered as a selection handler on the - * {@link #leaderboardTable}. - */ - private final Handler selectionChangeHandler; - - /** - * While the {@link #selectionChangeHandler} is registered as a selection change handler on the - * {@link #leaderboardTable}'s selection model, this field holds the registration which can be used to remove the - * registration again. We'll use this to temporarily suspend selection events when actively modifying / adjusting - * the table selection to match the {@link #competitorSelectionProvider}. - */ - private HandlerRegistration leaderboardAsTableSelectionModelRegistration; - - /** - * If this is null, all leaderboard columns added by updating the leaderboard from the server are - * automatically added to the table. Otherwise, only the column whose {@link RaceColumnDTO#getRaceIdentifier(String) - * race identifier} matches the value of this attribute will be added. - */ - private final RegattaAndRaceIdentifier preSelectedRace; - - private final FlowPanel contentPanel = new FlowPanel(); - - private HorizontalPanel refreshAndSettingsPanel; - private Label scoreCorrectionLastUpdateTimeLabel; - private Label scoreCorrectionCommentLabel; - private Label liveRaceLabel; - - private boolean isEmbedded; - - private ImageResource pauseIcon; - private ImageResource playIcon; - - /** - * For a leaderboard, zero or more tasks may be currently busy. The counter keeps track. If it goes to {@code 0}, - * the {@link #busyIndicator} is set to non-busy. If it goes from {@code 0} to {@code 1} the {@link #busyIndicator} - * is set to busy. - */ - private int busyTaskCounter; - private final BusyIndicator busyIndicator; - private final Set busyStateChangeListeners; - - private final AsyncActionsExecutor asyncActionsExecutor; - - /** - * See also {@link #getDefaultSortColumn()}. If no other column is explicitly selected for sorting and this - * attribute holds a non-null string identifying a valid race by name that is represented in this - * leaderboard panel then sort by it. Otherwise, default sorting will default to the overall rank column. - */ - private String raceNameForDefaultSorting; - - /** - * Can be used to disallow users to drill into the race details. - */ - private final boolean showRaceDetails; - - /** - * The {@link LastNRacesColumnSelection} column selection strategy requires a {@link RaceTimesInfoProvider}. This - * can either be injected by passing a non-null object of that type to the constructor, or such an - * object is created and remembered in this attribute when required the first time. - */ - private RaceTimesInfoProvider raceTimesInfoProvider; - private RaceTimesInfoProviderListener raceTimesInfoProviderListener; - - private int blurInOnSelectionChanged; - - /** - * When an element in the leaderboard receives focus, it needs to be blurred again to keep the surrounding scroll - * panel from scrolling anything into view - */ - private Element elementToBlur; - private boolean showSelectionCheckbox; - - private final List leaderboardUpdateListener = new ArrayList(); - - private boolean initialCompetitorFilterHasBeenApplied = false; - private final boolean showCompetitorFilterStatus; - - private CompetitorFilterPanel competitorFilterPanel; - - /** - * Whether or not a second scroll bar, synchronized with the invisible native scroll bar, shall appear at the bottom - * of the viewport. See {@link OverlayAssistantScrollPanel}. - */ - private final boolean enableSyncedScroller; - - public LeaderboardPanel(Component parent, ComponentContext context, SailingServiceAsync sailingService, - AsyncActionsExecutor asyncActionsExecutor, LeaderboardSettings settings, - CompetitorSelectionProvider competitorSelectionProvider, String leaderboardName, - ErrorReporter errorReporter, final StringMessages stringMessages, boolean showRaceDetails) { - this(parent, context, sailingService, asyncActionsExecutor, settings, false, /* preSelectedRace */null, - competitorSelectionProvider, null, leaderboardName, errorReporter, stringMessages, showRaceDetails); - } - - public LeaderboardPanel(Component parent, ComponentContext context, SailingServiceAsync sailingService, - AsyncActionsExecutor asyncActionsExecutor, LeaderboardSettings settings, boolean isEmbedded, - RegattaAndRaceIdentifier preSelectedRace, CompetitorSelectionProvider competitorSelectionProvider, - String leaderboardGroupName, String leaderboardName, ErrorReporter errorReporter, - final StringMessages stringMessages, boolean showRaceDetails) { - this(parent, context, sailingService, asyncActionsExecutor, settings, isEmbedded, preSelectedRace, - competitorSelectionProvider, - new Timer( - // perform the first request as "live" but don't by default auto-play - PlayModes.Live, PlayStates.Paused, - /* delayBetweenAutoAdvancesInMilliseconds */ LeaderboardEntryPoint.DEFAULT_REFRESH_INTERVAL_MILLIS), - leaderboardGroupName, leaderboardName, errorReporter, stringMessages, showRaceDetails, - /* competitorSearchTextBox */ null, /* showSelectionCheckbox */ true, - /* optionalRaceTimesInfoProvider */ null, /* autoExpandLastRaceColumn */ false, - /* adjustTimerDelay */ true, /* autoApplyTopNFilter */ false, /* showCompetitorFilterStatus */ false, - /* enableSyncScroller */ false); - } - - public LeaderboardPanel(Component parent, ComponentContext context, SailingServiceAsync sailingService, - AsyncActionsExecutor asyncActionsExecutor, LeaderboardSettings settings, boolean isEmbedded, - RegattaAndRaceIdentifier preSelectedRace, CompetitorSelectionProvider competitorSelectionProvider, - Timer timer, String leaderboardGroupName, String leaderboardName, final ErrorReporter errorReporter, - final StringMessages stringMessages, boolean showRaceDetails, CompetitorFilterPanel competitorSearchTextBox, - boolean showSelectionCheckbox, RaceTimesInfoProvider optionalRaceTimesInfoProvider, - boolean autoExpandLastRaceColumn, boolean adjustTimerDelay, boolean autoApplyTopNFilter, - boolean showCompetitorFilterStatus, boolean enableSyncScroller) { - super(parent, context); - - this.showSelectionCheckbox = showSelectionCheckbox; - this.showRaceDetails = showRaceDetails; - this.sailingService = sailingService; - this.asyncActionsExecutor = asyncActionsExecutor; - this.isEmbedded = isEmbedded; - this.preSelectedRace = preSelectedRace; - this.competitorSelectionProvider = competitorSelectionProvider; - competitorSelectionProvider.addCompetitorSelectionChangeListener(this); - this.setLeaderboardName(leaderboardName); - this.errorReporter = errorReporter; - this.stringMessages = stringMessages; - this.raceTimesInfoProvider = optionalRaceTimesInfoProvider; - this.adjustTimerDelay = adjustTimerDelay; - this.initialCompetitorFilterHasBeenApplied = !autoApplyTopNFilter; - this.showCompetitorFilterStatus = showCompetitorFilterStatus; - this.enableSyncedScroller = enableSyncScroller; - this.autoExpandLastRaceColumn = autoExpandLastRaceColumn; - this.timer = timer; - overallDetailColumnMap = createOverallDetailColumnMap(); - raceNameForDefaultSorting = settings.getNameOfRaceToSort(); - - if (settings.getLegDetailsToShow() != null) { - selectedLegDetails.addAll(settings.getLegDetailsToShow()); - } - if (settings.getOverallDetailsToShow() != null) { - selectedOverallDetailColumns.addAll(settings.getOverallDetailsToShow()); - } - timer.addPlayStateListener(this); - timer.addTimeListener(this); - - switch (settings.getActiveRaceColumnSelectionStrategy()) { - case EXPLICIT: - if (preSelectedRace == null) { - raceColumnSelection = new ExplicitRaceColumnSelection(); - } else { - raceColumnSelection = new ExplicitRaceColumnSelectionWithPreselectedRace(preSelectedRace); - } - break; - case LAST_N: - setRaceColumnSelectionToLastNStrategy(settings.getNumberOfLastRacesToShow()); - break; - } - - totalRankColumn = new TotalRankColumn(); - leaderboardTable = new FlushableSortedCellTableWithStylableHeaders(/* pageSize */10000, - TABLE_RESOURCES); - leaderboardTable.addCellPreviewHandler(new CellPreviewEvent.Handler() { - @Override - public void onCellPreview(CellPreviewEvent event) { - if (BrowserEvents.FOCUS.equals(event.getNativeEvent().getType())) { - elementToBlur = event.getNativeEvent().getEventTarget().cast(); - elementToBlur.blur(); - blurInOnSelectionChanged = 2; // blur a couple of times; doing it one time only doesn't seem to work - // reliably - blurFocusedElementAfterSelectionChange(); - } - } - }); - leaderboardTable.ensureDebugId("LeaderboardCellTable"); - selectionCheckboxColumn = new LeaderboardSelectionCheckboxColumn(competitorSelectionProvider); - leaderboardTable.setWidth("100%"); - leaderboardSelectionModel = new MultiSelectionModel(); - // remember handler registration so we can temporarily remove it and re-add it to suspend selection events while - // we're actively changing it - selectionChangeHandler = new Handler() { - @Override - public void onSelectionChange(SelectionChangeEvent event) { - List selection = new ArrayList(); - for (LeaderboardRowDTO row : getSelectedRows()) { - selection.add(row.competitor); - } - LeaderboardPanel.this.competitorSelectionProvider.setSelection(selection, - /* listenersNotToNotify */LeaderboardPanel.this); - if (blurInOnSelectionChanged > 0) { - blurInOnSelectionChanged--; - blurFocusedElementAfterSelectionChange(); - } - } - }; - leaderboardAsTableSelectionModelRegistration = leaderboardSelectionModel - .addSelectionChangeHandler(selectionChangeHandler); - leaderboardTable.setSelectionModel(leaderboardSelectionModel, selectionCheckboxColumn.getSelectionManager()); - - SimplePanel mainPanel = new SimplePanel(); - leaderboardTable.getElement().getStyle().setMarginTop(10, Unit.PX); - contentPanel.setStyleName(STYLE_LEADERBOARD_CONTENT); - busyIndicator = new SimpleBusyIndicator(false, 0.8f); - busyIndicator.ensureDebugId("BusyIndicator"); - busyStateChangeListeners = new HashSet<>(); - - // the information panel - if (!isEmbedded) { - Widget toolbarPanel = createToolbarPanel(); - contentPanel.add(toolbarPanel); - } - if (competitorSearchTextBox != null) { - competitorSearchTextBox.add(busyIndicator); - contentPanel.add(competitorSearchTextBox); - competitorSearchTextBox.getSettingsButton().addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - new SettingsDialog(LeaderboardPanel.this, stringMessages).show(); - } - }); - this.competitorFilterPanel = competitorSearchTextBox; - } - filterControlPanel = new HorizontalPanel(); - filterControlPanel.setStyleName("LeaderboardPanel-FilterControl-Panel"); - if (enableSyncedScroller) { - contentPanel.add(new OverlayAssistantScrollPanel(leaderboardTable)); - } else { - contentPanel.add(leaderboardTable); - } - if (showCompetitorFilterStatus) { - contentPanel.add(createFilterDeselectionControl()); - } - initWidget(mainPanel); - mainPanel.setWidget(contentPanel); - this.setTitle(stringMessages.leaderboard()); - if (timer.isInitialized()) { - loadCompleteLeaderboard(/* showProgress */ false); - } - - updateSettings(settings); - } - - private Widget createToolbarPanel() { - FlowPanel informationPanel = new FlowPanel(); - informationPanel.setStyleName(STYLE_LEADERBOARD_INFO); - scoreCorrectionLastUpdateTimeLabel = new Label(""); - scoreCorrectionCommentLabel = new Label(""); - informationPanel.add(scoreCorrectionCommentLabel); - informationPanel.add(scoreCorrectionLastUpdateTimeLabel); - - liveRaceLabel = new Label(stringMessages.live()); - liveRaceLabel.setStyleName(STYLE_LEADERBOARD_LIVE_RACE); - liveRaceLabel.getElement().getStyle().setFontWeight(FontWeight.BOLD); - liveRaceLabel.getElement().getStyle().setColor(IS_LIVE_TEXT_COLOR); - liveRaceLabel.setVisible(false); - informationPanel.add(liveRaceLabel); - - // the toolbar panel - DockPanel toolbarPanel = new DockPanel(); - toolbarPanel.ensureDebugId("ToolbarPanel"); - toolbarPanel.setStyleName(STYLE_LEADERBOARD_TOOLBAR); - if (!isEmbedded) { - toolbarPanel.add(informationPanel, DockPanel.WEST); - toolbarPanel.add(busyIndicator, DockPanel.WEST); - } - toolbarPanel.setWidth("100%"); - toolbarPanel.setVerticalAlignment(HasVerticalAlignment.ALIGN_MIDDLE); - ClickHandler playPauseHandler = new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - if (LeaderboardPanel.this.timer.getPlayState() == PlayStates.Playing) { - LeaderboardPanel.this.timer.pause(); - } else { - // playing the standalone leaderboard means putting it into live mode - LeaderboardPanel.this.timer.setPlayMode(PlayModes.Live); - } - } - }; - pauseIcon = RESOURCES.autoRefreshEnabledIcon(); - playIcon = RESOURCES.autoRefreshDisabledIcon(); - refreshAndSettingsPanel = new HorizontalPanel(); - refreshAndSettingsPanel.ensureDebugId("RefreshAndSettingsPanel"); - refreshAndSettingsPanel.setVerticalAlignment(HasVerticalAlignment.ALIGN_MIDDLE); - FlowPanel refreshPanel = new FlowPanel(); - refreshPanel.addStyleName("refreshPanel"); - toolbarPanel.setHorizontalAlignment(HasHorizontalAlignment.ALIGN_RIGHT); - toolbarPanel.addStyleName("refreshAndSettings"); - playPause = new Anchor(getPlayPauseImgHtml(timer.getPlayState())); - playPause.ensureDebugId("PlayAndPauseAnchor"); - playPause.addClickHandler(playPauseHandler); - playStateChanged(timer.getPlayState(), timer.getPlayMode()); - refreshPanel.add(playPause); - - refreshAndSettingsPanel.add(refreshPanel); - toolbarPanel.add(refreshAndSettingsPanel, DockPanel.EAST); - return toolbarPanel; - } - - @Override - public Widget getEntryWidget() { - return this; - } - - @Override - public Widget getHeaderWidget() { - return null; - } - - @Override - public Widget getContentWidget() { - return contentPanel; - } - - @Override - public Widget getToolbarWidget() { - return null; - } - - @Override - public Widget getLegendWidget() { - return null; - } - - @Override - public boolean isEmbedded() { - return isEmbedded; - } - - public FlowPanel getContentPanel() { - return contentPanel; - } - - protected ImageResource getSettingsIcon() { - return COMPONENT_RESOURCES.settingsIcon(); - } - - public void updateSettings(final LeaderboardSettings newSettings) { - this.currentSettings = newSettings; - boolean oldShallAddOverallDetails = shallAddOverallDetails(); - if (newSettings.getOverallDetailsToShow() != null) { - selectedOverallDetailColumns.clear(); - selectedOverallDetailColumns.addAll(newSettings.getOverallDetailsToShow()); - } - setShowAddedScores(newSettings.isShowAddedScores()); - setShowCompetitorSailId(newSettings.isShowCompetitorSailIdColumn()); - setShowCompetitorFullName(newSettings.isShowCompetitorFullNameColumn()); - setShowOverallColumnWithNumberOfRacesCompletedPerCompetitor( - newSettings.isShowOverallColumnWithNumberOfRacesCompletedPerCompetitor()); - final List> columnsToExpandAgain = new ArrayList>(); - for (int i = 0; i < getLeaderboardTable().getColumnCount(); i++) { - Column c = getLeaderboardTable().getColumn(i); - if (c instanceof ExpandableSortableColumn) { - ExpandableSortableColumn expandableSortableColumn = (ExpandableSortableColumn) c; - if (expandableSortableColumn.isExpanded()) { - // now toggle expansion back and forth, - // enforcing a re-build of the visible - // child columns - expandableSortableColumn.changeExpansionState(/* expand */ false); - columnsToExpandAgain.add(expandableSortableColumn); - } - } - } - if (newSettings.getManeuverDetailsToShow() != null) { - selectedManeuverDetails.clear(); - selectedManeuverDetails.addAll(newSettings.getManeuverDetailsToShow()); - } - if (newSettings.getLegDetailsToShow() != null) { - selectedLegDetails.clear(); - selectedLegDetails.addAll(newSettings.getLegDetailsToShow()); - } - if (newSettings.getRaceDetailsToShow() != null) { - selectedRaceDetails.clear(); - selectedRaceDetails.addAll(newSettings.getRaceDetailsToShow()); - } - - addBusyTask(); - Runnable doWhenNecessaryDetailHasBeenLoaded = new Runnable() { - @Override - public void run() { - try { - setAutoExpandPreSelectedRace(false); // avoid expansion during updateLeaderboard(...); will expand - // later - // if it was expanded before - // update leaderboard after settings panel column selection change - - // update strategy for determining the race columns to show; if settings' race columns to show is - // null, use the - // previously selected race columns / number of race columns for the new configuration - Iterable oldNamesOfRaceColumnsToShow = null; - if (newSettings.getNamesOfRaceColumnsToShow() == null) { - oldNamesOfRaceColumnsToShow = raceColumnSelection.getSelectedRaceColumnNames(); - } - switch (newSettings.getActiveRaceColumnSelectionStrategy()) { - case EXPLICIT: - if (preSelectedRace == null) { - raceColumnSelection = new ExplicitRaceColumnSelection(); - } else { - raceColumnSelection = new ExplicitRaceColumnSelectionWithPreselectedRace(preSelectedRace); - } - if (newSettings.getNamesOfRaceColumnsToShow() != null) { - raceColumnSelection.requestClear(); - for (String nameOfRaceColumnToShow : newSettings.getNamesOfRaceColumnsToShow()) { - RaceColumnDTO raceColumnToShow = getRaceByColumnName(nameOfRaceColumnToShow); - if (raceColumnToShow != null) { - raceColumnSelection.requestRaceColumnSelection(raceColumnToShow); - } - } - } else { - // apply the old column selections again - for (String oldNameOfRaceColumnToShow : oldNamesOfRaceColumnsToShow) { - final RaceColumnDTO raceColumnByName = getLeaderboard() - .getRaceColumnByName(oldNameOfRaceColumnToShow); - if (raceColumnByName != null) { - raceColumnSelection.requestRaceColumnSelection(raceColumnByName); - } - } - if (newSettings.getNamesOfRacesToShow() != null) { - raceColumnSelection.requestClear(); - for (String nameOfRaceToShow : newSettings.getNamesOfRacesToShow()) { - RaceColumnDTO raceColumnToShow = getRaceByName(nameOfRaceToShow); - if (raceColumnToShow != null) { - raceColumnSelection.requestRaceColumnSelection(raceColumnToShow); - } - } - } - } - break; - case LAST_N: - setRaceColumnSelectionToLastNStrategy(newSettings.getNumberOfLastRacesToShow()); - break; - } - - updateLeaderboard(leaderboard); - setAutoExpandPreSelectedRace(newSettings.isAutoExpandPreSelectedRace()); - - if (newSettings.getDelayBetweenAutoAdvancesInMilliseconds() != null) { - timer.setRefreshInterval(newSettings.getDelayBetweenAutoAdvancesInMilliseconds()); - } - for (ExpandableSortableColumn expandableSortableColumn : columnsToExpandAgain) { - expandableSortableColumn.changeExpansionState(/* expand */ true); - } - if (newSettings.getNameOfRaceToSort() != null) { - final RaceColumn raceColumnByRaceName = getRaceColumnByRaceName( - newSettings.getNameOfRaceToSort()); - if (raceColumnByRaceName != null) { - getLeaderboardTable().sortColumn(raceColumnByRaceName, /* ascending */true); - } - } - } finally { - removeBusyTask(); - } - } - }; - if (oldShallAddOverallDetails == shallAddOverallDetails() || oldShallAddOverallDetails - || getLeaderboard().hasOverallDetails()) { - doWhenNecessaryDetailHasBeenLoaded.run(); - } else { // meaning that now the details need to be loaded from the server - updateLeaderboardAndRun(doWhenNecessaryDetailHasBeenLoaded); - } - } - - /** - * @param callWhenExpansionDataIsLoaded - */ - private void updateLeaderboardAndRun(final Runnable callWhenExpansionDataIsLoaded) { - final LeaderboardDTO previousLeaderboard = getLeaderboard(); - getSailingService().getLeaderboardByName(getLeaderboardName(), - timer.getPlayMode() == PlayModes.Live ? null : getLeaderboardDisplayDate(), - /* namesOfRacesForWhichToLoadLegDetails */getNamesOfExpandedRaceColumns(), shallAddOverallDetails(), - previousLeaderboard.getId(), /* fillTotalPointsUncorrected */ false, - new MarkedAsyncCallback( - new AsyncCallback() { - @Override - public void onSuccess(IncrementalOrFullLeaderboardDTO result) { - updateLeaderboard(result.getLeaderboardDTO(previousLeaderboard)); - callWhenExpansionDataIsLoaded.run(); - } - - @Override - public void onFailure(Throwable caught) { - getErrorReporter().reportError( - stringMessages.errorTryingToObtainLeaderboardContents(caught.getMessage()), - true /* silentMode */); - } - })); - } - - public void setRaceColumnSelectionToLastNStrategy(final Integer numberOfLastRacesToShow) { - raceColumnSelection = new LastNRacesColumnSelection(numberOfLastRacesToShow, getRaceTimesInfoProvider()); - if (timer.getPlayState() != Timer.PlayStates.Playing) { - // wait for the first update and adjust leaderboard once the race times have been received - raceTimesInfoProviderListener = new RaceTimesInfoProviderListener() { - @Override - public void raceTimesInfosReceived(Map raceTimesInfo, - long clientTimeWhenRequestWasSent, Date serverTimeDuringRequest, - long clientTimeWhenResponseWasReceived) { - // remove - timer.adjustClientServerOffset(clientTimeWhenRequestWasSent, serverTimeDuringRequest, - clientTimeWhenResponseWasReceived); - // remove the listener only in case a leaderboard has already been loaded - if (getLeaderboard() != null) { - updateLeaderboard(getLeaderboard()); - getRaceTimesInfoProvider().removeRaceTimesInfoProviderListener(raceTimesInfoProviderListener); - raceTimesInfoProviderListener = null; - } - } - }; - getRaceTimesInfoProvider().addRaceTimesInfoProviderListener(raceTimesInfoProviderListener); - } - } - - /** - * A leaderboard panel may have been provided with a valid {@link RaceTimesInfoProvider} upon creation; in this - * case, that object will be returned. If none was provided to the constructor, one is created and remembered if no - * previously created/remembered object exists. - *

- * - * Precondition: {@link #timer} is not null - */ - private RaceTimesInfoProvider getRaceTimesInfoProvider() { - if (raceTimesInfoProvider == null) { - final List trackedRacesIdentifiers; - if (leaderboard != null && getTrackedRacesIdentifiers() != null) { - trackedRacesIdentifiers = getTrackedRacesIdentifiers(); - } else { - trackedRacesIdentifiers = Collections.emptyList(); - } - raceTimesInfoProvider = new RaceTimesInfoProvider(getSailingService(), asyncActionsExecutor, errorReporter, - trackedRacesIdentifiers, timer.getRefreshInterval()); - } - return raceTimesInfoProvider; - } - - public AsyncActionsExecutor getExecutor() { - return asyncActionsExecutor; - } - - protected class CompetitorColumn extends LeaderboardSortableColumnWithMinMax { - private final CompetitorColumnBase base; - - protected CompetitorColumn(CompetitorColumnBase base) { - super(base.getCell(getLeaderboard()), SortingOrder.ASCENDING, LeaderboardPanel.this); - this.base = base; - } - - public CompetitorColumn(CompositeCell compositeCell, - CompetitorColumnBase base) { - super(compositeCell, SortingOrder.ASCENDING, LeaderboardPanel.this); - this.base = base; - } - - @Override - public InvertibleComparator getComparator() { - return new InvertibleComparatorAdapter() { - @Override - public int compare(LeaderboardRowDTO o1, LeaderboardRowDTO o2) { - return Collator.getInstance().compare(getLeaderboard().getDisplayName(o1.competitor), - getLeaderboard().getDisplayName(o2.competitor)); - } - }; - } - - @Override - public SafeHtmlHeader getHeader() { - return base.getHeader(); - } - - @Override - public LeaderboardRowDTO getValue(LeaderboardRowDTO object) { - return object; - } - - protected void defaultRender(Context context, LeaderboardRowDTO object, SafeHtmlBuilder sb) { - super.render(context, object, sb); - } - - @Override - public void render(Context context, LeaderboardRowDTO object, SafeHtmlBuilder sb) { - String competitorColor = LeaderboardPanel.this.competitorSelectionProvider - .getColor(object.competitor, LeaderboardPanel.this.preSelectedRace).getAsHtml(); - String competitorColorBarStyle; - if (LeaderboardPanel.this.isEmbedded && preSelectedRace != null) { - competitorColorBarStyle = "style=\"border-bottom: 2px solid " + competitorColor + ";\""; - } else { - competitorColorBarStyle = "style=\"border: none;\""; - } - base.render(object, competitorColorBarStyle, sb); - } - } - - /** - * Shows the country flag and sail ID, if present - * - * @author Axel Uhl (d043530) - * - */ - private class SailIDColumn extends LeaderboardSortableColumnWithMinMax { - private final CompetitorFetcher competitorFetcher; - - protected SailIDColumn(CompetitorFetcher competitorFetcher) { - super(new TextCell(), SortingOrder.ASCENDING, LeaderboardPanel.this); - this.competitorFetcher = competitorFetcher; - // This style is adding to avoid contained images CSS property "max-width: 100%", which could cause - // an overflow to the next column (see https://bugzilla.sapsailing.com/bugzilla/show_bug.cgi?id=3537) - setCellStyleNames(TABLE_RESOURCES.cellTableStyle().cellTableSailIdColumn()); - } - - @Override - public InvertibleComparator getComparator() { - return new InvertibleComparatorAdapter() { - @Override - public int compare(T o1, T o2) { - return competitorFetcher.getCompetitor(o1).getSailID() == null - ? competitorFetcher.getCompetitor(o2).getSailID() == null ? 0 : -1 - : competitorFetcher.getCompetitor(o2).getSailID() == null ? 1 - : Collator.getInstance().compare(competitorFetcher.getCompetitor(o1).getSailID(), - competitorFetcher.getCompetitor(o2).getSailID()); - } - }; - } - - @Override - public SafeHtmlHeader getHeader() { - return new SafeHtmlHeaderWithTooltip(SafeHtmlUtils.fromString(stringMessages.competitor()), - stringMessages.sailIdColumnTooltip()); - } - - @Override - public void render(Context context, T object, SafeHtmlBuilder sb) { - ImageResourceRenderer renderer = new ImageResourceRenderer(); - CompetitorDTO competitor = competitorFetcher.getCompetitor(object); - final String twoLetterIsoCountryCode = competitor.getTwoLetterIsoCountryCode(); - final String flagImageURL = competitor.getFlagImageURL(); - - boolean showBoatColor = !isShowCompetitorFullName() && LeaderboardPanel.this.isEmbedded - && preSelectedRace != null; - if (showBoatColor) { - String competitorColor = LeaderboardPanel.this.competitorSelectionProvider - .getColor(competitorFetcher.getCompetitor(object), LeaderboardPanel.this.preSelectedRace) - .getAsHtml(); - sb.appendHtmlConstant("

"); - } - - if (flagImageURL != null && !flagImageURL.isEmpty()) { - sb.appendHtmlConstant(""); - sb.appendHtmlConstant(" "); - } else { - final ImageResource flagImageResource; - if (twoLetterIsoCountryCode == null || twoLetterIsoCountryCode.isEmpty()) { - flagImageResource = FlagImageResolver.getEmptyFlagImageResource(); - } else { - flagImageResource = FlagImageResolver.getFlagImageResource(twoLetterIsoCountryCode); - } - if (flagImageResource != null) { - sb.append(renderer.render(flagImageResource)); - sb.appendHtmlConstant(" "); - } - } - sb.appendEscaped(competitor.getSailID()); - if (showBoatColor) { - sb.appendHtmlConstant("
"); - } - } - - @Override - public String getValue(T object) { - return competitorFetcher.getCompetitor(object).getSailID(); - } - } - - /** - * Displays net/total points and possible max-points reasons based on a {@link LeaderboardRowDTO} and a race name - * and makes the column sortable by the total points. - * - * @author Axel Uhl (D043530) - * - */ - protected abstract class RaceColumn extends ExpandableSortableColumn { - private RaceColumnDTO race; - - private final String headerStyle; - private final String columnStyle; - - public RaceColumn(RaceColumnDTO race, boolean enableExpansion, Cell cell, SortingOrder preferredSortingOrder, - String headerStyle, String columnStyle) { - super(LeaderboardPanel.this, enableExpansion, cell, preferredSortingOrder, stringMessages, - LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, selectedRaceDetails, LeaderboardPanel.this); - setHorizontalAlignment(ALIGN_CENTER); - this.race = race; - this.headerStyle = headerStyle; - this.columnStyle = columnStyle; - } - - public RaceColumnDTO getRace() { - return race; - } - - public void setRace(RaceColumnDTO race) { - this.race = race; - } - - public String getRaceColumnName() { - return race.getRaceColumnName(); - } - - public boolean isMedalRace() { - return race.isMedalRace(); - } - - public boolean isLive(FleetDTO fleetDTO) { - return race.isLive(fleetDTO, timer.getLiveTimePointInMillis()); - } - - @Override - public String getColumnStyle() { - return columnStyle; - } - - /** - * Computes added scores for this RaceColumn - */ - private double computeAddedScores(LeaderboardRowDTO object) { - double addedScores = 0; - for (RaceColumnDTO raceColumn : getLeaderboard().getRaceList()) { - LeaderboardEntryDTO entryBefore = object.fieldsByRaceColumnName.get(raceColumn.getName()); - if (entryBefore.netPoints != null) { - addedScores += entryBefore.netPoints; - } - // else "add 0" - if (raceColumn.getName().equals(getRaceColumnName())) { - break; // we've reached the current column - stop here - } - } - return addedScores; - } - - /** - * Displays a combination of net points and maxPointsReason in bold, transparent, strike-through, depending on - * various criteria. Here's how: - * - *
-         *                                  net points                |    maxPointsReason
-         * -------------------------------+-----------------------------+-----------------------
-         *  not discarded, no maxPoints   | bold                        | none
-         *  not discarded, maxPoints      | bold                        | transparent
-         *  discarded, no maxPoints       | transparent, strike-through | none
-         *  discarded, maxPoints          | transparent, strike-through | transparent, strike-through
-         * 
- */ - @Override - public void render(Context context, LeaderboardRowDTO object, SafeHtmlBuilder html) { - LeaderboardEntryDTO entry = object.fieldsByRaceColumnName.get(getRaceColumnName()); - if (entry != null) { - boolean isLive = isLive(entry.fleet); - final String textColor = isLive ? IS_LIVE_TEXT_COLOR : DEFAULT_TEXT_COLOR; - final String addedScores = isShowAddedScores() ? scoreFormat.format(computeAddedScores(object)) : ""; - String netOrAddedPointsAsText = isShowAddedScores() ? addedScores - : entry.netPoints == null ? "" : scoreFormat.format(entry.netPoints); - String totalOrAddedPointsAsText = isShowAddedScores() ? addedScores - : entry.totalPoints == null ? "" : scoreFormat.format(entry.totalPoints); - if (entry.fleet != null && entry.fleet.getColor() != null) { - html.append(raceColumnTemplate.cellFrameWithTextColorAndFleetBorder(textColor, - entry.fleet.getColor().getAsHtml())); - } else { - html.append(raceColumnTemplate.cellFrameWithTextColor(textColor)); - } - // don't show points if max points / penalty - if (entry.reasonForMaxPoints == null || entry.reasonForMaxPoints == MaxPointsReason.NONE) { - if (!entry.discarded) { - html.appendHtmlConstant(""); - html.appendHtmlConstant(netOrAddedPointsAsText); - html.appendHtmlConstant(""); - } else { - html.appendHtmlConstant(" "); - html.appendHtmlConstant(totalOrAddedPointsAsText); - html.appendHtmlConstant(""); - } - } else { - html.appendHtmlConstant(" "); - if (entry.discarded) { - html.appendHtmlConstant(""); - } - html.appendEscaped( - entry.reasonForMaxPoints == MaxPointsReason.NONE ? "" : entry.reasonForMaxPoints.name()); - if (entry.discarded) { - html.appendHtmlConstant(""); - } - html.appendHtmlConstant(""); - } - html.appendHtmlConstant("
"); - } - } - - @Override - public InvertibleComparator getComparator() { - return new InvertibleComparatorAdapter() { - @Override - public int compare(LeaderboardRowDTO o1, LeaderboardRowDTO o2) { - final int result; - if (isShowAddedScores()) { - double o1AddedScore = computeAddedScores(o1); - double o2AddedScore = computeAddedScores(o2); - double intermediate_result = o1AddedScore == 0. - ? (o2AddedScore == 0. ? -1. : isAscending() ? 1. : -1.) - : o2AddedScore == 0. ? (isAscending() ? -1. : 1.) : o1AddedScore - o2AddedScore; - if (!getLeaderboard().isHigherScoreBetter()) { - result = intermediate_result > 0 ? 1 : intermediate_result < 0 ? -1 : 0; - } else { - result = intermediate_result > 0 ? -1 : intermediate_result < 0 ? 1 : 0; - } - } else { - List competitorsFromBestToWorst = getLeaderboard() - .getCompetitorsFromBestToWorst(race); - int o1Rank = competitorsFromBestToWorst.indexOf(o1.competitor) + 1; - int o2Rank = competitorsFromBestToWorst.indexOf(o2.competitor) + 1; - result = o1Rank == 0 ? o2Rank == 0 ? 0 : isAscending() ? 1 : -1 - : o2Rank == 0 ? isAscending() ? -1 : 1 : o1Rank - o2Rank; - } - return result; - } - }; - } - - @Override - public String getHeaderStyle() { - return headerStyle; - } - - @Override - public Header getHeader() { - SortableExpandableColumnHeader header = new SortableExpandableColumnHeader( - /* title */race.getRaceColumnName(), - /* iconURL */race.isMedalRace() ? "/gwt/images/medal_small.png" : null, LeaderboardPanel.this, this, - stringMessages); - return header; - } - } - - public static DetailType[] getAvailableRaceDetailColumnTypes() { - return new DetailType[] { DetailType.RACE_GAP_TO_LEADER_IN_SECONDS, - DetailType.RACE_AVERAGE_SPEED_OVER_GROUND_IN_KNOTS, DetailType.RACE_DISTANCE_TRAVELED, - DetailType.RACE_DISTANCE_TRAVELED_INCLUDING_GATE_START, DetailType.RACE_TIME_TRAVELED, - DetailType.RACE_CALCULATED_TIME_TRAVELED, - DetailType.RACE_CALCULATED_TIME_AT_ESTIMATED_ARRIVAL_AT_COMPETITOR_FARTHEST_AHEAD, - DetailType.RACE_CURRENT_SPEED_OVER_GROUND_IN_KNOTS, DetailType.RACE_CURRENT_RIDE_HEIGHT_IN_METERS, - DetailType.RACE_DISTANCE_TO_COMPETITOR_FARTHEST_AHEAD_IN_METERS, DetailType.NUMBER_OF_MANEUVERS, - DetailType.DISPLAY_LEGS, DetailType.CURRENT_LEG, - DetailType.RACE_AVERAGE_ABSOLUTE_CROSS_TRACK_ERROR_IN_METERS, - DetailType.RACE_AVERAGE_SIGNED_CROSS_TRACK_ERROR_IN_METERS, - DetailType.RACE_RATIO_BETWEEN_TIME_SINCE_LAST_POSITION_FIX_AND_AVERAGE_SAMPLING_INTERVAL }; - } - - public static DetailType[] getAvailableRaceStartAnalysisColumnTypes() { - return new DetailType[] { DetailType.RACE_DISTANCE_TO_START_FIVE_SECONDS_BEFORE_RACE_START, - DetailType.RACE_SPEED_OVER_GROUND_FIVE_SECONDS_BEFORE_START, DetailType.DISTANCE_TO_START_AT_RACE_START, - DetailType.TIME_BETWEEN_RACE_START_AND_COMPETITOR_START, DetailType.SPEED_OVER_GROUND_AT_RACE_START, - DetailType.SPEED_OVER_GROUND_WHEN_PASSING_START, - DetailType.DISTANCE_TO_STARBOARD_END_OF_STARTLINE_WHEN_PASSING_START_IN_METERS, DetailType.START_TACK }; - } - - public static DetailType[] getAvailableOverallDetailColumnTypes() { - return new DetailType[] { DetailType.REGATTA_RANK, DetailType.TOTAL_DISTANCE_TRAVELED, - DetailType.TOTAL_AVERAGE_SPEED_OVER_GROUND, DetailType.TOTAL_TIME_SAILED_IN_SECONDS, - DetailType.MAXIMUM_SPEED_OVER_GROUND_IN_KNOTS, DetailType.TIME_ON_TIME_FACTOR, - DetailType.TIME_ON_DISTANCE_ALLOWANCE_IN_SECONDS_PER_NAUTICAL_MILE }; - } - - private class TextRaceColumn extends RaceColumn implements RaceNameProvider { - /** - * Remembers the leg columns; null-padded, if {@link #getLegColumn(int)} asks for a column index - * not yet existing. It is important to remember the columns because column removal happens based on identity. - */ - private final List legColumns; - - public TextRaceColumn(RaceColumnDTO race, boolean expandable, SortingOrder preferredSortingOrder, - String headerStyle, String columnStyle) { - super(race, expandable, new TextCell(), preferredSortingOrder, headerStyle, columnStyle); - legColumns = new ArrayList(); - } - - public String getValue(LeaderboardRowDTO object) { - // The following code exists only for robustness. This method should never be called because - // RaceColumn implements its own render(...) method which doesn't make use of getValue(...) - final Double netPoints = object.fieldsByRaceColumnName.get(getRaceColumnName()).netPoints; - return "" + (netPoints == null ? "" : scoreFormat.format(netPoints)); - } - - @Override - protected void ensureExpansionDataIsLoaded(final Runnable callWhenExpansionDataIsLoaded) { - if (getLeaderboard().getLegCount(getRaceColumnName(), preSelectedRace) != -1) { - callWhenExpansionDataIsLoaded.run(); - } else { - updateLeaderboardAndRun(callWhenExpansionDataIsLoaded); - } - } - - @Override - protected Map> getDetailColumnMap( - LeaderboardPanel leaderboardPanel, StringMessages stringMessages, String detailHeaderStyle, - String detailColumnStyle) { - Map> result = new HashMap<>(); - result.put(DetailType.RACE_RATIO_BETWEEN_TIME_SINCE_LAST_POSITION_FIX_AND_AVERAGE_SAMPLING_INTERVAL, - new TimeSinceLastGpsFixColumn( - DetailType.RACE_RATIO_BETWEEN_TIME_SINCE_LAST_POSITION_FIX_AND_AVERAGE_SAMPLING_INTERVAL, - new RaceRatioBetweenTimeSinceLastPositionFixAndAverageSamplingInterval(), - LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); - result.put(DetailType.RACE_DISTANCE_TRAVELED, - new FormattedDoubleDetailTypeColumn(DetailType.RACE_DISTANCE_TRAVELED, - new RaceDistanceTraveledInMeters(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, - LeaderboardPanel.this)); - result.put(DetailType.RACE_DISTANCE_TRAVELED_INCLUDING_GATE_START, - new FormattedDoubleDetailTypeColumn(DetailType.RACE_DISTANCE_TRAVELED_INCLUDING_GATE_START, - new RaceDistanceTraveledIncludingGateStartInMeters(), LEG_COLUMN_HEADER_STYLE, - LEG_COLUMN_STYLE, LeaderboardPanel.this)); - result.put(DetailType.RACE_AVERAGE_SPEED_OVER_GROUND_IN_KNOTS, - new FormattedDoubleDetailTypeColumn(DetailType.RACE_AVERAGE_SPEED_OVER_GROUND_IN_KNOTS, - new RaceAverageSpeedInKnots(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, - LeaderboardPanel.this)); - result.put(DetailType.RACE_GAP_TO_LEADER_IN_SECONDS, - new FormattedDoubleDetailTypeColumn(DetailType.RACE_GAP_TO_LEADER_IN_SECONDS, - new RaceGapToLeaderInSeconds(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, - LeaderboardPanel.this)); - result.put(DetailType.RACE_CURRENT_SPEED_OVER_GROUND_IN_KNOTS, - new FormattedDoubleDetailTypeColumn(DetailType.RACE_CURRENT_SPEED_OVER_GROUND_IN_KNOTS, - new RaceCurrentSpeedOverGroundInKnots(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, - LeaderboardPanel.this)); - result.put(DetailType.RACE_CURRENT_RIDE_HEIGHT_IN_METERS, - new RideHeightColumn(DetailType.RACE_CURRENT_RIDE_HEIGHT_IN_METERS, - new RaceCurrentRideHeightInMeters(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, - LeaderboardPanel.this)); - result.put(DetailType.RACE_DISTANCE_TO_COMPETITOR_FARTHEST_AHEAD_IN_METERS, - new FormattedDoubleDetailTypeColumn(DetailType.RACE_DISTANCE_TO_COMPETITOR_FARTHEST_AHEAD_IN_METERS, - new RaceDistanceToCompetitorFarthestAheadInMeters(), LEG_COLUMN_HEADER_STYLE, - LEG_COLUMN_STYLE, LeaderboardPanel.this)); - result.put(DetailType.RACE_AVERAGE_ABSOLUTE_CROSS_TRACK_ERROR_IN_METERS, - new FormattedDoubleDetailTypeColumn(DetailType.RACE_AVERAGE_ABSOLUTE_CROSS_TRACK_ERROR_IN_METERS, - new RaceAverageAbsoluteCrossTrackErrorInMeters(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, - LeaderboardPanel.this)); - result.put(DetailType.RACE_AVERAGE_SIGNED_CROSS_TRACK_ERROR_IN_METERS, - new FormattedDoubleDetailTypeColumn(DetailType.RACE_AVERAGE_SIGNED_CROSS_TRACK_ERROR_IN_METERS, - new RaceAverageSignedCrossTrackErrorInMeters(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, - LeaderboardPanel.this)); - result.put(DetailType.RACE_DISTANCE_TO_START_FIVE_SECONDS_BEFORE_RACE_START, - new FormattedDoubleDetailTypeColumn( - DetailType.RACE_DISTANCE_TO_START_FIVE_SECONDS_BEFORE_RACE_START, - new DistanceToStartFiveSecondsBeforeStartInMeters(), LEG_COLUMN_HEADER_STYLE, - LEG_COLUMN_STYLE, LeaderboardPanel.this)); - result.put(DetailType.RACE_SPEED_OVER_GROUND_FIVE_SECONDS_BEFORE_START, - new FormattedDoubleDetailTypeColumn(DetailType.RACE_SPEED_OVER_GROUND_FIVE_SECONDS_BEFORE_START, - new SpeedFiveSecondsBeforeStartInKnots(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, - LeaderboardPanel.this)); - result.put(DetailType.DISTANCE_TO_START_AT_RACE_START, - new FormattedDoubleDetailTypeColumn(DetailType.DISTANCE_TO_START_AT_RACE_START, - new DistanceToStartAtRaceStartInMeters(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, - LeaderboardPanel.this)); - result.put(DetailType.TIME_BETWEEN_RACE_START_AND_COMPETITOR_START, - new FormattedDoubleDetailTypeColumn(DetailType.TIME_BETWEEN_RACE_START_AND_COMPETITOR_START, - new TimeBetweenRaceStartAndCompetitorStartInSeconds(), LEG_COLUMN_HEADER_STYLE, - LEG_COLUMN_STYLE, LeaderboardPanel.this)); - result.put(DetailType.SPEED_OVER_GROUND_AT_RACE_START, - new FormattedDoubleDetailTypeColumn(DetailType.SPEED_OVER_GROUND_AT_RACE_START, - new SpeedOverGroundAtRaceStartInKnots(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, - LeaderboardPanel.this)); - result.put(DetailType.SPEED_OVER_GROUND_WHEN_PASSING_START, - new FormattedDoubleDetailTypeColumn(DetailType.SPEED_OVER_GROUND_WHEN_PASSING_START, - new SpeedOverGroundWhenPassingStartInKnots(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, - LeaderboardPanel.this)); - result.put(DetailType.DISTANCE_TO_STARBOARD_END_OF_STARTLINE_WHEN_PASSING_START_IN_METERS, - new FormattedDoubleDetailTypeColumn( - DetailType.DISTANCE_TO_STARBOARD_END_OF_STARTLINE_WHEN_PASSING_START_IN_METERS, - new DistanceToStarboardSideOfStartLineInMeters(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, - LeaderboardPanel.this)); - result.put(DetailType.START_TACK, - new StartingTackColumn(new TackWhenStarting(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE)); - result.put(DetailType.NUMBER_OF_MANEUVERS, - new ManeuverCountRaceColumn(getLeaderboardPanel(), this, stringMessages, - LeaderboardPanel.this.selectedManeuverDetails, LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, - LEG_DETAIL_COLUMN_HEADER_STYLE, LEG_DETAIL_COLUMN_STYLE, LeaderboardPanel.this)); - result.put(DetailType.CURRENT_LEG, new FormattedDoubleDetailTypeColumn(DetailType.CURRENT_LEG, - new CurrentLeg(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); - result.put(DetailType.RACE_TIME_TRAVELED, - new TimeTraveledRaceColumn(getLeaderboardPanel(), this, stringMessages, LEG_COLUMN_HEADER_STYLE, - LEG_COLUMN_STYLE, LEG_DETAIL_COLUMN_HEADER_STYLE, LEG_DETAIL_COLUMN_STYLE)); - result.put(DetailType.RACE_CALCULATED_TIME_TRAVELED, - new FormattedDoubleDetailTypeColumn(DetailType.RACE_CALCULATED_TIME_TRAVELED, - new RaceCalculatedTimeTraveledInSeconds(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, - LeaderboardPanel.this)); - result.put(DetailType.RACE_CALCULATED_TIME_AT_ESTIMATED_ARRIVAL_AT_COMPETITOR_FARTHEST_AHEAD, - new FormattedDoubleDetailTypeColumn( - DetailType.RACE_CALCULATED_TIME_AT_ESTIMATED_ARRIVAL_AT_COMPETITOR_FARTHEST_AHEAD, - new RaceCalculatedTimeAtEstimatedArrivalAtCompetitorFarthestAheadInSeconds(), - LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); - return result; - } - - @Override - protected Iterable> getDirectChildren() { - List> result = new ArrayList<>(); - for (AbstractSortableColumnWithMinMax column : super.getDirectChildren()) { - result.add(column); - } - if (isExpanded() && selectedRaceDetails.contains(DetailType.DISPLAY_LEGS)) { - // it is important to re-use existing LegColumn objects because - // removing the columns from the table - // is based on column identity - int maxLegCount = getLeaderboard().getLegCount(getRaceColumnName(), preSelectedRace); - if (maxLegCount != -1) { - for (int i = 0; i < maxLegCount; i++) { - LegColumn legColumn = getLegColumn(i); - result.add(legColumn); - } - } else { - // the race is no longer part of the LeaderboardDTO; consider the non-null legs in legColumns: - for (LegColumn legColumn : legColumns) { - if (legColumn != null) { - result.add(legColumn); - } - } - } - } - return result; - } - - private LegColumn getLegColumn(int legNumber) { - LegColumn result; - if (legColumns.size() > legNumber && legColumns.get(legNumber) != null) { - result = legColumns.get(legNumber); - } else { - result = new LegColumn(LeaderboardPanel.this, getRaceColumnName(), legNumber, SortingOrder.ASCENDING, - stringMessages, Collections.unmodifiableList(selectedLegDetails), LEG_COLUMN_HEADER_STYLE, - LEG_COLUMN_STYLE, LEG_DETAIL_COLUMN_HEADER_STYLE, LEG_DETAIL_COLUMN_STYLE); - while (legColumns.size() <= legNumber) { - legColumns.add(null); - } - legColumns.set(legNumber, result); - } - return result; - } - - /** - * Reports the ratio of the time that passed since the last position fix and the average sampling interval of - * the competitor's track. On a perfect track, this value will never exceed 1.0. Immediately when a fix is - * received, this value goes to 0.0. For a competitor whose tracker is lagging, this value can grow considerably - * greater than 1. The value goes to null if there are no fixes in the track. - * - * @author Axel Uhl (D043530) - * - */ - private class RaceRatioBetweenTimeSinceLastPositionFixAndAverageSamplingInterval - implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - Double result = null; - LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null && fieldsForRace.timeSinceLastPositionFixInSeconds != null - && fieldsForRace.averageSamplingInterval != null) { - result = fieldsForRace.timeSinceLastPositionFixInSeconds - / fieldsForRace.averageSamplingInterval.asSeconds(); - } - return result; - } - } - - /** - * Accumulates the average speed over all legs of a race - * - * @author Axel Uhl (D043530) - */ - private class RaceAverageSpeedInKnots implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - final Distance distanceTraveledInMeters = row.getDistanceTraveledInMeters(getRaceColumnName()); - final Duration time = row.getTimeSailed(getRaceColumnName()); - final Double result; - if (distanceTraveledInMeters != null && time != null) { - result = distanceTraveledInMeters.inTime(time).getKnots(); - } else { - result = null; - } - return result; - } - } - - /** - * Fetches the average absolute (distance always counted as positive, no matter whether left or right) - * cross-track error for the race - * - * @author Axel Uhl (D043530) - */ - private class RaceAverageAbsoluteCrossTrackErrorInMeters implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - Double result = null; - LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null) { - result = fieldsForRace.averageAbsoluteCrossTrackErrorInMeters; - } - return result; - } - } - - /** - * Fetches the average signed (right course side is positive, left course side is negative) cross-track error - * for the race - * - * @author Axel Uhl (D043530) - */ - private class RaceAverageSignedCrossTrackErrorInMeters implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - Double result = null; - LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null) { - result = fieldsForRace.averageSignedCrossTrackErrorInMeters; - } - return result; - } - } - - /** - * Fetches the competitor's distance to the start mark at the time the race started - * - * @author Axel Uhl (D043530) - */ - private class DistanceToStartAtRaceStartInMeters implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - Double result = null; - LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null) { - result = fieldsForRace.distanceToStartLineAtStartOfRaceInMeters; - } - return result; - } - } - - /** - * Fetches the time between start of race and the competitors start mark passing, telling how long after the gun - * the competitor actually started. - * - * @author Axel Uhl (D043530) - */ - private class TimeBetweenRaceStartAndCompetitorStartInSeconds implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - Double result = null; - LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null) { - result = fieldsForRace.timeBetweenRaceStartAndCompetitorStartInSeconds; - } - return result; - } - } - - /** - * Fetches the competitor's speed over ground at the time the race started - * - * @author Axel Uhl (D043530) - */ - private class SpeedOverGroundAtRaceStartInKnots implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - Double result = null; - LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null) { - result = fieldsForRace.speedOverGroundAtStartOfRaceInKnots; - } - return result; - } - } - - /** - * Fetches the competitor's distance to the start mark five seconds before the race started - */ - private class DistanceToStartFiveSecondsBeforeStartInMeters implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - Double result = null; - LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null) { - result = fieldsForRace.distanceToStartLineFiveSecondsBeforeStartInMeters; - } - return result; - } - } - - /** - * Fetches the competitor's speed over ground five seconds before the race started - */ - private class SpeedFiveSecondsBeforeStartInKnots implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - Double result = null; - LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null) { - result = fieldsForRace.speedOverGroundFiveSecondsBeforeStartInKnots; - } - return result; - } - } - - /** - * Fetches the competitor's speed over ground at the time the competitor passed the start - * - * @author Axel Uhl (D043530) - */ - private class SpeedOverGroundWhenPassingStartInKnots implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - Double result = null; - LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null) { - result = fieldsForRace.speedOverGroundAtPassingStartWaypointInKnots; - } - return result; - } - } - - /** - * Fetches the competitor's distance to the starboard side of the start line when competitor passed the start. - * If the start waypoint is not a gate/line, the distance to the single buoy is used. - * - * @author Axel Uhl (D043530) - */ - private class DistanceToStarboardSideOfStartLineInMeters implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - Double result = null; - LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null) { - result = fieldsForRace.distanceToStarboardSideOfStartLineInMeters; - } - return result; - } - } - - /** - * Fetches the competitor's speed over ground at the time the competitor passed the start - * - * @author Axel Uhl (D043530) - */ - private class TackWhenStarting implements LegDetailField { - @Override - public Tack get(LeaderboardRowDTO row) { - Tack result = null; - LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null) { - result = fieldsForRace.startTack; - } - return result; - } - } - - /** - * Accumulates the distance traveled over all legs of a race - * - * @author Axel Uhl (D043530) - */ - private class RaceDistanceTraveledInMeters implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - Distance distanceForRaceColumn = row.getDistanceTraveledInMeters(getRaceColumnName()); - return distanceForRaceColumn == null ? null : distanceForRaceColumn.getMeters(); - } - } - - private class RaceCalculatedTimeTraveledInSeconds implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - Double result = null; - LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null) { - result = fieldsForRace.calculatedTime == null ? null : fieldsForRace.calculatedTime.asSeconds(); - } - return result; - } - } - - private class RaceCalculatedTimeAtEstimatedArrivalAtCompetitorFarthestAheadInSeconds - implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - Double result = null; - LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null) { - result = fieldsForRace.calculatedTimeAtEstimatedArrivalAtCompetitorFarthestAhead == null ? null - : fieldsForRace.calculatedTimeAtEstimatedArrivalAtCompetitorFarthestAhead.asSeconds(); - } - return result; - } - } - - /** - * Accumulates the distance traveled over all legs of a race and considers the specifics of a gate start. The - * first leg of a gate start gets as additional distance traveled the distance from the port side of the line to - * the point where the competitor started which helps normalize the distance traveled and make them comparable - * across early and late starters. - * - * @author Axel Uhl (D043530) - */ - private class RaceDistanceTraveledIncludingGateStartInMeters implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - Double result = null; - LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null && fieldsForRace.legDetails != null) { - for (LegEntryDTO legDetail : fieldsForRace.legDetails) { - if (legDetail != null) { - if (legDetail.distanceTraveledIncludingGateStartInMeters != null) { - if (result == null) { - result = 0.0; - } - result += legDetail.distanceTraveledIncludingGateStartInMeters; - } - } - } - } - return result; - } - } - - private class CurrentLeg implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - Double result = null; - LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null && fieldsForRace.legDetails != null && !fieldsForRace.legDetails.isEmpty()) { - for (LegEntryDTO legDetail : fieldsForRace.legDetails) { - if (legDetail != null) { - if (legDetail.started) { - if (result == null) { - result = 0.0; - } - result++; - } else { - if (result != null) { - result++; - } - break; - } - } - } - } - return result; - } - } - - /** - * Computes the gap to leader exploiting the ordering of the leg detail columns - * - * @author Axel Uhl (D043530) - */ - private class RaceGapToLeaderInSeconds implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - Double result = null; - LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null) { - result = fieldsForRace.gapToLeaderInOwnTime == null ? null - : fieldsForRace.gapToLeaderInOwnTime.asSeconds(); - } - return result; - } - } - - /** - * Computes the windward distance to the competitor farthest ahead in the race (not necessarily the leader with - * handicap scoring) in meters - * - * @author Axel Uhl (D043530) - */ - private class RaceDistanceToCompetitorFarthestAheadInMeters implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - Double result = null; - LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null && fieldsForRace.windwardDistanceToCompetitorFarthestAheadInMeters != null) { - result = fieldsForRace.windwardDistanceToCompetitorFarthestAheadInMeters; - } - return result; - } - } - - private abstract class AbstractLastLegDetailField> implements LegDetailField { - @Override - public final T get(LeaderboardRowDTO row) { - T result = null; - LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null && fieldsForRace.legDetails != null) { - int lastLegIndex = fieldsForRace.legDetails.size() - 1; - if (lastLegIndex >= 0) { - LegEntryDTO lastLegDetail = fieldsForRace.legDetails.get(lastLegIndex); - // competitor may be in leg prior to the one the leader is in; find competitors current leg - while (lastLegDetail == null && lastLegIndex > 0) { - lastLegDetail = fieldsForRace.legDetails.get(--lastLegIndex); - } - if (lastLegDetail != null) { - if (lastLegDetail.finished) { - result = getAfterLastLegFinished(row); - } else { - result = getBeforeLastLegFinished(lastLegDetail); - } - } - } - } - return result; - } - - protected abstract T getBeforeLastLegFinished(LegEntryDTO currentLegDetail); - - protected abstract T getAfterLastLegFinished(LeaderboardRowDTO row); - } - - private class RaceCurrentSpeedOverGroundInKnots extends AbstractLastLegDetailField { - @Override - protected Double getBeforeLastLegFinished(LegEntryDTO currentLegDetail) { - return currentLegDetail.currentSpeedOverGroundInKnots; - } - - @Override - protected Double getAfterLastLegFinished(LeaderboardRowDTO row) { - return new RaceAverageSpeedInKnots().get(row); - } - } - - private class RaceCurrentRideHeightInMeters extends AbstractLastLegDetailField { - @Override - protected Double getBeforeLastLegFinished(LegEntryDTO currentLegDetail) { - return currentLegDetail.currentRideHeightInMeters; - } - - @Override - protected Double getAfterLastLegFinished(LeaderboardRowDTO row) { - Double result = null; - LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null) { - result = fieldsForRace.averageRideHeightInMeters; - } - return result; - } - } - } - - /** - * Column that display the number of races a competitor has finished. Currently taking into account - * {@link MaxPointsReason#DNS}, {@link MaxPointsReason#DNF} and {@link MaxPointsReason#DNC}. Configurable over - * {@link LeaderboardSettings#isShowOverallColumnWithNumberOfRacesCompletedPerCompetitor()}. - * - * @author Simon Marcel Pamies - */ - private class TotalRacesCompletedColumn extends LeaderboardSortableColumnWithMinMax { - private final String columnStyle; - private final MaxPointsReason[] MAX_POINTS_REASONS_THAT_IDENTIFY_NON_FINISHED_RACES = new MaxPointsReason[] { - MaxPointsReason.DNS, MaxPointsReason.DNF, MaxPointsReason.DNC }; - - protected TotalRacesCompletedColumn(String columnStyle) { - super(new TextCell(), SortingOrder.ASCENDING, LeaderboardPanel.this); - this.columnStyle = columnStyle; - setHorizontalAlignment(ALIGN_CENTER); - } - - private int computeNumberOfRacesCompleted(LeaderboardRowDTO object) { - int racesSailedInRow = 0; - for (RaceColumnDTO raceColumn : getLeaderboard().getRaceList()) { - LeaderboardEntryDTO entryBefore = object.fieldsByRaceColumnName.get(raceColumn.getName()); - if (entryBefore.netPoints != null) { - if (entryBefore.reasonForMaxPoints.equals(MaxPointsReason.NONE) - || !Util.contains(Arrays.asList(MAX_POINTS_REASONS_THAT_IDENTIFY_NON_FINISHED_RACES), - entryBefore.reasonForMaxPoints)) { - racesSailedInRow++; - } - } - } - return racesSailedInRow; - } - - @Override - public String getValue(LeaderboardRowDTO object) { - return "" + computeNumberOfRacesCompleted(object); - } - - @Override - public void render(Context context, LeaderboardRowDTO object, SafeHtmlBuilder sb) { - String textColor = getLeaderboard().hasLiveRace(timer.getLiveTimePointInMillis()) ? IS_LIVE_TEXT_COLOR - : DEFAULT_TEXT_COLOR; - - sb.appendHtmlConstant(""); - sb.appendEscaped(getValue(object)); - sb.appendHtmlConstant(""); - } - - @Override - public InvertibleComparator getComparator() { - return new InvertibleComparatorAdapter() { - @Override - public int compare(LeaderboardRowDTO o1, LeaderboardRowDTO o2) { - double o1RacesSailed = computeNumberOfRacesCompleted(o1); - double o2RacesSailed = computeNumberOfRacesCompleted(o2); - double result = o1RacesSailed == 0. ? o2RacesSailed == 0. ? 0. : isAscending() ? 1. : -1. - : o2RacesSailed == 0. ? isAscending() ? 1. : -1. : o2RacesSailed - o1RacesSailed; - return result > 0 ? 1 : result < 0 ? -1 : 0; - } - }; - } - - @Override - public String getColumnStyle() { - return columnStyle; - } - - @Override - public SafeHtmlHeader getHeader() { - return new SafeHtmlHeaderWithTooltip(SafeHtmlUtils.fromString(stringMessages.racesScored()), - stringMessages.racesScoredTooltip()); - } - } - - /** - * Displays the net points totals for a competitor for the entire leaderboard. - * - * @author Axel Uhl (D043530) - * - */ - private class TotalNetPointssColumn extends LeaderboardSortableColumnWithMinMax { - private final String columnStyle; - - protected TotalNetPointssColumn(String columnStyle) { - super(new TextCell(), SortingOrder.ASCENDING, LeaderboardPanel.this); - this.columnStyle = columnStyle; - setHorizontalAlignment(ALIGN_CENTER); - } - - @Override - public String getValue(LeaderboardRowDTO object) { - Double netPoints = object.netPoints; - return "" + (netPoints == null ? "" : scoreFormat.format(netPoints)); - } - - @Override - public void render(Context context, LeaderboardRowDTO object, SafeHtmlBuilder sb) { - String textColor = getLeaderboard().hasLiveRace(timer.getLiveTimePointInMillis()) ? IS_LIVE_TEXT_COLOR - : DEFAULT_TEXT_COLOR; - - sb.appendHtmlConstant(""); - sb.appendEscaped(getValue(object)); - sb.appendHtmlConstant(""); - } - - @Override - public InvertibleComparator getComparator() { - return new InvertibleComparatorAdapter() { - @Override - public int compare(LeaderboardRowDTO o1, LeaderboardRowDTO o2) { - return getLeaderboard().competitors.indexOf(o1.competitor) - - getLeaderboard().competitors.indexOf(o2.competitor); - } - }; - } - - @Override - public String getColumnStyle() { - return columnStyle; - } - - @Override - public SafeHtmlHeader getHeader() { - return new SafeHtmlHeaderWithTooltip(SafeHtmlUtils.fromString(stringMessages.total()), - stringMessages.totalNetPointsColumnTooltip()); - } - } - - protected class CarryColumn extends LeaderboardSortableColumnWithMinMax { - public CarryColumn() { - super(new AbstractSafeHtmlCell(new AbstractSafeHtmlRenderer() { - @Override - public SafeHtml render(LeaderboardRowDTO object) { - return new SafeHtmlBuilder() - .appendEscaped(object.carriedPoints == null ? "" : scoreFormat.format(object.carriedPoints)) - .toSafeHtml(); - } - }) { - @Override - protected void render(com.google.gwt.cell.client.Cell.Context context, SafeHtml data, - SafeHtmlBuilder sb) { - sb.append(data); - } - }, SortingOrder.ASCENDING, LeaderboardPanel.this); - setSortable(true); - } - - protected CarryColumn(Cell cell) { - super(cell, SortingOrder.ASCENDING, LeaderboardPanel.this); - setSortable(true); - } - - @Override - public LeaderboardRowDTO getValue(LeaderboardRowDTO object) { - return object; - } - - @Override - public InvertibleComparator getComparator() { - return new InvertibleComparatorAdapter() { - @Override - public int compare(LeaderboardRowDTO o1, LeaderboardRowDTO o2) { - Double o1CarriedPoints = o1.carriedPoints; - if (o1CarriedPoints == null) { - o1CarriedPoints = 0.0; - } - Double o2CarriedPoints = o2.carriedPoints; - if (o2CarriedPoints == null) { - o2CarriedPoints = 0.0; - } - return o1CarriedPoints.compareTo(o2CarriedPoints); - } - }; - } - - @Override - public SafeHtmlHeader getHeader() { - return new SafeHtmlHeaderWithTooltip(SafeHtmlUtils.fromString(stringMessages.carry()), - stringMessages.carryColumnTooltip()); - } - } - - private class LeaderboardSelectionCheckboxColumn - extends com.sap.sailing.gwt.ui.client.shared.controls.SelectionCheckboxColumn - implements CompetitorSelectionChangeListener { - protected LeaderboardSelectionCheckboxColumn(final CompetitorSelectionProvider competitorSelectionProvider) { - super(TABLE_RESOURCES.cellTableStyle().cellTableCheckboxSelected(), - TABLE_RESOURCES.cellTableStyle().cellTableCheckboxDeselected(), - TABLE_RESOURCES.cellTableStyle().cellTableCheckboxColumnCell(), - new EntityIdentityComparator() { - @Override - public boolean representSameEntity(LeaderboardRowDTO dto1, LeaderboardRowDTO dto2) { - return dto1.competitor.getIdAsString().equals(dto2.competitor.getIdAsString()); - } - - @Override - public int hashCode(LeaderboardRowDTO t) { - return t.competitor.getIdAsString().hashCode(); - } - }, getData(), leaderboardTable); - competitorSelectionProvider.addCompetitorSelectionChangeListener(this); - } - - @Override - public Boolean getValue(LeaderboardRowDTO row) { - return competitorSelectionProvider.isSelected(row.competitor); - } - - @Override - public void competitorsListChanged(Iterable competitors) { - } - - @Override - public void filterChanged(FilterSet> oldFilterSet, - FilterSet> newFilterSet) { - } - - @Override - public void filteredCompetitorsListChanged(Iterable filteredCompetitors) { - } - - /** - * Ensure that the checkbox is redrawn when the competitor selection changes - */ - @Override - public void addedToSelection(CompetitorDTO competitor) { - final LeaderboardRowDTO row = getRow(competitor.getIdAsString()); - if (row != null) { - getSelectionModel().setSelected(row, true); - } - } - - /** - * Ensure that the checkbox is redrawn when the competitor selection changes - */ - @Override - public void removedFromSelection(CompetitorDTO competitor) { - final LeaderboardRowDTO row = getRow(competitor.getIdAsString()); - if (row != null) { - getSelectionModel().setSelected(row, false); - } - } - - @Override - public void updateMinMax() { - } - } - - private class TotalRankColumn extends LeaderboardSortableColumnWithMinMax { - public TotalRankColumn() { - super(new TextCell(), SortingOrder.ASCENDING, LeaderboardPanel.this); - setHorizontalAlignment(ALIGN_CENTER); - setSortable(true); - } - - @Override - public String getValue(LeaderboardRowDTO object) { - final int totalRank = getLeaderboard().getTotalRank(object.competitor); - return "" + (totalRank == 0 ? "" : totalRank); - } - - @Override - public InvertibleComparator getComparator() { - return new InvertibleComparatorAdapter() { - @Override - public int compare(LeaderboardRowDTO o1, LeaderboardRowDTO o2) { - final int totalRank1 = getLeaderboard().getTotalRank(o1.competitor); - final int totalRank2 = getLeaderboard().getTotalRank(o2.competitor); - return totalRank1 == 0 ? totalRank2 == 0 ? 0 : 1 : totalRank2 == 0 ? -1 : totalRank1 - totalRank2; - } - }; - } - - @Override - public SafeHtmlHeader getHeader() { - return new SafeHtmlHeaderWithTooltip(SafeHtmlUtils.fromString(stringMessages.totalRegattaRank()), - stringMessages.rankColumnTooltip()); - } - } - - private class StartingTackColumn extends DetailTypeColumn { - public StartingTackColumn(LegDetailField field, String headerStyle, String columnStyle) { - super(DetailType.START_TACK, field, new TextCell(), headerStyle, columnStyle, LeaderboardPanel.this); - } - - @Override - public String getValue(LeaderboardRowDTO row) { - return getField().get(row) == null ? null - : getField().get(row) == Tack.PORT ? stringMessages.portTack() : stringMessages.starboardTack(); - } - } - - private Widget createFilterDeselectionControl() { - filterStatusLabel = new Label(); - filterStatusLabel.setStyleName("LeaderboardPanel-FilterControl-StatusLabel"); - filterStatusLabel.setText(""); - filterControlPanel.add(filterStatusLabel); - filterClearButton = new Button(stringMessages.showAll()); - filterClearButton.setStyleName("LeaderboardPanel-FilterClear-Button"); - filterClearButton.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - competitorFilterPanel.clearAllActiveFilters(); - loadCompleteLeaderboard(/* showProgress */ true); - } - }); - filterControlPanel.add(filterClearButton); - filterControlPanel.setCellHorizontalAlignment(filterClearButton, HasHorizontalAlignment.ALIGN_RIGHT); - setFilterControlStatus(); - return filterControlPanel; - } - - private void setFilterControlStatus() { - if (showCompetitorFilterStatus) { - boolean filtersActive = competitorSelectionProvider.hasActiveFilters(); - if (filtersActive) { - String labelText = ""; - for (Filter filter : competitorSelectionProvider.getCompetitorsFilterSet() - .getFilters()) { - if (filter instanceof FilterWithUI) { - labelText += ((FilterWithUI) filter).getLocalizedDescription(stringMessages) - + ", "; - } else { - labelText += filter.getName() + ", "; - } - } - filterStatusLabel.setText("Active Filter(s): " + labelText.substring(0, labelText.length() - 2)); - filterClearButton.setVisible(true); - filterControlPanel.setVisible(true); - } else { - filterStatusLabel.setText(""); - filterClearButton.setVisible(false); - filterControlPanel.setVisible(false); - if (competitorFilterPanel != null) { - competitorFilterPanel.clearSelection(); - } - } - } - } - - private static class TotalDistanceTraveledInMetersField implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - return row.totalDistanceTraveledInMeters; - } - } - - private static class TimeOnTimeFactorColumn implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - return row.competitor.getTimeOnTimeFactor(); - } - } - - private static class TimeOnDistanceAllowanceInSecondsPerNauticalMileColumn implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - return row.competitor.getTimeOnDistanceAllowancePerNauticalMile() == null ? null - : row.competitor.getTimeOnDistanceAllowancePerNauticalMile().asSeconds(); - } - } - - private static class TotalAverageSpeedOverGroundField implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - final Double result; - if (row.totalDistanceTraveledInMeters != null && row.totalTimeSailedInSeconds != null - && row.totalTimeSailedInSeconds != 0.0) { - result = row.totalDistanceTraveledInMeters / row.totalTimeSailedInSeconds - / Mile.METERS_PER_NAUTICAL_MILE * 3600; - } else { - result = null; - } - return result; - } - } - - private Map> createOverallDetailColumnMap() { - Map> result = new HashMap<>(); - result.put(DetailType.TOTAL_DISTANCE_TRAVELED, - new FormattedDoubleDetailTypeColumn(DetailType.TOTAL_DISTANCE_TRAVELED, - new TotalDistanceTraveledInMetersField(), RACE_COLUMN_HEADER_STYLE, RACE_COLUMN_STYLE, this)); - result.put(DetailType.TOTAL_AVERAGE_SPEED_OVER_GROUND, - new FormattedDoubleDetailTypeColumn(DetailType.TOTAL_AVERAGE_SPEED_OVER_GROUND, - new TotalAverageSpeedOverGroundField(), RACE_COLUMN_HEADER_STYLE, RACE_COLUMN_STYLE, this)); - result.put(DetailType.MAXIMUM_SPEED_OVER_GROUND_IN_KNOTS, - new MaxSpeedOverallColumn(RACE_COLUMN_HEADER_STYLE, RACE_COLUMN_STYLE, this)); - result.put(DetailType.TOTAL_TIME_SAILED_IN_SECONDS, createOverallTimeTraveledColumn()); - result.put(DetailType.TIME_ON_TIME_FACTOR, new FormattedDoubleDetailTypeColumn(DetailType.TIME_ON_TIME_FACTOR, - new TimeOnTimeFactorColumn(), RACE_COLUMN_HEADER_STYLE, RACE_COLUMN_STYLE, this)); - result.put(DetailType.TIME_ON_DISTANCE_ALLOWANCE_IN_SECONDS_PER_NAUTICAL_MILE, - new FormattedDoubleDetailTypeColumn(DetailType.TIME_ON_DISTANCE_ALLOWANCE_IN_SECONDS_PER_NAUTICAL_MILE, - new TimeOnDistanceAllowanceInSecondsPerNauticalMileColumn(), RACE_COLUMN_HEADER_STYLE, - RACE_COLUMN_STYLE, this)); - - return result; - } - - private OverallTimeTraveledColumn createOverallTimeTraveledColumn() { - return new OverallTimeTraveledColumn(this, stringMessages, RACE_COLUMN_HEADER_STYLE, RACE_COLUMN_STYLE, - LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE); - } - - /** - * Largely for subclasses to add more stuff to this panel, such as more toolbar buttons. - */ - protected HorizontalPanel getRefreshAndSettingsPanel() { - return refreshAndSettingsPanel; - } - - private RaceColumnDTO getRaceByName(String raceName) { - if (getLeaderboard() != null) { - for (RaceColumnDTO race : getLeaderboard().getRaceList()) { - for (FleetDTO fleet : race.getFleets()) { - if (race.getRaceIdentifier(fleet) != null - && raceName.equals(race.getRaceIdentifier(fleet).getRaceName())) { - return race; - } - } - } - } - return null; - } - - private RaceColumnDTO getRaceByColumnName(String columnName) { - if (getLeaderboard() != null) { - for (RaceColumnDTO race : getLeaderboard().getRaceList()) { - if (columnName.equals(race.getRaceColumnName())) { - return race; - } - } - } - return null; - } - - private RaceColumn getRaceColumnByRaceName(String raceName) { - for (int i = 0; i < getLeaderboardTable().getColumnCount(); i++) { - Column column = getLeaderboardTable().getColumn(i); - if (column instanceof RaceColumn) { - RaceColumnDTO raceInLeaderboard = ((RaceColumn) column).getRace(); - for (FleetDTO fleet : raceInLeaderboard.getFleets()) { - final RegattaAndRaceIdentifier raceIdentifier = raceInLeaderboard.getRaceIdentifier(fleet); - if (raceIdentifier != null && raceIdentifier.getRaceName().equals(raceName)) { - return (RaceColumn) column; - } - } - } - } - return null; - } - - private RaceColumn getRaceColumnByRaceColumnName(String raceColumnName) { - for (int i = 0; i < getLeaderboardTable().getColumnCount(); i++) { - Column column = getLeaderboardTable().getColumn(i); - if (column instanceof RaceColumn) { - if (((RaceColumn) column).getRaceColumnName().equals(raceColumnName)) { - return (RaceColumn) column; - } - } - } - return null; - } - - private SafeHtml getPlayPauseImgHtml(PlayStates playState) { - if (playState == PlayStates.Playing) { - return AbstractImagePrototype.create(pauseIcon).getSafeHtml(); - } else { - return AbstractImagePrototype.create(playIcon).getSafeHtml(); - } - } - - private void setDelayInMilliseconds(long delayInMilliseconds) { - // If the timer is currently in a mode that causes null to be used as the leaderboard request time stamp, - // don't let a live play delay change cause another load request by unregistering this panel as timer - // listener before and re-registering it after adjusting the live play delay. - if (useNullAsTimePoint()) { - timer.removeTimeListener(this); - } - timer.setLivePlayDelayInMillis(delayInMilliseconds); - if (useNullAsTimePoint()) { - timer.addTimeListener(this); - } - } - - private boolean isAutoExpandPreSelectedRace() { - return autoExpandPreSelectedRace; - } - - private boolean isAutoExpandLastRaceColumn() { - return autoExpandLastRaceColumn; - } - - private void setAutoExpandPreSelectedRace(boolean autoExpandPreSelectedRace) { - this.autoExpandPreSelectedRace = autoExpandPreSelectedRace; - if (autoExpandPreSelectedRace) { - autoExpandPerformedOnce = false; - } - } - - private boolean isShowOverallColumnWithNumberOfRacesCompletedPerCompetitor() { - return showOverallColumnWithNumberOfRacesCompletedPerCompetitor; - } - - private void setShowOverallColumnWithNumberOfRacesCompletedPerCompetitor( - boolean showOverallColumnWithNumberOfRacesCompletedPerCompetitor) { - this.showOverallColumnWithNumberOfRacesCompletedPerCompetitor = showOverallColumnWithNumberOfRacesCompletedPerCompetitor; - } - - private boolean isShowAddedScores() { - return showAddedScores; - } - - private void setShowAddedScores(boolean showAddedScores) { - this.showAddedScores = showAddedScores; - } - - private boolean isShowCompetitorSailId() { - return showCompetitorSailId; - } - - private void setShowCompetitorSailId(boolean showCompetitorSailId) { - this.showCompetitorSailId = showCompetitorSailId; - } - - private boolean isShowCompetitorFullName() { - return showCompetitorFullName; - } - - private void setShowCompetitorFullName(boolean showCompetitorFullName) { - this.showCompetitorFullName = showCompetitorFullName; - } - - /** - * The time point for which the leaderboard currently shows results. In {@link PlayModes#Replay replay mode} this is - * the {@link #timer}'s time point. In {@link PlayModes#Live live mode} the {@link #timer}'s time is quantized to - * the closest full second to increase the likelihood of cache hits in the back end. - */ - protected Date getLeaderboardDisplayDate() { - return timer.getTime(); - } - - /** - * adds the column to the right end of the {@link #getLeaderboardTable() leaderboard table} and sets - * the column style according to the {@link LeaderboardSortableColumnWithMinMax#getColumnStyle() column's style - * definition}. - */ - protected void addColumn(AbstractSortableColumnWithMinMax column) { - leaderboardTable.addColumn(column, column.getHeader(), column.getComparator(), - column.getPreferredSortingOrder().isAscending()); - String columnStyle = column.getColumnStyle(); - if (columnStyle != null) { - getLeaderboardTable().addColumnStyleName(getLeaderboardTable().getColumnCount() - 1, columnStyle); - } - } - - protected void insertColumn(int beforeIndex, AbstractSortableColumnWithMinMax column) { - // remove column styles of those columns whose index will shift right by - // one: - removeColumnStyles(beforeIndex); - getLeaderboardTable().insertColumn(beforeIndex, column, column.getHeader(), column.getComparator(), - column.getPreferredSortingOrder().isAscending()); - addColumnStyles(beforeIndex); - } - - private void addColumnStyles(int startColumn) { - for (int i = startColumn; i < getLeaderboardTable().getColumnCount(); i++) { - AbstractSortableColumnWithMinMax columnToRemoveStyleFor = (AbstractSortableColumnWithMinMax) getLeaderboardTable() - .getColumn(i); - String columnStyle = columnToRemoveStyleFor.getColumnStyle(); - if (columnStyle != null) { - getLeaderboardTable().addColumnStyleName(i, columnStyle); - } - } - } - - private void removeColumnStyles(int startColumn) { - for (int i = startColumn; i < getLeaderboardTable().getColumnCount(); i++) { - AbstractSortableColumnWithMinMax columnToRemoveStyleFor = (AbstractSortableColumnWithMinMax) getLeaderboardTable() - .getColumn(i); - String columnStyle = columnToRemoveStyleFor.getColumnStyle(); - if (columnStyle != null) { - getLeaderboardTable().removeColumnStyleName(i, columnStyle); - } - } - } - - /** - * removes the column specified by columnIndex from the {@link #getLeaderboardTable() leaderboard - * table} and fixes the column styles again (see {@link #addColumnStyles(int)}). - */ - protected void removeColumn(int columnIndex) { - Column c = getLeaderboardTable().getColumn(columnIndex); - if (c instanceof ExpandableSortableColumn) { - ExpandableSortableColumn expandableColumn = (ExpandableSortableColumn) c; - if (expandableColumn.isExpanded()) { - // remove expanded child columns from the leaderboard... - expandableColumn.changeExpansionState(/* expand */ false); - // them remember that column c was expanded: - expandableColumn.setExpanded(true); - } - } - removeColumnStyles(/* startColumn */columnIndex); - getLeaderboardTable().removeColumn(columnIndex); - addColumnStyles(/* startColumn */columnIndex); - } - - protected void removeColumn(Column c) { - int columnIndex = getLeaderboardTable().getColumnIndex(c); - if (columnIndex != -1) { - removeColumn(columnIndex); - } - } - - public void loadCompleteLeaderboard(final boolean showProgress) { - final Date date = getLeaderboardDisplayDate(); - if (needsDataLoading()) { - if (showProgress) { - addBusyTask(); - } - GetLeaderboardByNameAction getLeaderboardByNameAction = new GetLeaderboardByNameAction(sailingService, - getLeaderboardName(), useNullAsTimePoint() ? null : date, - /* namesOfRaceColumnsForWhichToLoadLegDetails */getNamesOfExpandedRaceColumns(), - shallAddOverallDetails(), /* previousLeaderboard */ - getLeaderboard(), isFillTotalPointsUncorrected(), timer, errorReporter, stringMessages); - this.asyncActionsExecutor.execute(getLeaderboardByNameAction, LOAD_LEADERBOARD_DATA_CATEGORY, - new AsyncCallback() { - @Override - public void onSuccess(LeaderboardDTO result) { - currentSettings = currentSettings.overrideDefaultsForNamesOfRaceColumns(result.getNamesOfRaceColumns()); - - try { - updateLeaderboard(result); - // reapply, as columns might have changed - updateSettings(currentSettings); - } finally { - if (showProgress) { - removeBusyTask(); - } - } - } - - @Override - public void onFailure(Throwable caught) { - if (showProgress) { - removeBusyTask(); - } - getErrorReporter().reportError( - "Error trying to obtain leaderboard contents: " + caught.getMessage(), - true /* silentMode */); - } - }); - } - } - - protected boolean isFillTotalPointsUncorrected() { - return false; - } - - /** - * In {@link PlayModes#Live live mode}, when {@link #loadCompleteLeaderboard(boolean) loading the leaderboard - * contents}, null is used as time point. The condition for this is encapsulated in this method so - * others can find out. For example, when a time change is signaled due to local offset / delay adjustments, no - * additional call to {@link #loadCompleteLeaderboard(boolean)} would be required as null will be - * passed in any case, not being affected by local time offsets. - */ - private boolean useNullAsTimePoint() { - return timer.getPlayMode() == PlayModes.Live; - } - - private boolean needsDataLoading() { - return isVisible(); - } - - /** - * Determine from column expansion state which races need their leg details - */ - private Collection getNamesOfExpandedRaceColumns() { - Collection namesOfExpandedRaceColumns = new ArrayList(); - for (int i = 0; i < getLeaderboardTable().getColumnCount(); i++) { - Column column = getLeaderboardTable().getColumn(i); - if (column instanceof RaceColumn) { - RaceColumn raceColumn = (RaceColumn) column; - if (raceColumn.isExpanded()) { - namesOfExpandedRaceColumns.add(raceColumn.getRaceColumnName()); - } - } - } - return namesOfExpandedRaceColumns; - } - - private boolean shallAddOverallDetails() { - return !selectedOverallDetailColumns.isEmpty(); - } - - private void applyTop30FilterIfCompetitorSizeGreaterEqual40(LeaderboardDTO leaderboard) { - int maxRaceRank = 30; - if (leaderboard.competitors.size() >= 40) { - CompetitorRaceRankFilter raceRankFilter = new CompetitorRaceRankFilter(); - raceRankFilter.setLeaderboardFetcher(this); - raceRankFilter.setSelectedRace(preSelectedRace); - raceRankFilter.setQuickRankProvider(this.competitorFilterPanel.getQuickRankProvider()); - raceRankFilter.setOperator(new BinaryOperator(BinaryOperator.Operators.LessThanEquals)); - raceRankFilter.setValue(maxRaceRank); - FilterSet> activeFilterSet = competitorSelectionProvider - .getOrCreateCompetitorsFilterSet(stringMessages.topNCompetitorsByRaceRank(maxRaceRank)); - activeFilterSet.addFilter(raceRankFilter); - competitorSelectionProvider.setCompetitorsFilterSet(activeFilterSet); - } - } - - /** - * Assigns leaderboard to {@link #leaderboard} and updates the UI accordingly. Also updates the min/max - * values on the columns. - */ - public void updateLeaderboard(LeaderboardDTO leaderboard) { - if (leaderboard != null) { - Collection> columnsToCollapseAndExpandAgain = getExpandedRaceColumnsWhoseDisplayedLegCountChanged( - leaderboard); - for (RaceColumn columnToCollapseAndExpandAgain : columnsToCollapseAndExpandAgain) { - columnToCollapseAndExpandAgain.changeExpansionState(/* expand */ false); - } - competitorSelectionProvider.setCompetitors(leaderboard.competitors, /* listenersNotToNotify */this); - if (!initialCompetitorFilterHasBeenApplied) { - applyTop30FilterIfCompetitorSizeGreaterEqual40(leaderboard); - initialCompetitorFilterHasBeenApplied = true; - } - raceColumnSelection.autoUpdateRaceColumnSelectionForUpdatedLeaderboard(getLeaderboard(), leaderboard); - setLeaderboard(leaderboard); - adjustColumnLayout(leaderboard); - updateRaceColumnDTOsToRaceColumns(leaderboard); - for (RaceColumn columnToCollapseAndExpandAgain : columnsToCollapseAndExpandAgain) { - columnToCollapseAndExpandAgain.changeExpansionState(/* expand */ true); - } - adjustDelayToLive(); - final Map rowsToDisplay = getRowsToDisplay(); - Set rowsToAdd = new HashSet<>(rowsToDisplay.values()); - Map rowsToUpdate = new HashMap<>(); - synchronized (getData().getList()) { - int index = 0; - for (Iterator i = getData().getList().iterator(); i.hasNext();) { - LeaderboardRowDTO oldRow = i.next(); - LeaderboardRowDTO newRow = rowsToDisplay.get(oldRow.competitor); - if (newRow != null) { - rowsToUpdate.put(index++, newRow); // update row in place, preserving its selection state - rowsToAdd.remove(newRow); // no need to add this row when it was updated in-place - } else { - i.remove(); // old row's competitor not found in new rows' competitors; remove old row from - // table - } - } - for (Entry updateEntry : rowsToUpdate.entrySet()) { - LeaderboardRowDTO oldElement = getData().getList().set(updateEntry.getKey(), - updateEntry.getValue()); - leaderboardSelectionModel.setSelected(oldElement, false); // make sure the old element is no longer - // part of the selection - updateSelection(updateEntry.getValue()); - } - for (LeaderboardRowDTO rowToAdd : rowsToAdd) { - getData().getList().add(rowToAdd); - updateSelection(rowToAdd); - } - } - RaceColumn lastRaceColumn = null; - for (int i = getLeaderboardTable().getColumnCount() - 1; i >= 0; i--) { - if (getLeaderboardTable().getColumn(i) instanceof RaceColumn) { - lastRaceColumn = (RaceColumn) getLeaderboardTable().getColumn(i); - break; - } - } - for (int i = 0; i < getLeaderboardTable().getColumnCount(); i++) { - AbstractSortableColumnWithMinMax c = (AbstractSortableColumnWithMinMax) getLeaderboardTable() - .getColumn(i); - c.updateMinMax(); - // Toggle pre-selected race, if the setting is set and it isn't open yet, or the last race column if - // that was requested - if ((!autoExpandPerformedOnce && isAutoExpandPreSelectedRace() && c instanceof RaceColumn - && ((RaceColumn) c).getRace().hasTrackedRace(preSelectedRace)) - || (isAutoExpandLastRaceColumn() && c == lastRaceColumn)) { - ExpandableSortableColumn expandableSortableColumn = (ExpandableSortableColumn) c; - if (!expandableSortableColumn.isExpanded()) { - expandableSortableColumn.changeExpansionState(/* expand */ true); - autoExpandPerformedOnce = true; - } - } - if (c instanceof RaceColumn && ((RaceColumn) c).getRace().hasTrackedRace(preSelectedRace)) { - RaceColumnDTO raceColumn = ((RaceColumn) c).getRace(); - informLeaderboardUpdateListenersAboutRaceSelected(preSelectedRace, raceColumn); - } - } - if (leaderboardTable.getCurrentlySortedColumn() != null) { - leaderboardTable.sort(); - } else { - AbstractSortableColumnWithMinMax columnToSortFor = getDefaultSortColumn(); - leaderboardTable.sortColumn(columnToSortFor, columnToSortFor.getPreferredSortingOrder().isAscending()); - } - - if (!isEmbedded) { - scoreCorrectionCommentLabel.setText(leaderboard.getComment() != null ? leaderboard.getComment() : ""); - if (leaderboard.getTimePointOfLastCorrectionsValidity() != null) { - Date lastCorrectionDate = leaderboard.getTimePointOfLastCorrectionsValidity(); - String lastUpdate = DateAndTimeFormatterUtil.longDateFormatter.render(lastCorrectionDate) + ", " - + DateAndTimeFormatterUtil.longTimeFormatter.render(lastCorrectionDate); - scoreCorrectionLastUpdateTimeLabel.setText(stringMessages.lastScoreUpdate() + ": " + lastUpdate); - } else { - scoreCorrectionLastUpdateTimeLabel.setText(""); - } - - boolean hasLiveRace = !leaderboard.getLiveRaces(timer.getLiveTimePointInMillis()).isEmpty(); - liveRaceLabel.setText(hasLiveRace ? getLiveRacesText() : ""); - scoreCorrectionLastUpdateTimeLabel.setVisible(!hasLiveRace); - liveRaceLabel.setVisible(hasLiveRace); - } - informLeaderboardUpdateListenersAboutLeaderboardUpdated(leaderboard); - } - } - - /** - * Adjusts the row's selection in the {@link #leaderboardSelectionModel} so it matches its selection state in the - * {@link #competitorSelectionProvider}. - */ - private void updateSelection(LeaderboardRowDTO row) { - final boolean shallBeSelected = competitorSelectionProvider.isSelected(row.competitor); - if (leaderboardAsTableSelectionModelRegistration != null) { - // suspend selection events while actively adjusting the leaderboardSelectionModel to match the - // competitorSelectionProvider - leaderboardAsTableSelectionModelRegistration.removeHandler(); - leaderboardAsTableSelectionModelRegistration = null; - } - if (leaderboardSelectionModel.isSelected(row) != shallBeSelected) { - leaderboardSelectionModel.setSelected(row, shallBeSelected); - } - // register the selection change handler again - leaderboardAsTableSelectionModelRegistration = leaderboardTable.getSelectionModel() - .addSelectionChangeHandler(selectionChangeHandler); - } - - /** - * The race columns hold a now outdated copy of a {@link RaceColumnDTO} which needs to be updated from the - * {@link LeaderboardDTO} just received - */ - private void updateRaceColumnDTOsToRaceColumns(LeaderboardDTO leaderboard) { - for (RaceColumnDTO newRace : leaderboard.getRaceList()) { - RaceColumn raceColumn = getRaceColumnByRaceColumnName(newRace.getName()); - if (raceColumn != null) { - raceColumn.setRace(newRace); - } - } - } - - /** - * Due to a course change, a race may change its number of legs. All expanded race columns that show leg columns and - * whose leg count changed need to be collapsed before the leaderboard is replaced, and expanded afterwards again. - * Race columns whose toggling is {@link ExpandableSortableColumn#isTogglingInProcess() currently in progress} are - * not considered because their new state will be considered after replacing anyhow. - * - * @param newLeaderboard - * the new leaderboard before assigning to {@link #leaderboard} - * @return the columns that were collapsed in this step and that shall be expanded again after the leaderboard has - * been replaced - */ - private Collection> getExpandedRaceColumnsWhoseDisplayedLegCountChanged( - LeaderboardDTO newLeaderboard) { - Set> result = new HashSet>(); - if (selectedRaceDetails.contains(DetailType.DISPLAY_LEGS)) { - for (int i = 0; i < getLeaderboardTable().getColumnCount(); i++) { - Column c = getLeaderboardTable().getColumn(i); - if (c instanceof RaceColumn) { - RaceColumn rc = (RaceColumn) c; - // If the new leaderboard no longer contains the column, getLegCount will return -1, causing the - // column - // to be collapsed if it was expanded. This is correct because otherwise, removing it would no - // longer - // know the correct leg count. - if (!rc.isTogglingInProcess() && rc.isExpanded()) { - int oldLegCount = getLeaderboard().getLegCount(rc.getRaceColumnName(), preSelectedRace); - int newLegCount = newLeaderboard.getLegCount(rc.getRaceColumnName(), preSelectedRace); - if (oldLegCount != newLegCount) { - result.add(rc); - } - } - } - } - } - return result; - } - - /** - * Based on the {@link LeaderboardDTO#getDelayToLiveInMillisForLatestRace()} for the race that has the latest - * {@link RaceColumnDTO#getStartDate(FleetDTO) start time}, automatically adjusts the delay accordingly unless the - * {@link #adjustTimerDelay} flag is false - */ - private void adjustDelayToLive() { - if (adjustTimerDelay) { - if (leaderboard.getDelayToLiveInMillisForLatestRace() != null) { - setDelayInMilliseconds(leaderboard.getDelayToLiveInMillisForLatestRace()); - } - } - } - - /** - * Extracts the rows to display of the leaderboard. These are all {@link AbstractLeaderboardDTO#rows - * rows} in case {@link #preSelectedRace} is null, or only the rows of the competitors who scored in - * the race identified by {@link #preSelectedRace} otherwise. - */ - @Override - public Map getRowsToDisplay() { - Map result; - Iterable allFilteredCompetitors = competitorSelectionProvider.getFilteredCompetitors(); - result = new HashMap(); - if (preSelectedRace == null) { - for (CompetitorDTO competitor : leaderboard.rows.keySet()) { - if (Util.contains(allFilteredCompetitors, competitor)) { - result.put(competitor, leaderboard.rows.get(competitor)); - } - } - } else { - for (CompetitorDTO competitorInPreSelectedRace : getCompetitors(preSelectedRace)) { - if (Util.contains(allFilteredCompetitors, competitorInPreSelectedRace)) { - result.put(competitorInPreSelectedRace, leaderboard.rows.get(competitorInPreSelectedRace)); - } - } - } - return result; - } - - /** - * The {@link LeaderboardDTO} holds {@link LeaderboardDTO#getRaceList() races} as {@link RaceColumnDTO} objects. - * Those map their fleet names to the {@link RegattaAndRaceIdentifier} which identifies the tracked race - * representing the fleet race within the race column. - *

- * - * On the other hand, a {@link LeaderboardRowDTO} has {@link LeaderboardEntryDTO}, keyed by race column name. The - * entry DTOs, in turn, store the {@link RaceIdentifier} of the race in which the respective competitor achieved the - * score. - *

- * - * With this information it is possible to identify the competitors who participated in a particular tracked race, - * as identified through the race parameter. - * - * @return all competitors for which the {@link #getLeaderboard() leaderboard} has an entry whose - * {@link LeaderboardEntryDTO#race} equals race - */ - public Iterable getCompetitors(RaceIdentifier race) { - Set result = new HashSet(); - if (getLeaderboard() != null) { - for (RaceColumnDTO raceColumn : getLeaderboard().getRaceList()) { - if (raceColumn.hasTrackedRace(race)) { - for (Map.Entry e : getLeaderboard().rows.entrySet()) { - LeaderboardEntryDTO entry = e.getValue().fieldsByRaceColumnName - .get(raceColumn.getRaceColumnName()); - if (entry != null && entry.race != null && entry.race.equals(race)) { - result.add(e.getKey()); - } - } - } - } - } - return result; - } - - private AbstractSortableColumnWithMinMax getDefaultSortColumn() { - AbstractSortableColumnWithMinMax defaultSortColumn = null; - if (raceNameForDefaultSorting != null) { - defaultSortColumn = getRaceColumnByRaceName(raceNameForDefaultSorting); - } - if (defaultSortColumn == null) { - defaultSortColumn = getRankColumn(); - } - return defaultSortColumn; - } - - private TotalRankColumn getRankColumn() { - return totalRankColumn; - } - - protected void setLeaderboard(LeaderboardDTO leaderboard) { - this.leaderboard = leaderboard; - } - - @Override - public LeaderboardDTO getLeaderboard() { - return leaderboard; - } - - private void adjustColumnLayout(LeaderboardDTO leaderboard) { - int columnIndex = 0; - columnIndex = ensureSelectionCheckboxColumn(columnIndex); - columnIndex = ensureRankColumn(columnIndex); - columnIndex = ensureSailIDAndCompetitorColumn(columnIndex); - columnIndex = updateCarryColumn(leaderboard, columnIndex); - adjustOverallDetailColumns(leaderboard, columnIndex); - // first remove race columns no longer needed: - removeUnusedRaceColumns(leaderboard); - if (leaderboard != null) { - createMissingAndAdjustExistingRaceColumns(leaderboard); - ensureTotalsColumn(); - updateTotalRacesSailedColumn(); - } - } - - /** - * Ensures that the columns requested by {@link #selectedOverallDetailColumns} are in the table. Assumes that if - * there are any existing overall details columns, they start at indexOfFirstOverallDetailsColumn and - * are in the order defined by {@link #getAvailableOverallDetailColumnTypes()}. - * - * @param indexOfFirstOverallDetailsColumn - * tells the column index for the first overall details column - */ - private void adjustOverallDetailColumns(LeaderboardDTO leaderboard, int indexOfFirstOverallDetailsColumn) { - List> overallDetailColumnsToShow = new ArrayList>(); - // ensure the ordering in overallDetailColumnsToShow conforms to the ordering of - // getAvailableOverallDetailColumnTypes() - for (DetailType overallDetailType : getAvailableOverallDetailColumnTypes()) { - if (selectedOverallDetailColumns.contains(overallDetailType) - && overallDetailColumnMap.containsKey(overallDetailType)) { - overallDetailColumnsToShow.add(overallDetailColumnMap.get(overallDetailType)); - } - } - int currentColumnIndex = indexOfFirstOverallDetailsColumn; - int i = 0; // index into overallDetailColumnToShow - Column currentColumn = currentColumnIndex < getLeaderboardTable().getColumnCount() - ? getLeaderboardTable().getColumn(currentColumnIndex) : null; - // repeat until no more column to check for removal and no more column left to check for need to insert - while (i < overallDetailColumnsToShow.size() || overallDetailColumnMap.values().contains(currentColumn)) { - if (i < overallDetailColumnsToShow.size() && currentColumn == overallDetailColumnsToShow.get(i)) { - // found selected column in table; all good, advance both "pointers" - i++; - currentColumnIndex++; - currentColumn = getLeaderboardTable().getColumn(currentColumnIndex); - } else if (i < overallDetailColumnsToShow.size()) { - // selected column is missing; insert - insertColumn(currentColumnIndex++, overallDetailColumnsToShow.get(i++)); - } else { - // based on the while's condition, currentColumn is an overallDetailsColumnMap value; - // based on the previous if's failed condition, it is not selected. Remove: - removeColumn(currentColumnIndex); - currentColumn = getLeaderboardTable().getColumn(currentColumnIndex); - } - } - } - - /** - * If header information doesn't match the race column's actual state (tracked races attached meaning expandable; - * medal race), the column is removed and inserted again - * - * @param raceColumn - * the raceColumn to correct. - * @param selectedRaceColumn - * the new race column data for raceColumn - */ - private void correctColumnData(RaceColumn raceColumn, RaceColumnDTO race) { - int columnIndex = getRaceColumnPosition(raceColumn); - if (raceColumn.isExpansionEnabled() != race.hasTrackedRaces() - || race.isMedalRace() != raceColumn.isMedalRace()) { - if (raceColumn.isExpanded()) { - raceColumn.changeExpansionState(/* expand */ false); // remove children from table - } - removeColumn(columnIndex); - insertColumn(columnIndex, createRaceColumn(race)); - } - } - - /** - * Removes all RaceColumns, starting at count {@link raceColumnStartIndex raceColumnStartIndex} - * - * @param raceColumnStartIndex - * The index of the race column should be deleted from. - * @param raceName - * The name of the racing column until the table should be cleared. - */ - private void removeRaceColumnFromRaceColumnStartIndexBeforeRace(int raceColumnStartIndex, RaceColumnDTO race) { - int counter = 0; - for (int leaderboardposition = 0; leaderboardposition < getLeaderboardTable() - .getColumnCount(); leaderboardposition++) { - Column c = getLeaderboardTable().getColumn(leaderboardposition); - if (c instanceof RaceColumn) { - RaceColumn raceColumn = (RaceColumn) c; - if (!raceColumn.getRaceColumnName().equals(race.getRaceColumnName())) { - if (raceColumnStartIndex == counter) { - removeColumn(raceColumn); - } - } else { - return; - } - counter++; - } - } - } - - /** - * Gets a ColumnPosition of a raceColumn - * - * @param raceColumn - * The column for which the position is to be found in the leaderboard table - * @return the position. Returns -1 if raceColumn not existing in leaderboardTable. - */ - private int getRaceColumnPosition(RaceColumn raceColumn) { - for (int leaderboardposition = 0; leaderboardposition < getLeaderboardTable() - .getColumnCount(); leaderboardposition++) { - Column c = getLeaderboardTable().getColumn(leaderboardposition); - if (c instanceof RaceColumn) { - RaceColumn rc = (RaceColumn) c; - if (rc.equals(raceColumn)) { - return leaderboardposition; - } - } - } - return -1; - } - - /** - * This method returns the position where a race column should get inserted. - * - * @param raceName - * the name of the race to insert - * @param listpos - * the position of the race in the {@link selectedRaceColumns selectedRaceColumns} - * @return the position of a race column right before which to insert the race column so that it is the - * listposth race in the table or -1 if no such race column was found, e.g., in case the column - * needs to be inserted as the last race in the table - */ - private int getColumnPositionToInsert(RaceColumnDTO race, int listpos) { - int raceColumnCounter = 0; - int noRaceColumnCounter = 0; - boolean raceColumnFound = false; - for (int leaderboardPosition = 0; !raceColumnFound - && leaderboardPosition < getLeaderboardTable().getColumnCount(); leaderboardPosition++) { - Column c = getLeaderboardTable().getColumn(leaderboardPosition); - if (c instanceof RaceColumn) { - // RaceColumn raceColumn = (RaceColumn) c; - if (raceColumnCounter == listpos) { - raceColumnFound = true; - } else { - raceColumnCounter++; - } - } else { - noRaceColumnCounter++; - } - } - if (raceColumnFound) { - return raceColumnCounter + noRaceColumnCounter; - } else { - return -1; - } - } - - /** - * Removes all columns of type {@link RaceColumn} from the leaderboard table if their name is not in the list of - * names of the selectedRaceColumns. - */ - private void removeRaceColumnsNotSelected(Iterable selectedRaceColumns) { - Set selectedRaceColumnNames = new HashSet(); - for (RaceColumnDTO selectedRaceColumn : selectedRaceColumns) { - selectedRaceColumnNames.add(selectedRaceColumn.getRaceColumnName()); - } - List> columnsToRemove = new ArrayList>(); - for (int i = 0; i < getLeaderboardTable().getColumnCount(); i++) { - Column c = getLeaderboardTable().getColumn(i); - if (c instanceof RaceColumn && (leaderboard == null - || !selectedRaceColumnNames.contains(((RaceColumn) c).getRaceColumnName()))) { - columnsToRemove.add(c); - } - } - for (Column c : columnsToRemove) { - removeColumn(c); - } - } - - /** - * Existing and matching race columns may still need to be removed, re-created and inserted because the "tracked" - * property may have changed, changing the columns expandability. - */ - private void createMissingAndAdjustExistingRaceColumns(LeaderboardDTO leaderboard) { - // Correct order of races in selectedRaceColum - Iterable correctedOrderSelectedRaces = raceColumnSelection - .getSelectedRaceColumnsOrderedAsInLeaderboard(leaderboard); - removeRaceColumnsNotSelected(correctedOrderSelectedRaces); - for (int selectedRaceCount = 0; selectedRaceCount < Util - .size(correctedOrderSelectedRaces); selectedRaceCount++) { - RaceColumnDTO selectedRaceColumn = Util.get(correctedOrderSelectedRaces, selectedRaceCount); - final RaceColumn raceColumn = selectedRaceColumn == null ? null - : getRaceColumnByRaceColumnName(selectedRaceColumn.getName()); - if (raceColumn != null) { - // remove all raceColumns, starting at a specific selectedRaceCount, up to but excluding the selected - // raceName with the result that selectedRace is at position selectedRaceCount afterwards - removeRaceColumnFromRaceColumnStartIndexBeforeRace(selectedRaceCount, selectedRaceColumn); - correctColumnData(raceColumn, selectedRaceColumn); - } else { - // get correct position to insert the column - int positionToInsert = getColumnPositionToInsert(selectedRaceColumn, selectedRaceCount); - if (positionToInsert != -1) { - insertColumn(positionToInsert, createRaceColumn(selectedRaceColumn)); - } else { - // Add the raceColumn with addRaceColumn, if no RaceColumn is existing in leaderboard - addRaceColumn(createRaceColumn(selectedRaceColumn)); - } - } - } - } - - protected RaceColumn createRaceColumn(RaceColumnDTO raceInLeaderboard) { - TextRaceColumn textRaceColumn = new TextRaceColumn(raceInLeaderboard, shallExpandRaceColumn(raceInLeaderboard), - SortingOrder.ASCENDING, RACE_COLUMN_HEADER_STYLE, RACE_COLUMN_STYLE); - return textRaceColumn; - } - - /** - * To be expandable, a race column needs to have one or more tracked races associated and needs to have at least - * some GPS data available. Wind data is not required for expandability because several metrics can reasonably be - * determined even without reliable wind information. - */ - private boolean shallExpandRaceColumn(RaceColumnDTO raceColumnDTO) { - return showRaceDetails && raceColumnDTO.hasTrackedRaces() && raceColumnDTO.hasGPSData(); - } - - private void removeUnusedRaceColumns(LeaderboardDTO leaderboard) { - List> columnsToRemove = new ArrayList>(); - for (int i = 0; i < getLeaderboardTable().getColumnCount(); i++) { - Column c = getLeaderboardTable().getColumn(i); - if (c instanceof RaceColumn && (leaderboard == null - || !leaderboard.raceListContains(((RaceColumn) c).getRaceColumnName()))) { - columnsToRemove.add(c); - } - } - // Tricky issue: if the race column is currently expanded, we can't know anymore how many detail columns - // there are because the updated LeaderboardDTO object doesn't contain the race anymore. We have to - // collapse and remove all LegColumns following the RaceColumn - for (Column c : columnsToRemove) { - removeColumn(c); - } - } - - /** - * If the last column is the totals column, remove it. Add the race column as the last column. - */ - private void addRaceColumn(RaceColumn raceColumn) { - if (getLeaderboardTable() - .getColumn(getLeaderboardTable().getColumnCount() - 1) instanceof TotalNetPointssColumn) { - removeColumn(getLeaderboardTable().getColumnCount() - 1); - } - addColumn(raceColumn); - } - - /** - * The regatta rank column shall be displayed if an only if the {@link DetailType#REGATTA_RANK} detail is selected - * in the {@link #selectedOverallDetailColumns}. It will then be displayed after the selection checkbox column (if - * any) and before the sail number / competitor name columns. - */ - private boolean isShowRegattaRankColumn() { - return selectedOverallDetailColumns.contains(DetailType.REGATTA_RANK); - } - - /** - * @param rankColumnIndex - * the column index (0-based) where to put the rank column, if needed - * @return the column index (0-based) for the next column; will equal rankColumnIndex if the rank - * column is not {@link #isShowRegattaRankColumn() supposed to be shown}, or one greater otherwise. - */ - private int ensureRankColumn(int rankColumnIndex) { - final int indexOfNextColumn = rankColumnIndex + (isShowRegattaRankColumn() ? 1 : 0); - if (getLeaderboardTable().getColumnCount() > rankColumnIndex) { - if (isShowRegattaRankColumn()) { - if (getLeaderboardTable().getColumn(rankColumnIndex) != getRankColumn()) { - insertColumn(rankColumnIndex, getRankColumn()); - } // else, the column is needed and is already in place - } else { - if (getLeaderboardTable().getColumn(rankColumnIndex) == getRankColumn()) { - removeColumn(rankColumnIndex); - } - } - } else { - if (isShowRegattaRankColumn()) { - insertColumn(rankColumnIndex, getRankColumn()); - } - } - return indexOfNextColumn; - } - - /** - * @param selectionCheckboxColumnIndex - * the column index (0-based) where to put the selection checkbox column, if needed - * @return the column index (0-based) for the next column; will equal selectionCheckboxColumnIndex if - * the selection checkbox column is not {@link #showSelectionCheckbox supposed to be shown}, or one greater - * otherwise. - */ - private int ensureSelectionCheckboxColumn(int selectionCheckboxColumnIndex) { - final int indexOfNextColumn = selectionCheckboxColumnIndex + (showSelectionCheckbox ? 1 : 0); - if (getLeaderboardTable().getColumnCount() > selectionCheckboxColumnIndex) { - if (showSelectionCheckbox) { - if (getLeaderboardTable().getColumn(selectionCheckboxColumnIndex) != selectionCheckboxColumn) { - insertColumn(selectionCheckboxColumnIndex, selectionCheckboxColumn); - } // else, the column is needed and is already in place - } else { - if (getLeaderboardTable().getColumn(selectionCheckboxColumnIndex) == selectionCheckboxColumn) { - removeColumn(selectionCheckboxColumnIndex); - } - } - } else { - if (showSelectionCheckbox) { - insertColumn(selectionCheckboxColumnIndex, selectionCheckboxColumn); - } - } - return indexOfNextColumn; - } - - /** - * @return the 0-based index for the next column - */ - private int ensureSailIDAndCompetitorColumn(int columnIndexWhereToInsertTheNextColumn) { - if (isShowCompetitorSailId()) { - if (getLeaderboardTable().getColumnCount() <= columnIndexWhereToInsertTheNextColumn - || !(getLeaderboardTable() - .getColumn(columnIndexWhereToInsertTheNextColumn) instanceof SailIDColumn)) { - insertColumn(columnIndexWhereToInsertTheNextColumn, - new SailIDColumn(new CompetitorFetcher() { - @Override - public CompetitorDTO getCompetitor(LeaderboardRowDTO t) { - return t.competitor; - } - })); - } - columnIndexWhereToInsertTheNextColumn++; - } else { - if (getLeaderboardTable().getColumnCount() > columnIndexWhereToInsertTheNextColumn && getLeaderboardTable() - .getColumn(columnIndexWhereToInsertTheNextColumn) instanceof SailIDColumn) { - removeColumn(columnIndexWhereToInsertTheNextColumn); - } - } - if (isShowCompetitorFullName()) { - if (getLeaderboardTable().getColumnCount() <= columnIndexWhereToInsertTheNextColumn - || !(getLeaderboardTable() - .getColumn(columnIndexWhereToInsertTheNextColumn) instanceof CompetitorColumn)) { - insertColumn(columnIndexWhereToInsertTheNextColumn, createCompetitorColumn()); - } - columnIndexWhereToInsertTheNextColumn++; - } else { - if (getLeaderboardTable().getColumnCount() > columnIndexWhereToInsertTheNextColumn && getLeaderboardTable() - .getColumn(columnIndexWhereToInsertTheNextColumn) instanceof CompetitorColumn) { - removeColumn(columnIndexWhereToInsertTheNextColumn); - } - } - return columnIndexWhereToInsertTheNextColumn; - } - - protected CompetitorColumn createCompetitorColumn() { - return new CompetitorColumn(new CompetitorColumnBase(this, stringMessages, - new CompetitorFetcher() { - @Override - public CompetitorDTO getCompetitor(LeaderboardRowDTO t) { - return t.competitor; - } - })); - } - - private void ensureTotalsColumn() { - // add a totals column on the right - if (getLeaderboardTable().getColumnCount() == 0 || !(getLeaderboardTable() - .getColumn(getLeaderboardTable().getColumnCount() - 1) instanceof TotalNetPointssColumn)) { - addColumn(new TotalNetPointssColumn(TOTAL_COLUMN_STYLE)); - } - } - - private void ensureTotalRacesSailedColumn() { - // add a totals column on the right - if (getLeaderboardTable().getColumnCount() == 0 || !(getLeaderboardTable() - .getColumn(getLeaderboardTable().getColumnCount() - 2) instanceof TotalRacesCompletedColumn)) { - insertColumn(getLeaderboardTable().getColumnCount() - 1, new TotalRacesCompletedColumn(TOTAL_COLUMN_STYLE)); - } - } - - private void ensureNoTotalRacesSailedColumn() { - if ((getLeaderboardTable() - .getColumn(getLeaderboardTable().getColumnCount() - 2) instanceof TotalRacesCompletedColumn)) { - removeColumn(getLeaderboardTable().getColumnCount() - 2); - } - } - - protected void updateTotalRacesSailedColumn() { - final boolean showTotalRacesCompletedColumn = isShowOverallColumnWithNumberOfRacesCompletedPerCompetitor(); - if (showTotalRacesCompletedColumn) { - ensureTotalRacesSailedColumn(); - } else { - ensureNoTotalRacesSailedColumn(); - } - } - - /** - * If the leaderboard {@link LeaderboardDTO#hasCarriedPoints has carried points} and if column #1 - * (second column, right of the competitor column) does not exist or is not of type {@link CarryColumn}, all columns - * starting from #1 will be removed and a {@link CarryColumn} will be added. If the leaderboard has no carried - * points but the display still shows a carry column, the column is removed. - * - * @param zeroBasedIndexOfCarryColumn - * the 0-based column index where to put the carry column, if any - * @return the 0-based index of the next column following this column if the carry column is to be shown, or - * zeroBasedIndexOfCarryColumn otherwise - */ - protected int updateCarryColumn(LeaderboardDTO leaderboard, int zeroBasedIndexOfCarryColumn) { - final boolean needsCarryColumn = leaderboard != null && leaderboard.hasCarriedPoints; - if (needsCarryColumn) { - ensureCarryColumn(zeroBasedIndexOfCarryColumn); - } else { - ensureNoCarryColumn(zeroBasedIndexOfCarryColumn); - } - return needsCarryColumn ? zeroBasedIndexOfCarryColumn + 1 : zeroBasedIndexOfCarryColumn; - } - - private void ensureNoCarryColumn(int zeroBasedIndexOfCarryColumn) { - if (getLeaderboardTable().getColumnCount() > zeroBasedIndexOfCarryColumn - && getLeaderboardTable().getColumn(zeroBasedIndexOfCarryColumn) instanceof CarryColumn) { - removeColumn(zeroBasedIndexOfCarryColumn); - } - } - - protected void ensureCarryColumn(int zeroBasedIndexOfCarryColumn) { - if (getLeaderboardTable().getColumnCount() <= zeroBasedIndexOfCarryColumn - || !(getLeaderboardTable().getColumn(zeroBasedIndexOfCarryColumn) instanceof CarryColumn)) { - while (getLeaderboardTable().getColumnCount() > zeroBasedIndexOfCarryColumn) { - removeColumn(zeroBasedIndexOfCarryColumn); - } - addColumn(createCarryColumn()); - } - } - - protected CarryColumn createCarryColumn() { - return new CarryColumn(); - } - - public SortedCellTable getLeaderboardTable() { - return leaderboardTable; - } - - public SailingServiceAsync getSailingService() { - return sailingService; - } - - protected String getLeaderboardName() { - return leaderboardName; - } - - protected void setLeaderboardName(String leaderboardName) { - this.leaderboardName = leaderboardName; - } - - protected ErrorReporter getErrorReporter() { - return errorReporter; - } - - protected ListDataProvider getData() { - return getLeaderboardTable().getDataProvider(); - } - - /** - * @param newTime - * ignored; may be null. The time for loading the leaderboard is determined using - * {@link #getLeaderboardDisplayDate()}. - */ - @Override - public void timeChanged(Date newTime, Date oldTime) { - loadCompleteLeaderboard(/* showProgress */ false); - } - - @Override - public void playStateChanged(PlayStates playState, PlayModes playMode) { - if (!isEmbedded) { - playPause.setHTML(getPlayPauseImgHtml(playState)); - playPause.setTitle(playState == PlayStates.Playing ? stringMessages.pauseAutomaticRefresh() - : stringMessages.autoRefresh()); - } - } - - @Override - public void playSpeedFactorChanged(double newPlaySpeedFactor) { - // nothing to do - } - - private List getTrackedRacesIdentifiers() { - List result = new ArrayList(); - for (RaceColumnDTO raceColumn : getLeaderboard().getRaceList()) { - for (FleetDTO fleet : raceColumn.getFleets()) { - if (raceColumn.getRaceIdentifier(fleet) != null) { - result.add(raceColumn.getRaceIdentifier(fleet)); - } - } - } - return result; - } - - @Override - public boolean hasSettings() { - return true; - } - - @Override - public boolean hasToolbar() { - return false; - } - - @Override - public SettingsDialogComponent getSettingsDialogComponent() { - return new LeaderboardSettingsDialogComponent(getSettings(), leaderboard.getNamesOfRaceColumns(), - stringMessages); - } - - @Override - public LeaderboardSettings getSettings() { - Iterable selectedRaceColumns = raceColumnSelection - .getSelectedRaceColumnsOrderedAsInLeaderboard(leaderboard); - List namesOfRaceColumnsToShow = new ArrayList<>(); - for (RaceColumnDTO raceColumn : selectedRaceColumns) { - namesOfRaceColumnsToShow.add(raceColumn.getName()); - } - LeaderboardSettings leaderboardSettings = new LeaderboardSettings( - Collections.unmodifiableList(selectedManeuverDetails), Collections.unmodifiableList(selectedLegDetails), - Collections.unmodifiableList(selectedRaceDetails), - Collections.unmodifiableList(selectedOverallDetailColumns), namesOfRaceColumnsToShow, - /* namesOfRacesToShow */ null, raceColumnSelection.getNumberOfLastRaceColumnsToShow(), - autoExpandPreSelectedRace, timer.getRefreshInterval(), /* nameOfRaceToSort */ null, - /* sortAscending */ true, /* updateUponPlayStateChange */ true, raceColumnSelection.getType(), - isShowAddedScores(), isShowOverallColumnWithNumberOfRacesCompletedPerCompetitor(), - isShowCompetitorSailId(), isShowCompetitorFullName()); - SettingsDefaultValuesUtils.keepDefaults(currentSettings, leaderboardSettings); - return leaderboardSettings; - } - - @Override - public String getLocalizedShortName() { - return stringMessages.leaderboard(); - } - - private LeaderboardRowDTO getRow(String competitorIdAsString) { - synchronized (getData().getList()) { - for (LeaderboardRowDTO row : getData().getList()) { - if (row.competitor.getIdAsString().equals(competitorIdAsString)) { - return row; - } - } - } - return null; - } - - @Override - public void addedToSelection(CompetitorDTO competitor) { - LeaderboardRowDTO row = getRow(competitor.getIdAsString()); - if (row != null) { - leaderboardSelectionModel.setSelected(row, true); - } - } - - @Override - public void removedFromSelection(CompetitorDTO competitor) { - LeaderboardRowDTO row = getRow(competitor.getIdAsString()); - if (row != null) { - leaderboardSelectionModel.setSelected(row, false); - } - } - - private Iterable getSelectedRows() { - return leaderboardSelectionModel.getSelectedSet(); - } - - @Override - public void competitorsListChanged(Iterable competitors) { - setFilterControlStatus(); - if (timer.isInitialized()) { - timeChanged(timer.getTime(), null); - } - } - - @Override - public void filteredCompetitorsListChanged(Iterable filteredCompetitors) { - setFilterControlStatus(); - updateLeaderboard(getLeaderboard()); - } - - @Override - public void filterChanged(FilterSet> oldFilterSet, - FilterSet> newFilterSet) { - // nothing to do; if the list of filtered competitors has changed, a separate call to - // filteredCompetitorsListChanged will occur - setFilterControlStatus(); - } - - public RaceColumnSelection getRaceColumnSelection() { - return raceColumnSelection; - } - - public void removeAllListeners() { - if (raceTimesInfoProviderListener != null) { - getRaceTimesInfoProvider().removeRaceTimesInfoProviderListener(raceTimesInfoProviderListener); - } - if (raceColumnSelection != null && raceColumnSelection.getType() == RaceColumnSelectionStrategies.LAST_N) { - getRaceTimesInfoProvider() - .removeRaceTimesInfoProviderListener((LastNRacesColumnSelection) raceColumnSelection); - } - if (timer != null) { - timer.removeTimeListener(this); - } - if (leaderboardUpdateListener != null) { - leaderboardUpdateListener.clear(); - } - } - - protected Timer getTimer() { - return timer; - } - - protected void blurFocusedElementAfterSelectionChange() { - // now "blur" the selected leaderboard element because it seems to cause the cell table to scroll to its top; - // see bug 2093. - blur(); - final ScheduledCommand blurCommand = new ScheduledCommand() { - @Override - public void execute() { - blur(); - } - }; - Scheduler.get().scheduleDeferred(blurCommand); - } - - private void blur() { - if (elementToBlur != null) { - elementToBlur.blur(); - } - } - - @Override - public void setVisible(boolean visible) { - super.setVisible(visible); - if (visible) { - timeChanged(getLeaderboardDisplayDate(), null); - } - } - - @Override - public void addLeaderboardUpdateListener(LeaderboardUpdateListener listener) { - this.leaderboardUpdateListener.add(listener); - } - - @Override - public void removeLeaderboardUpdateListener(LeaderboardUpdateListener listener) { - this.leaderboardUpdateListener.remove(listener); - } - - protected void informLeaderboardUpdateListenersAboutLeaderboardUpdated(LeaderboardDTO leaderboard) { - for (LeaderboardUpdateListener listener : new ArrayList( - this.leaderboardUpdateListener)) { - listener.updatedLeaderboard(leaderboard); - } - } - - protected void informLeaderboardUpdateListenersAboutRaceSelected(RaceIdentifier raceIdentifier, - RaceColumnDTO raceColumn) { - for (LeaderboardUpdateListener listener : this.leaderboardUpdateListener) { - listener.currentRaceSelected(raceIdentifier, raceColumn); - } - } - - public boolean hasLiveRace() { - return getLeaderboard().hasLiveRace(timer.getLiveTimePointInMillis()); - } - - public String getLiveRacesText() { - String result = ""; - List> liveRaces = leaderboard.getLiveRaces(timer.getLiveTimePointInMillis()); - boolean isMeta = leaderboard.type.isMetaLeaderboard(); - if (!liveRaces.isEmpty()) { - if (liveRaces.size() == 1) { - String text = getLiveRaceText(liveRaces.get(0), isMeta); - result = isMeta ? stringMessages.regattaIsLive(text) : stringMessages.raceIsLive(text); - } else { - String names = ""; - for (Pair liveRace : liveRaces) { - names += getLiveRaceText(liveRace, isMeta) + ", "; - } - // remove last ", " - names = names.substring(0, names.length() - 2); - result = isMeta ? stringMessages.regattasAreLive(names) : stringMessages.racesAreLive(names); - } - } - return result; - } - - private String getLiveRaceText(Pair liveRace, boolean isMeta) { - String raceName = liveRace.getA().getRaceColumnName(), fleetName = liveRace.getB().getName(); - boolean isDefaultFleet = LeaderboardNameConstants.DEFAULT_FLEET_NAME.equals(fleetName); - return raceName + ((isDefaultFleet || isMeta) ? "" : (" (" + liveRace.getB().getName() + ")")); - } - - @Override - public String getDependentCssClassName() { - return "leaderboard"; - } - - @Override - public void addBusyStateChangeListener(BusyStateChangeListener listener) { - busyStateChangeListeners.add(listener); - } - - @Override - public void removeBusyStateChangeListener(BusyStateChangeListener listener) { - busyStateChangeListeners.remove(listener); - } - - @Override - public boolean isBusy() { - return busyIndicator.isBusy(); - } - - private void setBusyState(boolean isBusy) { - if (busyIndicator.isBusy() != isBusy) { - busyIndicator.setBusy(isBusy); - for (BusyStateChangeListener listener : busyStateChangeListeners) { - listener.onBusyStateChange(isBusy); - } - } - } - - @Override - public void addBusyTask() { - busyTaskCounter++; - if (busyTaskCounter == 1) { - setBusyState(true); - } - } - - @Override - public void removeBusyTask() { - busyTaskCounter--; - if (busyTaskCounter == 0) { - setBusyState(false); - } - } - - @Override - public String getId() { - return LeaderboardPanelLifecycle.ID; - } - -} +package com.sap.sailing.gwt.ui.leaderboard; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import com.google.gwt.cell.client.AbstractSafeHtmlCell; +import com.google.gwt.cell.client.Cell; +import com.google.gwt.cell.client.Cell.Context; +import com.google.gwt.cell.client.CompositeCell; +import com.google.gwt.cell.client.TextCell; +import com.google.gwt.core.client.GWT; +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.google.gwt.dom.client.BrowserEvents; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.Style.FontWeight; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.i18n.client.NumberFormat; +import com.google.gwt.resources.client.ImageResource; +import com.google.gwt.safecss.shared.SafeStyles; +import com.google.gwt.safecss.shared.SafeStylesBuilder; +import com.google.gwt.safehtml.client.SafeHtmlTemplates; +import com.google.gwt.safehtml.shared.SafeHtml; +import com.google.gwt.safehtml.shared.SafeHtmlBuilder; +import com.google.gwt.safehtml.shared.SafeHtmlUtils; +import com.google.gwt.text.shared.AbstractSafeHtmlRenderer; +import com.google.gwt.user.cellview.client.Column; +import com.google.gwt.user.cellview.client.Header; +import com.google.gwt.user.cellview.client.SafeHtmlHeader; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.gwt.user.client.ui.AbstractImagePrototype; +import com.google.gwt.user.client.ui.Anchor; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.DockPanel; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.HasHorizontalAlignment; +import com.google.gwt.user.client.ui.HasVerticalAlignment; +import com.google.gwt.user.client.ui.HorizontalPanel; +import com.google.gwt.user.client.ui.ImageResourceRenderer; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.SimplePanel; +import com.google.gwt.user.client.ui.Widget; +import com.google.gwt.view.client.CellPreviewEvent; +import com.google.gwt.view.client.ListDataProvider; +import com.google.gwt.view.client.MultiSelectionModel; +import com.google.gwt.view.client.SelectionChangeEvent; +import com.google.gwt.view.client.SelectionChangeEvent.Handler; +import com.sap.sailing.domain.common.DetailType; +import com.sap.sailing.domain.common.Distance; +import com.sap.sailing.domain.common.InvertibleComparator; +import com.sap.sailing.domain.common.LeaderboardNameConstants; +import com.sap.sailing.domain.common.MaxPointsReason; +import com.sap.sailing.domain.common.Mile; +import com.sap.sailing.domain.common.RaceIdentifier; +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.domain.common.SortingOrder; +import com.sap.sailing.domain.common.Tack; +import com.sap.sailing.domain.common.dto.AbstractLeaderboardDTO; +import com.sap.sailing.domain.common.dto.CompetitorDTO; +import com.sap.sailing.domain.common.dto.FleetDTO; +import com.sap.sailing.domain.common.dto.IncrementalOrFullLeaderboardDTO; +import com.sap.sailing.domain.common.dto.LeaderboardDTO; +import com.sap.sailing.domain.common.dto.LeaderboardEntryDTO; +import com.sap.sailing.domain.common.dto.LeaderboardRowDTO; +import com.sap.sailing.domain.common.dto.LegEntryDTO; +import com.sap.sailing.domain.common.dto.RaceColumnDTO; +import com.sap.sailing.domain.common.impl.InvertibleComparatorAdapter; +import com.sap.sailing.gwt.settings.client.leaderboard.LeaderboardPanelLifecycle; +import com.sap.sailing.gwt.settings.client.leaderboard.LeaderboardSettings; +import com.sap.sailing.gwt.settings.client.leaderboard.LeaderboardSettings.RaceColumnSelectionStrategies; +import com.sap.sailing.gwt.settings.client.leaderboard.LeaderboardSettingsDialogComponent; +import com.sap.sailing.gwt.ui.actions.GetLeaderboardByNameAction; +import com.sap.sailing.gwt.ui.client.Collator; +import com.sap.sailing.gwt.ui.client.CompetitorSelectionChangeListener; +import com.sap.sailing.gwt.ui.client.CompetitorSelectionProvider; +import com.sap.sailing.gwt.ui.client.FlagImageResolver; +import com.sap.sailing.gwt.ui.client.LeaderboardUpdateListener; +import com.sap.sailing.gwt.ui.client.LeaderboardUpdateProvider; +import com.sap.sailing.gwt.ui.client.RaceTimesInfoProvider; +import com.sap.sailing.gwt.ui.client.RaceTimesInfoProviderListener; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.client.shared.controls.AbstractSortableColumnWithMinMax; +import com.sap.sailing.gwt.ui.client.shared.controls.FlushableSortedCellTableWithStylableHeaders; +import com.sap.sailing.gwt.ui.client.shared.controls.SelectionCheckboxColumn; +import com.sap.sailing.gwt.ui.client.shared.filter.CompetitorRaceRankFilter; +import com.sap.sailing.gwt.ui.client.shared.filter.FilterWithUI; +import com.sap.sailing.gwt.ui.client.shared.filter.LeaderboardFetcher; +import com.sap.sailing.gwt.ui.common.client.DateAndTimeFormatterUtil; +import com.sap.sailing.gwt.ui.leaderboard.DetailTypeColumn.LegDetailField; +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.Util.Pair; +import com.sap.sse.common.filter.BinaryOperator; +import com.sap.sse.common.filter.Filter; +import com.sap.sse.common.filter.FilterSet; +import com.sap.sse.common.settings.util.SettingsDefaultValuesUtils; +import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.async.AsyncActionsExecutor; +import com.sap.sse.gwt.client.async.MarkedAsyncCallback; +import com.sap.sse.gwt.client.celltable.EntityIdentityComparator; +import com.sap.sse.gwt.client.controls.busyindicator.BusyIndicator; +import com.sap.sse.gwt.client.controls.busyindicator.BusyStateChangeListener; +import com.sap.sse.gwt.client.controls.busyindicator.BusyStateProvider; +import com.sap.sse.gwt.client.controls.busyindicator.SimpleBusyIndicator; +import com.sap.sse.gwt.client.panels.OverlayAssistantScrollPanel; +import com.sap.sse.gwt.client.player.PlayStateListener; +import com.sap.sse.gwt.client.player.TimeListener; +import com.sap.sse.gwt.client.player.Timer; +import com.sap.sse.gwt.client.player.Timer.PlayModes; +import com.sap.sse.gwt.client.player.Timer.PlayStates; +import com.sap.sse.gwt.client.shared.components.AbstractCompositeComponent; +import com.sap.sse.gwt.client.shared.components.Component; +import com.sap.sse.gwt.client.shared.components.ComponentResources; +import com.sap.sse.gwt.client.shared.components.IsEmbeddableComponent; +import com.sap.sse.gwt.client.shared.components.SettingsDialog; +import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; + +/** + * A leaderboard essentially consists of a table widget that in its columns displays the entries. + * + * @author Axel Uhl (D043530) + * + */ +public class LeaderboardPanel extends AbstractCompositeComponent + implements TimeListener, PlayStateListener, DisplayedLeaderboardRowsProvider, IsEmbeddableComponent, + CompetitorSelectionChangeListener, LeaderboardFetcher, BusyStateProvider, LeaderboardUpdateProvider { + private static final LeaderboardResources RESOURCES = GWT.create(LeaderboardResources.class); + private static final ComponentResources COMPONENT_RESOURCES = GWT.create(ComponentResources.class); + private static final LeaderboardTableResources TABLE_RESOURCES = GWT.create(LeaderboardTableResources.class); + + public static final String LOAD_LEADERBOARD_DATA_CATEGORY = "loadLeaderboardData"; + + protected static final NumberFormat scoreFormat = NumberFormat.getFormat("0.##"); + + private final SailingServiceAsync sailingService; + + private static String IS_LIVE_TEXT_COLOR = "#FF0000"; + private static String DEFAULT_TEXT_COLOR = "#000000"; + + private static final String STYLE_LEADERBOARD_CONTENT = "leaderboardContent"; + private static final String STYLE_LEADERBOARD_INFO = "leaderboardInfo"; + private static final String STYLE_LEADERBOARD_TOOLBAR = "leaderboardContent-toolbar"; + private static final String STYLE_LEADERBOARD_LIVE_RACE = "leaderboardContent-liverace"; + private static RaceColumnTemplate raceColumnTemplate = new RaceColumnTemplate(); + + static class RaceColumnTemplate { + private MyTemplate template = GWT.create(MyTemplate.class); + + interface MyTemplate extends SafeHtmlTemplates { + @SafeHtmlTemplates.Template("

") + SafeHtml styledDiv(SafeStyles style); + } + + /** + * Originally in the safehtml template: color:{0}; border-bottom: 3px solid {1}; + * + * @param textColor + * @param borderColor + * @return + */ + public SafeHtml cellFrameWithTextColorAndFleetBorder(String textColor, String borderColor) { + SafeStylesBuilder sb = new SafeStylesBuilder(); + sb.trustedColor(textColor); + sb.trustedNameAndValue("border-bottom", "3px solid " + borderColor); + return template.styledDiv(sb.toSafeStyles()); + } + + /** + * + * Originally in the safehtml template: color:{0}; + * + * @param textColor + * @return + */ + public SafeHtml cellFrameWithTextColor(String textColor) { + SafeStylesBuilder sb = new SafeStylesBuilder(); + sb.trustedColor(textColor); + return template.styledDiv(sb.toSafeStyles()); + } + } + + /** + * The leaderboard name is used to + * {@link SailingServiceAsync#getLeaderboardByName(String, java.util.Date, String[], boolean, String, com.google.gwt.user.client.rpc.AsyncCallback) + * obtain the leaderboard contents} from the server. It may change in case the leaderboard is renamed. + */ + + private LeaderboardSettings currentSettings; + + private String leaderboardName; + + private final ErrorReporter errorReporter; + + public final StringMessages stringMessages; + + private final FlushableSortedCellTableWithStylableHeaders leaderboardTable; + + private final MultiSelectionModel leaderboardSelectionModel; + + private LeaderboardDTO leaderboard; + + private final TotalRankColumn totalRankColumn; + + private final SelectionCheckboxColumn selectionCheckboxColumn; + + /** + * Passed to the {@link ManeuverCountRaceColumn}. Modifications to this list will modify the column's children list + * when updated the next time. + */ + private final List selectedManeuverDetails = new ArrayList();; + + /** + * Passed to the {@link LegColumn}. Modifications to this list will modify the column's children list when updated + * the next time. + */ + private final List selectedLegDetails = new ArrayList(); + + /** + * Passed to the {@link TextRaceColumn}. Modifications to this list will modify the column's children list when + * updated the next time. + */ + private final List selectedRaceDetails = new ArrayList(); + + private final List selectedOverallDetailColumns = new ArrayList();; + + private final Map> overallDetailColumnMap; + + private RaceColumnSelection raceColumnSelection; + + protected final String RACE_COLUMN_HEADER_STYLE = TABLE_RESOURCES.cellTableStyle().cellTableRaceColumnHeader(); + + protected final String LEG_DETAIL_COLUMN_HEADER_STYLE = TABLE_RESOURCES.cellTableStyle() + .cellTableLegDetailColumnHeader();; + + protected final String LEG_DETAIL_COLUMN_STYLE = TABLE_RESOURCES.cellTableStyle().cellTableLegDetailColumn(); + + protected final String LEG_COLUMN_HEADER_STYLE = TABLE_RESOURCES.cellTableStyle().cellTableLegColumnHeader(); + + protected final String RACE_COLUMN_STYLE = TABLE_RESOURCES.cellTableStyle().cellTableRaceColumn(); + + protected final String LEG_COLUMN_STYLE = TABLE_RESOURCES.cellTableStyle().cellTableLegColumn(); + + protected final String TOTAL_COLUMN_STYLE = TABLE_RESOURCES.cellTableStyle().cellTableTotalColumn(); + + private final Timer timer; + + /** + * A {@link LeaderboardDTO} tells something about the live delay through its + * {@link LeaderboardDTO#getDelayToLiveInMillisForLatestRace()} method. If this flag is true, the live + * delay in the {@link #timer} will be adjusted each time the {@link #updateLeaderboard(LeaderboardDTO)} method is + * invoked. Otherwise, the timer's delay will be left alone which is helpful, e.g., if the leaderboard panel is + * embedded in a race board panel that focuses on one particular race which may not be the same as the one + * controlling the leaderboard's overall live delay. + */ + private final boolean adjustTimerDelay; + + private boolean autoExpandPreSelectedRace; + + private boolean autoExpandLastRaceColumn; + + /** + * When true, the race columns don't display the competitors' scores in the race represented by the + * column but the cumulative score up to that race. + */ + private boolean showAddedScores; + + private boolean showCompetitorSailId; + private boolean showCompetitorFullName; + + /** + * When true then an additional column just before the overall points is displayed that sums up the + * number of races sailed per competitor. + */ + private boolean showOverallColumnWithNumberOfRacesCompletedPerCompetitor; + + /** + * Remembers whether the auto-expand of the pre-selected race (see {@link #autoExpandPreSelectedRace}) or last + * selected race {@link #autoExpandLastRaceColumn} has been performed once. It must not be performed another time. + */ + private boolean autoExpandPerformedOnce; + + /** + * This anchor's HTML holds the image tag for the play/pause button that needs to be updated when the {@link #timer} + * changes its playing state + */ + private Anchor playPause; + + private final CompetitorSelectionProvider competitorSelectionProvider; + private final HorizontalPanel filterControlPanel; + private Label filterStatusLabel; + private Button filterClearButton; + + /** + * The handler for changes in the leaderboard table's selection; its registration is kept in + * {@link #leaderboardAsTableSelectionModelRegistration} while it is registered as a selection handler on the + * {@link #leaderboardTable}. + */ + private final Handler selectionChangeHandler; + + /** + * While the {@link #selectionChangeHandler} is registered as a selection change handler on the + * {@link #leaderboardTable}'s selection model, this field holds the registration which can be used to remove the + * registration again. We'll use this to temporarily suspend selection events when actively modifying / adjusting + * the table selection to match the {@link #competitorSelectionProvider}. + */ + private HandlerRegistration leaderboardAsTableSelectionModelRegistration; + + /** + * If this is null, all leaderboard columns added by updating the leaderboard from the server are + * automatically added to the table. Otherwise, only the column whose {@link RaceColumnDTO#getRaceIdentifier(String) + * race identifier} matches the value of this attribute will be added. + */ + private final RegattaAndRaceIdentifier preSelectedRace; + + private final FlowPanel contentPanel = new FlowPanel(); + + private HorizontalPanel refreshAndSettingsPanel; + private Label scoreCorrectionLastUpdateTimeLabel; + private Label scoreCorrectionCommentLabel; + private Label liveRaceLabel; + + private boolean isEmbedded; + + private ImageResource pauseIcon; + private ImageResource playIcon; + + /** + * For a leaderboard, zero or more tasks may be currently busy. The counter keeps track. If it goes to {@code 0}, + * the {@link #busyIndicator} is set to non-busy. If it goes from {@code 0} to {@code 1} the {@link #busyIndicator} + * is set to busy. + */ + private int busyTaskCounter; + private final BusyIndicator busyIndicator; + private final Set busyStateChangeListeners; + + private final AsyncActionsExecutor asyncActionsExecutor; + + /** + * See also {@link #getDefaultSortColumn()}. If no other column is explicitly selected for sorting and this + * attribute holds a non-null string identifying a valid race by name that is represented in this + * leaderboard panel then sort by it. Otherwise, default sorting will default to the overall rank column. + */ + private String raceNameForDefaultSorting; + + /** + * Can be used to disallow users to drill into the race details. + */ + private final boolean showRaceDetails; + + /** + * The {@link LastNRacesColumnSelection} column selection strategy requires a {@link RaceTimesInfoProvider}. This + * can either be injected by passing a non-null object of that type to the constructor, or such an + * object is created and remembered in this attribute when required the first time. + */ + private RaceTimesInfoProvider raceTimesInfoProvider; + private RaceTimesInfoProviderListener raceTimesInfoProviderListener; + + private int blurInOnSelectionChanged; + + /** + * When an element in the leaderboard receives focus, it needs to be blurred again to keep the surrounding scroll + * panel from scrolling anything into view + */ + private Element elementToBlur; + private boolean showSelectionCheckbox; + + private final List leaderboardUpdateListener = new ArrayList(); + + private boolean initialCompetitorFilterHasBeenApplied = false; + private final boolean showCompetitorFilterStatus; + + private CompetitorFilterPanel competitorFilterPanel; + + /** + * Whether or not a second scroll bar, synchronized with the invisible native scroll bar, shall appear at the bottom + * of the viewport. See {@link OverlayAssistantScrollPanel}. + */ + private final boolean enableSyncedScroller; + + public LeaderboardPanel(Component parent, ComponentContext context, SailingServiceAsync sailingService, + AsyncActionsExecutor asyncActionsExecutor, LeaderboardSettings settings, + CompetitorSelectionProvider competitorSelectionProvider, String leaderboardName, + ErrorReporter errorReporter, final StringMessages stringMessages, boolean showRaceDetails) { + this(parent, context, sailingService, asyncActionsExecutor, settings, false, /* preSelectedRace */null, + competitorSelectionProvider, null, leaderboardName, errorReporter, stringMessages, showRaceDetails); + } + + public LeaderboardPanel(Component parent, ComponentContext context, SailingServiceAsync sailingService, + AsyncActionsExecutor asyncActionsExecutor, LeaderboardSettings settings, boolean isEmbedded, + RegattaAndRaceIdentifier preSelectedRace, CompetitorSelectionProvider competitorSelectionProvider, + String leaderboardGroupName, String leaderboardName, ErrorReporter errorReporter, + final StringMessages stringMessages, boolean showRaceDetails) { + this(parent, context, sailingService, asyncActionsExecutor, settings, isEmbedded, preSelectedRace, + competitorSelectionProvider, + new Timer( + // perform the first request as "live" but don't by default auto-play + PlayModes.Live, PlayStates.Paused, + /* delayBetweenAutoAdvancesInMilliseconds */ LeaderboardEntryPoint.DEFAULT_REFRESH_INTERVAL_MILLIS), + leaderboardGroupName, leaderboardName, errorReporter, stringMessages, showRaceDetails, + /* competitorSearchTextBox */ null, /* showSelectionCheckbox */ true, + /* optionalRaceTimesInfoProvider */ null, /* autoExpandLastRaceColumn */ false, + /* adjustTimerDelay */ true, /* autoApplyTopNFilter */ false, /* showCompetitorFilterStatus */ false, + /* enableSyncScroller */ false); + } + + public LeaderboardPanel(Component parent, ComponentContext context, SailingServiceAsync sailingService, + AsyncActionsExecutor asyncActionsExecutor, LeaderboardSettings settings, boolean isEmbedded, + RegattaAndRaceIdentifier preSelectedRace, CompetitorSelectionProvider competitorSelectionProvider, + Timer timer, String leaderboardGroupName, String leaderboardName, final ErrorReporter errorReporter, + final StringMessages stringMessages, boolean showRaceDetails, CompetitorFilterPanel competitorSearchTextBox, + boolean showSelectionCheckbox, RaceTimesInfoProvider optionalRaceTimesInfoProvider, + boolean autoExpandLastRaceColumn, boolean adjustTimerDelay, boolean autoApplyTopNFilter, + boolean showCompetitorFilterStatus, boolean enableSyncScroller) { + super(parent, context); + + this.showSelectionCheckbox = showSelectionCheckbox; + this.showRaceDetails = showRaceDetails; + this.sailingService = sailingService; + this.asyncActionsExecutor = asyncActionsExecutor; + this.isEmbedded = isEmbedded; + this.preSelectedRace = preSelectedRace; + this.competitorSelectionProvider = competitorSelectionProvider; + competitorSelectionProvider.addCompetitorSelectionChangeListener(this); + this.setLeaderboardName(leaderboardName); + this.errorReporter = errorReporter; + this.stringMessages = stringMessages; + this.raceTimesInfoProvider = optionalRaceTimesInfoProvider; + this.adjustTimerDelay = adjustTimerDelay; + this.initialCompetitorFilterHasBeenApplied = !autoApplyTopNFilter; + this.showCompetitorFilterStatus = showCompetitorFilterStatus; + this.enableSyncedScroller = enableSyncScroller; + this.autoExpandLastRaceColumn = autoExpandLastRaceColumn; + this.timer = timer; + overallDetailColumnMap = createOverallDetailColumnMap(); + raceNameForDefaultSorting = settings.getNameOfRaceToSort(); + + if (settings.getLegDetailsToShow() != null) { + selectedLegDetails.addAll(settings.getLegDetailsToShow()); + } + if (settings.getOverallDetailsToShow() != null) { + selectedOverallDetailColumns.addAll(settings.getOverallDetailsToShow()); + } + timer.addPlayStateListener(this); + timer.addTimeListener(this); + + switch (settings.getActiveRaceColumnSelectionStrategy()) { + case EXPLICIT: + if (preSelectedRace == null) { + raceColumnSelection = new ExplicitRaceColumnSelection(); + } else { + raceColumnSelection = new ExplicitRaceColumnSelectionWithPreselectedRace(preSelectedRace); + } + break; + case LAST_N: + setRaceColumnSelectionToLastNStrategy(settings.getNumberOfLastRacesToShow()); + break; + } + + totalRankColumn = new TotalRankColumn(); + leaderboardTable = new FlushableSortedCellTableWithStylableHeaders(/* pageSize */10000, + TABLE_RESOURCES); + leaderboardTable.addCellPreviewHandler(new CellPreviewEvent.Handler() { + @Override + public void onCellPreview(CellPreviewEvent event) { + if (BrowserEvents.FOCUS.equals(event.getNativeEvent().getType())) { + elementToBlur = event.getNativeEvent().getEventTarget().cast(); + elementToBlur.blur(); + blurInOnSelectionChanged = 2; // blur a couple of times; doing it one time only doesn't seem to work + // reliably + blurFocusedElementAfterSelectionChange(); + } + } + }); + leaderboardTable.ensureDebugId("LeaderboardCellTable"); + selectionCheckboxColumn = new LeaderboardSelectionCheckboxColumn(competitorSelectionProvider); + leaderboardTable.setWidth("100%"); + leaderboardSelectionModel = new MultiSelectionModel(); + // remember handler registration so we can temporarily remove it and re-add it to suspend selection events while + // we're actively changing it + selectionChangeHandler = new Handler() { + @Override + public void onSelectionChange(SelectionChangeEvent event) { + List selection = new ArrayList(); + for (LeaderboardRowDTO row : getSelectedRows()) { + selection.add(row.competitor); + } + LeaderboardPanel.this.competitorSelectionProvider.setSelection(selection, + /* listenersNotToNotify */LeaderboardPanel.this); + if (blurInOnSelectionChanged > 0) { + blurInOnSelectionChanged--; + blurFocusedElementAfterSelectionChange(); + } + } + }; + leaderboardAsTableSelectionModelRegistration = leaderboardSelectionModel + .addSelectionChangeHandler(selectionChangeHandler); + leaderboardTable.setSelectionModel(leaderboardSelectionModel, selectionCheckboxColumn.getSelectionManager()); + + SimplePanel mainPanel = new SimplePanel(); + leaderboardTable.getElement().getStyle().setMarginTop(10, Unit.PX); + contentPanel.setStyleName(STYLE_LEADERBOARD_CONTENT); + busyIndicator = new SimpleBusyIndicator(false, 0.8f); + busyIndicator.ensureDebugId("BusyIndicator"); + busyStateChangeListeners = new HashSet<>(); + + // the information panel + if (!isEmbedded) { + Widget toolbarPanel = createToolbarPanel(); + contentPanel.add(toolbarPanel); + } + if (competitorSearchTextBox != null) { + competitorSearchTextBox.add(busyIndicator); + contentPanel.add(competitorSearchTextBox); + competitorSearchTextBox.getSettingsButton().addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + new SettingsDialog(LeaderboardPanel.this, stringMessages).show(); + } + }); + this.competitorFilterPanel = competitorSearchTextBox; + } + filterControlPanel = new HorizontalPanel(); + filterControlPanel.setStyleName("LeaderboardPanel-FilterControl-Panel"); + if (enableSyncedScroller) { + contentPanel.add(new OverlayAssistantScrollPanel(leaderboardTable)); + } else { + contentPanel.add(leaderboardTable); + } + if (showCompetitorFilterStatus) { + contentPanel.add(createFilterDeselectionControl()); + } + initWidget(mainPanel); + mainPanel.setWidget(contentPanel); + this.setTitle(stringMessages.leaderboard()); + if (timer.isInitialized()) { + loadCompleteLeaderboard(/* showProgress */ false); + } + + updateSettings(settings); + } + + private Widget createToolbarPanel() { + FlowPanel informationPanel = new FlowPanel(); + informationPanel.setStyleName(STYLE_LEADERBOARD_INFO); + scoreCorrectionLastUpdateTimeLabel = new Label(""); + scoreCorrectionCommentLabel = new Label(""); + informationPanel.add(scoreCorrectionCommentLabel); + informationPanel.add(scoreCorrectionLastUpdateTimeLabel); + + liveRaceLabel = new Label(stringMessages.live()); + liveRaceLabel.setStyleName(STYLE_LEADERBOARD_LIVE_RACE); + liveRaceLabel.getElement().getStyle().setFontWeight(FontWeight.BOLD); + liveRaceLabel.getElement().getStyle().setColor(IS_LIVE_TEXT_COLOR); + liveRaceLabel.setVisible(false); + informationPanel.add(liveRaceLabel); + + // the toolbar panel + DockPanel toolbarPanel = new DockPanel(); + toolbarPanel.ensureDebugId("ToolbarPanel"); + toolbarPanel.setStyleName(STYLE_LEADERBOARD_TOOLBAR); + if (!isEmbedded) { + toolbarPanel.add(informationPanel, DockPanel.WEST); + toolbarPanel.add(busyIndicator, DockPanel.WEST); + } + toolbarPanel.setWidth("100%"); + toolbarPanel.setVerticalAlignment(HasVerticalAlignment.ALIGN_MIDDLE); + ClickHandler playPauseHandler = new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + if (LeaderboardPanel.this.timer.getPlayState() == PlayStates.Playing) { + LeaderboardPanel.this.timer.pause(); + } else { + // playing the standalone leaderboard means putting it into live mode + LeaderboardPanel.this.timer.setPlayMode(PlayModes.Live); + } + } + }; + pauseIcon = RESOURCES.autoRefreshEnabledIcon(); + playIcon = RESOURCES.autoRefreshDisabledIcon(); + refreshAndSettingsPanel = new HorizontalPanel(); + refreshAndSettingsPanel.ensureDebugId("RefreshAndSettingsPanel"); + refreshAndSettingsPanel.setVerticalAlignment(HasVerticalAlignment.ALIGN_MIDDLE); + FlowPanel refreshPanel = new FlowPanel(); + refreshPanel.addStyleName("refreshPanel"); + toolbarPanel.setHorizontalAlignment(HasHorizontalAlignment.ALIGN_RIGHT); + toolbarPanel.addStyleName("refreshAndSettings"); + playPause = new Anchor(getPlayPauseImgHtml(timer.getPlayState())); + playPause.ensureDebugId("PlayAndPauseAnchor"); + playPause.addClickHandler(playPauseHandler); + playStateChanged(timer.getPlayState(), timer.getPlayMode()); + refreshPanel.add(playPause); + + refreshAndSettingsPanel.add(refreshPanel); + toolbarPanel.add(refreshAndSettingsPanel, DockPanel.EAST); + return toolbarPanel; + } + + @Override + public Widget getEntryWidget() { + return this; + } + + @Override + public Widget getHeaderWidget() { + return null; + } + + @Override + public Widget getContentWidget() { + return contentPanel; + } + + @Override + public Widget getToolbarWidget() { + return null; + } + + @Override + public Widget getLegendWidget() { + return null; + } + + @Override + public boolean isEmbedded() { + return isEmbedded; + } + + public FlowPanel getContentPanel() { + return contentPanel; + } + + protected ImageResource getSettingsIcon() { + return COMPONENT_RESOURCES.settingsIcon(); + } + + public void updateSettings(final LeaderboardSettings newSettings) { + this.currentSettings = newSettings; + boolean oldShallAddOverallDetails = shallAddOverallDetails(); + if (newSettings.getOverallDetailsToShow() != null) { + selectedOverallDetailColumns.clear(); + selectedOverallDetailColumns.addAll(newSettings.getOverallDetailsToShow()); + } + setShowAddedScores(newSettings.isShowAddedScores()); + setShowCompetitorSailId(newSettings.isShowCompetitorSailIdColumn()); + setShowCompetitorFullName(newSettings.isShowCompetitorFullNameColumn()); + setShowOverallColumnWithNumberOfRacesCompletedPerCompetitor( + newSettings.isShowOverallColumnWithNumberOfRacesCompletedPerCompetitor()); + final List> columnsToExpandAgain = new ArrayList>(); + for (int i = 0; i < getLeaderboardTable().getColumnCount(); i++) { + Column c = getLeaderboardTable().getColumn(i); + if (c instanceof ExpandableSortableColumn) { + ExpandableSortableColumn expandableSortableColumn = (ExpandableSortableColumn) c; + if (expandableSortableColumn.isExpanded()) { + // now toggle expansion back and forth, + // enforcing a re-build of the visible + // child columns + expandableSortableColumn.changeExpansionState(/* expand */ false); + columnsToExpandAgain.add(expandableSortableColumn); + } + } + } + if (newSettings.getManeuverDetailsToShow() != null) { + selectedManeuverDetails.clear(); + selectedManeuverDetails.addAll(newSettings.getManeuverDetailsToShow()); + } + if (newSettings.getLegDetailsToShow() != null) { + selectedLegDetails.clear(); + selectedLegDetails.addAll(newSettings.getLegDetailsToShow()); + } + if (newSettings.getRaceDetailsToShow() != null) { + selectedRaceDetails.clear(); + selectedRaceDetails.addAll(newSettings.getRaceDetailsToShow()); + } + + addBusyTask(); + Runnable doWhenNecessaryDetailHasBeenLoaded = new Runnable() { + @Override + public void run() { + try { + setAutoExpandPreSelectedRace(false); // avoid expansion during updateLeaderboard(...); will expand + // later + // if it was expanded before + // update leaderboard after settings panel column selection change + + // update strategy for determining the race columns to show; if settings' race columns to show is + // null, use the + // previously selected race columns / number of race columns for the new configuration + Iterable oldNamesOfRaceColumnsToShow = null; + if (newSettings.getNamesOfRaceColumnsToShow() == null) { + oldNamesOfRaceColumnsToShow = raceColumnSelection.getSelectedRaceColumnNames(); + } + switch (newSettings.getActiveRaceColumnSelectionStrategy()) { + case EXPLICIT: + if (preSelectedRace == null) { + raceColumnSelection = new ExplicitRaceColumnSelection(); + } else { + raceColumnSelection = new ExplicitRaceColumnSelectionWithPreselectedRace(preSelectedRace); + } + if (newSettings.getNamesOfRaceColumnsToShow() != null) { + raceColumnSelection.requestClear(); + for (String nameOfRaceColumnToShow : newSettings.getNamesOfRaceColumnsToShow()) { + RaceColumnDTO raceColumnToShow = getRaceByColumnName(nameOfRaceColumnToShow); + if (raceColumnToShow != null) { + raceColumnSelection.requestRaceColumnSelection(raceColumnToShow); + } + } + } else { + // apply the old column selections again + for (String oldNameOfRaceColumnToShow : oldNamesOfRaceColumnsToShow) { + final RaceColumnDTO raceColumnByName = getLeaderboard() + .getRaceColumnByName(oldNameOfRaceColumnToShow); + if (raceColumnByName != null) { + raceColumnSelection.requestRaceColumnSelection(raceColumnByName); + } + } + if (newSettings.getNamesOfRacesToShow() != null) { + raceColumnSelection.requestClear(); + for (String nameOfRaceToShow : newSettings.getNamesOfRacesToShow()) { + RaceColumnDTO raceColumnToShow = getRaceByName(nameOfRaceToShow); + if (raceColumnToShow != null) { + raceColumnSelection.requestRaceColumnSelection(raceColumnToShow); + } + } + } + } + break; + case LAST_N: + setRaceColumnSelectionToLastNStrategy(newSettings.getNumberOfLastRacesToShow()); + break; + } + + updateLeaderboard(leaderboard); + setAutoExpandPreSelectedRace(newSettings.isAutoExpandPreSelectedRace()); + + if (newSettings.getDelayBetweenAutoAdvancesInMilliseconds() != null) { + timer.setRefreshInterval(newSettings.getDelayBetweenAutoAdvancesInMilliseconds()); + } + for (ExpandableSortableColumn expandableSortableColumn : columnsToExpandAgain) { + expandableSortableColumn.changeExpansionState(/* expand */ true); + } + if (newSettings.getNameOfRaceToSort() != null) { + final RaceColumn raceColumnByRaceName = getRaceColumnByRaceName( + newSettings.getNameOfRaceToSort()); + if (raceColumnByRaceName != null) { + getLeaderboardTable().sortColumn(raceColumnByRaceName, /* ascending */true); + } + } + } finally { + removeBusyTask(); + } + } + }; + if (oldShallAddOverallDetails == shallAddOverallDetails() || oldShallAddOverallDetails + || getLeaderboard().hasOverallDetails()) { + doWhenNecessaryDetailHasBeenLoaded.run(); + } else { // meaning that now the details need to be loaded from the server + updateLeaderboardAndRun(doWhenNecessaryDetailHasBeenLoaded); + } + } + + /** + * @param callWhenExpansionDataIsLoaded + */ + private void updateLeaderboardAndRun(final Runnable callWhenExpansionDataIsLoaded) { + final LeaderboardDTO previousLeaderboard = getLeaderboard(); + getSailingService().getLeaderboardByName(getLeaderboardName(), + timer.getPlayMode() == PlayModes.Live ? null : getLeaderboardDisplayDate(), + /* namesOfRacesForWhichToLoadLegDetails */getNamesOfExpandedRaceColumns(), shallAddOverallDetails(), + previousLeaderboard.getId(), /* fillTotalPointsUncorrected */ false, + new MarkedAsyncCallback( + new AsyncCallback() { + @Override + public void onSuccess(IncrementalOrFullLeaderboardDTO result) { + updateLeaderboard(result.getLeaderboardDTO(previousLeaderboard)); + callWhenExpansionDataIsLoaded.run(); + } + + @Override + public void onFailure(Throwable caught) { + getErrorReporter().reportError( + stringMessages.errorTryingToObtainLeaderboardContents(caught.getMessage()), + true /* silentMode */); + } + })); + } + + public void setRaceColumnSelectionToLastNStrategy(final Integer numberOfLastRacesToShow) { + raceColumnSelection = new LastNRacesColumnSelection(numberOfLastRacesToShow, getRaceTimesInfoProvider()); + if (timer.getPlayState() != Timer.PlayStates.Playing) { + // wait for the first update and adjust leaderboard once the race times have been received + raceTimesInfoProviderListener = new RaceTimesInfoProviderListener() { + @Override + public void raceTimesInfosReceived(Map raceTimesInfo, + long clientTimeWhenRequestWasSent, Date serverTimeDuringRequest, + long clientTimeWhenResponseWasReceived) { + // remove + timer.adjustClientServerOffset(clientTimeWhenRequestWasSent, serverTimeDuringRequest, + clientTimeWhenResponseWasReceived); + // remove the listener only in case a leaderboard has already been loaded + if (getLeaderboard() != null) { + updateLeaderboard(getLeaderboard()); + getRaceTimesInfoProvider().removeRaceTimesInfoProviderListener(raceTimesInfoProviderListener); + raceTimesInfoProviderListener = null; + } + } + }; + getRaceTimesInfoProvider().addRaceTimesInfoProviderListener(raceTimesInfoProviderListener); + } + } + + /** + * A leaderboard panel may have been provided with a valid {@link RaceTimesInfoProvider} upon creation; in this + * case, that object will be returned. If none was provided to the constructor, one is created and remembered if no + * previously created/remembered object exists. + *

+ * + * Precondition: {@link #timer} is not null + */ + private RaceTimesInfoProvider getRaceTimesInfoProvider() { + if (raceTimesInfoProvider == null) { + final List trackedRacesIdentifiers; + if (leaderboard != null && getTrackedRacesIdentifiers() != null) { + trackedRacesIdentifiers = getTrackedRacesIdentifiers(); + } else { + trackedRacesIdentifiers = Collections.emptyList(); + } + raceTimesInfoProvider = new RaceTimesInfoProvider(getSailingService(), asyncActionsExecutor, errorReporter, + trackedRacesIdentifiers, timer.getRefreshInterval()); + } + return raceTimesInfoProvider; + } + + public AsyncActionsExecutor getExecutor() { + return asyncActionsExecutor; + } + + protected class CompetitorColumn extends LeaderboardSortableColumnWithMinMax { + private final CompetitorColumnBase base; + + protected CompetitorColumn(CompetitorColumnBase base) { + super(base.getCell(getLeaderboard()), SortingOrder.ASCENDING, LeaderboardPanel.this); + this.base = base; + } + + public CompetitorColumn(CompositeCell compositeCell, + CompetitorColumnBase base) { + super(compositeCell, SortingOrder.ASCENDING, LeaderboardPanel.this); + this.base = base; + } + + @Override + public InvertibleComparator getComparator() { + return new InvertibleComparatorAdapter() { + @Override + public int compare(LeaderboardRowDTO o1, LeaderboardRowDTO o2) { + return Collator.getInstance().compare(getLeaderboard().getDisplayName(o1.competitor), + getLeaderboard().getDisplayName(o2.competitor)); + } + }; + } + + @Override + public SafeHtmlHeader getHeader() { + return base.getHeader(); + } + + @Override + public LeaderboardRowDTO getValue(LeaderboardRowDTO object) { + return object; + } + + protected void defaultRender(Context context, LeaderboardRowDTO object, SafeHtmlBuilder sb) { + super.render(context, object, sb); + } + + @Override + public void render(Context context, LeaderboardRowDTO object, SafeHtmlBuilder sb) { + String competitorColor = LeaderboardPanel.this.competitorSelectionProvider + .getColor(object.competitor, LeaderboardPanel.this.preSelectedRace).getAsHtml(); + String competitorColorBarStyle; + if (LeaderboardPanel.this.isEmbedded && preSelectedRace != null) { + competitorColorBarStyle = "style=\"border-bottom: 2px solid " + competitorColor + ";\""; + } else { + competitorColorBarStyle = "style=\"border: none;\""; + } + base.render(object, competitorColorBarStyle, sb); + } + } + + /** + * Shows the country flag and sail ID, if present + * + * @author Axel Uhl (d043530) + * + */ + private class SailIDColumn extends LeaderboardSortableColumnWithMinMax { + private final CompetitorFetcher competitorFetcher; + + protected SailIDColumn(CompetitorFetcher competitorFetcher) { + super(new TextCell(), SortingOrder.ASCENDING, LeaderboardPanel.this); + this.competitorFetcher = competitorFetcher; + // This style is adding to avoid contained images CSS property "max-width: 100%", which could cause + // an overflow to the next column (see https://bugzilla.sapsailing.com/bugzilla/show_bug.cgi?id=3537) + setCellStyleNames(TABLE_RESOURCES.cellTableStyle().cellTableSailIdColumn()); + } + + @Override + public InvertibleComparator getComparator() { + return new InvertibleComparatorAdapter() { + @Override + public int compare(T o1, T o2) { + return competitorFetcher.getCompetitor(o1).getSailID() == null + ? competitorFetcher.getCompetitor(o2).getSailID() == null ? 0 : -1 + : competitorFetcher.getCompetitor(o2).getSailID() == null ? 1 + : Collator.getInstance().compare(competitorFetcher.getCompetitor(o1).getSailID(), + competitorFetcher.getCompetitor(o2).getSailID()); + } + }; + } + + @Override + public SafeHtmlHeader getHeader() { + return new SafeHtmlHeaderWithTooltip(SafeHtmlUtils.fromString(stringMessages.competitor()), + stringMessages.sailIdColumnTooltip()); + } + + @Override + public void render(Context context, T object, SafeHtmlBuilder sb) { + ImageResourceRenderer renderer = new ImageResourceRenderer(); + CompetitorDTO competitor = competitorFetcher.getCompetitor(object); + final String twoLetterIsoCountryCode = competitor.getTwoLetterIsoCountryCode(); + final String flagImageURL = competitor.getFlagImageURL(); + + boolean showBoatColor = !isShowCompetitorFullName() && LeaderboardPanel.this.isEmbedded + && preSelectedRace != null; + if (showBoatColor) { + String competitorColor = LeaderboardPanel.this.competitorSelectionProvider + .getColor(competitorFetcher.getCompetitor(object), LeaderboardPanel.this.preSelectedRace) + .getAsHtml(); + sb.appendHtmlConstant("

"); + } + + if (flagImageURL != null && !flagImageURL.isEmpty()) { + sb.appendHtmlConstant(""); + sb.appendHtmlConstant(" "); + } else { + final ImageResource flagImageResource; + if (twoLetterIsoCountryCode == null || twoLetterIsoCountryCode.isEmpty()) { + flagImageResource = FlagImageResolver.getEmptyFlagImageResource(); + } else { + flagImageResource = FlagImageResolver.getFlagImageResource(twoLetterIsoCountryCode); + } + if (flagImageResource != null) { + sb.append(renderer.render(flagImageResource)); + sb.appendHtmlConstant(" "); + } + } + sb.appendEscaped(competitor.getSailID()); + if (showBoatColor) { + sb.appendHtmlConstant("
"); + } + } + + @Override + public String getValue(T object) { + return competitorFetcher.getCompetitor(object).getSailID(); + } + } + + /** + * Displays net/total points and possible max-points reasons based on a {@link LeaderboardRowDTO} and a race name + * and makes the column sortable by the total points. + * + * @author Axel Uhl (D043530) + * + */ + protected abstract class RaceColumn extends ExpandableSortableColumn { + private RaceColumnDTO race; + + private final String headerStyle; + private final String columnStyle; + + public RaceColumn(RaceColumnDTO race, boolean enableExpansion, Cell cell, SortingOrder preferredSortingOrder, + String headerStyle, String columnStyle) { + super(LeaderboardPanel.this, enableExpansion, cell, preferredSortingOrder, stringMessages, + LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, selectedRaceDetails, LeaderboardPanel.this); + setHorizontalAlignment(ALIGN_CENTER); + this.race = race; + this.headerStyle = headerStyle; + this.columnStyle = columnStyle; + } + + public RaceColumnDTO getRace() { + return race; + } + + public void setRace(RaceColumnDTO race) { + this.race = race; + } + + public String getRaceColumnName() { + return race.getRaceColumnName(); + } + + public boolean isMedalRace() { + return race.isMedalRace(); + } + + public boolean isLive(FleetDTO fleetDTO) { + return race.isLive(fleetDTO, timer.getLiveTimePointInMillis()); + } + + @Override + public String getColumnStyle() { + return columnStyle; + } + + /** + * Computes added scores for this RaceColumn + */ + private double computeAddedScores(LeaderboardRowDTO object) { + double addedScores = 0; + for (RaceColumnDTO raceColumn : getLeaderboard().getRaceList()) { + LeaderboardEntryDTO entryBefore = object.fieldsByRaceColumnName.get(raceColumn.getName()); + if (entryBefore.netPoints != null) { + addedScores += entryBefore.netPoints; + } + // else "add 0" + if (raceColumn.getName().equals(getRaceColumnName())) { + break; // we've reached the current column - stop here + } + } + return addedScores; + } + + /** + * Displays a combination of net points and maxPointsReason in bold, transparent, strike-through, depending on + * various criteria. Here's how: + * + *
+         *                                  net points                |    maxPointsReason
+         * -------------------------------+-----------------------------+-----------------------
+         *  not discarded, no maxPoints   | bold                        | none
+         *  not discarded, maxPoints      | bold                        | transparent
+         *  discarded, no maxPoints       | transparent, strike-through | none
+         *  discarded, maxPoints          | transparent, strike-through | transparent, strike-through
+         * 
+ */ + @Override + public void render(Context context, LeaderboardRowDTO object, SafeHtmlBuilder html) { + LeaderboardEntryDTO entry = object.fieldsByRaceColumnName.get(getRaceColumnName()); + if (entry != null) { + boolean isLive = isLive(entry.fleet); + final String textColor = isLive ? IS_LIVE_TEXT_COLOR : DEFAULT_TEXT_COLOR; + final String addedScores = isShowAddedScores() ? scoreFormat.format(computeAddedScores(object)) : ""; + String netOrAddedPointsAsText = isShowAddedScores() ? addedScores + : entry.netPoints == null ? "" : scoreFormat.format(entry.netPoints); + String totalOrAddedPointsAsText = isShowAddedScores() ? addedScores + : entry.totalPoints == null ? "" : scoreFormat.format(entry.totalPoints); + if (entry.fleet != null && entry.fleet.getColor() != null) { + html.append(raceColumnTemplate.cellFrameWithTextColorAndFleetBorder(textColor, + entry.fleet.getColor().getAsHtml())); + } else { + html.append(raceColumnTemplate.cellFrameWithTextColor(textColor)); + } + // don't show points if max points / penalty + if (entry.reasonForMaxPoints == null || entry.reasonForMaxPoints == MaxPointsReason.NONE) { + if (!entry.discarded) { + html.appendHtmlConstant(""); + html.appendHtmlConstant(netOrAddedPointsAsText); + html.appendHtmlConstant(""); + } else { + html.appendHtmlConstant(" "); + html.appendHtmlConstant(totalOrAddedPointsAsText); + html.appendHtmlConstant(""); + } + } else { + html.appendHtmlConstant(" "); + if (entry.discarded) { + html.appendHtmlConstant(""); + } + html.appendEscaped( + entry.reasonForMaxPoints == MaxPointsReason.NONE ? "" : entry.reasonForMaxPoints.name()); + if (entry.discarded) { + html.appendHtmlConstant(""); + } + html.appendHtmlConstant(""); + } + html.appendHtmlConstant("
"); + } + } + + @Override + public InvertibleComparator getComparator() { + return new InvertibleComparatorAdapter() { + @Override + public int compare(LeaderboardRowDTO o1, LeaderboardRowDTO o2) { + final int result; + if (isShowAddedScores()) { + double o1AddedScore = computeAddedScores(o1); + double o2AddedScore = computeAddedScores(o2); + double intermediate_result = o1AddedScore == 0. + ? (o2AddedScore == 0. ? -1. : isAscending() ? 1. : -1.) + : o2AddedScore == 0. ? (isAscending() ? -1. : 1.) : o1AddedScore - o2AddedScore; + if (!getLeaderboard().isHigherScoreBetter()) { + result = intermediate_result > 0 ? 1 : intermediate_result < 0 ? -1 : 0; + } else { + result = intermediate_result > 0 ? -1 : intermediate_result < 0 ? 1 : 0; + } + } else { + List competitorsFromBestToWorst = getLeaderboard() + .getCompetitorsFromBestToWorst(race); + int o1Rank = competitorsFromBestToWorst.indexOf(o1.competitor) + 1; + int o2Rank = competitorsFromBestToWorst.indexOf(o2.competitor) + 1; + result = o1Rank == 0 ? o2Rank == 0 ? 0 : isAscending() ? 1 : -1 + : o2Rank == 0 ? isAscending() ? -1 : 1 : o1Rank - o2Rank; + } + return result; + } + }; + } + + @Override + public String getHeaderStyle() { + return headerStyle; + } + + @Override + public Header getHeader() { + SortableExpandableColumnHeader header = new SortableExpandableColumnHeader( + /* title */race.getRaceColumnName(), + /* iconURL */race.isMedalRace() ? "/gwt/images/medal_small.png" : null, LeaderboardPanel.this, this, + stringMessages); + return header; + } + } + + public static DetailType[] getAvailableRaceDetailColumnTypes() { + return new DetailType[] { DetailType.RACE_GAP_TO_LEADER_IN_SECONDS, + DetailType.RACE_AVERAGE_SPEED_OVER_GROUND_IN_KNOTS, DetailType.RACE_DISTANCE_TRAVELED, + DetailType.RACE_DISTANCE_TRAVELED_INCLUDING_GATE_START, DetailType.RACE_TIME_TRAVELED, + DetailType.RACE_CALCULATED_TIME_TRAVELED, + DetailType.RACE_CALCULATED_TIME_AT_ESTIMATED_ARRIVAL_AT_COMPETITOR_FARTHEST_AHEAD, + DetailType.RACE_CURRENT_SPEED_OVER_GROUND_IN_KNOTS, DetailType.RACE_CURRENT_RIDE_HEIGHT_IN_METERS, + DetailType.RACE_DISTANCE_TO_COMPETITOR_FARTHEST_AHEAD_IN_METERS, DetailType.NUMBER_OF_MANEUVERS, + DetailType.DISPLAY_LEGS, DetailType.CURRENT_LEG, + DetailType.RACE_AVERAGE_ABSOLUTE_CROSS_TRACK_ERROR_IN_METERS, + DetailType.RACE_AVERAGE_SIGNED_CROSS_TRACK_ERROR_IN_METERS, + DetailType.RACE_RATIO_BETWEEN_TIME_SINCE_LAST_POSITION_FIX_AND_AVERAGE_SAMPLING_INTERVAL }; + } + + public static DetailType[] getAvailableRaceStartAnalysisColumnTypes() { + return new DetailType[] { DetailType.RACE_DISTANCE_TO_START_FIVE_SECONDS_BEFORE_RACE_START, + DetailType.RACE_SPEED_OVER_GROUND_FIVE_SECONDS_BEFORE_START, DetailType.DISTANCE_TO_START_AT_RACE_START, + DetailType.TIME_BETWEEN_RACE_START_AND_COMPETITOR_START, DetailType.SPEED_OVER_GROUND_AT_RACE_START, + DetailType.SPEED_OVER_GROUND_WHEN_PASSING_START, + DetailType.DISTANCE_TO_STARBOARD_END_OF_STARTLINE_WHEN_PASSING_START_IN_METERS, DetailType.START_TACK }; + } + + public static DetailType[] getAvailableOverallDetailColumnTypes() { + return new DetailType[] { DetailType.REGATTA_RANK, DetailType.TOTAL_DISTANCE_TRAVELED, + DetailType.TOTAL_AVERAGE_SPEED_OVER_GROUND, DetailType.TOTAL_TIME_SAILED_IN_SECONDS, + DetailType.MAXIMUM_SPEED_OVER_GROUND_IN_KNOTS, DetailType.TIME_ON_TIME_FACTOR, + DetailType.TIME_ON_DISTANCE_ALLOWANCE_IN_SECONDS_PER_NAUTICAL_MILE }; + } + + private class TextRaceColumn extends RaceColumn implements RaceNameProvider { + /** + * Remembers the leg columns; null-padded, if {@link #getLegColumn(int)} asks for a column index + * not yet existing. It is important to remember the columns because column removal happens based on identity. + */ + private final List legColumns; + + public TextRaceColumn(RaceColumnDTO race, boolean expandable, SortingOrder preferredSortingOrder, + String headerStyle, String columnStyle) { + super(race, expandable, new TextCell(), preferredSortingOrder, headerStyle, columnStyle); + legColumns = new ArrayList(); + } + + public String getValue(LeaderboardRowDTO object) { + // The following code exists only for robustness. This method should never be called because + // RaceColumn implements its own render(...) method which doesn't make use of getValue(...) + final Double netPoints = object.fieldsByRaceColumnName.get(getRaceColumnName()).netPoints; + return "" + (netPoints == null ? "" : scoreFormat.format(netPoints)); + } + + @Override + protected void ensureExpansionDataIsLoaded(final Runnable callWhenExpansionDataIsLoaded) { + if (getLeaderboard().getLegCount(getRaceColumnName(), preSelectedRace) != -1) { + callWhenExpansionDataIsLoaded.run(); + } else { + updateLeaderboardAndRun(callWhenExpansionDataIsLoaded); + } + } + + @Override + protected Map> getDetailColumnMap( + LeaderboardPanel leaderboardPanel, StringMessages stringMessages, String detailHeaderStyle, + String detailColumnStyle) { + Map> result = new HashMap<>(); + result.put(DetailType.RACE_RATIO_BETWEEN_TIME_SINCE_LAST_POSITION_FIX_AND_AVERAGE_SAMPLING_INTERVAL, + new TimeSinceLastGpsFixColumn( + DetailType.RACE_RATIO_BETWEEN_TIME_SINCE_LAST_POSITION_FIX_AND_AVERAGE_SAMPLING_INTERVAL, + new RaceRatioBetweenTimeSinceLastPositionFixAndAverageSamplingInterval(), + LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); + result.put(DetailType.RACE_DISTANCE_TRAVELED, + new FormattedDoubleDetailTypeColumn(DetailType.RACE_DISTANCE_TRAVELED, + new RaceDistanceTraveledInMeters(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, + LeaderboardPanel.this)); + result.put(DetailType.RACE_DISTANCE_TRAVELED_INCLUDING_GATE_START, + new FormattedDoubleDetailTypeColumn(DetailType.RACE_DISTANCE_TRAVELED_INCLUDING_GATE_START, + new RaceDistanceTraveledIncludingGateStartInMeters(), LEG_COLUMN_HEADER_STYLE, + LEG_COLUMN_STYLE, LeaderboardPanel.this)); + result.put(DetailType.RACE_AVERAGE_SPEED_OVER_GROUND_IN_KNOTS, + new FormattedDoubleDetailTypeColumn(DetailType.RACE_AVERAGE_SPEED_OVER_GROUND_IN_KNOTS, + new RaceAverageSpeedInKnots(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, + LeaderboardPanel.this)); + result.put(DetailType.RACE_GAP_TO_LEADER_IN_SECONDS, + new FormattedDoubleDetailTypeColumn(DetailType.RACE_GAP_TO_LEADER_IN_SECONDS, + new RaceGapToLeaderInSeconds(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, + LeaderboardPanel.this)); + result.put(DetailType.RACE_CURRENT_SPEED_OVER_GROUND_IN_KNOTS, + new FormattedDoubleDetailTypeColumn(DetailType.RACE_CURRENT_SPEED_OVER_GROUND_IN_KNOTS, + new RaceCurrentSpeedOverGroundInKnots(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, + LeaderboardPanel.this)); + result.put(DetailType.RACE_CURRENT_RIDE_HEIGHT_IN_METERS, + new RideHeightColumn(DetailType.RACE_CURRENT_RIDE_HEIGHT_IN_METERS, + new RaceCurrentRideHeightInMeters(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, + LeaderboardPanel.this)); + result.put(DetailType.RACE_DISTANCE_TO_COMPETITOR_FARTHEST_AHEAD_IN_METERS, + new FormattedDoubleDetailTypeColumn(DetailType.RACE_DISTANCE_TO_COMPETITOR_FARTHEST_AHEAD_IN_METERS, + new RaceDistanceToCompetitorFarthestAheadInMeters(), LEG_COLUMN_HEADER_STYLE, + LEG_COLUMN_STYLE, LeaderboardPanel.this)); + result.put(DetailType.RACE_AVERAGE_ABSOLUTE_CROSS_TRACK_ERROR_IN_METERS, + new FormattedDoubleDetailTypeColumn(DetailType.RACE_AVERAGE_ABSOLUTE_CROSS_TRACK_ERROR_IN_METERS, + new RaceAverageAbsoluteCrossTrackErrorInMeters(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, + LeaderboardPanel.this)); + result.put(DetailType.RACE_AVERAGE_SIGNED_CROSS_TRACK_ERROR_IN_METERS, + new FormattedDoubleDetailTypeColumn(DetailType.RACE_AVERAGE_SIGNED_CROSS_TRACK_ERROR_IN_METERS, + new RaceAverageSignedCrossTrackErrorInMeters(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, + LeaderboardPanel.this)); + result.put(DetailType.RACE_DISTANCE_TO_START_FIVE_SECONDS_BEFORE_RACE_START, + new FormattedDoubleDetailTypeColumn( + DetailType.RACE_DISTANCE_TO_START_FIVE_SECONDS_BEFORE_RACE_START, + new DistanceToStartFiveSecondsBeforeStartInMeters(), LEG_COLUMN_HEADER_STYLE, + LEG_COLUMN_STYLE, LeaderboardPanel.this)); + result.put(DetailType.RACE_SPEED_OVER_GROUND_FIVE_SECONDS_BEFORE_START, + new FormattedDoubleDetailTypeColumn(DetailType.RACE_SPEED_OVER_GROUND_FIVE_SECONDS_BEFORE_START, + new SpeedFiveSecondsBeforeStartInKnots(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, + LeaderboardPanel.this)); + result.put(DetailType.DISTANCE_TO_START_AT_RACE_START, + new FormattedDoubleDetailTypeColumn(DetailType.DISTANCE_TO_START_AT_RACE_START, + new DistanceToStartAtRaceStartInMeters(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, + LeaderboardPanel.this)); + result.put(DetailType.TIME_BETWEEN_RACE_START_AND_COMPETITOR_START, + new FormattedDoubleDetailTypeColumn(DetailType.TIME_BETWEEN_RACE_START_AND_COMPETITOR_START, + new TimeBetweenRaceStartAndCompetitorStartInSeconds(), LEG_COLUMN_HEADER_STYLE, + LEG_COLUMN_STYLE, LeaderboardPanel.this)); + result.put(DetailType.SPEED_OVER_GROUND_AT_RACE_START, + new FormattedDoubleDetailTypeColumn(DetailType.SPEED_OVER_GROUND_AT_RACE_START, + new SpeedOverGroundAtRaceStartInKnots(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, + LeaderboardPanel.this)); + result.put(DetailType.SPEED_OVER_GROUND_WHEN_PASSING_START, + new FormattedDoubleDetailTypeColumn(DetailType.SPEED_OVER_GROUND_WHEN_PASSING_START, + new SpeedOverGroundWhenPassingStartInKnots(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, + LeaderboardPanel.this)); + result.put(DetailType.DISTANCE_TO_STARBOARD_END_OF_STARTLINE_WHEN_PASSING_START_IN_METERS, + new FormattedDoubleDetailTypeColumn( + DetailType.DISTANCE_TO_STARBOARD_END_OF_STARTLINE_WHEN_PASSING_START_IN_METERS, + new DistanceToStarboardSideOfStartLineInMeters(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, + LeaderboardPanel.this)); + result.put(DetailType.START_TACK, + new StartingTackColumn(new TackWhenStarting(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE)); + result.put(DetailType.NUMBER_OF_MANEUVERS, + new ManeuverCountRaceColumn(getLeaderboardPanel(), this, stringMessages, + LeaderboardPanel.this.selectedManeuverDetails, LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, + LEG_DETAIL_COLUMN_HEADER_STYLE, LEG_DETAIL_COLUMN_STYLE, LeaderboardPanel.this)); + result.put(DetailType.CURRENT_LEG, new FormattedDoubleDetailTypeColumn(DetailType.CURRENT_LEG, + new CurrentLeg(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); + result.put(DetailType.RACE_TIME_TRAVELED, + new TimeTraveledRaceColumn(getLeaderboardPanel(), this, stringMessages, LEG_COLUMN_HEADER_STYLE, + LEG_COLUMN_STYLE, LEG_DETAIL_COLUMN_HEADER_STYLE, LEG_DETAIL_COLUMN_STYLE)); + result.put(DetailType.RACE_CALCULATED_TIME_TRAVELED, + new FormattedDoubleDetailTypeColumn(DetailType.RACE_CALCULATED_TIME_TRAVELED, + new RaceCalculatedTimeTraveledInSeconds(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, + LeaderboardPanel.this)); + result.put(DetailType.RACE_CALCULATED_TIME_AT_ESTIMATED_ARRIVAL_AT_COMPETITOR_FARTHEST_AHEAD, + new FormattedDoubleDetailTypeColumn( + DetailType.RACE_CALCULATED_TIME_AT_ESTIMATED_ARRIVAL_AT_COMPETITOR_FARTHEST_AHEAD, + new RaceCalculatedTimeAtEstimatedArrivalAtCompetitorFarthestAheadInSeconds(), + LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); + return result; + } + + @Override + protected Iterable> getDirectChildren() { + List> result = new ArrayList<>(); + for (AbstractSortableColumnWithMinMax column : super.getDirectChildren()) { + result.add(column); + } + if (isExpanded() && selectedRaceDetails.contains(DetailType.DISPLAY_LEGS)) { + // it is important to re-use existing LegColumn objects because + // removing the columns from the table + // is based on column identity + int maxLegCount = getLeaderboard().getLegCount(getRaceColumnName(), preSelectedRace); + if (maxLegCount != -1) { + for (int i = 0; i < maxLegCount; i++) { + LegColumn legColumn = getLegColumn(i); + result.add(legColumn); + } + } else { + // the race is no longer part of the LeaderboardDTO; consider the non-null legs in legColumns: + for (LegColumn legColumn : legColumns) { + if (legColumn != null) { + result.add(legColumn); + } + } + } + } + return result; + } + + private LegColumn getLegColumn(int legNumber) { + LegColumn result; + if (legColumns.size() > legNumber && legColumns.get(legNumber) != null) { + result = legColumns.get(legNumber); + } else { + result = new LegColumn(LeaderboardPanel.this, getRaceColumnName(), legNumber, SortingOrder.ASCENDING, + stringMessages, Collections.unmodifiableList(selectedLegDetails), LEG_COLUMN_HEADER_STYLE, + LEG_COLUMN_STYLE, LEG_DETAIL_COLUMN_HEADER_STYLE, LEG_DETAIL_COLUMN_STYLE); + while (legColumns.size() <= legNumber) { + legColumns.add(null); + } + legColumns.set(legNumber, result); + } + return result; + } + + /** + * Reports the ratio of the time that passed since the last position fix and the average sampling interval of + * the competitor's track. On a perfect track, this value will never exceed 1.0. Immediately when a fix is + * received, this value goes to 0.0. For a competitor whose tracker is lagging, this value can grow considerably + * greater than 1. The value goes to null if there are no fixes in the track. + * + * @author Axel Uhl (D043530) + * + */ + private class RaceRatioBetweenTimeSinceLastPositionFixAndAverageSamplingInterval + implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + Double result = null; + LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); + if (fieldsForRace != null && fieldsForRace.timeSinceLastPositionFixInSeconds != null + && fieldsForRace.averageSamplingInterval != null) { + result = fieldsForRace.timeSinceLastPositionFixInSeconds + / fieldsForRace.averageSamplingInterval.asSeconds(); + } + return result; + } + } + + /** + * Accumulates the average speed over all legs of a race + * + * @author Axel Uhl (D043530) + */ + private class RaceAverageSpeedInKnots implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + final Distance distanceTraveledInMeters = row.getDistanceTraveledInMeters(getRaceColumnName()); + final Duration time = row.getTimeSailed(getRaceColumnName()); + final Double result; + if (distanceTraveledInMeters != null && time != null) { + result = distanceTraveledInMeters.inTime(time).getKnots(); + } else { + result = null; + } + return result; + } + } + + /** + * Fetches the average absolute (distance always counted as positive, no matter whether left or right) + * cross-track error for the race + * + * @author Axel Uhl (D043530) + */ + private class RaceAverageAbsoluteCrossTrackErrorInMeters implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + Double result = null; + LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); + if (fieldsForRace != null) { + result = fieldsForRace.averageAbsoluteCrossTrackErrorInMeters; + } + return result; + } + } + + /** + * Fetches the average signed (right course side is positive, left course side is negative) cross-track error + * for the race + * + * @author Axel Uhl (D043530) + */ + private class RaceAverageSignedCrossTrackErrorInMeters implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + Double result = null; + LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); + if (fieldsForRace != null) { + result = fieldsForRace.averageSignedCrossTrackErrorInMeters; + } + return result; + } + } + + /** + * Fetches the competitor's distance to the start mark at the time the race started + * + * @author Axel Uhl (D043530) + */ + private class DistanceToStartAtRaceStartInMeters implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + Double result = null; + LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); + if (fieldsForRace != null) { + result = fieldsForRace.distanceToStartLineAtStartOfRaceInMeters; + } + return result; + } + } + + /** + * Fetches the time between start of race and the competitors start mark passing, telling how long after the gun + * the competitor actually started. + * + * @author Axel Uhl (D043530) + */ + private class TimeBetweenRaceStartAndCompetitorStartInSeconds implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + Double result = null; + LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); + if (fieldsForRace != null) { + result = fieldsForRace.timeBetweenRaceStartAndCompetitorStartInSeconds; + } + return result; + } + } + + /** + * Fetches the competitor's speed over ground at the time the race started + * + * @author Axel Uhl (D043530) + */ + private class SpeedOverGroundAtRaceStartInKnots implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + Double result = null; + LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); + if (fieldsForRace != null) { + result = fieldsForRace.speedOverGroundAtStartOfRaceInKnots; + } + return result; + } + } + + /** + * Fetches the competitor's distance to the start mark five seconds before the race started + */ + private class DistanceToStartFiveSecondsBeforeStartInMeters implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + Double result = null; + LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); + if (fieldsForRace != null) { + result = fieldsForRace.distanceToStartLineFiveSecondsBeforeStartInMeters; + } + return result; + } + } + + /** + * Fetches the competitor's speed over ground five seconds before the race started + */ + private class SpeedFiveSecondsBeforeStartInKnots implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + Double result = null; + LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); + if (fieldsForRace != null) { + result = fieldsForRace.speedOverGroundFiveSecondsBeforeStartInKnots; + } + return result; + } + } + + /** + * Fetches the competitor's speed over ground at the time the competitor passed the start + * + * @author Axel Uhl (D043530) + */ + private class SpeedOverGroundWhenPassingStartInKnots implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + Double result = null; + LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); + if (fieldsForRace != null) { + result = fieldsForRace.speedOverGroundAtPassingStartWaypointInKnots; + } + return result; + } + } + + /** + * Fetches the competitor's distance to the starboard side of the start line when competitor passed the start. + * If the start waypoint is not a gate/line, the distance to the single buoy is used. + * + * @author Axel Uhl (D043530) + */ + private class DistanceToStarboardSideOfStartLineInMeters implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + Double result = null; + LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); + if (fieldsForRace != null) { + result = fieldsForRace.distanceToStarboardSideOfStartLineInMeters; + } + return result; + } + } + + /** + * Fetches the competitor's speed over ground at the time the competitor passed the start + * + * @author Axel Uhl (D043530) + */ + private class TackWhenStarting implements LegDetailField { + @Override + public Tack get(LeaderboardRowDTO row) { + Tack result = null; + LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); + if (fieldsForRace != null) { + result = fieldsForRace.startTack; + } + return result; + } + } + + /** + * Accumulates the distance traveled over all legs of a race + * + * @author Axel Uhl (D043530) + */ + private class RaceDistanceTraveledInMeters implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + Distance distanceForRaceColumn = row.getDistanceTraveledInMeters(getRaceColumnName()); + return distanceForRaceColumn == null ? null : distanceForRaceColumn.getMeters(); + } + } + + private class RaceCalculatedTimeTraveledInSeconds implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + Double result = null; + LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); + if (fieldsForRace != null) { + result = fieldsForRace.calculatedTime == null ? null : fieldsForRace.calculatedTime.asSeconds(); + } + return result; + } + } + + private class RaceCalculatedTimeAtEstimatedArrivalAtCompetitorFarthestAheadInSeconds + implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + Double result = null; + LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); + if (fieldsForRace != null) { + result = fieldsForRace.calculatedTimeAtEstimatedArrivalAtCompetitorFarthestAhead == null ? null + : fieldsForRace.calculatedTimeAtEstimatedArrivalAtCompetitorFarthestAhead.asSeconds(); + } + return result; + } + } + + /** + * Accumulates the distance traveled over all legs of a race and considers the specifics of a gate start. The + * first leg of a gate start gets as additional distance traveled the distance from the port side of the line to + * the point where the competitor started which helps normalize the distance traveled and make them comparable + * across early and late starters. + * + * @author Axel Uhl (D043530) + */ + private class RaceDistanceTraveledIncludingGateStartInMeters implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + Double result = null; + LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); + if (fieldsForRace != null && fieldsForRace.legDetails != null) { + for (LegEntryDTO legDetail : fieldsForRace.legDetails) { + if (legDetail != null) { + if (legDetail.distanceTraveledIncludingGateStartInMeters != null) { + if (result == null) { + result = 0.0; + } + result += legDetail.distanceTraveledIncludingGateStartInMeters; + } + } + } + } + return result; + } + } + + private class CurrentLeg implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + Double result = null; + LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); + if (fieldsForRace != null && fieldsForRace.legDetails != null && !fieldsForRace.legDetails.isEmpty()) { + for (LegEntryDTO legDetail : fieldsForRace.legDetails) { + if (legDetail != null) { + if (legDetail.started) { + if (result == null) { + result = 0.0; + } + result++; + } else { + if (result != null) { + result++; + } + break; + } + } + } + } + return result; + } + } + + /** + * Computes the gap to leader exploiting the ordering of the leg detail columns + * + * @author Axel Uhl (D043530) + */ + private class RaceGapToLeaderInSeconds implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + Double result = null; + LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); + if (fieldsForRace != null) { + result = fieldsForRace.gapToLeaderInOwnTime == null ? null + : fieldsForRace.gapToLeaderInOwnTime.asSeconds(); + } + return result; + } + } + + /** + * Computes the windward distance to the competitor farthest ahead in the race (not necessarily the leader with + * handicap scoring) in meters + * + * @author Axel Uhl (D043530) + */ + private class RaceDistanceToCompetitorFarthestAheadInMeters implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + Double result = null; + LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); + if (fieldsForRace != null && fieldsForRace.windwardDistanceToCompetitorFarthestAheadInMeters != null) { + result = fieldsForRace.windwardDistanceToCompetitorFarthestAheadInMeters; + } + return result; + } + } + + private abstract class AbstractLastLegDetailField> implements LegDetailField { + @Override + public final T get(LeaderboardRowDTO row) { + T result = null; + LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); + if (fieldsForRace != null && fieldsForRace.legDetails != null) { + int lastLegIndex = fieldsForRace.legDetails.size() - 1; + if (lastLegIndex >= 0) { + LegEntryDTO lastLegDetail = fieldsForRace.legDetails.get(lastLegIndex); + // competitor may be in leg prior to the one the leader is in; find competitors current leg + while (lastLegDetail == null && lastLegIndex > 0) { + lastLegDetail = fieldsForRace.legDetails.get(--lastLegIndex); + } + if (lastLegDetail != null) { + if (lastLegDetail.finished) { + result = getAfterLastLegFinished(row); + } else { + result = getBeforeLastLegFinished(lastLegDetail); + } + } + } + } + return result; + } + + protected abstract T getBeforeLastLegFinished(LegEntryDTO currentLegDetail); + + protected abstract T getAfterLastLegFinished(LeaderboardRowDTO row); + } + + private class RaceCurrentSpeedOverGroundInKnots extends AbstractLastLegDetailField { + @Override + protected Double getBeforeLastLegFinished(LegEntryDTO currentLegDetail) { + return currentLegDetail.currentSpeedOverGroundInKnots; + } + + @Override + protected Double getAfterLastLegFinished(LeaderboardRowDTO row) { + return new RaceAverageSpeedInKnots().get(row); + } + } + + private class RaceCurrentRideHeightInMeters extends AbstractLastLegDetailField { + @Override + protected Double getBeforeLastLegFinished(LegEntryDTO currentLegDetail) { + return currentLegDetail.currentRideHeightInMeters; + } + + @Override + protected Double getAfterLastLegFinished(LeaderboardRowDTO row) { + Double result = null; + LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); + if (fieldsForRace != null) { + result = fieldsForRace.averageRideHeightInMeters; + } + return result; + } + } + } + + /** + * Column that display the number of races a competitor has finished. Currently taking into account + * {@link MaxPointsReason#DNS}, {@link MaxPointsReason#DNF} and {@link MaxPointsReason#DNC}. Configurable over + * {@link LeaderboardSettings#isShowOverallColumnWithNumberOfRacesCompletedPerCompetitor()}. + * + * @author Simon Marcel Pamies + */ + private class TotalRacesCompletedColumn extends LeaderboardSortableColumnWithMinMax { + private final String columnStyle; + private final MaxPointsReason[] MAX_POINTS_REASONS_THAT_IDENTIFY_NON_FINISHED_RACES = new MaxPointsReason[] { + MaxPointsReason.DNS, MaxPointsReason.DNF, MaxPointsReason.DNC }; + + protected TotalRacesCompletedColumn(String columnStyle) { + super(new TextCell(), SortingOrder.ASCENDING, LeaderboardPanel.this); + this.columnStyle = columnStyle; + setHorizontalAlignment(ALIGN_CENTER); + } + + private int computeNumberOfRacesCompleted(LeaderboardRowDTO object) { + int racesSailedInRow = 0; + for (RaceColumnDTO raceColumn : getLeaderboard().getRaceList()) { + LeaderboardEntryDTO entryBefore = object.fieldsByRaceColumnName.get(raceColumn.getName()); + if (entryBefore.netPoints != null) { + if (entryBefore.reasonForMaxPoints.equals(MaxPointsReason.NONE) + || !Util.contains(Arrays.asList(MAX_POINTS_REASONS_THAT_IDENTIFY_NON_FINISHED_RACES), + entryBefore.reasonForMaxPoints)) { + racesSailedInRow++; + } + } + } + return racesSailedInRow; + } + + @Override + public String getValue(LeaderboardRowDTO object) { + return "" + computeNumberOfRacesCompleted(object); + } + + @Override + public void render(Context context, LeaderboardRowDTO object, SafeHtmlBuilder sb) { + String textColor = getLeaderboard().hasLiveRace(timer.getLiveTimePointInMillis()) ? IS_LIVE_TEXT_COLOR + : DEFAULT_TEXT_COLOR; + + sb.appendHtmlConstant(""); + sb.appendEscaped(getValue(object)); + sb.appendHtmlConstant(""); + } + + @Override + public InvertibleComparator getComparator() { + return new InvertibleComparatorAdapter() { + @Override + public int compare(LeaderboardRowDTO o1, LeaderboardRowDTO o2) { + double o1RacesSailed = computeNumberOfRacesCompleted(o1); + double o2RacesSailed = computeNumberOfRacesCompleted(o2); + double result = o1RacesSailed == 0. ? o2RacesSailed == 0. ? 0. : isAscending() ? 1. : -1. + : o2RacesSailed == 0. ? isAscending() ? 1. : -1. : o2RacesSailed - o1RacesSailed; + return result > 0 ? 1 : result < 0 ? -1 : 0; + } + }; + } + + @Override + public String getColumnStyle() { + return columnStyle; + } + + @Override + public SafeHtmlHeader getHeader() { + return new SafeHtmlHeaderWithTooltip(SafeHtmlUtils.fromString(stringMessages.racesScored()), + stringMessages.racesScoredTooltip()); + } + } + + /** + * Displays the net points totals for a competitor for the entire leaderboard. + * + * @author Axel Uhl (D043530) + * + */ + private class TotalNetPointssColumn extends LeaderboardSortableColumnWithMinMax { + private final String columnStyle; + + protected TotalNetPointssColumn(String columnStyle) { + super(new TextCell(), SortingOrder.ASCENDING, LeaderboardPanel.this); + this.columnStyle = columnStyle; + setHorizontalAlignment(ALIGN_CENTER); + } + + @Override + public String getValue(LeaderboardRowDTO object) { + Double netPoints = object.netPoints; + return "" + (netPoints == null ? "" : scoreFormat.format(netPoints)); + } + + @Override + public void render(Context context, LeaderboardRowDTO object, SafeHtmlBuilder sb) { + String textColor = getLeaderboard().hasLiveRace(timer.getLiveTimePointInMillis()) ? IS_LIVE_TEXT_COLOR + : DEFAULT_TEXT_COLOR; + + sb.appendHtmlConstant(""); + sb.appendEscaped(getValue(object)); + sb.appendHtmlConstant(""); + } + + @Override + public InvertibleComparator getComparator() { + return new InvertibleComparatorAdapter() { + @Override + public int compare(LeaderboardRowDTO o1, LeaderboardRowDTO o2) { + return getLeaderboard().competitors.indexOf(o1.competitor) + - getLeaderboard().competitors.indexOf(o2.competitor); + } + }; + } + + @Override + public String getColumnStyle() { + return columnStyle; + } + + @Override + public SafeHtmlHeader getHeader() { + return new SafeHtmlHeaderWithTooltip(SafeHtmlUtils.fromString(stringMessages.total()), + stringMessages.totalNetPointsColumnTooltip()); + } + } + + protected class CarryColumn extends LeaderboardSortableColumnWithMinMax { + public CarryColumn() { + super(new AbstractSafeHtmlCell(new AbstractSafeHtmlRenderer() { + @Override + public SafeHtml render(LeaderboardRowDTO object) { + return new SafeHtmlBuilder() + .appendEscaped(object.carriedPoints == null ? "" : scoreFormat.format(object.carriedPoints)) + .toSafeHtml(); + } + }) { + @Override + protected void render(com.google.gwt.cell.client.Cell.Context context, SafeHtml data, + SafeHtmlBuilder sb) { + sb.append(data); + } + }, SortingOrder.ASCENDING, LeaderboardPanel.this); + setSortable(true); + } + + protected CarryColumn(Cell cell) { + super(cell, SortingOrder.ASCENDING, LeaderboardPanel.this); + setSortable(true); + } + + @Override + public LeaderboardRowDTO getValue(LeaderboardRowDTO object) { + return object; + } + + @Override + public InvertibleComparator getComparator() { + return new InvertibleComparatorAdapter() { + @Override + public int compare(LeaderboardRowDTO o1, LeaderboardRowDTO o2) { + Double o1CarriedPoints = o1.carriedPoints; + if (o1CarriedPoints == null) { + o1CarriedPoints = 0.0; + } + Double o2CarriedPoints = o2.carriedPoints; + if (o2CarriedPoints == null) { + o2CarriedPoints = 0.0; + } + return o1CarriedPoints.compareTo(o2CarriedPoints); + } + }; + } + + @Override + public SafeHtmlHeader getHeader() { + return new SafeHtmlHeaderWithTooltip(SafeHtmlUtils.fromString(stringMessages.carry()), + stringMessages.carryColumnTooltip()); + } + } + + private class LeaderboardSelectionCheckboxColumn + extends com.sap.sailing.gwt.ui.client.shared.controls.SelectionCheckboxColumn + implements CompetitorSelectionChangeListener { + protected LeaderboardSelectionCheckboxColumn(final CompetitorSelectionProvider competitorSelectionProvider) { + super(TABLE_RESOURCES.cellTableStyle().cellTableCheckboxSelected(), + TABLE_RESOURCES.cellTableStyle().cellTableCheckboxDeselected(), + TABLE_RESOURCES.cellTableStyle().cellTableCheckboxColumnCell(), + new EntityIdentityComparator() { + @Override + public boolean representSameEntity(LeaderboardRowDTO dto1, LeaderboardRowDTO dto2) { + return dto1.competitor.getIdAsString().equals(dto2.competitor.getIdAsString()); + } + + @Override + public int hashCode(LeaderboardRowDTO t) { + return t.competitor.getIdAsString().hashCode(); + } + }, getData(), leaderboardTable); + competitorSelectionProvider.addCompetitorSelectionChangeListener(this); + } + + @Override + public Boolean getValue(LeaderboardRowDTO row) { + return competitorSelectionProvider.isSelected(row.competitor); + } + + @Override + public void competitorsListChanged(Iterable competitors) { + } + + @Override + public void filterChanged(FilterSet> oldFilterSet, + FilterSet> newFilterSet) { + } + + @Override + public void filteredCompetitorsListChanged(Iterable filteredCompetitors) { + } + + /** + * Ensure that the checkbox is redrawn when the competitor selection changes + */ + @Override + public void addedToSelection(CompetitorDTO competitor) { + final LeaderboardRowDTO row = getRow(competitor.getIdAsString()); + if (row != null) { + getSelectionModel().setSelected(row, true); + } + } + + /** + * Ensure that the checkbox is redrawn when the competitor selection changes + */ + @Override + public void removedFromSelection(CompetitorDTO competitor) { + final LeaderboardRowDTO row = getRow(competitor.getIdAsString()); + if (row != null) { + getSelectionModel().setSelected(row, false); + } + } + + @Override + public void updateMinMax() { + } + } + + private class TotalRankColumn extends LeaderboardSortableColumnWithMinMax { + public TotalRankColumn() { + super(new TextCell(), SortingOrder.ASCENDING, LeaderboardPanel.this); + setHorizontalAlignment(ALIGN_CENTER); + setSortable(true); + } + + @Override + public String getValue(LeaderboardRowDTO object) { + final int totalRank = getLeaderboard().getTotalRank(object.competitor); + return "" + (totalRank == 0 ? "" : totalRank); + } + + @Override + public InvertibleComparator getComparator() { + return new InvertibleComparatorAdapter() { + @Override + public int compare(LeaderboardRowDTO o1, LeaderboardRowDTO o2) { + final int totalRank1 = getLeaderboard().getTotalRank(o1.competitor); + final int totalRank2 = getLeaderboard().getTotalRank(o2.competitor); + return totalRank1 == 0 ? totalRank2 == 0 ? 0 : 1 : totalRank2 == 0 ? -1 : totalRank1 - totalRank2; + } + }; + } + + @Override + public SafeHtmlHeader getHeader() { + return new SafeHtmlHeaderWithTooltip(SafeHtmlUtils.fromString(stringMessages.totalRegattaRank()), + stringMessages.rankColumnTooltip()); + } + } + + private class StartingTackColumn extends DetailTypeColumn { + public StartingTackColumn(LegDetailField field, String headerStyle, String columnStyle) { + super(DetailType.START_TACK, field, new TextCell(), headerStyle, columnStyle, LeaderboardPanel.this); + } + + @Override + public String getValue(LeaderboardRowDTO row) { + return getField().get(row) == null ? null + : getField().get(row) == Tack.PORT ? stringMessages.portTack() : stringMessages.starboardTack(); + } + } + + private Widget createFilterDeselectionControl() { + filterStatusLabel = new Label(); + filterStatusLabel.setStyleName("LeaderboardPanel-FilterControl-StatusLabel"); + filterStatusLabel.setText(""); + filterControlPanel.add(filterStatusLabel); + filterClearButton = new Button(stringMessages.showAll()); + filterClearButton.setStyleName("LeaderboardPanel-FilterClear-Button"); + filterClearButton.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + competitorFilterPanel.clearAllActiveFilters(); + loadCompleteLeaderboard(/* showProgress */ true); + } + }); + filterControlPanel.add(filterClearButton); + filterControlPanel.setCellHorizontalAlignment(filterClearButton, HasHorizontalAlignment.ALIGN_RIGHT); + setFilterControlStatus(); + return filterControlPanel; + } + + private void setFilterControlStatus() { + if (showCompetitorFilterStatus) { + boolean filtersActive = competitorSelectionProvider.hasActiveFilters(); + if (filtersActive) { + String labelText = ""; + for (Filter filter : competitorSelectionProvider.getCompetitorsFilterSet() + .getFilters()) { + if (filter instanceof FilterWithUI) { + labelText += ((FilterWithUI) filter).getLocalizedDescription(stringMessages) + + ", "; + } else { + labelText += filter.getName() + ", "; + } + } + filterStatusLabel.setText("Active Filter(s): " + labelText.substring(0, labelText.length() - 2)); + filterClearButton.setVisible(true); + filterControlPanel.setVisible(true); + } else { + filterStatusLabel.setText(""); + filterClearButton.setVisible(false); + filterControlPanel.setVisible(false); + if (competitorFilterPanel != null) { + competitorFilterPanel.clearSelection(); + } + } + } + } + + private static class TotalDistanceTraveledInMetersField implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + return row.totalDistanceTraveledInMeters; + } + } + + private static class TimeOnTimeFactorColumn implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + return row.competitor.getTimeOnTimeFactor(); + } + } + + private static class TimeOnDistanceAllowanceInSecondsPerNauticalMileColumn implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + return row.competitor.getTimeOnDistanceAllowancePerNauticalMile() == null ? null + : row.competitor.getTimeOnDistanceAllowancePerNauticalMile().asSeconds(); + } + } + + private static class TotalAverageSpeedOverGroundField implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + final Double result; + if (row.totalDistanceTraveledInMeters != null && row.totalTimeSailedInSeconds != null + && row.totalTimeSailedInSeconds != 0.0) { + result = row.totalDistanceTraveledInMeters / row.totalTimeSailedInSeconds + / Mile.METERS_PER_NAUTICAL_MILE * 3600; + } else { + result = null; + } + return result; + } + } + + private Map> createOverallDetailColumnMap() { + Map> result = new HashMap<>(); + result.put(DetailType.TOTAL_DISTANCE_TRAVELED, + new FormattedDoubleDetailTypeColumn(DetailType.TOTAL_DISTANCE_TRAVELED, + new TotalDistanceTraveledInMetersField(), RACE_COLUMN_HEADER_STYLE, RACE_COLUMN_STYLE, this)); + result.put(DetailType.TOTAL_AVERAGE_SPEED_OVER_GROUND, + new FormattedDoubleDetailTypeColumn(DetailType.TOTAL_AVERAGE_SPEED_OVER_GROUND, + new TotalAverageSpeedOverGroundField(), RACE_COLUMN_HEADER_STYLE, RACE_COLUMN_STYLE, this)); + result.put(DetailType.MAXIMUM_SPEED_OVER_GROUND_IN_KNOTS, + new MaxSpeedOverallColumn(RACE_COLUMN_HEADER_STYLE, RACE_COLUMN_STYLE, this)); + result.put(DetailType.TOTAL_TIME_SAILED_IN_SECONDS, createOverallTimeTraveledColumn()); + result.put(DetailType.TIME_ON_TIME_FACTOR, new FormattedDoubleDetailTypeColumn(DetailType.TIME_ON_TIME_FACTOR, + new TimeOnTimeFactorColumn(), RACE_COLUMN_HEADER_STYLE, RACE_COLUMN_STYLE, this)); + result.put(DetailType.TIME_ON_DISTANCE_ALLOWANCE_IN_SECONDS_PER_NAUTICAL_MILE, + new FormattedDoubleDetailTypeColumn(DetailType.TIME_ON_DISTANCE_ALLOWANCE_IN_SECONDS_PER_NAUTICAL_MILE, + new TimeOnDistanceAllowanceInSecondsPerNauticalMileColumn(), RACE_COLUMN_HEADER_STYLE, + RACE_COLUMN_STYLE, this)); + + return result; + } + + private OverallTimeTraveledColumn createOverallTimeTraveledColumn() { + return new OverallTimeTraveledColumn(this, stringMessages, RACE_COLUMN_HEADER_STYLE, RACE_COLUMN_STYLE, + LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE); + } + + /** + * Largely for subclasses to add more stuff to this panel, such as more toolbar buttons. + */ + protected HorizontalPanel getRefreshAndSettingsPanel() { + return refreshAndSettingsPanel; + } + + private RaceColumnDTO getRaceByName(String raceName) { + if (getLeaderboard() != null) { + for (RaceColumnDTO race : getLeaderboard().getRaceList()) { + for (FleetDTO fleet : race.getFleets()) { + if (race.getRaceIdentifier(fleet) != null + && raceName.equals(race.getRaceIdentifier(fleet).getRaceName())) { + return race; + } + } + } + } + return null; + } + + private RaceColumnDTO getRaceByColumnName(String columnName) { + if (getLeaderboard() != null) { + for (RaceColumnDTO race : getLeaderboard().getRaceList()) { + if (columnName.equals(race.getRaceColumnName())) { + return race; + } + } + } + return null; + } + + private RaceColumn getRaceColumnByRaceName(String raceName) { + for (int i = 0; i < getLeaderboardTable().getColumnCount(); i++) { + Column column = getLeaderboardTable().getColumn(i); + if (column instanceof RaceColumn) { + RaceColumnDTO raceInLeaderboard = ((RaceColumn) column).getRace(); + for (FleetDTO fleet : raceInLeaderboard.getFleets()) { + final RegattaAndRaceIdentifier raceIdentifier = raceInLeaderboard.getRaceIdentifier(fleet); + if (raceIdentifier != null && raceIdentifier.getRaceName().equals(raceName)) { + return (RaceColumn) column; + } + } + } + } + return null; + } + + private RaceColumn getRaceColumnByRaceColumnName(String raceColumnName) { + for (int i = 0; i < getLeaderboardTable().getColumnCount(); i++) { + Column column = getLeaderboardTable().getColumn(i); + if (column instanceof RaceColumn) { + if (((RaceColumn) column).getRaceColumnName().equals(raceColumnName)) { + return (RaceColumn) column; + } + } + } + return null; + } + + private SafeHtml getPlayPauseImgHtml(PlayStates playState) { + if (playState == PlayStates.Playing) { + return AbstractImagePrototype.create(pauseIcon).getSafeHtml(); + } else { + return AbstractImagePrototype.create(playIcon).getSafeHtml(); + } + } + + private void setDelayInMilliseconds(long delayInMilliseconds) { + // If the timer is currently in a mode that causes null to be used as the leaderboard request time stamp, + // don't let a live play delay change cause another load request by unregistering this panel as timer + // listener before and re-registering it after adjusting the live play delay. + if (useNullAsTimePoint()) { + timer.removeTimeListener(this); + } + timer.setLivePlayDelayInMillis(delayInMilliseconds); + if (useNullAsTimePoint()) { + timer.addTimeListener(this); + } + } + + private boolean isAutoExpandPreSelectedRace() { + return autoExpandPreSelectedRace; + } + + private boolean isAutoExpandLastRaceColumn() { + return autoExpandLastRaceColumn; + } + + private void setAutoExpandPreSelectedRace(boolean autoExpandPreSelectedRace) { + this.autoExpandPreSelectedRace = autoExpandPreSelectedRace; + if (autoExpandPreSelectedRace) { + autoExpandPerformedOnce = false; + } + } + + private boolean isShowOverallColumnWithNumberOfRacesCompletedPerCompetitor() { + return showOverallColumnWithNumberOfRacesCompletedPerCompetitor; + } + + private void setShowOverallColumnWithNumberOfRacesCompletedPerCompetitor( + boolean showOverallColumnWithNumberOfRacesCompletedPerCompetitor) { + this.showOverallColumnWithNumberOfRacesCompletedPerCompetitor = showOverallColumnWithNumberOfRacesCompletedPerCompetitor; + } + + private boolean isShowAddedScores() { + return showAddedScores; + } + + private void setShowAddedScores(boolean showAddedScores) { + this.showAddedScores = showAddedScores; + } + + private boolean isShowCompetitorSailId() { + return showCompetitorSailId; + } + + private void setShowCompetitorSailId(boolean showCompetitorSailId) { + this.showCompetitorSailId = showCompetitorSailId; + } + + private boolean isShowCompetitorFullName() { + return showCompetitorFullName; + } + + private void setShowCompetitorFullName(boolean showCompetitorFullName) { + this.showCompetitorFullName = showCompetitorFullName; + } + + /** + * The time point for which the leaderboard currently shows results. In {@link PlayModes#Replay replay mode} this is + * the {@link #timer}'s time point. In {@link PlayModes#Live live mode} the {@link #timer}'s time is quantized to + * the closest full second to increase the likelihood of cache hits in the back end. + */ + protected Date getLeaderboardDisplayDate() { + return timer.getTime(); + } + + /** + * adds the column to the right end of the {@link #getLeaderboardTable() leaderboard table} and sets + * the column style according to the {@link LeaderboardSortableColumnWithMinMax#getColumnStyle() column's style + * definition}. + */ + protected void addColumn(AbstractSortableColumnWithMinMax column) { + leaderboardTable.addColumn(column, column.getHeader(), column.getComparator(), + column.getPreferredSortingOrder().isAscending()); + String columnStyle = column.getColumnStyle(); + if (columnStyle != null) { + getLeaderboardTable().addColumnStyleName(getLeaderboardTable().getColumnCount() - 1, columnStyle); + } + } + + protected void insertColumn(int beforeIndex, AbstractSortableColumnWithMinMax column) { + // remove column styles of those columns whose index will shift right by + // one: + removeColumnStyles(beforeIndex); + getLeaderboardTable().insertColumn(beforeIndex, column, column.getHeader(), column.getComparator(), + column.getPreferredSortingOrder().isAscending()); + addColumnStyles(beforeIndex); + } + + private void addColumnStyles(int startColumn) { + for (int i = startColumn; i < getLeaderboardTable().getColumnCount(); i++) { + AbstractSortableColumnWithMinMax columnToRemoveStyleFor = (AbstractSortableColumnWithMinMax) getLeaderboardTable() + .getColumn(i); + String columnStyle = columnToRemoveStyleFor.getColumnStyle(); + if (columnStyle != null) { + getLeaderboardTable().addColumnStyleName(i, columnStyle); + } + } + } + + private void removeColumnStyles(int startColumn) { + for (int i = startColumn; i < getLeaderboardTable().getColumnCount(); i++) { + AbstractSortableColumnWithMinMax columnToRemoveStyleFor = (AbstractSortableColumnWithMinMax) getLeaderboardTable() + .getColumn(i); + String columnStyle = columnToRemoveStyleFor.getColumnStyle(); + if (columnStyle != null) { + getLeaderboardTable().removeColumnStyleName(i, columnStyle); + } + } + } + + /** + * removes the column specified by columnIndex from the {@link #getLeaderboardTable() leaderboard + * table} and fixes the column styles again (see {@link #addColumnStyles(int)}). + */ + protected void removeColumn(int columnIndex) { + Column c = getLeaderboardTable().getColumn(columnIndex); + if (c instanceof ExpandableSortableColumn) { + ExpandableSortableColumn expandableColumn = (ExpandableSortableColumn) c; + if (expandableColumn.isExpanded()) { + // remove expanded child columns from the leaderboard... + expandableColumn.changeExpansionState(/* expand */ false); + // them remember that column c was expanded: + expandableColumn.setExpanded(true); + } + } + removeColumnStyles(/* startColumn */columnIndex); + getLeaderboardTable().removeColumn(columnIndex); + addColumnStyles(/* startColumn */columnIndex); + } + + protected void removeColumn(Column c) { + int columnIndex = getLeaderboardTable().getColumnIndex(c); + if (columnIndex != -1) { + removeColumn(columnIndex); + } + } + + public void loadCompleteLeaderboard(final boolean showProgress) { + final Date date = getLeaderboardDisplayDate(); + if (needsDataLoading()) { + if (showProgress) { + addBusyTask(); + } + GetLeaderboardByNameAction getLeaderboardByNameAction = new GetLeaderboardByNameAction(sailingService, + getLeaderboardName(), useNullAsTimePoint() ? null : date, + /* namesOfRaceColumnsForWhichToLoadLegDetails */getNamesOfExpandedRaceColumns(), + shallAddOverallDetails(), /* previousLeaderboard */ + getLeaderboard(), isFillTotalPointsUncorrected(), timer, errorReporter, stringMessages); + this.asyncActionsExecutor.execute(getLeaderboardByNameAction, LOAD_LEADERBOARD_DATA_CATEGORY, + new AsyncCallback() { + @Override + public void onSuccess(LeaderboardDTO result) { + currentSettings = currentSettings.overrideDefaultsForNamesOfRaceColumns(result.getNamesOfRaceColumns()); + + try { + updateLeaderboard(result); + // reapply, as columns might have changed + updateSettings(currentSettings); + } finally { + if (showProgress) { + removeBusyTask(); + } + } + } + + @Override + public void onFailure(Throwable caught) { + if (showProgress) { + removeBusyTask(); + } + getErrorReporter().reportError( + "Error trying to obtain leaderboard contents: " + caught.getMessage(), + true /* silentMode */); + } + }); + } + } + + protected boolean isFillTotalPointsUncorrected() { + return false; + } + + /** + * In {@link PlayModes#Live live mode}, when {@link #loadCompleteLeaderboard(boolean) loading the leaderboard + * contents}, null is used as time point. The condition for this is encapsulated in this method so + * others can find out. For example, when a time change is signaled due to local offset / delay adjustments, no + * additional call to {@link #loadCompleteLeaderboard(boolean)} would be required as null will be + * passed in any case, not being affected by local time offsets. + */ + private boolean useNullAsTimePoint() { + return timer.getPlayMode() == PlayModes.Live; + } + + private boolean needsDataLoading() { + return isVisible(); + } + + /** + * Determine from column expansion state which races need their leg details + */ + private Collection getNamesOfExpandedRaceColumns() { + Collection namesOfExpandedRaceColumns = new ArrayList(); + for (int i = 0; i < getLeaderboardTable().getColumnCount(); i++) { + Column column = getLeaderboardTable().getColumn(i); + if (column instanceof RaceColumn) { + RaceColumn raceColumn = (RaceColumn) column; + if (raceColumn.isExpanded()) { + namesOfExpandedRaceColumns.add(raceColumn.getRaceColumnName()); + } + } + } + return namesOfExpandedRaceColumns; + } + + private boolean shallAddOverallDetails() { + return !selectedOverallDetailColumns.isEmpty(); + } + + private void applyTop30FilterIfCompetitorSizeGreaterEqual40(LeaderboardDTO leaderboard) { + int maxRaceRank = 30; + if (leaderboard.competitors.size() >= 40) { + CompetitorRaceRankFilter raceRankFilter = new CompetitorRaceRankFilter(); + raceRankFilter.setLeaderboardFetcher(this); + raceRankFilter.setSelectedRace(preSelectedRace); + raceRankFilter.setQuickRankProvider(this.competitorFilterPanel.getQuickRankProvider()); + raceRankFilter.setOperator(new BinaryOperator(BinaryOperator.Operators.LessThanEquals)); + raceRankFilter.setValue(maxRaceRank); + FilterSet> activeFilterSet = competitorSelectionProvider + .getOrCreateCompetitorsFilterSet(stringMessages.topNCompetitorsByRaceRank(maxRaceRank)); + activeFilterSet.addFilter(raceRankFilter); + competitorSelectionProvider.setCompetitorsFilterSet(activeFilterSet); + } + } + + /** + * Assigns leaderboard to {@link #leaderboard} and updates the UI accordingly. Also updates the min/max + * values on the columns. + */ + public void updateLeaderboard(LeaderboardDTO leaderboard) { + if (leaderboard != null) { + Collection> columnsToCollapseAndExpandAgain = getExpandedRaceColumnsWhoseDisplayedLegCountChanged( + leaderboard); + for (RaceColumn columnToCollapseAndExpandAgain : columnsToCollapseAndExpandAgain) { + columnToCollapseAndExpandAgain.changeExpansionState(/* expand */ false); + } + competitorSelectionProvider.setCompetitors(leaderboard.competitors, /* listenersNotToNotify */this); + if (!initialCompetitorFilterHasBeenApplied) { + applyTop30FilterIfCompetitorSizeGreaterEqual40(leaderboard); + initialCompetitorFilterHasBeenApplied = true; + } + raceColumnSelection.autoUpdateRaceColumnSelectionForUpdatedLeaderboard(getLeaderboard(), leaderboard); + setLeaderboard(leaderboard); + adjustColumnLayout(leaderboard); + updateRaceColumnDTOsToRaceColumns(leaderboard); + for (RaceColumn columnToCollapseAndExpandAgain : columnsToCollapseAndExpandAgain) { + columnToCollapseAndExpandAgain.changeExpansionState(/* expand */ true); + } + adjustDelayToLive(); + final Map rowsToDisplay = getRowsToDisplay(); + Set rowsToAdd = new HashSet<>(rowsToDisplay.values()); + Map rowsToUpdate = new HashMap<>(); + synchronized (getData().getList()) { + int index = 0; + for (Iterator i = getData().getList().iterator(); i.hasNext();) { + LeaderboardRowDTO oldRow = i.next(); + LeaderboardRowDTO newRow = rowsToDisplay.get(oldRow.competitor); + if (newRow != null) { + rowsToUpdate.put(index++, newRow); // update row in place, preserving its selection state + rowsToAdd.remove(newRow); // no need to add this row when it was updated in-place + } else { + i.remove(); // old row's competitor not found in new rows' competitors; remove old row from + // table + } + } + for (Entry updateEntry : rowsToUpdate.entrySet()) { + LeaderboardRowDTO oldElement = getData().getList().set(updateEntry.getKey(), + updateEntry.getValue()); + leaderboardSelectionModel.setSelected(oldElement, false); // make sure the old element is no longer + // part of the selection + updateSelection(updateEntry.getValue()); + } + for (LeaderboardRowDTO rowToAdd : rowsToAdd) { + getData().getList().add(rowToAdd); + updateSelection(rowToAdd); + } + } + RaceColumn lastRaceColumn = null; + for (int i = getLeaderboardTable().getColumnCount() - 1; i >= 0; i--) { + if (getLeaderboardTable().getColumn(i) instanceof RaceColumn) { + lastRaceColumn = (RaceColumn) getLeaderboardTable().getColumn(i); + break; + } + } + for (int i = 0; i < getLeaderboardTable().getColumnCount(); i++) { + AbstractSortableColumnWithMinMax c = (AbstractSortableColumnWithMinMax) getLeaderboardTable() + .getColumn(i); + c.updateMinMax(); + // Toggle pre-selected race, if the setting is set and it isn't open yet, or the last race column if + // that was requested + if ((!autoExpandPerformedOnce && isAutoExpandPreSelectedRace() && c instanceof RaceColumn + && ((RaceColumn) c).getRace().hasTrackedRace(preSelectedRace)) + || (isAutoExpandLastRaceColumn() && c == lastRaceColumn)) { + ExpandableSortableColumn expandableSortableColumn = (ExpandableSortableColumn) c; + if (!expandableSortableColumn.isExpanded()) { + expandableSortableColumn.changeExpansionState(/* expand */ true); + autoExpandPerformedOnce = true; + } + } + if (c instanceof RaceColumn && ((RaceColumn) c).getRace().hasTrackedRace(preSelectedRace)) { + RaceColumnDTO raceColumn = ((RaceColumn) c).getRace(); + informLeaderboardUpdateListenersAboutRaceSelected(preSelectedRace, raceColumn); + } + } + if (leaderboardTable.getCurrentlySortedColumn() != null) { + leaderboardTable.sort(); + } else { + AbstractSortableColumnWithMinMax columnToSortFor = getDefaultSortColumn(); + leaderboardTable.sortColumn(columnToSortFor, columnToSortFor.getPreferredSortingOrder().isAscending()); + } + + if (!isEmbedded) { + scoreCorrectionCommentLabel.setText(leaderboard.getComment() != null ? leaderboard.getComment() : ""); + if (leaderboard.getTimePointOfLastCorrectionsValidity() != null) { + Date lastCorrectionDate = leaderboard.getTimePointOfLastCorrectionsValidity(); + String lastUpdate = DateAndTimeFormatterUtil.longDateFormatter.render(lastCorrectionDate) + ", " + + DateAndTimeFormatterUtil.longTimeFormatter.render(lastCorrectionDate); + scoreCorrectionLastUpdateTimeLabel.setText(stringMessages.lastScoreUpdate() + ": " + lastUpdate); + } else { + scoreCorrectionLastUpdateTimeLabel.setText(""); + } + + boolean hasLiveRace = !leaderboard.getLiveRaces(timer.getLiveTimePointInMillis()).isEmpty(); + liveRaceLabel.setText(hasLiveRace ? getLiveRacesText() : ""); + scoreCorrectionLastUpdateTimeLabel.setVisible(!hasLiveRace); + liveRaceLabel.setVisible(hasLiveRace); + } + informLeaderboardUpdateListenersAboutLeaderboardUpdated(leaderboard); + } + } + + /** + * Adjusts the row's selection in the {@link #leaderboardSelectionModel} so it matches its selection state in the + * {@link #competitorSelectionProvider}. + */ + private void updateSelection(LeaderboardRowDTO row) { + final boolean shallBeSelected = competitorSelectionProvider.isSelected(row.competitor); + if (leaderboardAsTableSelectionModelRegistration != null) { + // suspend selection events while actively adjusting the leaderboardSelectionModel to match the + // competitorSelectionProvider + leaderboardAsTableSelectionModelRegistration.removeHandler(); + leaderboardAsTableSelectionModelRegistration = null; + } + if (leaderboardSelectionModel.isSelected(row) != shallBeSelected) { + leaderboardSelectionModel.setSelected(row, shallBeSelected); + } + // register the selection change handler again + leaderboardAsTableSelectionModelRegistration = leaderboardTable.getSelectionModel() + .addSelectionChangeHandler(selectionChangeHandler); + } + + /** + * The race columns hold a now outdated copy of a {@link RaceColumnDTO} which needs to be updated from the + * {@link LeaderboardDTO} just received + */ + private void updateRaceColumnDTOsToRaceColumns(LeaderboardDTO leaderboard) { + for (RaceColumnDTO newRace : leaderboard.getRaceList()) { + RaceColumn raceColumn = getRaceColumnByRaceColumnName(newRace.getName()); + if (raceColumn != null) { + raceColumn.setRace(newRace); + } + } + } + + /** + * Due to a course change, a race may change its number of legs. All expanded race columns that show leg columns and + * whose leg count changed need to be collapsed before the leaderboard is replaced, and expanded afterwards again. + * Race columns whose toggling is {@link ExpandableSortableColumn#isTogglingInProcess() currently in progress} are + * not considered because their new state will be considered after replacing anyhow. + * + * @param newLeaderboard + * the new leaderboard before assigning to {@link #leaderboard} + * @return the columns that were collapsed in this step and that shall be expanded again after the leaderboard has + * been replaced + */ + private Collection> getExpandedRaceColumnsWhoseDisplayedLegCountChanged( + LeaderboardDTO newLeaderboard) { + Set> result = new HashSet>(); + if (selectedRaceDetails.contains(DetailType.DISPLAY_LEGS)) { + for (int i = 0; i < getLeaderboardTable().getColumnCount(); i++) { + Column c = getLeaderboardTable().getColumn(i); + if (c instanceof RaceColumn) { + RaceColumn rc = (RaceColumn) c; + // If the new leaderboard no longer contains the column, getLegCount will return -1, causing the + // column + // to be collapsed if it was expanded. This is correct because otherwise, removing it would no + // longer + // know the correct leg count. + if (!rc.isTogglingInProcess() && rc.isExpanded()) { + int oldLegCount = getLeaderboard().getLegCount(rc.getRaceColumnName(), preSelectedRace); + int newLegCount = newLeaderboard.getLegCount(rc.getRaceColumnName(), preSelectedRace); + if (oldLegCount != newLegCount) { + result.add(rc); + } + } + } + } + } + return result; + } + + /** + * Based on the {@link LeaderboardDTO#getDelayToLiveInMillisForLatestRace()} for the race that has the latest + * {@link RaceColumnDTO#getStartDate(FleetDTO) start time}, automatically adjusts the delay accordingly unless the + * {@link #adjustTimerDelay} flag is false + */ + private void adjustDelayToLive() { + if (adjustTimerDelay) { + if (leaderboard.getDelayToLiveInMillisForLatestRace() != null) { + setDelayInMilliseconds(leaderboard.getDelayToLiveInMillisForLatestRace()); + } + } + } + + /** + * Extracts the rows to display of the leaderboard. These are all {@link AbstractLeaderboardDTO#rows + * rows} in case {@link #preSelectedRace} is null, or only the rows of the competitors who scored in + * the race identified by {@link #preSelectedRace} otherwise. + */ + @Override + public Map getRowsToDisplay() { + Map result; + Iterable allFilteredCompetitors = competitorSelectionProvider.getFilteredCompetitors(); + result = new HashMap(); + if (preSelectedRace == null) { + for (CompetitorDTO competitor : leaderboard.rows.keySet()) { + if (Util.contains(allFilteredCompetitors, competitor)) { + result.put(competitor, leaderboard.rows.get(competitor)); + } + } + } else { + for (CompetitorDTO competitorInPreSelectedRace : getCompetitors(preSelectedRace)) { + if (Util.contains(allFilteredCompetitors, competitorInPreSelectedRace)) { + result.put(competitorInPreSelectedRace, leaderboard.rows.get(competitorInPreSelectedRace)); + } + } + } + return result; + } + + /** + * The {@link LeaderboardDTO} holds {@link LeaderboardDTO#getRaceList() races} as {@link RaceColumnDTO} objects. + * Those map their fleet names to the {@link RegattaAndRaceIdentifier} which identifies the tracked race + * representing the fleet race within the race column. + *

+ * + * On the other hand, a {@link LeaderboardRowDTO} has {@link LeaderboardEntryDTO}, keyed by race column name. The + * entry DTOs, in turn, store the {@link RaceIdentifier} of the race in which the respective competitor achieved the + * score. + *

+ * + * With this information it is possible to identify the competitors who participated in a particular tracked race, + * as identified through the race parameter. + * + * @return all competitors for which the {@link #getLeaderboard() leaderboard} has an entry whose + * {@link LeaderboardEntryDTO#race} equals race + */ + public Iterable getCompetitors(RaceIdentifier race) { + Set result = new HashSet(); + if (getLeaderboard() != null) { + for (RaceColumnDTO raceColumn : getLeaderboard().getRaceList()) { + if (raceColumn.hasTrackedRace(race)) { + for (Map.Entry e : getLeaderboard().rows.entrySet()) { + LeaderboardEntryDTO entry = e.getValue().fieldsByRaceColumnName + .get(raceColumn.getRaceColumnName()); + if (entry != null && entry.race != null && entry.race.equals(race)) { + result.add(e.getKey()); + } + } + } + } + } + return result; + } + + private AbstractSortableColumnWithMinMax getDefaultSortColumn() { + AbstractSortableColumnWithMinMax defaultSortColumn = null; + if (raceNameForDefaultSorting != null) { + defaultSortColumn = getRaceColumnByRaceName(raceNameForDefaultSorting); + } + if (defaultSortColumn == null) { + defaultSortColumn = getRankColumn(); + } + return defaultSortColumn; + } + + private TotalRankColumn getRankColumn() { + return totalRankColumn; + } + + protected void setLeaderboard(LeaderboardDTO leaderboard) { + this.leaderboard = leaderboard; + } + + @Override + public LeaderboardDTO getLeaderboard() { + return leaderboard; + } + + private void adjustColumnLayout(LeaderboardDTO leaderboard) { + int columnIndex = 0; + columnIndex = ensureSelectionCheckboxColumn(columnIndex); + columnIndex = ensureRankColumn(columnIndex); + columnIndex = ensureSailIDAndCompetitorColumn(columnIndex); + columnIndex = updateCarryColumn(leaderboard, columnIndex); + adjustOverallDetailColumns(leaderboard, columnIndex); + // first remove race columns no longer needed: + removeUnusedRaceColumns(leaderboard); + if (leaderboard != null) { + createMissingAndAdjustExistingRaceColumns(leaderboard); + ensureTotalsColumn(); + updateTotalRacesSailedColumn(); + } + } + + /** + * Ensures that the columns requested by {@link #selectedOverallDetailColumns} are in the table. Assumes that if + * there are any existing overall details columns, they start at indexOfFirstOverallDetailsColumn and + * are in the order defined by {@link #getAvailableOverallDetailColumnTypes()}. + * + * @param indexOfFirstOverallDetailsColumn + * tells the column index for the first overall details column + */ + private void adjustOverallDetailColumns(LeaderboardDTO leaderboard, int indexOfFirstOverallDetailsColumn) { + List> overallDetailColumnsToShow = new ArrayList>(); + // ensure the ordering in overallDetailColumnsToShow conforms to the ordering of + // getAvailableOverallDetailColumnTypes() + for (DetailType overallDetailType : getAvailableOverallDetailColumnTypes()) { + if (selectedOverallDetailColumns.contains(overallDetailType) + && overallDetailColumnMap.containsKey(overallDetailType)) { + overallDetailColumnsToShow.add(overallDetailColumnMap.get(overallDetailType)); + } + } + int currentColumnIndex = indexOfFirstOverallDetailsColumn; + int i = 0; // index into overallDetailColumnToShow + Column currentColumn = currentColumnIndex < getLeaderboardTable().getColumnCount() + ? getLeaderboardTable().getColumn(currentColumnIndex) : null; + // repeat until no more column to check for removal and no more column left to check for need to insert + while (i < overallDetailColumnsToShow.size() || overallDetailColumnMap.values().contains(currentColumn)) { + if (i < overallDetailColumnsToShow.size() && currentColumn == overallDetailColumnsToShow.get(i)) { + // found selected column in table; all good, advance both "pointers" + i++; + currentColumnIndex++; + currentColumn = getLeaderboardTable().getColumn(currentColumnIndex); + } else if (i < overallDetailColumnsToShow.size()) { + // selected column is missing; insert + insertColumn(currentColumnIndex++, overallDetailColumnsToShow.get(i++)); + } else { + // based on the while's condition, currentColumn is an overallDetailsColumnMap value; + // based on the previous if's failed condition, it is not selected. Remove: + removeColumn(currentColumnIndex); + currentColumn = getLeaderboardTable().getColumn(currentColumnIndex); + } + } + } + + /** + * If header information doesn't match the race column's actual state (tracked races attached meaning expandable; + * medal race), the column is removed and inserted again + * + * @param raceColumn + * the raceColumn to correct. + * @param selectedRaceColumn + * the new race column data for raceColumn + */ + private void correctColumnData(RaceColumn raceColumn, RaceColumnDTO race) { + int columnIndex = getRaceColumnPosition(raceColumn); + if (raceColumn.isExpansionEnabled() != race.hasTrackedRaces() + || race.isMedalRace() != raceColumn.isMedalRace()) { + if (raceColumn.isExpanded()) { + raceColumn.changeExpansionState(/* expand */ false); // remove children from table + } + removeColumn(columnIndex); + insertColumn(columnIndex, createRaceColumn(race)); + } + } + + /** + * Removes all RaceColumns, starting at count {@link raceColumnStartIndex raceColumnStartIndex} + * + * @param raceColumnStartIndex + * The index of the race column should be deleted from. + * @param raceName + * The name of the racing column until the table should be cleared. + */ + private void removeRaceColumnFromRaceColumnStartIndexBeforeRace(int raceColumnStartIndex, RaceColumnDTO race) { + int counter = 0; + for (int leaderboardposition = 0; leaderboardposition < getLeaderboardTable() + .getColumnCount(); leaderboardposition++) { + Column c = getLeaderboardTable().getColumn(leaderboardposition); + if (c instanceof RaceColumn) { + RaceColumn raceColumn = (RaceColumn) c; + if (!raceColumn.getRaceColumnName().equals(race.getRaceColumnName())) { + if (raceColumnStartIndex == counter) { + removeColumn(raceColumn); + } + } else { + return; + } + counter++; + } + } + } + + /** + * Gets a ColumnPosition of a raceColumn + * + * @param raceColumn + * The column for which the position is to be found in the leaderboard table + * @return the position. Returns -1 if raceColumn not existing in leaderboardTable. + */ + private int getRaceColumnPosition(RaceColumn raceColumn) { + for (int leaderboardposition = 0; leaderboardposition < getLeaderboardTable() + .getColumnCount(); leaderboardposition++) { + Column c = getLeaderboardTable().getColumn(leaderboardposition); + if (c instanceof RaceColumn) { + RaceColumn rc = (RaceColumn) c; + if (rc.equals(raceColumn)) { + return leaderboardposition; + } + } + } + return -1; + } + + /** + * This method returns the position where a race column should get inserted. + * + * @param raceName + * the name of the race to insert + * @param listpos + * the position of the race in the {@link selectedRaceColumns selectedRaceColumns} + * @return the position of a race column right before which to insert the race column so that it is the + * listposth race in the table or -1 if no such race column was found, e.g., in case the column + * needs to be inserted as the last race in the table + */ + private int getColumnPositionToInsert(RaceColumnDTO race, int listpos) { + int raceColumnCounter = 0; + int noRaceColumnCounter = 0; + boolean raceColumnFound = false; + for (int leaderboardPosition = 0; !raceColumnFound + && leaderboardPosition < getLeaderboardTable().getColumnCount(); leaderboardPosition++) { + Column c = getLeaderboardTable().getColumn(leaderboardPosition); + if (c instanceof RaceColumn) { + // RaceColumn raceColumn = (RaceColumn) c; + if (raceColumnCounter == listpos) { + raceColumnFound = true; + } else { + raceColumnCounter++; + } + } else { + noRaceColumnCounter++; + } + } + if (raceColumnFound) { + return raceColumnCounter + noRaceColumnCounter; + } else { + return -1; + } + } + + /** + * Removes all columns of type {@link RaceColumn} from the leaderboard table if their name is not in the list of + * names of the selectedRaceColumns. + */ + private void removeRaceColumnsNotSelected(Iterable selectedRaceColumns) { + Set selectedRaceColumnNames = new HashSet(); + for (RaceColumnDTO selectedRaceColumn : selectedRaceColumns) { + selectedRaceColumnNames.add(selectedRaceColumn.getRaceColumnName()); + } + List> columnsToRemove = new ArrayList>(); + for (int i = 0; i < getLeaderboardTable().getColumnCount(); i++) { + Column c = getLeaderboardTable().getColumn(i); + if (c instanceof RaceColumn && (leaderboard == null + || !selectedRaceColumnNames.contains(((RaceColumn) c).getRaceColumnName()))) { + columnsToRemove.add(c); + } + } + for (Column c : columnsToRemove) { + removeColumn(c); + } + } + + /** + * Existing and matching race columns may still need to be removed, re-created and inserted because the "tracked" + * property may have changed, changing the columns expandability. + */ + private void createMissingAndAdjustExistingRaceColumns(LeaderboardDTO leaderboard) { + // Correct order of races in selectedRaceColum + Iterable correctedOrderSelectedRaces = raceColumnSelection + .getSelectedRaceColumnsOrderedAsInLeaderboard(leaderboard); + removeRaceColumnsNotSelected(correctedOrderSelectedRaces); + for (int selectedRaceCount = 0; selectedRaceCount < Util + .size(correctedOrderSelectedRaces); selectedRaceCount++) { + RaceColumnDTO selectedRaceColumn = Util.get(correctedOrderSelectedRaces, selectedRaceCount); + final RaceColumn raceColumn = selectedRaceColumn == null ? null + : getRaceColumnByRaceColumnName(selectedRaceColumn.getName()); + if (raceColumn != null) { + // remove all raceColumns, starting at a specific selectedRaceCount, up to but excluding the selected + // raceName with the result that selectedRace is at position selectedRaceCount afterwards + removeRaceColumnFromRaceColumnStartIndexBeforeRace(selectedRaceCount, selectedRaceColumn); + correctColumnData(raceColumn, selectedRaceColumn); + } else { + // get correct position to insert the column + int positionToInsert = getColumnPositionToInsert(selectedRaceColumn, selectedRaceCount); + if (positionToInsert != -1) { + insertColumn(positionToInsert, createRaceColumn(selectedRaceColumn)); + } else { + // Add the raceColumn with addRaceColumn, if no RaceColumn is existing in leaderboard + addRaceColumn(createRaceColumn(selectedRaceColumn)); + } + } + } + } + + protected RaceColumn createRaceColumn(RaceColumnDTO raceInLeaderboard) { + TextRaceColumn textRaceColumn = new TextRaceColumn(raceInLeaderboard, shallExpandRaceColumn(raceInLeaderboard), + SortingOrder.ASCENDING, RACE_COLUMN_HEADER_STYLE, RACE_COLUMN_STYLE); + return textRaceColumn; + } + + /** + * To be expandable, a race column needs to have one or more tracked races associated and needs to have at least + * some GPS data available. Wind data is not required for expandability because several metrics can reasonably be + * determined even without reliable wind information. + */ + private boolean shallExpandRaceColumn(RaceColumnDTO raceColumnDTO) { + return showRaceDetails && raceColumnDTO.hasTrackedRaces() && raceColumnDTO.hasGPSData(); + } + + private void removeUnusedRaceColumns(LeaderboardDTO leaderboard) { + List> columnsToRemove = new ArrayList>(); + for (int i = 0; i < getLeaderboardTable().getColumnCount(); i++) { + Column c = getLeaderboardTable().getColumn(i); + if (c instanceof RaceColumn && (leaderboard == null + || !leaderboard.raceListContains(((RaceColumn) c).getRaceColumnName()))) { + columnsToRemove.add(c); + } + } + // Tricky issue: if the race column is currently expanded, we can't know anymore how many detail columns + // there are because the updated LeaderboardDTO object doesn't contain the race anymore. We have to + // collapse and remove all LegColumns following the RaceColumn + for (Column c : columnsToRemove) { + removeColumn(c); + } + } + + /** + * If the last column is the totals column, remove it. Add the race column as the last column. + */ + private void addRaceColumn(RaceColumn raceColumn) { + if (getLeaderboardTable() + .getColumn(getLeaderboardTable().getColumnCount() - 1) instanceof TotalNetPointssColumn) { + removeColumn(getLeaderboardTable().getColumnCount() - 1); + } + addColumn(raceColumn); + } + + /** + * The regatta rank column shall be displayed if an only if the {@link DetailType#REGATTA_RANK} detail is selected + * in the {@link #selectedOverallDetailColumns}. It will then be displayed after the selection checkbox column (if + * any) and before the sail number / competitor name columns. + */ + private boolean isShowRegattaRankColumn() { + return selectedOverallDetailColumns.contains(DetailType.REGATTA_RANK); + } + + /** + * @param rankColumnIndex + * the column index (0-based) where to put the rank column, if needed + * @return the column index (0-based) for the next column; will equal rankColumnIndex if the rank + * column is not {@link #isShowRegattaRankColumn() supposed to be shown}, or one greater otherwise. + */ + private int ensureRankColumn(int rankColumnIndex) { + final int indexOfNextColumn = rankColumnIndex + (isShowRegattaRankColumn() ? 1 : 0); + if (getLeaderboardTable().getColumnCount() > rankColumnIndex) { + if (isShowRegattaRankColumn()) { + if (getLeaderboardTable().getColumn(rankColumnIndex) != getRankColumn()) { + insertColumn(rankColumnIndex, getRankColumn()); + } // else, the column is needed and is already in place + } else { + if (getLeaderboardTable().getColumn(rankColumnIndex) == getRankColumn()) { + removeColumn(rankColumnIndex); + } + } + } else { + if (isShowRegattaRankColumn()) { + insertColumn(rankColumnIndex, getRankColumn()); + } + } + return indexOfNextColumn; + } + + /** + * @param selectionCheckboxColumnIndex + * the column index (0-based) where to put the selection checkbox column, if needed + * @return the column index (0-based) for the next column; will equal selectionCheckboxColumnIndex if + * the selection checkbox column is not {@link #showSelectionCheckbox supposed to be shown}, or one greater + * otherwise. + */ + private int ensureSelectionCheckboxColumn(int selectionCheckboxColumnIndex) { + final int indexOfNextColumn = selectionCheckboxColumnIndex + (showSelectionCheckbox ? 1 : 0); + if (getLeaderboardTable().getColumnCount() > selectionCheckboxColumnIndex) { + if (showSelectionCheckbox) { + if (getLeaderboardTable().getColumn(selectionCheckboxColumnIndex) != selectionCheckboxColumn) { + insertColumn(selectionCheckboxColumnIndex, selectionCheckboxColumn); + } // else, the column is needed and is already in place + } else { + if (getLeaderboardTable().getColumn(selectionCheckboxColumnIndex) == selectionCheckboxColumn) { + removeColumn(selectionCheckboxColumnIndex); + } + } + } else { + if (showSelectionCheckbox) { + insertColumn(selectionCheckboxColumnIndex, selectionCheckboxColumn); + } + } + return indexOfNextColumn; + } + + /** + * @return the 0-based index for the next column + */ + private int ensureSailIDAndCompetitorColumn(int columnIndexWhereToInsertTheNextColumn) { + if (isShowCompetitorSailId()) { + if (getLeaderboardTable().getColumnCount() <= columnIndexWhereToInsertTheNextColumn + || !(getLeaderboardTable() + .getColumn(columnIndexWhereToInsertTheNextColumn) instanceof SailIDColumn)) { + insertColumn(columnIndexWhereToInsertTheNextColumn, + new SailIDColumn(new CompetitorFetcher() { + @Override + public CompetitorDTO getCompetitor(LeaderboardRowDTO t) { + return t.competitor; + } + })); + } + columnIndexWhereToInsertTheNextColumn++; + } else { + if (getLeaderboardTable().getColumnCount() > columnIndexWhereToInsertTheNextColumn && getLeaderboardTable() + .getColumn(columnIndexWhereToInsertTheNextColumn) instanceof SailIDColumn) { + removeColumn(columnIndexWhereToInsertTheNextColumn); + } + } + if (isShowCompetitorFullName()) { + if (getLeaderboardTable().getColumnCount() <= columnIndexWhereToInsertTheNextColumn + || !(getLeaderboardTable() + .getColumn(columnIndexWhereToInsertTheNextColumn) instanceof CompetitorColumn)) { + insertColumn(columnIndexWhereToInsertTheNextColumn, createCompetitorColumn()); + } + columnIndexWhereToInsertTheNextColumn++; + } else { + if (getLeaderboardTable().getColumnCount() > columnIndexWhereToInsertTheNextColumn && getLeaderboardTable() + .getColumn(columnIndexWhereToInsertTheNextColumn) instanceof CompetitorColumn) { + removeColumn(columnIndexWhereToInsertTheNextColumn); + } + } + return columnIndexWhereToInsertTheNextColumn; + } + + protected CompetitorColumn createCompetitorColumn() { + return new CompetitorColumn(new CompetitorColumnBase(this, stringMessages, + new CompetitorFetcher() { + @Override + public CompetitorDTO getCompetitor(LeaderboardRowDTO t) { + return t.competitor; + } + })); + } + + private void ensureTotalsColumn() { + // add a totals column on the right + if (getLeaderboardTable().getColumnCount() == 0 || !(getLeaderboardTable() + .getColumn(getLeaderboardTable().getColumnCount() - 1) instanceof TotalNetPointssColumn)) { + addColumn(new TotalNetPointssColumn(TOTAL_COLUMN_STYLE)); + } + } + + private void ensureTotalRacesSailedColumn() { + // add a totals column on the right + if (getLeaderboardTable().getColumnCount() == 0 || !(getLeaderboardTable() + .getColumn(getLeaderboardTable().getColumnCount() - 2) instanceof TotalRacesCompletedColumn)) { + insertColumn(getLeaderboardTable().getColumnCount() - 1, new TotalRacesCompletedColumn(TOTAL_COLUMN_STYLE)); + } + } + + private void ensureNoTotalRacesSailedColumn() { + if ((getLeaderboardTable() + .getColumn(getLeaderboardTable().getColumnCount() - 2) instanceof TotalRacesCompletedColumn)) { + removeColumn(getLeaderboardTable().getColumnCount() - 2); + } + } + + protected void updateTotalRacesSailedColumn() { + final boolean showTotalRacesCompletedColumn = isShowOverallColumnWithNumberOfRacesCompletedPerCompetitor(); + if (showTotalRacesCompletedColumn) { + ensureTotalRacesSailedColumn(); + } else { + ensureNoTotalRacesSailedColumn(); + } + } + + /** + * If the leaderboard {@link LeaderboardDTO#hasCarriedPoints has carried points} and if column #1 + * (second column, right of the competitor column) does not exist or is not of type {@link CarryColumn}, all columns + * starting from #1 will be removed and a {@link CarryColumn} will be added. If the leaderboard has no carried + * points but the display still shows a carry column, the column is removed. + * + * @param zeroBasedIndexOfCarryColumn + * the 0-based column index where to put the carry column, if any + * @return the 0-based index of the next column following this column if the carry column is to be shown, or + * zeroBasedIndexOfCarryColumn otherwise + */ + protected int updateCarryColumn(LeaderboardDTO leaderboard, int zeroBasedIndexOfCarryColumn) { + final boolean needsCarryColumn = leaderboard != null && leaderboard.hasCarriedPoints; + if (needsCarryColumn) { + ensureCarryColumn(zeroBasedIndexOfCarryColumn); + } else { + ensureNoCarryColumn(zeroBasedIndexOfCarryColumn); + } + return needsCarryColumn ? zeroBasedIndexOfCarryColumn + 1 : zeroBasedIndexOfCarryColumn; + } + + private void ensureNoCarryColumn(int zeroBasedIndexOfCarryColumn) { + if (getLeaderboardTable().getColumnCount() > zeroBasedIndexOfCarryColumn + && getLeaderboardTable().getColumn(zeroBasedIndexOfCarryColumn) instanceof CarryColumn) { + removeColumn(zeroBasedIndexOfCarryColumn); + } + } + + protected void ensureCarryColumn(int zeroBasedIndexOfCarryColumn) { + if (getLeaderboardTable().getColumnCount() <= zeroBasedIndexOfCarryColumn + || !(getLeaderboardTable().getColumn(zeroBasedIndexOfCarryColumn) instanceof CarryColumn)) { + while (getLeaderboardTable().getColumnCount() > zeroBasedIndexOfCarryColumn) { + removeColumn(zeroBasedIndexOfCarryColumn); + } + addColumn(createCarryColumn()); + } + } + + protected CarryColumn createCarryColumn() { + return new CarryColumn(); + } + + public SortedCellTable getLeaderboardTable() { + return leaderboardTable; + } + + public SailingServiceAsync getSailingService() { + return sailingService; + } + + protected String getLeaderboardName() { + return leaderboardName; + } + + protected void setLeaderboardName(String leaderboardName) { + this.leaderboardName = leaderboardName; + } + + protected ErrorReporter getErrorReporter() { + return errorReporter; + } + + protected ListDataProvider getData() { + return getLeaderboardTable().getDataProvider(); + } + + /** + * @param newTime + * ignored; may be null. The time for loading the leaderboard is determined using + * {@link #getLeaderboardDisplayDate()}. + */ + @Override + public void timeChanged(Date newTime, Date oldTime) { + loadCompleteLeaderboard(/* showProgress */ false); + } + + @Override + public void playStateChanged(PlayStates playState, PlayModes playMode) { + if (!isEmbedded) { + playPause.setHTML(getPlayPauseImgHtml(playState)); + playPause.setTitle(playState == PlayStates.Playing ? stringMessages.pauseAutomaticRefresh() + : stringMessages.autoRefresh()); + } + } + + @Override + public void playSpeedFactorChanged(double newPlaySpeedFactor) { + // nothing to do + } + + private List getTrackedRacesIdentifiers() { + List result = new ArrayList(); + for (RaceColumnDTO raceColumn : getLeaderboard().getRaceList()) { + for (FleetDTO fleet : raceColumn.getFleets()) { + if (raceColumn.getRaceIdentifier(fleet) != null) { + result.add(raceColumn.getRaceIdentifier(fleet)); + } + } + } + return result; + } + + @Override + public boolean hasSettings() { + return true; + } + + @Override + public boolean hasToolbar() { + return false; + } + + @Override + public SettingsDialogComponent getSettingsDialogComponent() { + return new LeaderboardSettingsDialogComponent(getSettings(), leaderboard.getNamesOfRaceColumns(), + stringMessages); + } + + @Override + public LeaderboardSettings getSettings() { + Iterable selectedRaceColumns = raceColumnSelection + .getSelectedRaceColumnsOrderedAsInLeaderboard(leaderboard); + List namesOfRaceColumnsToShow = new ArrayList<>(); + for (RaceColumnDTO raceColumn : selectedRaceColumns) { + namesOfRaceColumnsToShow.add(raceColumn.getName()); + } + LeaderboardSettings leaderboardSettings = new LeaderboardSettings( + Collections.unmodifiableList(selectedManeuverDetails), Collections.unmodifiableList(selectedLegDetails), + Collections.unmodifiableList(selectedRaceDetails), + Collections.unmodifiableList(selectedOverallDetailColumns), namesOfRaceColumnsToShow, + /* namesOfRacesToShow */ null, raceColumnSelection.getNumberOfLastRaceColumnsToShow(), + autoExpandPreSelectedRace, timer.getRefreshInterval(), /* nameOfRaceToSort */ null, + /* sortAscending */ true, /* updateUponPlayStateChange */ true, raceColumnSelection.getType(), + isShowAddedScores(), isShowOverallColumnWithNumberOfRacesCompletedPerCompetitor(), + isShowCompetitorSailId(), isShowCompetitorFullName()); + SettingsDefaultValuesUtils.keepDefaults(currentSettings, leaderboardSettings); + return leaderboardSettings; + } + + @Override + public String getLocalizedShortName() { + return stringMessages.leaderboard(); + } + + private LeaderboardRowDTO getRow(String competitorIdAsString) { + synchronized (getData().getList()) { + for (LeaderboardRowDTO row : getData().getList()) { + if (row.competitor.getIdAsString().equals(competitorIdAsString)) { + return row; + } + } + } + return null; + } + + @Override + public void addedToSelection(CompetitorDTO competitor) { + LeaderboardRowDTO row = getRow(competitor.getIdAsString()); + if (row != null) { + leaderboardSelectionModel.setSelected(row, true); + } + } + + @Override + public void removedFromSelection(CompetitorDTO competitor) { + LeaderboardRowDTO row = getRow(competitor.getIdAsString()); + if (row != null) { + leaderboardSelectionModel.setSelected(row, false); + } + } + + private Iterable getSelectedRows() { + return leaderboardSelectionModel.getSelectedSet(); + } + + @Override + public void competitorsListChanged(Iterable competitors) { + setFilterControlStatus(); + if (timer.isInitialized()) { + timeChanged(timer.getTime(), null); + } + } + + @Override + public void filteredCompetitorsListChanged(Iterable filteredCompetitors) { + setFilterControlStatus(); + updateLeaderboard(getLeaderboard()); + } + + @Override + public void filterChanged(FilterSet> oldFilterSet, + FilterSet> newFilterSet) { + // nothing to do; if the list of filtered competitors has changed, a separate call to + // filteredCompetitorsListChanged will occur + setFilterControlStatus(); + } + + public RaceColumnSelection getRaceColumnSelection() { + return raceColumnSelection; + } + + public void removeAllListeners() { + if (raceTimesInfoProviderListener != null) { + getRaceTimesInfoProvider().removeRaceTimesInfoProviderListener(raceTimesInfoProviderListener); + } + if (raceColumnSelection != null && raceColumnSelection.getType() == RaceColumnSelectionStrategies.LAST_N) { + getRaceTimesInfoProvider() + .removeRaceTimesInfoProviderListener((LastNRacesColumnSelection) raceColumnSelection); + } + if (timer != null) { + timer.removeTimeListener(this); + } + if (leaderboardUpdateListener != null) { + leaderboardUpdateListener.clear(); + } + } + + protected Timer getTimer() { + return timer; + } + + protected void blurFocusedElementAfterSelectionChange() { + // now "blur" the selected leaderboard element because it seems to cause the cell table to scroll to its top; + // see bug 2093. + blur(); + final ScheduledCommand blurCommand = new ScheduledCommand() { + @Override + public void execute() { + blur(); + } + }; + Scheduler.get().scheduleDeferred(blurCommand); + } + + private void blur() { + if (elementToBlur != null) { + elementToBlur.blur(); + } + } + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + if (visible) { + timeChanged(getLeaderboardDisplayDate(), null); + } + } + + @Override + public void addLeaderboardUpdateListener(LeaderboardUpdateListener listener) { + this.leaderboardUpdateListener.add(listener); + } + + @Override + public void removeLeaderboardUpdateListener(LeaderboardUpdateListener listener) { + this.leaderboardUpdateListener.remove(listener); + } + + protected void informLeaderboardUpdateListenersAboutLeaderboardUpdated(LeaderboardDTO leaderboard) { + for (LeaderboardUpdateListener listener : new ArrayList( + this.leaderboardUpdateListener)) { + listener.updatedLeaderboard(leaderboard); + } + } + + protected void informLeaderboardUpdateListenersAboutRaceSelected(RaceIdentifier raceIdentifier, + RaceColumnDTO raceColumn) { + for (LeaderboardUpdateListener listener : this.leaderboardUpdateListener) { + listener.currentRaceSelected(raceIdentifier, raceColumn); + } + } + + public boolean hasLiveRace() { + return getLeaderboard().hasLiveRace(timer.getLiveTimePointInMillis()); + } + + public String getLiveRacesText() { + String result = ""; + List> liveRaces = leaderboard.getLiveRaces(timer.getLiveTimePointInMillis()); + boolean isMeta = leaderboard.type.isMetaLeaderboard(); + if (!liveRaces.isEmpty()) { + if (liveRaces.size() == 1) { + String text = getLiveRaceText(liveRaces.get(0), isMeta); + result = isMeta ? stringMessages.regattaIsLive(text) : stringMessages.raceIsLive(text); + } else { + String names = ""; + for (Pair liveRace : liveRaces) { + names += getLiveRaceText(liveRace, isMeta) + ", "; + } + // remove last ", " + names = names.substring(0, names.length() - 2); + result = isMeta ? stringMessages.regattasAreLive(names) : stringMessages.racesAreLive(names); + } + } + return result; + } + + private String getLiveRaceText(Pair liveRace, boolean isMeta) { + String raceName = liveRace.getA().getRaceColumnName(), fleetName = liveRace.getB().getName(); + boolean isDefaultFleet = LeaderboardNameConstants.DEFAULT_FLEET_NAME.equals(fleetName); + return raceName + ((isDefaultFleet || isMeta) ? "" : (" (" + liveRace.getB().getName() + ")")); + } + + @Override + public String getDependentCssClassName() { + return "leaderboard"; + } + + @Override + public void addBusyStateChangeListener(BusyStateChangeListener listener) { + busyStateChangeListeners.add(listener); + } + + @Override + public void removeBusyStateChangeListener(BusyStateChangeListener listener) { + busyStateChangeListeners.remove(listener); + } + + @Override + public boolean isBusy() { + return busyIndicator.isBusy(); + } + + private void setBusyState(boolean isBusy) { + if (busyIndicator.isBusy() != isBusy) { + busyIndicator.setBusy(isBusy); + for (BusyStateChangeListener listener : busyStateChangeListeners) { + listener.onBusyStateChange(isBusy); + } + } + } + + @Override + public void addBusyTask() { + busyTaskCounter++; + if (busyTaskCounter == 1) { + setBusyState(true); + } + } + + @Override + public void removeBusyTask() { + busyTaskCounter--; + if (busyTaskCounter == 0) { + setBusyState(false); + } + } + + @Override + public String getId() { + return LeaderboardPanelLifecycle.ID; + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardViewer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardViewer.java index 85869658f77..aff7f65ccf6 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardViewer.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardViewer.java @@ -20,8 +20,8 @@ import com.sap.sse.gwt.client.async.AsyncActionsExecutor; import com.sap.sse.gwt.client.async.MarkedAsyncCallback; import com.sap.sse.gwt.client.player.Timer; import com.sap.sse.gwt.client.shared.components.Component; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; import com.sap.sse.gwt.client.shared.perspective.PerspectiveCompositeSettings; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; /** * A viewer for a single leaderboard and a leaderboard chart. diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/MetaLeaderboardViewer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/MetaLeaderboardViewer.java index 957ca877ac6..3cb2d243a72 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/MetaLeaderboardViewer.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/MetaLeaderboardViewer.java @@ -18,8 +18,8 @@ import com.sap.sse.gwt.client.ErrorReporter; import com.sap.sse.gwt.client.async.AsyncActionsExecutor; import com.sap.sse.gwt.client.player.Timer; import com.sap.sse.gwt.client.shared.components.Component; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; import com.sap.sse.gwt.client.shared.perspective.PerspectiveCompositeSettings; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; /** * A viewer for an overall series leaderboard. Additionally the viewer can render a chart for the series leaderboard and diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/MultiLeaderboardPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/MultiLeaderboardPanel.java index 44af235ab37..3d383ec9688 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/MultiLeaderboardPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/MultiLeaderboardPanel.java @@ -1,37 +1,37 @@ -package com.sap.sailing.gwt.ui.leaderboard; - -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sailing.gwt.settings.client.leaderboard.LeaderboardSettings; -import com.sap.sailing.gwt.ui.client.CompetitorSelectionProvider; -import com.sap.sailing.gwt.ui.client.RaceTimesInfoProvider; -import com.sap.sailing.gwt.ui.client.SailingServiceAsync; -import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sse.gwt.client.ErrorReporter; -import com.sap.sse.gwt.client.async.AsyncActionsExecutor; -import com.sap.sse.gwt.client.player.Timer; -import com.sap.sse.gwt.client.shared.components.Component; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; - -/** - * Represents a single Leaderboard in the Context of a MultiLeaderboard - * - */ -public class MultiLeaderboardPanel extends LeaderboardPanel { - public MultiLeaderboardPanel(Component parent, ComponentContext context, - SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, LeaderboardSettings settings, - boolean isEmbedded, RegattaAndRaceIdentifier preSelectedRace, - CompetitorSelectionProvider competitorSelectionProvider, Timer timer, String leaderboardGroupName, - String leaderboardName, ErrorReporter errorReporter, StringMessages stringMessages, - boolean showRaceDetails, CompetitorFilterPanel competitorSearchTextBox, - boolean showSelectionCheckbox, RaceTimesInfoProvider optionalRaceTimesInfoProvider, - boolean autoExpandLastRaceColumn, boolean adjustTimerDelay, boolean autoApplyTopNFilter, - boolean showCompetitorFilterStatus, boolean enableSyncScroller) { - super(parent, context, sailingService, asyncActionsExecutor, settings, isEmbedded, preSelectedRace, - competitorSelectionProvider, timer, leaderboardGroupName, leaderboardName, errorReporter, - stringMessages, showRaceDetails, competitorSearchTextBox, showSelectionCheckbox, - optionalRaceTimesInfoProvider, autoExpandLastRaceColumn, adjustTimerDelay, autoApplyTopNFilter, - showCompetitorFilterStatus, enableSyncScroller); - } - - -} +package com.sap.sailing.gwt.ui.leaderboard; + +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.gwt.settings.client.leaderboard.LeaderboardSettings; +import com.sap.sailing.gwt.ui.client.CompetitorSelectionProvider; +import com.sap.sailing.gwt.ui.client.RaceTimesInfoProvider; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.async.AsyncActionsExecutor; +import com.sap.sse.gwt.client.player.Timer; +import com.sap.sse.gwt.client.shared.components.Component; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; + +/** + * Represents a single Leaderboard in the Context of a MultiLeaderboard + * + */ +public class MultiLeaderboardPanel extends LeaderboardPanel { + public MultiLeaderboardPanel(Component parent, ComponentContext context, + SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, LeaderboardSettings settings, + boolean isEmbedded, RegattaAndRaceIdentifier preSelectedRace, + CompetitorSelectionProvider competitorSelectionProvider, Timer timer, String leaderboardGroupName, + String leaderboardName, ErrorReporter errorReporter, StringMessages stringMessages, + boolean showRaceDetails, CompetitorFilterPanel competitorSearchTextBox, + boolean showSelectionCheckbox, RaceTimesInfoProvider optionalRaceTimesInfoProvider, + boolean autoExpandLastRaceColumn, boolean adjustTimerDelay, boolean autoApplyTopNFilter, + boolean showCompetitorFilterStatus, boolean enableSyncScroller) { + super(parent, context, sailingService, asyncActionsExecutor, settings, isEmbedded, preSelectedRace, + competitorSelectionProvider, timer, leaderboardGroupName, leaderboardName, errorReporter, + stringMessages, showRaceDetails, competitorSearchTextBox, showSelectionCheckbox, + optionalRaceTimesInfoProvider, autoExpandLastRaceColumn, adjustTimerDelay, autoApplyTopNFilter, + showCompetitorFilterStatus, enableSyncScroller); + } + + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/MultiLeaderboardProxyPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/MultiLeaderboardProxyPanel.java index d44863b85ba..6450e22455a 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/MultiLeaderboardProxyPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/MultiLeaderboardProxyPanel.java @@ -34,8 +34,7 @@ import com.sap.sse.gwt.client.player.Timer; import com.sap.sse.gwt.client.shared.components.AbstractLazyComponent; import com.sap.sse.gwt.client.shared.components.Component; import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; -import com.sap.sse.gwt.client.shared.perspective.ComponentContextWithSettingsStorage; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; /** * A panel managing multiple {@link LeaderboardPanel}s (e.g. from a meta leaderboard) so that the user can switch between them. @@ -156,17 +155,10 @@ public class MultiLeaderboardProxyPanel extends AbstractLazyComponent ctx = getComponentContext(); // store contextspecific setting in this hashmap, so they are not lost on tab change - if (ctx instanceof ComponentContextWithSettingsStorage) { - @SuppressWarnings("unchecked") - ComponentContextWithSettingsStorage cctx = (ComponentContextWithSettingsStorage) ctx; - LeaderboardSettings contextSettings = cctx.extractContextSpecificSettings(this, newSettings); - contextStore.put(selectedLeaderboardPanel.getLeaderboardName(), contextSettings); - } + contextStore.put(selectedLeaderboardPanel.getLeaderboardName(), newSettings); selectedLeaderboardPanel.updateSettings(newSettings); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/OverallLeaderboardPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/OverallLeaderboardPanel.java index 518b59dbae0..b24d89d855f 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/OverallLeaderboardPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/OverallLeaderboardPanel.java @@ -1,38 +1,38 @@ -package com.sap.sailing.gwt.ui.leaderboard; - -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sailing.gwt.settings.client.leaderboard.LeaderboardSettings; -import com.sap.sailing.gwt.settings.client.leaderboard.OverallLeaderboardPanelLifecycle; -import com.sap.sailing.gwt.ui.client.CompetitorSelectionProvider; -import com.sap.sailing.gwt.ui.client.RaceTimesInfoProvider; -import com.sap.sailing.gwt.ui.client.SailingServiceAsync; -import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sse.gwt.client.ErrorReporter; -import com.sap.sse.gwt.client.async.AsyncActionsExecutor; -import com.sap.sse.gwt.client.player.Timer; -import com.sap.sse.gwt.client.shared.components.Component; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; - -public class OverallLeaderboardPanel extends LeaderboardPanel { - public OverallLeaderboardPanel(Component parent, ComponentContext context, - SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, LeaderboardSettings settings, - boolean isEmbedded, RegattaAndRaceIdentifier preSelectedRace, - CompetitorSelectionProvider competitorSelectionProvider, Timer timer, String leaderboardGroupName, - String leaderboardName, ErrorReporter errorReporter, StringMessages stringMessages, - boolean showRaceDetails, CompetitorFilterPanel competitorSearchTextBox, - boolean showSelectionCheckbox, RaceTimesInfoProvider optionalRaceTimesInfoProvider, - boolean autoExpandLastRaceColumn, boolean adjustTimerDelay, boolean autoApplyTopNFilter, - boolean showCompetitorFilterStatus, boolean enableSyncScroller) { - super(parent, context, sailingService, asyncActionsExecutor, settings, isEmbedded, preSelectedRace, - competitorSelectionProvider, timer, leaderboardGroupName, leaderboardName, errorReporter, - stringMessages, showRaceDetails, competitorSearchTextBox, showSelectionCheckbox, - optionalRaceTimesInfoProvider, autoExpandLastRaceColumn, adjustTimerDelay, autoApplyTopNFilter, - showCompetitorFilterStatus, enableSyncScroller); - } - - @Override - public String getId() { - return OverallLeaderboardPanelLifecycle.ID; - } - -} +package com.sap.sailing.gwt.ui.leaderboard; + +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.gwt.settings.client.leaderboard.LeaderboardSettings; +import com.sap.sailing.gwt.settings.client.leaderboard.OverallLeaderboardPanelLifecycle; +import com.sap.sailing.gwt.ui.client.CompetitorSelectionProvider; +import com.sap.sailing.gwt.ui.client.RaceTimesInfoProvider; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.async.AsyncActionsExecutor; +import com.sap.sse.gwt.client.player.Timer; +import com.sap.sse.gwt.client.shared.components.Component; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; + +public class OverallLeaderboardPanel extends LeaderboardPanel { + public OverallLeaderboardPanel(Component parent, ComponentContext context, + SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, LeaderboardSettings settings, + boolean isEmbedded, RegattaAndRaceIdentifier preSelectedRace, + CompetitorSelectionProvider competitorSelectionProvider, Timer timer, String leaderboardGroupName, + String leaderboardName, ErrorReporter errorReporter, StringMessages stringMessages, + boolean showRaceDetails, CompetitorFilterPanel competitorSearchTextBox, + boolean showSelectionCheckbox, RaceTimesInfoProvider optionalRaceTimesInfoProvider, + boolean autoExpandLastRaceColumn, boolean adjustTimerDelay, boolean autoApplyTopNFilter, + boolean showCompetitorFilterStatus, boolean enableSyncScroller) { + super(parent, context, sailingService, asyncActionsExecutor, settings, isEmbedded, preSelectedRace, + competitorSelectionProvider, timer, leaderboardGroupName, leaderboardName, errorReporter, + stringMessages, showRaceDetails, competitorSearchTextBox, showSelectionCheckbox, + optionalRaceTimesInfoProvider, autoExpandLastRaceColumn, adjustTimerDelay, autoApplyTopNFilter, + showCompetitorFilterStatus, enableSyncScroller); + } + + @Override + public String getId() { + return OverallLeaderboardPanelLifecycle.ID; + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/polarmining/PolarBackendResultsPresenter.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/polarmining/PolarBackendResultsPresenter.java index ea90733a3fb..e19e6760e8d 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/polarmining/PolarBackendResultsPresenter.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/polarmining/PolarBackendResultsPresenter.java @@ -31,7 +31,7 @@ import com.sap.sse.datamining.shared.GroupKey; import com.sap.sse.datamining.shared.impl.dto.QueryResultDTO; import com.sap.sse.gwt.client.shared.components.Component; import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; /** * Is able to present {@link PolarBackendData}.
Has one polar chart showing the perAngle regression data and two diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/polarmining/PolarResultsPresenter.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/polarmining/PolarResultsPresenter.java index 78c559e6616..3bae35ba165 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/polarmining/PolarResultsPresenter.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/polarmining/PolarResultsPresenter.java @@ -45,7 +45,7 @@ import com.sap.sse.datamining.shared.GroupKey; import com.sap.sse.datamining.shared.impl.dto.QueryResultDTO; import com.sap.sse.gwt.client.shared.components.Component; import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; /** * Allows presentation of {@link PolarAggregation} data. diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardComponentContext.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardComponentContext.java index 336c6a35630..17859a4016c 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardComponentContext.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardComponentContext.java @@ -1,78 +1,169 @@ -package com.sap.sailing.gwt.ui.raceboard; - -import com.sap.sailing.gwt.settings.client.raceboard.RaceBoardPerspectiveOwnSettings; -import com.sap.sse.common.settings.Settings; -import com.sap.sse.common.settings.generic.GenericSerializableSettings; -import com.sap.sse.common.settings.util.SettingsDefaultValuesUtils; -import com.sap.sse.common.settings.util.SettingsMergeUtils; -import com.sap.sse.gwt.client.shared.components.Component; -import com.sap.sse.gwt.client.shared.perspective.OnSettingsLoadedCallback; -import com.sap.sse.gwt.client.shared.perspective.PerspectiveCompositeSettings; -import com.sap.sse.gwt.client.shared.perspective.PipelineLevel; -import com.sap.sse.security.ui.settings.ComponentContextWithSettingsStorageAndPatching; -import com.sap.sse.security.ui.settings.SettingsPatch; -import com.sap.sse.security.ui.settings.UserSettingsStorageManagerWithPatching; - -public class RaceBoardComponentContext extends ComponentContextWithSettingsStorageAndPatching> { - - public RaceBoardComponentContext( - RaceBoardPerspectiveLifecycle rootLifecycle, - UserSettingsStorageManagerWithPatching> settingsStorageManager) { - super(rootLifecycle, settingsStorageManager); - } - - public void addModesPatching(Component component, CS additiveSettings, OnSettingsPatchedCallback patchCallback) { - addPatchForLoadingSettings(component, PipelineLevel.GLOBAL_DEFAULTS, new ModeLoadingSettingsPatch<>(additiveSettings)); - addPatchForStoringSettings(component, PipelineLevel.CONTEXT_SPECIFIC_DEFAULTS, new ModeStoringSettingsPatch<>(additiveSettings)); - getInitialSettingsForComponent(component, new OnSettingsLoadedCallback() { - - @Override - public void onError(Throwable caught, CS fallbackDefaultSettings) { - onSuccess(fallbackDefaultSettings); - } - - @Override - public void onSuccess(CS settings) { - SettingsDefaultValuesUtils.keepDefaults(component.getSettings(), settings); - patchCallback.settingsPatched(settings); - } - }); - } - - public interface OnSettingsPatchedCallback { - void settingsPatched(CS patchedSettings); - } - - private static class ModeLoadingSettingsPatch implements SettingsPatch { - - private final CS additiveSettings; - - public ModeLoadingSettingsPatch(CS additiveSettings) { - this.additiveSettings = additiveSettings; - } - - @Override - public CS patchSettings(CS settingsToPatch) { - SettingsMergeUtils.mergeSettings(additiveSettings, settingsToPatch); - return settingsToPatch; - } - - } - - private static class ModeStoringSettingsPatch implements SettingsPatch { - - private final CS contextSpecificDefaults; - - public ModeStoringSettingsPatch(CS contextSpecificDefaults) { - this.contextSpecificDefaults = contextSpecificDefaults; - } - - @Override - public CS patchSettings(CS settingsToPatch) { - SettingsMergeUtils.mergeDefaults(contextSpecificDefaults, settingsToPatch); - return settingsToPatch; - } - - } - -} +package com.sap.sailing.gwt.ui.raceboard; + +import com.sap.sailing.gwt.settings.client.raceboard.RaceBoardPerspectiveOwnSettings; +import com.sap.sse.common.settings.Settings; +import com.sap.sse.common.settings.generic.GenericSerializableSettings; +import com.sap.sse.common.settings.util.SettingsDefaultValuesUtils; +import com.sap.sse.common.settings.util.SettingsMergeUtils; +import com.sap.sse.gwt.client.shared.components.Component; +import com.sap.sse.gwt.client.shared.components.ComponentLifecycle; +import com.sap.sse.gwt.client.shared.perspective.PerspectiveCompositeSettings; +import com.sap.sse.gwt.client.shared.settings.ComponentContextWithSettingsStorage; +import com.sap.sse.gwt.client.shared.settings.OnSettingsLoadedCallback; +import com.sap.sse.gwt.client.shared.settings.PipelineLevel; +import com.sap.sse.security.ui.settings.ComponentContextWithSettingsStorageAndPatching; +import com.sap.sse.security.ui.settings.SettingsPatch; +import com.sap.sse.security.ui.settings.UserSettingsStorageManagerWithPatching; + +/** + * A specialization of {@link ComponentContextWithSettingsStorageAndPatching} which is specially designed for + * handling of RaceModes dependent default settings. This implementation offers convenience methods that can + * be used by RaceModes to add Mode-specific default settings and inject them in the underlying settings building + * pipeline. + * + * + * @author Vladislav Chumak + * @see ComponentContextWithSettingsStorageAndPatching + * @see ComponentContextWithSettingsStorage + * + */ +public class RaceBoardComponentContext extends ComponentContextWithSettingsStorageAndPatching> { + + /** + * @param rootLifecycle + * The {@link ComponentLifecycle} of the root component/perspective + * @param settingsStorageManager + * The {@link UserSettingsStorageManagerWithPatching} to be used to access stored settings and to store new settings + */ + public RaceBoardComponentContext( + RaceBoardPerspectiveLifecycle rootLifecycle, + UserSettingsStorageManagerWithPatching> settingsStorageManager) { + super(rootLifecycle, settingsStorageManager); + } + + /** + * Adds new settings patches in order to customize default settings construction according to the current race mode. + * There are two settings patches which are constructed and added to the underlying settings building pipeline - + * a patch for settings object construction (loading settings), and a patch for settings storing. + * + *

+ *
Loading settings patch
+ *
A loading settings patch is applied on top of the User Settings, when the settings object is constructed. + * The settings are patched in the following way: + * All settings values of provided {@code additiveSettings} parameter, which are set to a non-default value + * will override the resulting settings values. When the settings value type is a collection, the values of + * {@code additiveSettings} and the resulting settings will be merged as following (read carefully): + *
    + *
  • If {@code additiveSettings} default values contain a value, which is not contained in the values of + * {@code additiveSettings}, the not contained value gets removed from the resulting settings values
  • + *
  • If {@code additiveSettings} values contain a value, which is not contained in the values of + * the resulting settings, the not contained value gets added to the resulting settings values + *
+ *
+ * + *
Storing settings patch
+ *
A storing settings patch is applied on top of the Document Settings, when the settings are stored for a context (see + * {@link #storeSettingsForContext(Component, Settings, com.sap.sse.gwt.client.shared.perspective.OnSettingsStoredCallback)}) + * The patch implementation patches the default values of the resulting settings to be stored in order to produce + * a custom diff (see {@link GenericSerializableSettings Settings Framework} of the persisted settings representation. + * That means, in RaceBoard the user settings are serialized and stored by diffing with System Default Settings, + * and Document Settings are serialized and stored by diffing with Mode-Default Settings. In order to achieve this, + * the default values of the resulting settings are patched in the following way: + * + * All settings values of provided {@code additiveSettings} parameter, which are set to a non-default value + * will override the default values of the resulting settings. When the settings value type is a collection, the values of + * {@code additiveSettings} and the default values of the resulting settings will be merged as following (read carefully): + *
    + *
  • If {@code additiveSettings} default values contain a value, which is not contained in the values of + * {@code additiveSettings}, the not contained value gets removed from the resulting settings default values
  • + *
  • If {@code additiveSettings} values contain a value, which is not contained in the values of + * the resulting settings, the not contained value gets added to the resulting settings default values + *
+ *
+ * + * @param component The component which maintains the target settings for the provided settings patch + * @param additiveSettings The additiveSettings for settings patches (see description above) + * @param patchCallback The callback which gets called when the new settings has been reloaded and patched with all patches including the new patch + */ + public void addModesPatching(Component component, CS additiveSettings, OnSettingsPatchedCallback patchCallback) { + addPatchForLoadingSettings(component, PipelineLevel.GLOBAL_DEFAULTS, new ModeLoadingSettingsPatch<>(additiveSettings)); + addPatchForStoringSettings(component, PipelineLevel.CONTEXT_SPECIFIC_DEFAULTS, new ModeStoringSettingsPatch<>(additiveSettings)); + getInitialSettingsForComponent(component, new OnSettingsLoadedCallback() { + + @Override + public void onError(Throwable caught, CS fallbackDefaultSettings) { + onSuccess(fallbackDefaultSettings); + } + + @Override + public void onSuccess(CS settings) { + SettingsDefaultValuesUtils.keepDefaults(component.getSettings(), settings); + patchCallback.settingsPatched(settings); + } + }); + } + + /** + * The callback interface used to accept the reloaded and patched settings. + * + * @author Vladislav Chumak + * + * @param The type of the patched settings + */ + public interface OnSettingsPatchedCallback { + + /** + * Gets called, when the new patched settings are available. + * + * @param patchedSettings The new patched settings + */ + void settingsPatched(CS patchedSettings); + } + + /** + * Patch for Loading Settings pipeline + * + * @author Vladislav Chumak + * + * @param The type of the settings to patch + */ + private static class ModeLoadingSettingsPatch implements SettingsPatch { + + private final CS additiveSettings; + + public ModeLoadingSettingsPatch(CS additiveSettings) { + this.additiveSettings = additiveSettings; + } + + @Override + public CS patchSettings(CS settingsToPatch) { + SettingsMergeUtils.mergeSettings(additiveSettings, settingsToPatch); + return settingsToPatch; + } + + } + + /** + * Patch for Storing Settings pipeline + * + * @author Vladislav Chumak + * + * @param The type of the settings to patch + */ + private static class ModeStoringSettingsPatch implements SettingsPatch { + + private final CS contextSpecificDefaults; + + public ModeStoringSettingsPatch(CS contextSpecificDefaults) { + this.contextSpecificDefaults = contextSpecificDefaults; + } + + @Override + public CS patchSettings(CS settingsToPatch) { + SettingsMergeUtils.mergeDefaults(contextSpecificDefaults, settingsToPatch); + return settingsToPatch; + } + + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardEntryPoint.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardEntryPoint.java index 7b0f9a6a047..0894567df8d 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardEntryPoint.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardEntryPoint.java @@ -1,163 +1,163 @@ - -package com.sap.sailing.gwt.ui.raceboard; - -import java.util.Collections; - -import com.google.gwt.core.client.GWT; -import com.google.gwt.dom.client.Style.Unit; -import com.google.gwt.user.client.Window; -import com.google.gwt.user.client.rpc.AsyncCallback; -import com.google.gwt.user.client.rpc.ServiceDefTarget; -import com.google.gwt.user.client.ui.DockLayoutPanel; -import com.google.gwt.user.client.ui.Label; -import com.google.gwt.user.client.ui.RootLayoutPanel; -import com.sap.sailing.gwt.common.client.formfactor.DeviceDetector; -import com.sap.sailing.gwt.settings.client.raceboard.RaceBoardPerspectiveOwnSettings; -import com.sap.sailing.gwt.settings.client.raceboard.RaceboardContextDefinition; -import com.sap.sailing.gwt.settings.client.utils.StorageDefinitionIdFactory; -import com.sap.sailing.gwt.ui.client.AbstractSailingEntryPoint; -import com.sap.sailing.gwt.ui.client.LogoAndTitlePanel; -import com.sap.sailing.gwt.ui.client.MediaService; -import com.sap.sailing.gwt.ui.client.MediaServiceAsync; -import com.sap.sailing.gwt.ui.client.RaceTimesInfoProvider; -import com.sap.sailing.gwt.ui.client.RemoteServiceMappingConstants; -import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sailing.gwt.ui.shared.RaceWithCompetitorsDTO; -import com.sap.sailing.gwt.ui.shared.RaceboardDataDTO; -import com.sap.sse.gwt.client.EntryPointHelper; -import com.sap.sse.gwt.client.async.AsyncActionsExecutor; -import com.sap.sse.gwt.client.player.Timer; -import com.sap.sse.gwt.client.player.Timer.PlayModes; -import com.sap.sse.gwt.client.shared.components.Component; -import com.sap.sse.gwt.client.shared.perspective.ComponentContextWithSettingsStorage; -import com.sap.sse.gwt.client.shared.perspective.DefaultOnSettingsLoadedCallback; -import com.sap.sse.gwt.client.shared.perspective.PerspectiveCompositeSettings; -import com.sap.sse.gwt.settings.SettingsToUrlSerializer; -import com.sap.sse.security.ui.settings.StorageDefinitionId; -import com.sap.sse.security.ui.settings.UserSettingsStorageManagerWithPatching; - -public class RaceBoardEntryPoint extends AbstractSailingEntryPoint { - private RaceWithCompetitorsDTO selectedRace; - - /** - * Controls the predefined mode into which to switch or configure the race viewer. - */ - private final MediaServiceAsync mediaService = GWT.create(MediaService.class); - - private RaceboardContextDefinition raceboardContextDefinition; - - @Override - protected void doOnModuleLoad() { - super.doOnModuleLoad(); - EntryPointHelper.registerASyncService((ServiceDefTarget) mediaService, RemoteServiceMappingConstants.mediaServiceRemotePath); - - raceboardContextDefinition = new SettingsToUrlSerializer() - .deserializeFromCurrentLocation(new RaceboardContextDefinition()); - - final RaceBoardModes finalMode; - if (raceboardContextDefinition.getMode() != null) { - finalMode = RaceBoardModes.valueOf(raceboardContextDefinition.getMode()); - } else { - finalMode = null; - } - - if (raceboardContextDefinition.getRegattaName() == null || raceboardContextDefinition.getRegattaName().isEmpty() - || raceboardContextDefinition.getRaceName() == null || raceboardContextDefinition.getRaceName().isEmpty() - || raceboardContextDefinition.getLeaderboardName() == null - || raceboardContextDefinition.getLeaderboardName().isEmpty()) { - createErrorPage("This page requires a valid regatta name, race name and leaderboard name."); - return; - } - - AsyncCallback asyncCallback = new AsyncCallback() { - @Override - public void onSuccess(RaceboardDataDTO raceboardData) { - if (!raceboardData.isValidLeaderboard()) { - createErrorPage(getStringMessages().noSuchLeaderboard()); - return; - } - if (raceboardContextDefinition.getEventId() != null && !raceboardData.isValidEvent()) { - createErrorPage(getStringMessages().noSuchEvent()); - } - if (raceboardContextDefinition.getLeaderboardGroupName() != null) { - if (!raceboardData.isValidLeaderboardGroup()) { - createErrorPage(getStringMessages().leaderboardNotContainedInLeaderboardGroup( - raceboardContextDefinition.getLeaderboardName(), - raceboardContextDefinition.getLeaderboardGroupName())); - return; - } - if (raceboardContextDefinition.getEventId() != null && raceboardData.isValidLeaderboardGroup() - && !raceboardData.isValidEvent()) { - createErrorPage(getStringMessages().leaderboardGroupNotContainedInEvent( - raceboardContextDefinition.getLeaderboardGroupName(), - raceboardContextDefinition.getEventId().toString())); - return; - } - } - if (raceboardData.getRace() == null) { - createErrorPage("Could not obtain a race with name " + raceboardContextDefinition.getRaceName() - + " for a regatta with name " + raceboardContextDefinition.getRegattaName()); - return; - } - - final boolean showChartMarkEditMediaButtonsAndVideo = !DeviceDetector.isMobile(); - final StorageDefinitionId storageDefinitionId = StorageDefinitionIdFactory.createStorageDefinitionIdForRaceBoard(raceboardContextDefinition); - final Timer timer = new Timer(PlayModes.Replay, 1000l); - final RaceBoardPerspectiveLifecycle lifeCycle = new RaceBoardPerspectiveLifecycle(timer, raceboardData.getRace().getRaceIdentifier(), StringMessages.INSTANCE); - RaceBoardComponentContext context = new RaceBoardComponentContext(lifeCycle, new UserSettingsStorageManagerWithPatching<>(getUserService(), storageDefinitionId)); - - context.getInitialSettings(new DefaultOnSettingsLoadedCallback>() { - @Override - public void onSuccess(PerspectiveCompositeSettings initialSettings) { - final RaceBoardPanel raceBoardPanel = createPerspectivePage(null, context, initialSettings, - raceboardData, showChartMarkEditMediaButtonsAndVideo, lifeCycle, timer); - if (finalMode != null) { - finalMode.getMode().applyTo(raceBoardPanel); - } - } - }); - } - - @Override - public void onFailure(Throwable caught) { - reportError("Error trying to create the raceboard: " + caught.getMessage()); - } - }; - - sailingService.getRaceboardData(raceboardContextDefinition.getRegattaName(), - raceboardContextDefinition.getRaceName(), raceboardContextDefinition.getLeaderboardName(), - raceboardContextDefinition.getLeaderboardGroupName(), raceboardContextDefinition.getEventId(), - asyncCallback); - } - - private void createErrorPage(String message) { - final DockLayoutPanel vp = new DockLayoutPanel(Unit.PX); - LogoAndTitlePanel logoAndTitlePanel = new LogoAndTitlePanel(getStringMessages(), this, getUserService()); - logoAndTitlePanel.addStyleName("LogoAndTitlePanel"); - RootLayoutPanel.get().add(vp); - vp.addNorth(logoAndTitlePanel, 100); - vp.add(new Label(message)); - } - - private RaceBoardPanel createPerspectivePage(Component parent, - ComponentContextWithSettingsStorage> context, - PerspectiveCompositeSettings settings, RaceboardDataDTO raceboardData, - boolean showChartMarkEditMediaButtonsAndVideo, RaceBoardPerspectiveLifecycle raceLifeCycle, Timer timer) { - selectedRace = raceboardData.getRace(); - Window.setTitle(selectedRace.getName()); - AsyncActionsExecutor asyncActionsExecutor = new AsyncActionsExecutor(); - RaceTimesInfoProvider raceTimesInfoProvider = new RaceTimesInfoProvider(sailingService, asyncActionsExecutor, this, - Collections.singletonList(selectedRace.getRaceIdentifier()), 5000l /* requestInterval*/); - - RaceBoardPanel raceBoardPerspective = new RaceBoardPanel(parent, context, - raceLifeCycle, - settings, - sailingService, mediaService, getUserService(), asyncActionsExecutor, - raceboardData.getCompetitorAndTheirBoats(), timer, selectedRace.getRaceIdentifier(), - raceboardContextDefinition.getLeaderboardName(), raceboardContextDefinition.getLeaderboardGroupName(), - raceboardContextDefinition.getEventId(), RaceBoardEntryPoint.this, getStringMessages(), userAgent, - raceTimesInfoProvider, showChartMarkEditMediaButtonsAndVideo); - RootLayoutPanel.get().add(raceBoardPerspective.getEntryWidget()); - return raceBoardPerspective; - } -} + +package com.sap.sailing.gwt.ui.raceboard; + +import java.util.Collections; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.gwt.user.client.rpc.ServiceDefTarget; +import com.google.gwt.user.client.ui.DockLayoutPanel; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.RootLayoutPanel; +import com.sap.sailing.gwt.common.client.formfactor.DeviceDetector; +import com.sap.sailing.gwt.settings.client.raceboard.RaceBoardPerspectiveOwnSettings; +import com.sap.sailing.gwt.settings.client.raceboard.RaceboardContextDefinition; +import com.sap.sailing.gwt.settings.client.utils.StorageDefinitionIdFactory; +import com.sap.sailing.gwt.ui.client.AbstractSailingEntryPoint; +import com.sap.sailing.gwt.ui.client.LogoAndTitlePanel; +import com.sap.sailing.gwt.ui.client.MediaService; +import com.sap.sailing.gwt.ui.client.MediaServiceAsync; +import com.sap.sailing.gwt.ui.client.RaceTimesInfoProvider; +import com.sap.sailing.gwt.ui.client.RemoteServiceMappingConstants; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.shared.RaceWithCompetitorsDTO; +import com.sap.sailing.gwt.ui.shared.RaceboardDataDTO; +import com.sap.sse.gwt.client.EntryPointHelper; +import com.sap.sse.gwt.client.async.AsyncActionsExecutor; +import com.sap.sse.gwt.client.player.Timer; +import com.sap.sse.gwt.client.player.Timer.PlayModes; +import com.sap.sse.gwt.client.shared.components.Component; +import com.sap.sse.gwt.client.shared.perspective.PerspectiveCompositeSettings; +import com.sap.sse.gwt.client.shared.settings.ComponentContextWithSettingsStorage; +import com.sap.sse.gwt.client.shared.settings.DefaultOnSettingsLoadedCallback; +import com.sap.sse.gwt.settings.SettingsToUrlSerializer; +import com.sap.sse.security.ui.settings.StorageDefinitionId; +import com.sap.sse.security.ui.settings.UserSettingsStorageManagerWithPatching; + +public class RaceBoardEntryPoint extends AbstractSailingEntryPoint { + private RaceWithCompetitorsDTO selectedRace; + + /** + * Controls the predefined mode into which to switch or configure the race viewer. + */ + private final MediaServiceAsync mediaService = GWT.create(MediaService.class); + + private RaceboardContextDefinition raceboardContextDefinition; + + @Override + protected void doOnModuleLoad() { + super.doOnModuleLoad(); + EntryPointHelper.registerASyncService((ServiceDefTarget) mediaService, RemoteServiceMappingConstants.mediaServiceRemotePath); + + raceboardContextDefinition = new SettingsToUrlSerializer() + .deserializeFromCurrentLocation(new RaceboardContextDefinition()); + + final RaceBoardModes finalMode; + if (raceboardContextDefinition.getMode() != null) { + finalMode = RaceBoardModes.valueOf(raceboardContextDefinition.getMode()); + } else { + finalMode = null; + } + + if (raceboardContextDefinition.getRegattaName() == null || raceboardContextDefinition.getRegattaName().isEmpty() + || raceboardContextDefinition.getRaceName() == null || raceboardContextDefinition.getRaceName().isEmpty() + || raceboardContextDefinition.getLeaderboardName() == null + || raceboardContextDefinition.getLeaderboardName().isEmpty()) { + createErrorPage("This page requires a valid regatta name, race name and leaderboard name."); + return; + } + + AsyncCallback asyncCallback = new AsyncCallback() { + @Override + public void onSuccess(RaceboardDataDTO raceboardData) { + if (!raceboardData.isValidLeaderboard()) { + createErrorPage(getStringMessages().noSuchLeaderboard()); + return; + } + if (raceboardContextDefinition.getEventId() != null && !raceboardData.isValidEvent()) { + createErrorPage(getStringMessages().noSuchEvent()); + } + if (raceboardContextDefinition.getLeaderboardGroupName() != null) { + if (!raceboardData.isValidLeaderboardGroup()) { + createErrorPage(getStringMessages().leaderboardNotContainedInLeaderboardGroup( + raceboardContextDefinition.getLeaderboardName(), + raceboardContextDefinition.getLeaderboardGroupName())); + return; + } + if (raceboardContextDefinition.getEventId() != null && raceboardData.isValidLeaderboardGroup() + && !raceboardData.isValidEvent()) { + createErrorPage(getStringMessages().leaderboardGroupNotContainedInEvent( + raceboardContextDefinition.getLeaderboardGroupName(), + raceboardContextDefinition.getEventId().toString())); + return; + } + } + if (raceboardData.getRace() == null) { + createErrorPage("Could not obtain a race with name " + raceboardContextDefinition.getRaceName() + + " for a regatta with name " + raceboardContextDefinition.getRegattaName()); + return; + } + + final boolean showChartMarkEditMediaButtonsAndVideo = !DeviceDetector.isMobile(); + final StorageDefinitionId storageDefinitionId = StorageDefinitionIdFactory.createStorageDefinitionIdForRaceBoard(raceboardContextDefinition); + final Timer timer = new Timer(PlayModes.Replay, 1000l); + final RaceBoardPerspectiveLifecycle lifeCycle = new RaceBoardPerspectiveLifecycle(timer, raceboardData.getRace().getRaceIdentifier(), StringMessages.INSTANCE); + RaceBoardComponentContext context = new RaceBoardComponentContext(lifeCycle, new UserSettingsStorageManagerWithPatching<>(getUserService(), storageDefinitionId)); + + context.getInitialSettings(new DefaultOnSettingsLoadedCallback>() { + @Override + public void onSuccess(PerspectiveCompositeSettings initialSettings) { + final RaceBoardPanel raceBoardPanel = createPerspectivePage(null, context, initialSettings, + raceboardData, showChartMarkEditMediaButtonsAndVideo, lifeCycle, timer); + if (finalMode != null) { + finalMode.getMode().applyTo(raceBoardPanel); + } + } + }); + } + + @Override + public void onFailure(Throwable caught) { + reportError("Error trying to create the raceboard: " + caught.getMessage()); + } + }; + + sailingService.getRaceboardData(raceboardContextDefinition.getRegattaName(), + raceboardContextDefinition.getRaceName(), raceboardContextDefinition.getLeaderboardName(), + raceboardContextDefinition.getLeaderboardGroupName(), raceboardContextDefinition.getEventId(), + asyncCallback); + } + + private void createErrorPage(String message) { + final DockLayoutPanel vp = new DockLayoutPanel(Unit.PX); + LogoAndTitlePanel logoAndTitlePanel = new LogoAndTitlePanel(getStringMessages(), this, getUserService()); + logoAndTitlePanel.addStyleName("LogoAndTitlePanel"); + RootLayoutPanel.get().add(vp); + vp.addNorth(logoAndTitlePanel, 100); + vp.add(new Label(message)); + } + + private RaceBoardPanel createPerspectivePage(Component parent, + ComponentContextWithSettingsStorage> context, + PerspectiveCompositeSettings settings, RaceboardDataDTO raceboardData, + boolean showChartMarkEditMediaButtonsAndVideo, RaceBoardPerspectiveLifecycle raceLifeCycle, Timer timer) { + selectedRace = raceboardData.getRace(); + Window.setTitle(selectedRace.getName()); + AsyncActionsExecutor asyncActionsExecutor = new AsyncActionsExecutor(); + RaceTimesInfoProvider raceTimesInfoProvider = new RaceTimesInfoProvider(sailingService, asyncActionsExecutor, this, + Collections.singletonList(selectedRace.getRaceIdentifier()), 5000l /* requestInterval*/); + + RaceBoardPanel raceBoardPerspective = new RaceBoardPanel(parent, context, + raceLifeCycle, + settings, + sailingService, mediaService, getUserService(), asyncActionsExecutor, + raceboardData.getCompetitorAndTheirBoats(), timer, selectedRace.getRaceIdentifier(), + raceboardContextDefinition.getLeaderboardName(), raceboardContextDefinition.getLeaderboardGroupName(), + raceboardContextDefinition.getEventId(), RaceBoardEntryPoint.this, getStringMessages(), userAgent, + raceTimesInfoProvider, showChartMarkEditMediaButtonsAndVideo); + RootLayoutPanel.get().add(raceBoardPerspective.getEntryWidget()); + return raceBoardPerspective; + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java index d1e500728d8..2a8adf9a75c 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java @@ -90,8 +90,8 @@ import com.sap.sse.gwt.client.shared.components.Component; import com.sap.sse.gwt.client.shared.components.SettingsDialog; import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; import com.sap.sse.gwt.client.shared.perspective.AbstractPerspectiveComposite; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; import com.sap.sse.gwt.client.shared.perspective.PerspectiveCompositeSettings; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; import com.sap.sse.gwt.client.useragent.UserAgentDetails; import com.sap.sse.security.ui.authentication.generic.GenericAuthentication; import com.sap.sse.security.ui.authentication.view.AuthenticationMenuView; diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/ValueCollection.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/ValueCollection.java index 1bc8da872b5..66f273a1e54 100644 --- a/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/ValueCollection.java +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/ValueCollection.java @@ -7,7 +7,5 @@ public interface ValueCollection extends Value { void setValues(Iterable values, ValueConverter converter); - void addValue(T value, ValueConverter converter); - void clear(); } diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/ValueCollectionValue.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/ValueCollectionValue.java index 80c49aad086..72ec892fe92 100644 --- a/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/ValueCollectionValue.java +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/ValueCollectionValue.java @@ -43,7 +43,6 @@ public abstract class ValueCollectionValue> implemen this.values.clear(); } - @Override public void addValue(T value, ValueConverter converter) { this.values.add(converter.toValue(value)); } diff --git a/java/com.sap.sse.gwt/META-INF/MANIFEST.MF b/java/com.sap.sse.gwt/META-INF/MANIFEST.MF index 64dce9d5f5a..05e71f89fbb 100644 --- a/java/com.sap.sse.gwt/META-INF/MANIFEST.MF +++ b/java/com.sap.sse.gwt/META-INF/MANIFEST.MF @@ -43,6 +43,7 @@ Export-Package: com.google.gwt.user.client.rpc.core.java.util, com.sap.sse.gwt.client.sapheader, com.sap.sse.gwt.client.shared.components, com.sap.sse.gwt.client.shared.perspective, + com.sap.sse.gwt.client.shared.settings, com.sap.sse.gwt.client.useragent, com.sap.sse.gwt.common, com.sap.sse.gwt.dispatch.client, diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/components/AbstractComponent.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/components/AbstractComponent.java index 811773f5016..d63e2b16dc2 100755 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/components/AbstractComponent.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/components/AbstractComponent.java @@ -1,31 +1,31 @@ -package com.sap.sse.gwt.client.shared.components; - -import java.util.ArrayList; - -import com.sap.sse.common.settings.Settings; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; - -public abstract class AbstractComponent implements Component { - private Component parentComponent; - private ComponentContext componentContext; - - public AbstractComponent(Component parent, ComponentContext componentContext) { - this.parentComponent = parent; - this.componentContext = componentContext; - } - - @Override - public Component getParentComponent() { - return parentComponent; - } - - @Override - public ArrayList getPath() { - return ComponentPathDeterminer.determinePath(this); - } - - @Override - public final ComponentContext getComponentContext() { - return componentContext; - } -} +package com.sap.sse.gwt.client.shared.components; + +import java.util.ArrayList; + +import com.sap.sse.common.settings.Settings; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; + +public abstract class AbstractComponent implements Component { + private Component parentComponent; + private ComponentContext componentContext; + + public AbstractComponent(Component parent, ComponentContext componentContext) { + this.parentComponent = parent; + this.componentContext = componentContext; + } + + @Override + public Component getParentComponent() { + return parentComponent; + } + + @Override + public ArrayList getPath() { + return ComponentPathDeterminer.determinePath(this); + } + + @Override + public final ComponentContext getComponentContext() { + return componentContext; + } +} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/components/AbstractCompositeComponent.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/components/AbstractCompositeComponent.java index 71847c026b2..9ead8952660 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/components/AbstractCompositeComponent.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/components/AbstractCompositeComponent.java @@ -1,39 +1,39 @@ -package com.sap.sse.gwt.client.shared.components; - -import java.util.ArrayList; - -import com.google.gwt.core.client.GWT; -import com.google.gwt.user.client.ui.Composite; -import com.sap.sse.common.settings.Settings; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; - -public abstract class AbstractCompositeComponent extends Composite implements Component { - private Component parentComponent; - private ComponentContext componentContext; - - public AbstractCompositeComponent(Component parent, ComponentContext componentContext) { - this.componentContext = componentContext; - this.parentComponent = parent; - if (parentComponent == null) { - GWT.log("No parent for component, validate if correct " + this.getClass().getName()); - } - if (componentContext == null) { - GWT.log("No context for component, validate if correct " + this.getClass().getName()); - } - } - - public Component getParentComponent() { - return parentComponent; - } - - @Override - public ArrayList getPath() { - return ComponentPathDeterminer.determinePath(this); - } - - @Override - public ComponentContext getComponentContext() { - return componentContext; - } - -} +package com.sap.sse.gwt.client.shared.components; + +import java.util.ArrayList; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.user.client.ui.Composite; +import com.sap.sse.common.settings.Settings; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; + +public abstract class AbstractCompositeComponent extends Composite implements Component { + private Component parentComponent; + private ComponentContext componentContext; + + public AbstractCompositeComponent(Component parent, ComponentContext componentContext) { + this.componentContext = componentContext; + this.parentComponent = parent; + if (parentComponent == null) { + GWT.log("No parent for component, validate if correct " + this.getClass().getName()); + } + if (componentContext == null) { + GWT.log("No context for component, validate if correct " + this.getClass().getName()); + } + } + + public Component getParentComponent() { + return parentComponent; + } + + @Override + public ArrayList getPath() { + return ComponentPathDeterminer.determinePath(this); + } + + @Override + public ComponentContext getComponentContext() { + return componentContext; + } + +} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/components/AbstractLazyComponent.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/components/AbstractLazyComponent.java index 773fcc1cf5b..075cde4235e 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/components/AbstractLazyComponent.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/components/AbstractLazyComponent.java @@ -5,7 +5,7 @@ import java.util.ArrayList; import com.google.gwt.user.client.ui.LazyPanel; import com.google.gwt.user.client.ui.Widget; import com.sap.sse.common.settings.Settings; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; public abstract class AbstractLazyComponent extends LazyPanel implements LazyComponent { private Component parent; diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/components/Component.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/components/Component.java index 6402f533996..adce9888c52 100755 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/components/Component.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/components/Component.java @@ -1,75 +1,75 @@ -package com.sap.sse.gwt.client.shared.components; - -import java.util.ArrayList; - -import com.google.gwt.user.client.ui.Widget; -import com.sap.sse.common.settings.Settings; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; -import com.sap.sse.gwt.client.shared.perspective.Perspective; - -public interface Component { - ArrayList getPath(); - - /** - * Each component instance has an ID that has to be unique in the context in which the component is used - * and has its siblings. In particular, multiple components of the same type but with distinct IDs may - * exist, e.g., multiple wind charts showing wind readings coming from different sensors. The uniqueness - * constraint applies only to the single level (in a composite pattern of components, such as - * {@link Perspective}) in which this component instance is used. - */ - String getId(); - - /** - * @return the name to display to a user for quick navigation to this component - */ - String getLocalizedShortName(); - - /** - * A component may or may not have a {@link Widget} used to render its contents. Typically, - * entries with sub-entries don't afford a widget of their own but let the framework choose a container - * in which to aggregate their entries' widgets. - */ - Widget getEntryWidget(); - - boolean isVisible(); - - void setVisible(boolean visibility); - - /** - * @return whether this component has settings that a user may change; if so, - */ - boolean hasSettings(); - - /** - * If this component {@link #hasSettings has settings}, this method may return a component for the settings dialog. - * It will be used to obtain a widget shown in the settings dialog, a validator for the component-specific settings, - * as well as to produce a result from the widget's state when the settings dialog wants to validate or return the - * settings. - */ - SettingsDialogComponent getSettingsDialogComponent(); - - /** - * @return the current settings of the component or {@code null} if the component has no settings. - */ - SettingsType getSettings(); - - /** - * Updates the settings of this component. Expected to be called when a settings dialog using this component's - * {@link #getSettingsDialogComponent()} has been confirmed. - */ - void updateSettings(SettingsType newSettings); - - /** - * If the component wants to add its specific styling rules, e.g., for the expand/collapse toggle buttons or - * the dragger handle, it should return a non-null value from this method which is then used as - * a dependent CSS class name for those components. - */ - String getDependentCssClassName(); - - /** - * returns the parentComponent or null - */ - Component getParentComponent(); - - ComponentContext getComponentContext(); -} +package com.sap.sse.gwt.client.shared.components; + +import java.util.ArrayList; + +import com.google.gwt.user.client.ui.Widget; +import com.sap.sse.common.settings.Settings; +import com.sap.sse.gwt.client.shared.perspective.Perspective; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; + +public interface Component { + ArrayList getPath(); + + /** + * Each component instance has an ID that has to be unique in the context in which the component is used + * and has its siblings. In particular, multiple components of the same type but with distinct IDs may + * exist, e.g., multiple wind charts showing wind readings coming from different sensors. The uniqueness + * constraint applies only to the single level (in a composite pattern of components, such as + * {@link Perspective}) in which this component instance is used. + */ + String getId(); + + /** + * @return the name to display to a user for quick navigation to this component + */ + String getLocalizedShortName(); + + /** + * A component may or may not have a {@link Widget} used to render its contents. Typically, + * entries with sub-entries don't afford a widget of their own but let the framework choose a container + * in which to aggregate their entries' widgets. + */ + Widget getEntryWidget(); + + boolean isVisible(); + + void setVisible(boolean visibility); + + /** + * @return whether this component has settings that a user may change; if so, + */ + boolean hasSettings(); + + /** + * If this component {@link #hasSettings has settings}, this method may return a component for the settings dialog. + * It will be used to obtain a widget shown in the settings dialog, a validator for the component-specific settings, + * as well as to produce a result from the widget's state when the settings dialog wants to validate or return the + * settings. + */ + SettingsDialogComponent getSettingsDialogComponent(); + + /** + * @return the current settings of the component or {@code null} if the component has no settings. + */ + SettingsType getSettings(); + + /** + * Updates the settings of this component. Expected to be called when a settings dialog using this component's + * {@link #getSettingsDialogComponent()} has been confirmed. + */ + void updateSettings(SettingsType newSettings); + + /** + * If the component wants to add its specific styling rules, e.g., for the expand/collapse toggle buttons or + * the dragger handle, it should return a non-null value from this method which is then used as + * a dependent CSS class name for those components. + */ + String getDependentCssClassName(); + + /** + * returns the parentComponent or null + */ + Component getParentComponent(); + + ComponentContext getComponentContext(); +} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/components/ComponentLifecycle.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/components/ComponentLifecycle.java index 3342db43761..22c4522c652 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/components/ComponentLifecycle.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/components/ComponentLifecycle.java @@ -1,6 +1,7 @@ package com.sap.sse.gwt.client.shared.components; import com.sap.sse.common.settings.Settings; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; /** * A {@link ComponentLifecycle} decouples the lifecycle of a component from the corresponding component instance. This way we @@ -32,8 +33,22 @@ public interface ComponentLifecycle { */ boolean hasSettings(); + /** + * Extracts User Settings from provided {@link Settings}. + * + * @param settings The settings which belong to the component maintained by this lifecycle + * @return User Settings extracted from the provided settings + * @see ComponentContext + */ S extractGlobalSettings(S settings); + /** + * Extracts Document Settings from provided {@link Settings}. + * + * @param settings The settings which belong to the component maintained by this lifecycle + * @return Document Settings extracted from the provided settings + * @see ComponentContext + */ S extractContextSpecificSettings(S settings); } \ No newline at end of file diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/components/ComponentWithoutSettings.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/components/ComponentWithoutSettings.java index d8225b73f75..c87258dfa95 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/components/ComponentWithoutSettings.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/components/ComponentWithoutSettings.java @@ -1,7 +1,7 @@ package com.sap.sse.gwt.client.shared.components; import com.sap.sse.common.settings.AbstractSettings; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; /** * An abstract implementation of the component interfaces indented to use for components without the need of having settings. diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/components/LinkWithSettingsGenerator.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/components/LinkWithSettingsGenerator.java index be03cba30ae..b5ea9087297 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/components/LinkWithSettingsGenerator.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/components/LinkWithSettingsGenerator.java @@ -1,90 +1,90 @@ -package com.sap.sse.gwt.client.shared.components; - -import com.google.gwt.core.client.EntryPoint; -import com.google.gwt.http.client.UrlBuilder; -import com.sap.sse.common.settings.Settings; -import com.sap.sse.common.settings.generic.GenericSerializableSettings; -import com.sap.sse.common.settings.generic.SettingsMap; -import com.sap.sse.gwt.client.shared.perspective.ComponentContext; -import com.sap.sse.gwt.client.shared.perspective.Perspective; -import com.sap.sse.gwt.settings.SettingsToUrlSerializer; -import com.sap.sse.gwt.settings.UrlBuilderUtil; - -/** - * EntryPoints based on {@link Component} or {@link Perspective} instances may use {@link ComponentContext} to handle - * {@link Settings} related functionality such as loading settings and saving settings. This class helps creating links - * to such an {@link EntryPoint} without the need of manually concatenating URL Parameters. Instead, the link is - * constructed using the exact {@link Settings} instances used by the component. - * - * @param - * The type of Settings the target Component/Perspective uses. - */ -public class LinkWithSettingsGenerator { - - private final SettingsToUrlSerializer settingsToUrlSerializer = new SettingsToUrlSerializer(); - - private final String path; - private final GenericSerializableSettings[] contextDefinition; - - /** - * Constructs a link based on the current location's URL including the path. - * - * @param contextDefinition - * {@link GenericSerializableSettings} instances that in sum define the context parameters of the - * {@link EntryPoint} the link is created for. - */ - public LinkWithSettingsGenerator(GenericSerializableSettings... contextDefinition) { - this(null, contextDefinition); - } - - /** - * Constructs a link based on the current location's URL but using the given path instead of the current path. - * - * @param path - * the path to use for the generated link - * @param contextDefinition - * {@link GenericSerializableSettings} instances that in sum define the context parameters of the - * {@link EntryPoint} the link is created for. - */ - public LinkWithSettingsGenerator(String path, GenericSerializableSettings... contextDefinition) { - this.path = path; - this.contextDefinition = contextDefinition; - } - - /** - * Creates a link using the contextDefinition (see constructors) but with no other {@link Settings}. - */ - public String createUrl() { - return createUrl(null); - } - - /** - * Creates a link using the contextDefinition (see constructors) and the given {@link Settings} instance. - * - */ - public String createUrl(S settings) { - final UrlBuilder urlBuilder; - if (path == null) { - urlBuilder = UrlBuilderUtil.createUrlBuilderFromCurrentLocationWithCleanParameters(); - } else { - urlBuilder = UrlBuilderUtil.createUrlBuilderFromCurrentLocationWithCleanParametersAndPath(path); - } - serializeSettingsToUrlBuilder(urlBuilder, settings, contextDefinition); - return urlBuilder.buildString(); - } - - /** - * To be overwritten by subclasses to customize the link creation. - */ - protected void serializeSettingsToUrlBuilder(UrlBuilder urlBuilder, S settings, - GenericSerializableSettings... contextDefinition) { - for(GenericSerializableSettings contextDefinitionItem : contextDefinition) { - settingsToUrlSerializer.serializeToUrlBuilder(contextDefinitionItem, urlBuilder); - } - if (settings instanceof SettingsMap) { - settingsToUrlSerializer.serializeSettingsMapToUrlBuilder((SettingsMap) settings, urlBuilder); - } else if (settings instanceof GenericSerializableSettings) { - settingsToUrlSerializer.serializeToUrlBuilder((GenericSerializableSettings) settings, urlBuilder); - } - } -} +package com.sap.sse.gwt.client.shared.components; + +import com.google.gwt.core.client.EntryPoint; +import com.google.gwt.http.client.UrlBuilder; +import com.sap.sse.common.settings.Settings; +import com.sap.sse.common.settings.generic.GenericSerializableSettings; +import com.sap.sse.common.settings.generic.SettingsMap; +import com.sap.sse.gwt.client.shared.perspective.Perspective; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; +import com.sap.sse.gwt.settings.SettingsToUrlSerializer; +import com.sap.sse.gwt.settings.UrlBuilderUtil; + +/** + * EntryPoints based on {@link Component} or {@link Perspective} instances may use {@link ComponentContext} to handle + * {@link Settings} related functionality such as loading settings and saving settings. This class helps creating links + * to such an {@link EntryPoint} without the need of manually concatenating URL Parameters. Instead, the link is + * constructed using the exact {@link Settings} instances used by the component. + * + * @param + * The type of Settings the target Component/Perspective uses. + */ +public class LinkWithSettingsGenerator { + + private final SettingsToUrlSerializer settingsToUrlSerializer = new SettingsToUrlSerializer(); + + private final String path; + private final GenericSerializableSettings[] contextDefinition; + + /** + * Constructs a link based on the current location's URL including the path. + * + * @param contextDefinition + * {@link GenericSerializableSettings} instances that in sum define the context parameters of the + * {@link EntryPoint} the link is created for. + */ + public LinkWithSettingsGenerator(GenericSerializableSettings... contextDefinition) { + this(null, contextDefinition); + } + + /** + * Constructs a link based on the current location's URL but using the given path instead of the current path. + * + * @param path + * the path to use for the generated link + * @param contextDefinition + * {@link GenericSerializableSettings} instances that in sum define the context parameters of the + * {@link EntryPoint} the link is created for. + */ + public LinkWithSettingsGenerator(String path, GenericSerializableSettings... contextDefinition) { + this.path = path; + this.contextDefinition = contextDefinition; + } + + /** + * Creates a link using the contextDefinition (see constructors) but with no other {@link Settings}. + */ + public String createUrl() { + return createUrl(null); + } + + /** + * Creates a link using the contextDefinition (see constructors) and the given {@link Settings} instance. + * + */ + public String createUrl(S settings) { + final UrlBuilder urlBuilder; + if (path == null) { + urlBuilder = UrlBuilderUtil.createUrlBuilderFromCurrentLocationWithCleanParameters(); + } else { + urlBuilder = UrlBuilderUtil.createUrlBuilderFromCurrentLocationWithCleanParametersAndPath(path); + } + serializeSettingsToUrlBuilder(urlBuilder, settings, contextDefinition); + return urlBuilder.buildString(); + } + + /** + * To be overwritten by subclasses to customize the link creation. + */ + protected void serializeSettingsToUrlBuilder(UrlBuilder urlBuilder, S settings, + GenericSerializableSettings... contextDefinition) { + for(GenericSerializableSettings contextDefinitionItem : contextDefinition) { + settingsToUrlSerializer.serializeToUrlBuilder(contextDefinitionItem, urlBuilder); + } + if (settings instanceof SettingsMap) { + settingsToUrlSerializer.serializeSettingsMapToUrlBuilder((SettingsMap) settings, urlBuilder); + } else if (settings instanceof GenericSerializableSettings) { + settingsToUrlSerializer.serializeToUrlBuilder((GenericSerializableSettings) settings, urlBuilder); + } + } +} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/components/SettingsDialog.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/components/SettingsDialog.java index 0b591173d77..49e634b79fe 100755 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/components/SettingsDialog.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/components/SettingsDialog.java @@ -1,136 +1,136 @@ -package com.sap.sse.gwt.client.shared.components; - -import com.google.gwt.dom.client.Style.Unit; -import com.google.gwt.event.dom.client.ClickEvent; -import com.google.gwt.event.dom.client.ClickHandler; -import com.google.gwt.user.client.Window; -import com.google.gwt.user.client.ui.Button; -import com.sap.sse.common.settings.Settings; -import com.sap.sse.gwt.client.StringMessages; -import com.sap.sse.gwt.client.shared.perspective.DummyOnSettingsStoredCallback; -import com.sap.sse.gwt.client.shared.perspective.OnSettingsStoredCallback; - -public class SettingsDialog extends AbstractSettingsDialog { - - public SettingsDialog(final Component component, StringMessages stringMessages) { - this(component, stringMessages, /* animationEnabled */ true, null); - } - - public SettingsDialog(final Component component, StringMessages stringMessages, LinkWithSettingsGenerator linkWithSettingsGenerator) { - this(component, stringMessages, /* animationEnabled */ true, linkWithSettingsGenerator); - } - - public SettingsDialog(final Component component, StringMessages stringMessages, - boolean animationEnabled, LinkWithSettingsGenerator linkWithSettingsGenerator) { - this(component, component.getSettingsDialogComponent(), stringMessages, animationEnabled, linkWithSettingsGenerator, null); - } - - public SettingsDialog(final Component component, StringMessages stringMessages, - DialogCallback callback) { - this(component, component.getSettingsDialogComponent(), stringMessages, /* animationEnabled */ true, null, callback); - } - - /** - * Creates a settings button for component that, when clicked, opens a settings dialog for that - * component and when confirmed, updates that component's settings. The button has no CSS style attached to give - * callers full flexibility as to how to style the button. - */ - public static Button createSettingsButton(final Component component, - final StringMessages stringMessages) { - Button settingsButton = new Button(); - settingsButton.setTitle(stringMessages.settings()); - settingsButton.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - new SettingsDialog(component, stringMessages).show(); - } - }); - return settingsButton; - } - - /** - * This auxiliary constructor is required to avoid duplicate calls to {@link Component#getSettingsDialogComponent()} - * which may choose to create a new instance each call. Such duplicate instances would cause the validator to - * operate on a different instance as the one used for displaying, hence not allowing the validator to use the UI - * elements, neither for update nor read. - */ - private SettingsDialog(final Component component, - SettingsDialogComponent dialogComponent, StringMessages stringMessages, - boolean animationEnabled, LinkWithSettingsGenerator linkWithSettingsGenerator, final DialogCallback callback) { - super(component.getLocalizedShortName(), dialogComponent, stringMessages, animationEnabled, linkWithSettingsGenerator, - new SettingsDialogCallback<>(component, callback)); - - if (component.getComponentContext() != null - && component.getComponentContext().hasMakeCustomDefaultSettingsSupport(component)) { - initMakeDefaultButtons(component, stringMessages); - } - } - - private void initMakeDefaultButtons(final Component component, final StringMessages stringMessages) { - final Button makeDefaultButton = new Button(stringMessages.makeDefault()); - makeDefaultButton.getElement().getStyle().setMargin(3, Unit.PX); - makeDefaultButton.ensureDebugId("MakeDefaultButton"); - getLeftButtonPannel().add(makeDefaultButton); - makeDefaultButton.addClickHandler(new ClickHandler() { - public void onClick(ClickEvent event) { - setButtonSavingState(true); - component.getComponentContext().makeSettingsDefault(component, getResult(), new OnSettingsStoredCallback() { - - @Override - public void onSuccess() { - setButtonSavingState(false); - Window.alert(stringMessages.settingsSavedMessage()); - } - - @Override - public void onError(Throwable caught) { - setButtonSavingState(false); - Window.alert(stringMessages.settingsSaveErrorMessage()); - } - - - }); - } - - private void setButtonSavingState(boolean savingState) { - if(savingState) { - makeDefaultButton.setEnabled(false); - makeDefaultButton.setText(stringMessages.makeDefaultInProgress()); - } else { - makeDefaultButton.setEnabled(true); - makeDefaultButton.setText(stringMessages.makeDefault()); - } - } - }); - } - - private static class SettingsDialogCallback implements DialogCallback { - - private final DialogCallback nestedCallback; - private final Component component; - - public SettingsDialogCallback(Component component, DialogCallback nestedCallback) { - this.component = component; - this.nestedCallback = nestedCallback; - } - - @Override - public void ok(SettingsType editedSettings) { - if (component.getComponentContext() != null - && component.getComponentContext().hasMakeCustomDefaultSettingsSupport(component)) { - component.getComponentContext().storeSettingsForContext(component, editedSettings, new DummyOnSettingsStoredCallback()); - } - component.updateSettings(editedSettings); - if(nestedCallback != null) { - nestedCallback.ok(editedSettings); - } - } - @Override - public void cancel() { - if(nestedCallback != null) { - nestedCallback.cancel(); - } - } - } - -} +package com.sap.sse.gwt.client.shared.components; + +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.ui.Button; +import com.sap.sse.common.settings.Settings; +import com.sap.sse.gwt.client.StringMessages; +import com.sap.sse.gwt.client.shared.settings.DummyOnSettingsStoredCallback; +import com.sap.sse.gwt.client.shared.settings.OnSettingsStoredCallback; + +public class SettingsDialog extends AbstractSettingsDialog { + + public SettingsDialog(final Component component, StringMessages stringMessages) { + this(component, stringMessages, /* animationEnabled */ true, null); + } + + public SettingsDialog(final Component component, StringMessages stringMessages, LinkWithSettingsGenerator linkWithSettingsGenerator) { + this(component, stringMessages, /* animationEnabled */ true, linkWithSettingsGenerator); + } + + public SettingsDialog(final Component component, StringMessages stringMessages, + boolean animationEnabled, LinkWithSettingsGenerator linkWithSettingsGenerator) { + this(component, component.getSettingsDialogComponent(), stringMessages, animationEnabled, linkWithSettingsGenerator, null); + } + + public SettingsDialog(final Component component, StringMessages stringMessages, + DialogCallback callback) { + this(component, component.getSettingsDialogComponent(), stringMessages, /* animationEnabled */ true, null, callback); + } + + /** + * Creates a settings button for component that, when clicked, opens a settings dialog for that + * component and when confirmed, updates that component's settings. The button has no CSS style attached to give + * callers full flexibility as to how to style the button. + */ + public static Button createSettingsButton(final Component component, + final StringMessages stringMessages) { + Button settingsButton = new Button(); + settingsButton.setTitle(stringMessages.settings()); + settingsButton.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + new SettingsDialog(component, stringMessages).show(); + } + }); + return settingsButton; + } + + /** + * This auxiliary constructor is required to avoid duplicate calls to {@link Component#getSettingsDialogComponent()} + * which may choose to create a new instance each call. Such duplicate instances would cause the validator to + * operate on a different instance as the one used for displaying, hence not allowing the validator to use the UI + * elements, neither for update nor read. + */ + private SettingsDialog(final Component component, + SettingsDialogComponent dialogComponent, StringMessages stringMessages, + boolean animationEnabled, LinkWithSettingsGenerator linkWithSettingsGenerator, final DialogCallback callback) { + super(component.getLocalizedShortName(), dialogComponent, stringMessages, animationEnabled, linkWithSettingsGenerator, + new SettingsDialogCallback<>(component, callback)); + + if (component.getComponentContext() != null + && component.getComponentContext().hasMakeCustomDefaultSettingsSupport(component)) { + initMakeDefaultButtons(component, stringMessages); + } + } + + private void initMakeDefaultButtons(final Component component, final StringMessages stringMessages) { + final Button makeDefaultButton = new Button(stringMessages.makeDefault()); + makeDefaultButton.getElement().getStyle().setMargin(3, Unit.PX); + makeDefaultButton.ensureDebugId("MakeDefaultButton"); + getLeftButtonPannel().add(makeDefaultButton); + makeDefaultButton.addClickHandler(new ClickHandler() { + public void onClick(ClickEvent event) { + setButtonSavingState(true); + component.getComponentContext().makeSettingsDefault(component, getResult(), new OnSettingsStoredCallback() { + + @Override + public void onSuccess() { + setButtonSavingState(false); + Window.alert(stringMessages.settingsSavedMessage()); + } + + @Override + public void onError(Throwable caught) { + setButtonSavingState(false); + Window.alert(stringMessages.settingsSaveErrorMessage()); + } + + + }); + } + + private void setButtonSavingState(boolean savingState) { + if(savingState) { + makeDefaultButton.setEnabled(false); + makeDefaultButton.setText(stringMessages.makeDefaultInProgress()); + } else { + makeDefaultButton.setEnabled(true); + makeDefaultButton.setText(stringMessages.makeDefault()); + } + } + }); + } + + private static class SettingsDialogCallback implements DialogCallback { + + private final DialogCallback nestedCallback; + private final Component component; + + public SettingsDialogCallback(Component component, DialogCallback nestedCallback) { + this.component = component; + this.nestedCallback = nestedCallback; + } + + @Override + public void ok(SettingsType editedSettings) { + if (component.getComponentContext() != null + && component.getComponentContext().hasMakeCustomDefaultSettingsSupport(component)) { + component.getComponentContext().storeSettingsForContext(component, editedSettings, new DummyOnSettingsStoredCallback()); + } + component.updateSettings(editedSettings); + if(nestedCallback != null) { + nestedCallback.ok(editedSettings); + } + } + @Override + public void cancel() { + if(nestedCallback != null) { + nestedCallback.cancel(); + } + } + } + +} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/AbstractPerspectiveComposite.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/AbstractPerspectiveComposite.java index d1af6b4ea69..6a5c785c14b 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/AbstractPerspectiveComposite.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/AbstractPerspectiveComposite.java @@ -10,6 +10,7 @@ import com.sap.sse.common.settings.Settings; import com.sap.sse.gwt.client.shared.components.AbstractCompositeComponent; import com.sap.sse.gwt.client.shared.components.Component; import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; /** * An abstract base class for a {@link Perspective} with a widget. diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/ComponentContext.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/ComponentContext.java deleted file mode 100644 index 46cb0c725a7..00000000000 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/ComponentContext.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.sap.sse.gwt.client.shared.perspective; - -import com.sap.sse.common.settings.Settings; -import com.sap.sse.gwt.client.shared.components.Component; -import com.sap.sse.gwt.client.shared.components.ComponentLifecycle; - -/** - * Manages all default settings of a component/perspective and its subcomponents if there are any. - * It supplies components with initial settings and determines whether the settings of components are storable or not. - * - * @author Vladislav Chumak - * - * @param The {@link Settings} type of the settings of the root component/perspective containing all the settings for itself and its subcomponents - */ -public interface ComponentContext { - - /** - * Stores the {@link Settings} of the provided {@link Component} in the default component settings tree. Make sure to - * call this method only when {@link #hasMakeCustomDefaultSettingsSupport(Component)} method returns {@code true} - * for the passed {@link Component}. - * - * @param component - * The component which corresponds to the provided {@link Settings} - * @param newDefaultSettings - * The {@link Settings} to be stored - * @param onSettingsStoredCallback The callback which is called when the settings storage process finishes - */ - void makeSettingsDefault(Component component, CS newDefaultSettings, OnSettingsStoredCallback onSettingsStoredCallback); - - void storeSettingsForContext(Component component, CS newSettings, OnSettingsStoredCallback onSettingsStoredCallback); - - /** - * Gets the {@link ComponentLifecycle} of the root component managed by - * this {@link ComponentContext}. - * - * @return The {@link ComponentLifecycle} of the root component - */ - ComponentLifecycle getRootLifecycle(); - - /** - * Checks whether the {@link Settings} of the passed {@link Component} are - * storable and whether the underlying implementation supports - * {@link #makeSettingsDefault(Component, Settings)} calls for it. - * - * @param component The component with potentially storable settings - * @return {@code true} if the settings of the component are storable - * AND the {@link ComponentContext} implementation has settings - * storage support - */ - boolean hasMakeCustomDefaultSettingsSupport(Component component); - - void getInitialSettingsForComponent(final Component component, final OnSettingsLoadedCallback callback); - - /** - * Gets initial settings and passes these settings to the provided callback. - * The provided callback gets called when initial/default settings are available. That means, - * if the initialisation of this instance has been already finished, the provided callback is called - * immediately. If initialisation is not done yet, then the callback gets called when - * the initialisation gets finished. This method produces no side-effects if it gets called multiple times. - * - * @param settingsReceiverCallback The callback which supplies the caller with initial settings - * - * @see #initInitialSettings() - */ - void getInitialSettings(OnSettingsLoadedCallback onInitialSettingsLoaded); - - void dispose(); - -} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/PerspectiveCompositeLifecycleTabbedSettingsComponent.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/PerspectiveCompositeLifecycleTabbedSettingsComponent.java index ccf44e345f7..f96ba9c6724 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/PerspectiveCompositeLifecycleTabbedSettingsComponent.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/PerspectiveCompositeLifecycleTabbedSettingsComponent.java @@ -1,93 +1,94 @@ -package com.sap.sse.gwt.client.shared.perspective; - -import com.google.gwt.user.client.ui.Widget; -import com.sap.sse.common.settings.Settings; -import com.sap.sse.gwt.client.shared.components.AbstractComponent; -import com.sap.sse.gwt.client.shared.components.Component; -import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; - -/** - * A component that contains a collection of settings components in a tabbed panel. - * - * @author Frank (c5163874) - * @param - * the {@link PerspectiveLifecycle} type - * @param - * the {@link Perspective} settings type - */ -public class PerspectiveCompositeLifecycleTabbedSettingsComponent, PS extends Settings> - extends AbstractComponent> { - - private final String title; - private PerspectiveCompositeSettings perspectiveCompositeSettings; - private PL lifecycle; - - public PerspectiveCompositeLifecycleTabbedSettingsComponent(Component parent, ComponentContext context, - PL lifecycle, - PerspectiveCompositeSettings settings) { - this(parent, context, lifecycle, settings, null); - } - - public PerspectiveCompositeLifecycleTabbedSettingsComponent(Component parent, ComponentContext context, - PL lifecycle, - PerspectiveCompositeSettings settings, - String title) { - super(parent, context); - this.lifecycle = lifecycle; - this.perspectiveCompositeSettings = settings; - this.title = title; - } - - @Override - public boolean hasSettings() { - return true; - } - - @Override - public SettingsDialogComponent> getSettingsDialogComponent() { - return new PerspectiveCompositeTabbedSettingsDialogComponent(lifecycle, perspectiveCompositeSettings); - } - - @Override - public PerspectiveCompositeSettings getSettings() { - return perspectiveCompositeSettings; - } - - @Override - public void updateSettings(PerspectiveCompositeSettings newSettings) { - this.perspectiveCompositeSettings = newSettings; - } - - @Override - public String getLocalizedShortName() { - if (title != null && !title.isEmpty()) { - return title; - } - return lifecycle.getLocalizedShortName(); - } - - @Override - public Widget getEntryWidget() { - throw new RuntimeException("Virtual composite component doesn't have a widget of its own"); - } - - @Override - public boolean isVisible() { - return false; - } - - @Override - public void setVisible(boolean visibility) { - throw new RuntimeException("Virtual composite component doesn't know how to make itself visible"); - } - - @Override - public String getDependentCssClassName() { - return null; - } - - @Override - public String getId() { - return "PerspectiveCompositeLifecycleTabbedSettingsComponentFor" + lifecycle.getComponentId(); - } +package com.sap.sse.gwt.client.shared.perspective; + +import com.google.gwt.user.client.ui.Widget; +import com.sap.sse.common.settings.Settings; +import com.sap.sse.gwt.client.shared.components.AbstractComponent; +import com.sap.sse.gwt.client.shared.components.Component; +import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; + +/** + * A component that contains a collection of settings components in a tabbed panel. + * + * @author Frank (c5163874) + * @param + * the {@link PerspectiveLifecycle} type + * @param + * the {@link Perspective} settings type + */ +public class PerspectiveCompositeLifecycleTabbedSettingsComponent, PS extends Settings> + extends AbstractComponent> { + + private final String title; + private PerspectiveCompositeSettings perspectiveCompositeSettings; + private PL lifecycle; + + public PerspectiveCompositeLifecycleTabbedSettingsComponent(Component parent, ComponentContext context, + PL lifecycle, + PerspectiveCompositeSettings settings) { + this(parent, context, lifecycle, settings, null); + } + + public PerspectiveCompositeLifecycleTabbedSettingsComponent(Component parent, ComponentContext context, + PL lifecycle, + PerspectiveCompositeSettings settings, + String title) { + super(parent, context); + this.lifecycle = lifecycle; + this.perspectiveCompositeSettings = settings; + this.title = title; + } + + @Override + public boolean hasSettings() { + return true; + } + + @Override + public SettingsDialogComponent> getSettingsDialogComponent() { + return new PerspectiveCompositeTabbedSettingsDialogComponent(lifecycle, perspectiveCompositeSettings); + } + + @Override + public PerspectiveCompositeSettings getSettings() { + return perspectiveCompositeSettings; + } + + @Override + public void updateSettings(PerspectiveCompositeSettings newSettings) { + this.perspectiveCompositeSettings = newSettings; + } + + @Override + public String getLocalizedShortName() { + if (title != null && !title.isEmpty()) { + return title; + } + return lifecycle.getLocalizedShortName(); + } + + @Override + public Widget getEntryWidget() { + throw new RuntimeException("Virtual composite component doesn't have a widget of its own"); + } + + @Override + public boolean isVisible() { + return false; + } + + @Override + public void setVisible(boolean visibility) { + throw new RuntimeException("Virtual composite component doesn't know how to make itself visible"); + } + + @Override + public String getDependentCssClassName() { + return null; + } + + @Override + public String getId() { + return "PerspectiveCompositeLifecycleTabbedSettingsComponentFor" + lifecycle.getComponentId(); + } } \ No newline at end of file diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/PerspectiveCompositeLifecycleTabbedSettingsDialog.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/PerspectiveCompositeLifecycleTabbedSettingsDialog.java index 994f0172c82..9cb8e7b9099 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/PerspectiveCompositeLifecycleTabbedSettingsDialog.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/PerspectiveCompositeLifecycleTabbedSettingsDialog.java @@ -1,39 +1,40 @@ -package com.sap.sse.gwt.client.shared.perspective; - -import com.sap.sse.common.settings.Settings; -import com.sap.sse.gwt.client.StringMessages; -import com.sap.sse.gwt.client.shared.components.Component; -import com.sap.sse.gwt.client.shared.components.SettingsDialog; - -/** - * A composite settings dialog that combines all settings of a perspective provide by a {@link PerspectiveLifecycle}. - * - * @author Frank Mittag (c5163874) - * @param - * the type of the PerspectiveLifecycle - * @param - * the type of the Perspective settings - * - */ -public class PerspectiveCompositeLifecycleTabbedSettingsDialog, PS extends Settings> - extends SettingsDialog> { - - public PerspectiveCompositeLifecycleTabbedSettingsDialog(Component parent, ComponentContext context, - StringMessages stringConstants, - PL lifecycle, - PerspectiveCompositeSettings settings, String title) { - super(new PerspectiveCompositeLifecycleTabbedSettingsComponent(parent, context, lifecycle, settings, - title), - stringConstants); - } - - public PerspectiveCompositeLifecycleTabbedSettingsDialog(Component parent, ComponentContext context, - StringMessages stringConstants, - PL lifecycle, - PerspectiveCompositeSettings settings, - String title, DialogCallback> callback) { - super(new PerspectiveCompositeLifecycleTabbedSettingsComponent(parent, context, lifecycle, settings, - title), - stringConstants, callback); - } -} +package com.sap.sse.gwt.client.shared.perspective; + +import com.sap.sse.common.settings.Settings; +import com.sap.sse.gwt.client.StringMessages; +import com.sap.sse.gwt.client.shared.components.Component; +import com.sap.sse.gwt.client.shared.components.SettingsDialog; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; + +/** + * A composite settings dialog that combines all settings of a perspective provide by a {@link PerspectiveLifecycle}. + * + * @author Frank Mittag (c5163874) + * @param + * the type of the PerspectiveLifecycle + * @param + * the type of the Perspective settings + * + */ +public class PerspectiveCompositeLifecycleTabbedSettingsDialog, PS extends Settings> + extends SettingsDialog> { + + public PerspectiveCompositeLifecycleTabbedSettingsDialog(Component parent, ComponentContext context, + StringMessages stringConstants, + PL lifecycle, + PerspectiveCompositeSettings settings, String title) { + super(new PerspectiveCompositeLifecycleTabbedSettingsComponent(parent, context, lifecycle, settings, + title), + stringConstants); + } + + public PerspectiveCompositeLifecycleTabbedSettingsDialog(Component parent, ComponentContext context, + StringMessages stringConstants, + PL lifecycle, + PerspectiveCompositeSettings settings, + String title, DialogCallback> callback) { + super(new PerspectiveCompositeLifecycleTabbedSettingsComponent(parent, context, lifecycle, settings, + title), + stringConstants, callback); + } +} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/SettingsBuildingPipeline.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/SettingsBuildingPipeline.java deleted file mode 100644 index 92d83af6e42..00000000000 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/SettingsBuildingPipeline.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.sap.sse.gwt.client.shared.perspective; - -import java.util.List; - -import com.google.gwt.json.client.JSONValue; -import com.sap.sse.common.settings.Settings; - -public interface SettingsBuildingPipeline { - - S getSettingsObject(S defaultSettings, SettingsJsons settingsJsons); - - S getSettingsObject(S defaultSettings); - - S getSettingsObject(S defaultSettings, SettingsStrings settingsStrings); - - JSONValue getJsonObject(Settings settings, PipelineLevel pipelineLevel, List path); - - SettingsStringConverter getSettingsStringConverter(); - -} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/AbstractSettingsBuildingPipeline.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/AbstractSettingsBuildingPipeline.java similarity index 92% rename from java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/AbstractSettingsBuildingPipeline.java rename to java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/AbstractSettingsBuildingPipeline.java index 731894e9022..fdae25a94e4 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/AbstractSettingsBuildingPipeline.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/AbstractSettingsBuildingPipeline.java @@ -1,32 +1,32 @@ -package com.sap.sse.gwt.client.shared.perspective; - -import com.sap.sse.common.settings.Settings; - -public abstract class AbstractSettingsBuildingPipeline implements SettingsBuildingPipeline { - - protected final SettingsStringConverter settingsStringConverter; - - public AbstractSettingsBuildingPipeline() { - this(new SettingsStringConverter()); - } - - public AbstractSettingsBuildingPipeline(SettingsStringConverter settingsStringConverter) { - this.settingsStringConverter = settingsStringConverter; - } - - @Override - public S getSettingsObject(S defaultSettings) { - return getSettingsObject(defaultSettings, new SettingsJsons(null, null)); - } - - @Override - public S getSettingsObject(S defaultSettings, SettingsStrings settingsStrings) { - SettingsJsons settingsJsons = settingsStringConverter.convertToSettingsJson(settingsStrings); - return getSettingsObject(defaultSettings, settingsJsons); - } - - public SettingsStringConverter getSettingsStringConverter() { - return settingsStringConverter; - } - -} +package com.sap.sse.gwt.client.shared.settings; + +import com.sap.sse.common.settings.Settings; + +public abstract class AbstractSettingsBuildingPipeline implements SettingsBuildingPipeline { + + protected final SettingsStringConverter settingsStringConverter; + + public AbstractSettingsBuildingPipeline() { + this(new SettingsStringConverter()); + } + + public AbstractSettingsBuildingPipeline(SettingsStringConverter settingsStringConverter) { + this.settingsStringConverter = settingsStringConverter; + } + + @Override + public S getSettingsObject(S defaultSettings, SettingsStrings settingsStrings) { + SettingsJsons settingsJsons = settingsStringConverter.convertToSettingsJson(settingsStrings); + return getSettingsObject(defaultSettings, settingsJsons); + } + + @Override + public S getSettingsObject(S defaultSettings) { + return getSettingsObject(defaultSettings, new SettingsJsons(null, null)); + } + + public SettingsStringConverter getSettingsStringConverter() { + return settingsStringConverter; + } + +} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/ComponentContext.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/ComponentContext.java new file mode 100644 index 00000000000..92e019f7a67 --- /dev/null +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/ComponentContext.java @@ -0,0 +1,115 @@ +package com.sap.sse.gwt.client.shared.settings; + +import com.sap.sse.common.settings.Settings; +import com.sap.sse.gwt.client.shared.components.Component; +import com.sap.sse.gwt.client.shared.components.ComponentLifecycle; + +/** + * Manages all default/initial settings of a component/perspective and its subcomponents if there are any. + * It supplies components with initial settings and determines whether the settings of components are storable or not. + * The settings can be stored with different scopes. The scope determines the influence of the stored settings on the + * initial settings of the same component which is used to present a context (e.g. event or race). + * Currently, there are two basic scopes which can be used for settings storage: + *
+ *
Document Settings
+ *
The settings are stored only for the current context (e.g. a race presented in a RaceBoard). This settings are not considered in any other context. See {@link #storeSettingsForContext(Component, Settings, OnSettingsStoredCallback)}
+ * + *
User Settings
+ *
The settings are stored for a component/perspective context-independently and may effect a different context presented by the same perspective/component, e.g. a different race in a RaceBoard. See {@link #makeSettingsDefault(Component, Settings, OnSettingsStoredCallback)}
+ *
+ * A {@link SettingsStorageManager} is used as a persistence layer for settings. + * When initial settings are loaded, {@link SettingsBuildingPipeline} is used to construct the settings object based on the data from the underlying settings store. + * An example of such a settings construction might look as follows: + *
    + *
  1. Get "System Default" settings by means of component lifecycles (see {@link ComponentLifecycle#createDefaultSettings()})
  2. + *
  3. Override the settings by values which are stored explicitly in the "User Settings"
  4. + *
  5. Override the settings by values which are stored explicitly in the "Document Settings"
  6. + *
  7. Override the settings by values which are set over URL
  8. + *
+ * The whole settings construction pipeline, as well as {@link SettingsStorageManager} are customizable. For more information and possibilities see the existing implementations. + * + * + * @author Vladislav Chumak + * + * @param The {@link Settings} type of the settings of the root component/perspective containing all the settings for itself and its subcomponents + */ +public interface ComponentContext { + + /** + * Stores the provided {@link Settings} of the provided {@link Component} as "User Settings" globally. Make sure to + * call this method only when {@link #hasMakeCustomDefaultSettingsSupport(Component)} method returns {@code true} + * for the provided {@link Component}. + * + * @param component + * The component which corresponds to the provided {@link Settings} + * @param newDefaultSettings + * The {@link Settings} to be stored + * @param onSettingsStoredCallback The callback which is called when the settings storage process finishes + */ + void makeSettingsDefault(Component component, CS newDefaultSettings, OnSettingsStoredCallback onSettingsStoredCallback); + + /** + * Stores the provided {@link Settings} of the provided {@link Component} as "Document Settings" (old term "Context specific Settings") for the current context. Make sure to + * call this method only when {@link #hasMakeCustomDefaultSettingsSupport(Component)} method returns {@code true} + * for the provided {@link Component}. + * + * @param component + * The component which corresponds to the provided {@link Settings} + * @param newSettings + * The {@link Settings} to be stored + * @param onSettingsStoredCallback The callback which is called when the settings storage process finishes + */ + void storeSettingsForContext(Component component, CS newSettings, OnSettingsStoredCallback onSettingsStoredCallback); + + /** + * Gets the {@link ComponentLifecycle} of the root component managed by + * this {@link ComponentContext}. + * + * @return The {@link ComponentLifecycle} of the root component + */ + ComponentLifecycle getRootLifecycle(); + + /** + * Checks whether the {@link Settings} of the provided {@link Component} are + * storable and whether the underlying implementation supports + * {@link #makeSettingsDefault(Component, Settings)} calls for it. + * + * @param component The component with potentially storable settings + * @return {@code true} if the settings of the component are storable + * AND the {@link ComponentContext} implementation has settings + * storage support + */ + boolean hasMakeCustomDefaultSettingsSupport(Component component); + + /** + * Gets initial settings and passes these settings to the provided callback. + * The provided callback gets called when initial/default settings are available. Depending on implementation + * the callback may be called after a delay, e.g. after the settings have been retrieved from a server, + * or immediately, e.g. when the implementation does not query information from server. + * This method produces no side-effects if it gets called multiple times. + * + * @param settingsReceiverCallback The callback which supplies the caller with initial settings + */ + void getInitialSettings(OnSettingsLoadedCallback onInitialSettingsLoaded); + + /** + * Gets initial settings of the provided component and passes these settings to the provided callback. + * The provided callback gets called when initial/default settings are available. Depending on implementation + * the callback may be called after a delay, e.g. after the settings have been retrieved from a server, + * or immediately, e.g. when the implementation does not query information from server. + * This method produces no side-effects if it gets called multiple times. Be aware, that each call of this method + * may produce a server round-trip. That's why {@link #getInitialSettings(OnSettingsLoadedCallback)} should be + * preferred for initial settings retrievement. Use this method only, if due to some event a separate retrievement + * of settings for a component is required. + * + * @param settingsReceiverCallback The callback which supplies the caller with initial settings + */ + void getInitialSettingsForComponent(final Component component, final OnSettingsLoadedCallback callback); + + /** + * Releases all resources and listener registrations acquired by this instance. You must use this method, if instances + * of {@link ComponentContext} are created AND destroyed dynamically during runtime. + */ + void dispose(); + +} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/ComponentContextWithSettingsStorage.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/ComponentContextWithSettingsStorage.java similarity index 66% rename from java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/ComponentContextWithSettingsStorage.java rename to java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/ComponentContextWithSettingsStorage.java index e44b6334052..10f75504909 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/ComponentContextWithSettingsStorage.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/ComponentContextWithSettingsStorage.java @@ -1,298 +1,297 @@ -package com.sap.sse.gwt.client.shared.perspective; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import com.google.gwt.core.client.GWT; -import com.google.gwt.json.client.JSONObject; -import com.google.gwt.json.client.JSONValue; -import com.google.gwt.user.client.rpc.AsyncCallback; -import com.sap.sse.common.settings.Settings; -import com.sap.sse.common.settings.generic.GenericSerializableSettings; -import com.sap.sse.common.settings.generic.SettingsMap; -import com.sap.sse.common.settings.util.SettingsDefaultValuesUtils; -import com.sap.sse.common.settings.util.SettingsMergeUtils; -import com.sap.sse.gwt.client.shared.components.Component; -import com.sap.sse.gwt.client.shared.components.ComponentLifecycle; - -/** - * Manages all the default settings of a component/perspective and its subcomponents if there are any. It supplies - * components with initial settings and determines whether the settings of components are storable or not. This - * implementation provides settings storage support if the storable settings either implement {@link SettingsMap} or - * {@link GenericSerializableSettings}. There are two kinds of settings which are stored separately - global - * settings and context specific settings. - *
    - *
  • Global settings are the settings which are applied globally to all components
  • - *
  • Context specific settings have higher precedence than global settings and are applied to components only if the - * current context (e.g. event or race) matches the context of stored settings.
  • - *
- * That means that context specific settings are stored per context (e.g. race or event) whereas global settings are - * stored globally for all possible contexts (independent of event or race). - * - * - * @author Vladislav Chumak - * - * @param - * The {@link Settings} type of the settings of the root component/perspective containing all the settings - * for itself and its subcomponents - */ -public class ComponentContextWithSettingsStorage extends SimpleComponentContext { - - /** - * Manages the persistence layer of settings. - */ - protected final SettingsStorageManager settingsStorageManager; - - /** - * - * @param settingsStorageManager - * The {@link SettingsStorageManager} to be used to access stored settings and to store new settings - */ - public ComponentContextWithSettingsStorage(ComponentLifecycle rootLifecycle, - SettingsStorageManager settingsStorageManager) { - super(rootLifecycle); - this.settingsStorageManager = settingsStorageManager; - } - - @Override - public void getInitialSettings(OnSettingsLoadedCallback callback) { - S systemDefaultSettings = rootLifecycle.createDefaultSettings(); - settingsStorageManager.retrieveDefaultSettings(systemDefaultSettings, callback); - } - - @Override - public void getInitialSettingsForComponent(final Component component, final OnSettingsLoadedCallback callback) { - OnSettingsLoadedCallback internalCallback = new OnSettingsLoadedCallback() { - - @Override - public void onError(Throwable caught, S fallbackDefaultSettings) { - CS componentFallbackSettings = ComponentUtils.determineComponentSettingsFromPerspectiveSettings(new ArrayList<>(component.getPath()), fallbackDefaultSettings); - callback.onError(caught, componentFallbackSettings); - } - - @Override - public void onSuccess(S settings) { - CS componentSettings = ComponentUtils.determineComponentSettingsFromPerspectiveSettings(new ArrayList<>(component.getPath()), settings); - callback.onSuccess(componentSettings); - } - }; - getInitialSettings(internalCallback); - } - - /** - * Checks whether the {@link Settings} of the provided {@link Component} are - * storable and whether the underlying implementation supports - * {@link #makeSettingsDefault(Component, Settings)} calls for it. - * - * @param component The component with potentially storable settings - * @return {@code true} if the settings of the component are storable - * AND the {@link ComponentContext} implementation has settings - * storage support - */ - @Override - public boolean hasMakeCustomDefaultSettingsSupport(Component component) { - if (!component.hasSettings()) { - return false; - } - if(!settingsStorageManager.supportsStore()) { - return false; - } - Settings settings = component.getSettings(); - if (settings instanceof SettingsMap || settings instanceof GenericSerializableSettings) { - return true; - } - return false; - } - - /** - * Extracts global settings from provided {@link Settings} of the specified component. - * - * @param component The settings of the specified component - * @param componentSettings The settings of the specified component - * @return The global settings extracted, or {@code null} if there aren't any - * global settings to be stored - */ - public CS extractGlobalSettings(Component component, CS componentSettings) { - ComponentLifecycle targetLifecycle = ComponentUtils.determineLifecycle(new ArrayList<>(component.getPath()), rootLifecycle); - return targetLifecycle.extractGlobalSettings(componentSettings); - } - - /** - * Extracts context specific settings from provided {@link Settings} of the specified component. - * - * @param component The component which the provided settings correspond to - * @param componentSettings The settings of the specified component - * @return The context specific settings extracted, or {@code null} if there aren't any - * context specific settings to be stored - */ - public CS extractContextSpecificSettings(Component component, CS componentSettings) { - ComponentLifecycle targetLifecycle = ComponentUtils.determineLifecycle(new ArrayList<>(component.getPath()), rootLifecycle); - return targetLifecycle.extractContextSpecificSettings(componentSettings); - } - - /** - * Stores the {@link Settings} of the provided {@link Component} in the default component settings tree. Make sure to - * call this method only when {@link #hasMakeCustomDefaultSettingsSupport(Component)} method returns {@code true} - * for the passed {@link Component}. - * - * @param component - * The component which corresponds to the provided {@link Settings} - * @param newDefaultSettings - * The {@link Settings} to be stored - * * @param onSettingsStoredCallback - * The callback which is called when the settings storage process finishes - */ - @Override - public void makeSettingsDefault(Component component, CS newDefaultSettings, final OnSettingsStoredCallback onSettingsStoredCallback) { - ComponentLifecycle targetLifecycle = ComponentUtils.determineLifecycle(new ArrayList<>(component.getPath()), rootLifecycle); - CS globalSettings = targetLifecycle.extractGlobalSettings(newDefaultSettings); - updateGlobalSettings(component.getPath(), globalSettings, onSettingsStoredCallback); - } - - @Override - public void storeSettingsForContext(Component component, CS newSettings, OnSettingsStoredCallback onSettingsStoredCallback) { - ComponentLifecycle targetLifecycle = ComponentUtils.determineLifecycle(new ArrayList<>(component.getPath()), rootLifecycle); - CS contextSpecificSettings = targetLifecycle.extractContextSpecificSettings(newSettings); - updateContextSpecificSettings(component.getPath(), contextSpecificSettings, onSettingsStoredCallback); - } - - - /** - * Patches the settings tree with the new settings provided. The settings node with the specified path - * is going to be created/replaced with the new settings. - * - * @param root The root node of the settings tree - * @param path The path of the node to create/update - * @param newSettings The new settings with that the target node in the settings tree is going to be updated - * @return - */ - /** - * Patches the settings tree with the new settings provided. The settings node with the specified path - * is going to be created/replaced with the new settings. - * - * @param root The root node of the settings tree - * @param path The path of the node to create/update - * @param newSettings The new settings with that the target node in the settings tree is going to be updated - * @return - */ - private JSONObject patchJsonObject(JSONObject root, List path, Settings newSettings, PipelineLevel pipelineLevel) { - newSettings = cloneSettings(newSettings, path); - return patchJsonObject(root, path, Collections.unmodifiableList(new ArrayList<>(path)), newSettings, pipelineLevel); - } - - @SuppressWarnings("unchecked") - private CS cloneSettings(CS settingsToClone, List path) { - CS clonedSettings = ComponentUtils.determineComponentSettingsFromPerspectiveSettings(new ArrayList<>(path), rootLifecycle.createDefaultSettings()); - if(clonedSettings instanceof GenericSerializableSettings) { - GenericSerializableSettings newSettings = (GenericSerializableSettings) clonedSettings; - GenericSerializableSettings originalSettings = (GenericSerializableSettings) settingsToClone; - SettingsDefaultValuesUtils.keepDefaults(originalSettings, newSettings); - SettingsMergeUtils.setValues(originalSettings, newSettings); - clonedSettings = (CS) newSettings; - } - //TODO maybe add support for SettingsMap? - return clonedSettings; - } - - private JSONObject patchJsonObject(JSONObject root, List path, List originalPath, Settings newSettings, PipelineLevel pipelineLevel) { - if(path.isEmpty()) { - return (JSONObject) settingsStorageManager.settingsToJSON(newSettings, pipelineLevel, originalPath); - } - if (root == null) { - root = new JSONObject(); - } - String current = path.remove(path.size() - 1); - // we need to go further - if (!path.isEmpty()) { - JSONValue child = root.get(current); - boolean haskey = root.containsKey(current); - if (child == null || child.isObject() == null) { - if (haskey) { - GWT.log("Warning: replacing some subtree element that is wrong type!"); - } - child = new JSONObject(); - root.put(current, child); - } - return patchJsonObject(child.isObject(), path, originalPath, newSettings, pipelineLevel); - } else { - JSONValue json = settingsStorageManager.settingsToJSON(newSettings, pipelineLevel, originalPath); - root.put(current, json); - } - return root; - } - - /** - * Updates the settings tree with the new settings provided. The settings node with the specified path - * is going to be created/replaced with the new settings. This method may produce a server round-trip - * if the user is logged in. - * - * @param path The path of the settings' node to create/update - * @param globalSettings The global settings of the component which corresponds to the provided path - * @param contextSettings The context specific settings of the component which corresponds to the provided path - * @param onSettingsStoredCallback The callback which is called when the settings storage process finishes - */ - @SuppressWarnings("unused") - private void updateSettings(final ArrayList path, final Settings globalSettings, - final Settings contextSettings, final OnSettingsStoredCallback onSettingsStoredCallback) { - settingsStorageManager.retrieveSettingsJsons(new AsyncCallback() { - - @Override - public void onSuccess(SettingsJsons result) { - final JSONObject patchedContextSpecificSettings = patchJsonObject(result.getContextSpecificSettingsJson(), new ArrayList<>(path), - contextSettings, PipelineLevel.CONTEXT_SPECIFIC_DEFAULTS); - final JSONObject patchedGlobalSettings = patchJsonObject(result.getGlobalSettingsJson(), new ArrayList<>(path), - globalSettings, PipelineLevel.GLOBAL_DEFAULTS); - SettingsJsons settingsJsons = new SettingsJsons(patchedGlobalSettings, patchedContextSpecificSettings); - settingsStorageManager.storeSettingsJsons(settingsJsons, onSettingsStoredCallback); - } - - @Override - public void onFailure(Throwable caught) { - onSettingsStoredCallback.onError(caught); - } - }); - } - - private void updateGlobalSettings(final ArrayList path, final Settings globalSettings, - final OnSettingsStoredCallback onSettingsStoredCallback) { - settingsStorageManager.retrieveSettingsJsons(new AsyncCallback() { - - @Override - public void onSuccess(SettingsJsons result) { - final JSONObject patchedGlobalSettings = patchJsonObject(result.getGlobalSettingsJson(), new ArrayList<>(path), - globalSettings, PipelineLevel.GLOBAL_DEFAULTS); - settingsStorageManager.storeGlobalSettingsJson(patchedGlobalSettings, onSettingsStoredCallback); - } - - @Override - public void onFailure(Throwable caught) { - onSettingsStoredCallback.onError(caught); - } - }); - } - - private void updateContextSpecificSettings(final ArrayList path, final Settings contextSpecificSettings, - final OnSettingsStoredCallback onSettingsStoredCallback) { - settingsStorageManager.retrieveSettingsJsons(new AsyncCallback() { - - @Override - public void onSuccess(SettingsJsons result) { - final JSONObject patchedContextSpecificSettings = patchJsonObject(result.getContextSpecificSettingsJson(), new ArrayList<>(path), - contextSpecificSettings, PipelineLevel.CONTEXT_SPECIFIC_DEFAULTS); - settingsStorageManager.storeContextSpecificSettingsJson(patchedContextSpecificSettings, onSettingsStoredCallback); - } - - @Override - public void onFailure(Throwable caught) { - onSettingsStoredCallback.onError(caught); - } - }); - } - - @Override - public void dispose() { - super.dispose(); - settingsStorageManager.dispose(); - } -} +package com.sap.sse.gwt.client.shared.settings; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.json.client.JSONObject; +import com.google.gwt.json.client.JSONValue; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.sap.sse.common.settings.Settings; +import com.sap.sse.common.settings.generic.GenericSerializableSettings; +import com.sap.sse.common.settings.generic.SettingsMap; +import com.sap.sse.common.settings.util.SettingsDefaultValuesUtils; +import com.sap.sse.common.settings.util.SettingsMergeUtils; +import com.sap.sse.gwt.client.shared.components.Component; +import com.sap.sse.gwt.client.shared.components.ComponentLifecycle; + +/** + * Manages all the default/initial settings of a component/perspective and its subcomponents if there are any. It supplies + * components with initial settings and determines whether the settings of components are storable or not. This + * implementation provides settings storage support if the storable settings either implement {@link SettingsMap} or + * {@link GenericSerializableSettings}. There are two kinds of settings which are stored separately - User + * Settings (old term "Global Settings") and Document Settings (old term "Context specific Settings"). + *
+ *
Document Settings
+ *
The settings are stored only for the current context (e.g. a race presented in a RaceBoard). This settings are not considered in any other context. See {@link #storeSettingsForContext(Component, Settings, OnSettingsStoredCallback)}
+ * + *
User Settings
+ *
The settings are stored for a component/perspective context-independently and may effect a different context presented by the same perspective/component, e.g. a different race in a RaceBoard. See {@link #makeSettingsDefault(Component, Settings, OnSettingsStoredCallback)}
+ *
+ * That means that Document Settings are stored per context (e.g. race in RaceBoard) whereas User Settings are + * stored globally for all possible contexts (e.g. for the whole RaceBoard independently from selected race). + * + * + * @author Vladislav Chumak + * @see ComponentContext + * + * @param + * The {@link Settings} type of the settings of the root component/perspective containing all the settings + * for itself and its subcomponents + * + */ +public class ComponentContextWithSettingsStorage extends SimpleComponentContext { + + /** + * Manages the persistence layer of settings. + */ + protected final SettingsStorageManager settingsStorageManager; + + /** + * @param rootLifecycle + * The {@link ComponentLifecycle} of the root component/perspective + * @param settingsStorageManager + * The {@link SettingsStorageManager} to be used to access stored settings and to store new settings + */ + public ComponentContextWithSettingsStorage(ComponentLifecycle rootLifecycle, + SettingsStorageManager settingsStorageManager) { + super(rootLifecycle); + this.settingsStorageManager = settingsStorageManager; + } + + /** + * Gets initial settings and passes these settings to the provided callback. + * The provided callback gets called when initial/default settings are available. The callback may be called after a delay, e.g. after the settings have been retrieved from a server, + * or immediately, e.g. when the implementation does not query information from server. + * This method produces no side-effects if it gets called multiple times. + * + * @param settingsReceiverCallback The callback which supplies the caller with initial settings + */ + @Override + public void getInitialSettings(OnSettingsLoadedCallback callback) { + S systemDefaultSettings = rootLifecycle.createDefaultSettings(); + settingsStorageManager.retrieveDefaultSettings(systemDefaultSettings, callback); + } + + /** + * Gets initial settings of the provided component and passes these settings to the provided callback. + * The provided callback gets called when initial/default settings are available. The callback may be called after a delay, e.g. after the settings have been retrieved from a server, + * or immediately, e.g. when the implementation does not query information from server. + * This method produces no side-effects if it gets called multiple times. Be aware, that each call of this method + * may produce a server round-trip. That's why {@link #getInitialSettings(OnSettingsLoadedCallback)} should be + * preferred for initial settings retrievement. Use this method only, if due to some event a separate retrievement + * of settings for a component is required. + * + * @param settingsReceiverCallback The callback which supplies the caller with initial settings + */ + @Override + public void getInitialSettingsForComponent(final Component component, final OnSettingsLoadedCallback callback) { + OnSettingsLoadedCallback internalCallback = new OnSettingsLoadedCallback() { + + @Override + public void onError(Throwable caught, S fallbackDefaultSettings) { + CS componentFallbackSettings = ComponentUtils.determineComponentSettingsFromPerspectiveSettings(new ArrayList<>(component.getPath()), fallbackDefaultSettings); + callback.onError(caught, componentFallbackSettings); + } + + @Override + public void onSuccess(S settings) { + CS componentSettings = ComponentUtils.determineComponentSettingsFromPerspectiveSettings(new ArrayList<>(component.getPath()), settings); + callback.onSuccess(componentSettings); + } + }; + getInitialSettings(internalCallback); + } + + /** + * Checks whether the {@link Settings} of the provided {@link Component} are + * storable and whether the underlying implementation supports + * {@link #makeSettingsDefault(Component, Settings)} calls for it. + * + * @param component The component with potentially storable settings + * @return {@code true} if the settings of the component are storable + * AND the {@link ComponentContext} implementation has settings + * storage support + */ + @Override + public boolean hasMakeCustomDefaultSettingsSupport(Component component) { + if (!component.hasSettings()) { + return false; + } + if(!settingsStorageManager.supportsStore()) { + return false; + } + Settings settings = component.getSettings(); + if (settings instanceof SettingsMap || settings instanceof GenericSerializableSettings) { + return true; + } + return false; + } + + /** + * Stores the provided {@link Settings} of the provided {@link Component} as "User Settings" globally. Make sure to + * call this method only when {@link #hasMakeCustomDefaultSettingsSupport(Component)} method returns {@code true} + * for the provided {@link Component}. + * + * @param component + * The component which corresponds to the provided {@link Settings} + * @param newDefaultSettings + * The {@link Settings} to be stored + * @param onSettingsStoredCallback The callback which is called when the settings storage process finishes + */ + @Override + public void makeSettingsDefault(Component component, CS newDefaultSettings, final OnSettingsStoredCallback onSettingsStoredCallback) { + ComponentLifecycle targetLifecycle = ComponentUtils.determineLifecycle(new ArrayList<>(component.getPath()), rootLifecycle); + CS globalSettings = targetLifecycle.extractGlobalSettings(newDefaultSettings); + updateGlobalSettings(component.getPath(), globalSettings, onSettingsStoredCallback); + } + + /** + * Stores the provided {@link Settings} of the provided {@link Component} as "Document Settings" (old term "Context specific Settings") for the current context. Make sure to + * call this method only when {@link #hasMakeCustomDefaultSettingsSupport(Component)} method returns {@code true} + * for the provided {@link Component}. + * + * @param component + * The component which corresponds to the provided {@link Settings} + * @param newSettings + * The {@link Settings} to be stored + * @param onSettingsStoredCallback The callback which is called when the settings storage process finishes + */ + @Override + public void storeSettingsForContext(Component component, CS newSettings, OnSettingsStoredCallback onSettingsStoredCallback) { + ComponentLifecycle targetLifecycle = ComponentUtils.determineLifecycle(new ArrayList<>(component.getPath()), rootLifecycle); + CS contextSpecificSettings = targetLifecycle.extractContextSpecificSettings(newSettings); + updateContextSpecificSettings(component.getPath(), contextSpecificSettings, onSettingsStoredCallback); + } + + + /** + * Patches the settings tree with the new settings provided. The settings node with the provided path + * is going to be created/replaced with the new settings. + * + * @param root The root node of the settings tree + * @param path The path of the node to create/update + * @param newSettings The new settings with that the target node in the settings tree is going to be updated + * @param pipelineLevel The scope of settings used for settings storage (e.g. User Settings, or Document Settings) + * @return The patched settings located by provided path as JSON + */ + private JSONObject patchJsonObject(JSONObject root, List path, Settings newSettings, PipelineLevel pipelineLevel) { + newSettings = cloneStorableSettings(newSettings, path); + return patchJsonObject(root, path, Collections.unmodifiableList(new ArrayList<>(path)), newSettings, pipelineLevel); + } + + @SuppressWarnings("unchecked") + private CS cloneStorableSettings(CS settingsToClone, List path) { + CS clonedSettings = ComponentUtils.determineComponentSettingsFromPerspectiveSettings(new ArrayList<>(path), rootLifecycle.createDefaultSettings()); + if(clonedSettings instanceof GenericSerializableSettings) { + GenericSerializableSettings newSettings = (GenericSerializableSettings) clonedSettings; + GenericSerializableSettings originalSettings = (GenericSerializableSettings) settingsToClone; + SettingsDefaultValuesUtils.keepDefaults(originalSettings, newSettings); + SettingsMergeUtils.setValues(originalSettings, newSettings); + clonedSettings = (CS) newSettings; + } + //TODO maybe add support for SettingsMap? Currently, SettingsMap is never provided as parameter to this method + return clonedSettings; + } + + /** + * Internal helper method for {@link #patchJsonObject(JSONObject, List, Settings, PipelineLevel)} + * + * @param root + * @param path + * @param originalPath + * @param newSettings + * @param pipelineLevel + * @return + */ + private JSONObject patchJsonObject(JSONObject root, List path, List originalPath, Settings newSettings, PipelineLevel pipelineLevel) { + if(path.isEmpty()) { + return (JSONObject) settingsStorageManager.settingsToJSON(newSettings, pipelineLevel, originalPath); + } + if (root == null) { + root = new JSONObject(); + } + String current = path.remove(path.size() - 1); + // we need to go further + if (!path.isEmpty()) { + JSONValue child = root.get(current); + boolean haskey = root.containsKey(current); + if (child == null || child.isObject() == null) { + if (haskey) { + GWT.log("Warning: replacing some subtree element that is wrong type!"); + } + child = new JSONObject(); + root.put(current, child); + } + return patchJsonObject(child.isObject(), path, originalPath, newSettings, pipelineLevel); + } else { + JSONValue json = settingsStorageManager.settingsToJSON(newSettings, pipelineLevel, originalPath); + root.put(current, json); + } + return root; + } + + /** + * Updates the User Settings' tree with the new settings provided. The settings node with the provided path + * is going to be created/replaced with the new settings. This method may produce a server round-trip + * if the user is logged in. + * + * @param path The path of the settings' node to create/update + * @param globalSettings User Settings which corresponds to the provided path + * @param onSettingsStoredCallback The callback which is called when the settings storage process finishes + */ + private void updateGlobalSettings(final ArrayList path, final Settings globalSettings, + final OnSettingsStoredCallback onSettingsStoredCallback) { + settingsStorageManager.retrieveSettingsJsons(new AsyncCallback() { + + @Override + public void onSuccess(SettingsJsons result) { + final JSONObject patchedGlobalSettings = patchJsonObject(result.getGlobalSettingsJson(), new ArrayList<>(path), + globalSettings, PipelineLevel.GLOBAL_DEFAULTS); + settingsStorageManager.storeGlobalSettingsJson(patchedGlobalSettings, onSettingsStoredCallback); + } + + @Override + public void onFailure(Throwable caught) { + onSettingsStoredCallback.onError(caught); + } + }); + } + + /** + * Updates the Document Settings' tree with the new settings provided. The settings node with the provided path + * is going to be created/replaced with the new settings. This method may produce a server round-trip + * if the user is logged in. + * + * @param path The path of the settings' node to create/update + * @param contextSpecificSettings Document Settings which corresponds to the provided path + * @param onSettingsStoredCallback The callback which is called when the settings storage process finishes + */ + private void updateContextSpecificSettings(final ArrayList path, final Settings contextSpecificSettings, + final OnSettingsStoredCallback onSettingsStoredCallback) { + settingsStorageManager.retrieveSettingsJsons(new AsyncCallback() { + + @Override + public void onSuccess(SettingsJsons result) { + final JSONObject patchedContextSpecificSettings = patchJsonObject(result.getContextSpecificSettingsJson(), new ArrayList<>(path), + contextSpecificSettings, PipelineLevel.CONTEXT_SPECIFIC_DEFAULTS); + settingsStorageManager.storeContextSpecificSettingsJson(patchedContextSpecificSettings, onSettingsStoredCallback); + } + + @Override + public void onFailure(Throwable caught) { + onSettingsStoredCallback.onError(caught); + } + }); + } + + /** + * {@inheritDoc} + */ + @Override + public void dispose() { + super.dispose(); + settingsStorageManager.dispose(); + } +} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/ComponentUtils.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/ComponentUtils.java similarity index 96% rename from java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/ComponentUtils.java rename to java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/ComponentUtils.java index 70e4b73a0b8..352954de363 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/ComponentUtils.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/ComponentUtils.java @@ -1,98 +1,100 @@ -package com.sap.sse.gwt.client.shared.perspective; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; - -import com.sap.sse.common.settings.Settings; -import com.sap.sse.gwt.client.shared.components.ComponentLifecycle; - -public class ComponentUtils { - - /** - * Determines the lifecycle from the provided current lifecycle tree which corresponds to the provided path. - * - * @param path The path of the component which lifecycle should be determined - * @param current The root lifecycle from that the desired lifecycle is reachable by the provided path - * @return The lifecycle which was determined for the provided path starting from the provided root lifecycle - * @throws IllegalStateException When the path cannot be resolved from the provided root lifecycle - */ - @SuppressWarnings("unchecked") - public static ComponentLifecycle determineLifecycle(List path, ComponentLifecycle current) { - while (current instanceof PerspectiveLifecycle && !path.isEmpty()) { - String last = path.remove(path.size() - 1); - current = ((PerspectiveLifecycle) current).getLifecycleForId(last); - } - if (!path.isEmpty() || current == null) { - throw new IllegalStateException("Settings path is not finished, but no perspective at current level"); - } - return (ComponentLifecycle) current; - } - - @SuppressWarnings("unchecked") - public static CS determineComponentSettingsFromPerspectiveSettings(List path, Settings current) { - while (current instanceof PerspectiveCompositeSettings && !path.isEmpty()) { - String last = path.remove(path.size() - 1); - current = ((PerspectiveCompositeSettings) current).findSettingsByComponentId(last); - } - if (!path.isEmpty() || current == null) { - throw new IllegalStateException("Settings path is not finished, but no settings at current level"); - } - return (CS) current; - } - - @SuppressWarnings("unchecked") - public static CS patchSettingsTree(List path, Settings newSettings, CS settingsTree) { - - List> perspectiveSettingsPath = new ArrayList<>(); - - if(path.isEmpty() && !(settingsTree instanceof PerspectiveCompositeSettings)) { - //top level settings belong to a component, not perspective - return (CS) newSettings; - } - - if (!(settingsTree instanceof PerspectiveCompositeSettings)) { - throw new IllegalStateException("Settings tree does not belong to a perspective, but due to a not empty path it has to"); - } - - Settings current = settingsTree; - PerspectiveCompositeSettings perspectiveSettings = (PerspectiveCompositeSettings) settingsTree; - - String childComponentId = null; - while (current instanceof PerspectiveCompositeSettings && !path.isEmpty()) { - childComponentId = path.remove(path.size() - 1); - perspectiveSettings = (PerspectiveCompositeSettings) current; - perspectiveSettingsPath.add(perspectiveSettings); - current = perspectiveSettings.findSettingsByComponentId(childComponentId); - } - if (!path.isEmpty() || current == null) { - throw new IllegalStateException("Settings path is not finished, but no settings at current level"); - } - - if(current instanceof PerspectiveCompositeSettings) { - Settings perspectiveOwnSettings = ((PerspectiveCompositeSettings) current).getPerspectiveOwnSettings(); - newSettings = new PerspectiveCompositeSettings(perspectiveOwnSettings, perspectiveSettings.getSettingsPerComponentId()); - } - - while(!perspectiveSettingsPath.isEmpty()) { - PerspectiveCompositeSettings parent = perspectiveSettingsPath.remove(perspectiveSettingsPath.size() - 1); - Map originalSettingsPerComponent = parent.getSettingsPerComponentId(); - Map newSettingsPerComponent = new HashMap<>(); - for (Entry entry : originalSettingsPerComponent.entrySet()) { - String componentId = entry.getKey(); - if(childComponentId.equals(componentId)) { - newSettingsPerComponent.put(childComponentId, newSettings); - } else { - newSettingsPerComponent.put(componentId, entry.getValue()); - } - } - - newSettings = new PerspectiveCompositeSettings(parent.getPerspectiveOwnSettings(), newSettingsPerComponent); - } - - return (CS) newSettings; - } - -} +package com.sap.sse.gwt.client.shared.settings; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import com.sap.sse.common.settings.Settings; +import com.sap.sse.gwt.client.shared.components.ComponentLifecycle; +import com.sap.sse.gwt.client.shared.perspective.PerspectiveCompositeSettings; +import com.sap.sse.gwt.client.shared.perspective.PerspectiveLifecycle; + +public class ComponentUtils { + + /** + * Determines the lifecycle from the provided current lifecycle tree which corresponds to the provided path. + * + * @param path The path of the component which lifecycle should be determined + * @param current The root lifecycle from that the desired lifecycle is reachable by the provided path + * @return The lifecycle which was determined for the provided path starting from the provided root lifecycle + * @throws IllegalStateException When the path cannot be resolved from the provided root lifecycle + */ + @SuppressWarnings("unchecked") + public static ComponentLifecycle determineLifecycle(List path, ComponentLifecycle current) { + while (current instanceof PerspectiveLifecycle && !path.isEmpty()) { + String last = path.remove(path.size() - 1); + current = ((PerspectiveLifecycle) current).getLifecycleForId(last); + } + if (!path.isEmpty() || current == null) { + throw new IllegalStateException("Settings path is not finished, but no perspective at current level"); + } + return (ComponentLifecycle) current; + } + + @SuppressWarnings("unchecked") + public static CS determineComponentSettingsFromPerspectiveSettings(List path, Settings current) { + while (current instanceof PerspectiveCompositeSettings && !path.isEmpty()) { + String last = path.remove(path.size() - 1); + current = ((PerspectiveCompositeSettings) current).findSettingsByComponentId(last); + } + if (!path.isEmpty() || current == null) { + throw new IllegalStateException("Settings path is not finished, but no settings at current level"); + } + return (CS) current; + } + + @SuppressWarnings("unchecked") + public static CS patchSettingsTree(List path, Settings newSettings, CS settingsTree) { + + List> perspectiveSettingsPath = new ArrayList<>(); + + if(path.isEmpty() && !(settingsTree instanceof PerspectiveCompositeSettings)) { + //top level settings belong to a component, not perspective + return (CS) newSettings; + } + + if (!(settingsTree instanceof PerspectiveCompositeSettings)) { + throw new IllegalStateException("Settings tree does not belong to a perspective, but due to a not empty path it has to"); + } + + Settings current = settingsTree; + PerspectiveCompositeSettings perspectiveSettings = (PerspectiveCompositeSettings) settingsTree; + + String childComponentId = null; + while (current instanceof PerspectiveCompositeSettings && !path.isEmpty()) { + childComponentId = path.remove(path.size() - 1); + perspectiveSettings = (PerspectiveCompositeSettings) current; + perspectiveSettingsPath.add(perspectiveSettings); + current = perspectiveSettings.findSettingsByComponentId(childComponentId); + } + if (!path.isEmpty() || current == null) { + throw new IllegalStateException("Settings path is not finished, but no settings at current level"); + } + + if(current instanceof PerspectiveCompositeSettings) { + Settings perspectiveOwnSettings = ((PerspectiveCompositeSettings) current).getPerspectiveOwnSettings(); + newSettings = new PerspectiveCompositeSettings(perspectiveOwnSettings, perspectiveSettings.getSettingsPerComponentId()); + } + + while(!perspectiveSettingsPath.isEmpty()) { + PerspectiveCompositeSettings parent = perspectiveSettingsPath.remove(perspectiveSettingsPath.size() - 1); + Map originalSettingsPerComponent = parent.getSettingsPerComponentId(); + Map newSettingsPerComponent = new HashMap<>(); + for (Entry entry : originalSettingsPerComponent.entrySet()) { + String componentId = entry.getKey(); + if(childComponentId.equals(componentId)) { + newSettingsPerComponent.put(childComponentId, newSettings); + } else { + newSettingsPerComponent.put(componentId, entry.getValue()); + } + } + + newSettings = new PerspectiveCompositeSettings(parent.getPerspectiveOwnSettings(), newSettingsPerComponent); + } + + return (CS) newSettings; + } + +} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/DefaultOnSettingsLoadedCallback.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/DefaultOnSettingsLoadedCallback.java similarity index 83% rename from java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/DefaultOnSettingsLoadedCallback.java rename to java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/DefaultOnSettingsLoadedCallback.java index de650e2f916..d3cab4de3c0 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/DefaultOnSettingsLoadedCallback.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/DefaultOnSettingsLoadedCallback.java @@ -1,11 +1,11 @@ -package com.sap.sse.gwt.client.shared.perspective; - -import com.sap.sse.common.settings.Settings; - -public abstract class DefaultOnSettingsLoadedCallback implements OnSettingsLoadedCallback { - @Override - public void onError(Throwable caught, S fallbackDefaultSettings) { - onSuccess(fallbackDefaultSettings); - } - +package com.sap.sse.gwt.client.shared.settings; + +import com.sap.sse.common.settings.Settings; + +public abstract class DefaultOnSettingsLoadedCallback implements OnSettingsLoadedCallback { + @Override + public void onError(Throwable caught, S fallbackDefaultSettings) { + onSuccess(fallbackDefaultSettings); + } + } \ No newline at end of file diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/DummyOnSettingsStoredCallback.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/DummyOnSettingsStoredCallback.java similarity index 76% rename from java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/DummyOnSettingsStoredCallback.java rename to java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/DummyOnSettingsStoredCallback.java index 38815ed75bc..ca59467faa5 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/DummyOnSettingsStoredCallback.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/DummyOnSettingsStoredCallback.java @@ -1,13 +1,13 @@ -package com.sap.sse.gwt.client.shared.perspective; - -public class DummyOnSettingsStoredCallback implements OnSettingsStoredCallback { - - @Override - public void onError(Throwable caught) { - } - - @Override - public void onSuccess() { - } - -} +package com.sap.sse.gwt.client.shared.settings; + +public class DummyOnSettingsStoredCallback implements OnSettingsStoredCallback { + + @Override + public void onError(Throwable caught) { + } + + @Override + public void onSuccess() { + } + +} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/OnSettingsLoadedCallback.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/OnSettingsLoadedCallback.java similarity index 77% rename from java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/OnSettingsLoadedCallback.java rename to java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/OnSettingsLoadedCallback.java index 663e8eeeda8..943d15fba19 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/OnSettingsLoadedCallback.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/OnSettingsLoadedCallback.java @@ -1,8 +1,8 @@ -package com.sap.sse.gwt.client.shared.perspective; - -import com.sap.sse.common.settings.Settings; - -public interface OnSettingsLoadedCallback { - void onError(Throwable caught, S fallbackDefaultSettings); - void onSuccess(S settings); -} +package com.sap.sse.gwt.client.shared.settings; + +import com.sap.sse.common.settings.Settings; + +public interface OnSettingsLoadedCallback { + void onError(Throwable caught, S fallbackDefaultSettings); + void onSuccess(S settings); +} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/OnSettingsStoredCallback.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/OnSettingsStoredCallback.java similarity index 64% rename from java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/OnSettingsStoredCallback.java rename to java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/OnSettingsStoredCallback.java index a22dc29ae99..e8284151a87 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/OnSettingsStoredCallback.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/OnSettingsStoredCallback.java @@ -1,6 +1,6 @@ -package com.sap.sse.gwt.client.shared.perspective; - -public interface OnSettingsStoredCallback { - void onError(Throwable caught); - void onSuccess(); -} +package com.sap.sse.gwt.client.shared.settings; + +public interface OnSettingsStoredCallback { + void onError(Throwable caught); + void onSuccess(); +} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/PipelineLevel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/PipelineLevel.java similarity index 89% rename from java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/PipelineLevel.java rename to java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/PipelineLevel.java index 3d0eab16f74..693062e5e60 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/PipelineLevel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/PipelineLevel.java @@ -1,21 +1,21 @@ -package com.sap.sse.gwt.client.shared.perspective; - -import java.util.LinkedList; -import java.util.List; - -public enum PipelineLevel { - SYSTEM_DEFAULTS, GLOBAL_DEFAULTS, CONTEXT_SPECIFIC_DEFAULTS; - - public List getSortedLevelsUntilCurrent() { - List levels = new LinkedList<>(); - switch(this) { - case CONTEXT_SPECIFIC_DEFAULTS: - levels.add(0, CONTEXT_SPECIFIC_DEFAULTS); - case GLOBAL_DEFAULTS: - levels.add(0, GLOBAL_DEFAULTS); - case SYSTEM_DEFAULTS: - levels.add(0, SYSTEM_DEFAULTS); - } - return levels; - } +package com.sap.sse.gwt.client.shared.settings; + +import java.util.LinkedList; +import java.util.List; + +public enum PipelineLevel { + SYSTEM_DEFAULTS, GLOBAL_DEFAULTS, CONTEXT_SPECIFIC_DEFAULTS; + + public List getSortedLevelsUntilCurrent() { + List levels = new LinkedList<>(); + switch(this) { + case CONTEXT_SPECIFIC_DEFAULTS: + levels.add(0, CONTEXT_SPECIFIC_DEFAULTS); + case GLOBAL_DEFAULTS: + levels.add(0, GLOBAL_DEFAULTS); + case SYSTEM_DEFAULTS: + levels.add(0, SYSTEM_DEFAULTS); + } + return levels; + } } \ No newline at end of file diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/SettingsBuildingPipeline.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/SettingsBuildingPipeline.java new file mode 100644 index 00000000000..26d8ac5c969 --- /dev/null +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/SettingsBuildingPipeline.java @@ -0,0 +1,75 @@ +package com.sap.sse.gwt.client.shared.settings; + +import java.util.List; + +import com.google.gwt.json.client.JSONValue; +import com.sap.sse.common.settings.Settings; + +/** + * Defines the settings construction process. The construction of the settings may be influenced + * by various conditions, for example URL, UI, additional settings scopes and etc. It is up to the + * implementation of this interface to define the desired way of how the settings are constructed from its persisted + * JSON representation, and how the settings are transformed back into its persisted JSON representation. + * The pipeline is being used in {@link SettingsStorageManager} when it loads and stores settings. + * It is highly recommended to use at least {@link AbstractSettingsBuildingPipeline} as basic class for + * implementation of this interface. + * + * @author Vladislav Chumak + * + * @see SettingsStorageManager + * @see AbstractSettingsBuildingPipeline + * + */ +public interface SettingsBuildingPipeline { + + /** + * Constructs a settings object by means of provided defaultSettings by considering + * User Settings and Document Settings. + * This method implements the settings construction pipeline for a settings object which is used for settings loading operations. + * + * @param defaultSettings The basic settings to be used + * @param settingsJsons The persisted representation of User Settings and Document Settings + * @return The constructed settings object + */ + S getSettingsObject(S defaultSettings, SettingsJsons settingsJsons); + + /** + * Constructs a settings object by means of provided defaultSettings by considering + * User Settings and Document Settings. + * This method implements the settings construction pipeline for a settings object which is used for settings loading operations. + * + * @param defaultSettings The basic settings to be used + * @param settingsJsons The persisted representation of User Settings and Document Settings + * @return The constructed settings object + */ + S getSettingsObject(S defaultSettings, SettingsStrings settingsStrings); + + /** + * Constructs a settings object by means of provided defaultSettings without considering + * User Settings and Document Settings. + * This method implements the settings construction pipeline for a settings object which is used for settings loading operations. + * + * @param defaultSettings The basic settings to be used + * @return The constructed settings object + */ + S getSettingsObject(S defaultSettings); + + /** + * Converts the provided settings according the storage scope and path of the provided settings in the settings tree. + * This method implements the JSON building pipeline which is used for settings storing operations. + * + * @param settings The settings to convert to JSON representation + * @param pipelineLevel The pipeline level which indicates the storage scope, e.g. User Settings or Document Settings. + * @param path The path of the settings in the settings tree + * @return The JSON representation of the provided settings + */ + JSONValue getJsonObject(Settings settings, PipelineLevel pipelineLevel, List path); + + /** + * Gets the {@link SettingsStringConverter} which is used by this instance for conversion between settings objects and JSON. + * + * @return The conversion helper instance used by this pipeline + */ + SettingsStringConverter getSettingsStringConverter(); + +} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/SettingsJsons.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/SettingsJsons.java similarity index 75% rename from java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/SettingsJsons.java rename to java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/SettingsJsons.java index b5f5d7cbe07..32e33d28037 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/SettingsJsons.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/SettingsJsons.java @@ -1,23 +1,30 @@ -package com.sap.sse.gwt.client.shared.perspective; - -import com.google.gwt.json.client.JSONObject; - -public class SettingsJsons { - - private final JSONObject globalSettingsJson; - private final JSONObject contextSpecificSettingsJson; - - public SettingsJsons(JSONObject globalSettingsJson, JSONObject contextSpecificSettingsJson) { - this.globalSettingsJson = globalSettingsJson; - this.contextSpecificSettingsJson = contextSpecificSettingsJson; - } - - public JSONObject getGlobalSettingsJson() { - return globalSettingsJson; - } - - public JSONObject getContextSpecificSettingsJson() { - return contextSpecificSettingsJson; - } - -} +package com.sap.sse.gwt.client.shared.settings; + +import com.google.gwt.json.client.JSONObject; + +/** + * Simple class which wraps the JSON representations User Settings and Document Settings + * as two {@link JSONObject}s. + * + * @author Vladislav Chumak + * + */ +public class SettingsJsons { + + private final JSONObject globalSettingsJson; + private final JSONObject contextSpecificSettingsJson; + + public SettingsJsons(JSONObject globalSettingsJson, JSONObject contextSpecificSettingsJson) { + this.globalSettingsJson = globalSettingsJson; + this.contextSpecificSettingsJson = contextSpecificSettingsJson; + } + + public JSONObject getGlobalSettingsJson() { + return globalSettingsJson; + } + + public JSONObject getContextSpecificSettingsJson() { + return contextSpecificSettingsJson; + } + +} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/SettingsStorageManager.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/SettingsStorageManager.java similarity index 54% rename from java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/SettingsStorageManager.java rename to java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/SettingsStorageManager.java index 04dff1a641b..4fcb456fa44 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/SettingsStorageManager.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/SettingsStorageManager.java @@ -1,60 +1,85 @@ -package com.sap.sse.gwt.client.shared.perspective; - -import java.util.List; - -import com.google.gwt.json.client.JSONObject; -import com.google.gwt.json.client.JSONValue; -import com.google.gwt.user.client.rpc.AsyncCallback; -import com.sap.sse.common.settings.Settings; -import com.sap.sse.common.settings.generic.GenericSerializableSettings; - -/** - * Manages the persistence of settings. The interface was designed to support {@link ComponentContextWithSettingsStorage}. - * The {@link SettingsStorageManager} stores values of {@link GenericSerializableSettings} which do not match the default values, - * in other words the delta between current settings values and the default settings values. - * The implementation may use different kind of storages, e.g. LocalStorage, and/or {@link UserService}, and etc. - * - * @author Vladislav Chumak - * - * @param The {@link Settings} type of the settings of the root component/perspective containing all the settings for itself and its subcomponents - * @see ComponentContextWithSettingsStorage - */ -public interface SettingsStorageManager { - - boolean supportsStore(); - - /** - * Retrieves the {@link Settings} of the root component using the provided callback. - * Depending on the storage media, the callback may be called whether synchronously - * or anynchronously. - * - * @param defaultSettings The system default settings for the root component - * @param asyncCallback The callback to be called when the settings have been retrieved - * from the settings storage - */ - void retrieveDefaultSettings(S defaultSettings, - OnSettingsLoadedCallback asyncCallback); - - /** - * Stores provided settings. Only the delta between default settings and provided settings - * will be stored. - * - * @param settingsJsons The settings to store - * @param onSettingsStoredCallback The callback which is called when the settings storage process finishes - * @see ComponentContextWithSettingsStorage - * @see GenericSerializableSettings - */ - void storeSettingsJsons(SettingsJsons settingsJsons, OnSettingsStoredCallback onSettingsStoredCallback); - - void storeGlobalSettingsJson(JSONObject globalSettingsJson, OnSettingsStoredCallback onSettingsStoredCallback); - - void storeContextSpecificSettingsJson(JSONObject contextSpecificSettingsJson, - OnSettingsStoredCallback onSettingsStoredCallback); - - JSONValue settingsToJSON(Settings newSettings, PipelineLevel pipelineLevel, List path); - - void retrieveSettingsJsons(AsyncCallback asyncCallback); - - void dispose(); - -} +package com.sap.sse.gwt.client.shared.settings; + +import java.util.List; + +import com.google.gwt.json.client.JSONObject; +import com.google.gwt.json.client.JSONValue; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.sap.sse.common.settings.Settings; +import com.sap.sse.common.settings.generic.GenericSerializableSettings; + +/** + * Manages the persistence of settings. The interface was designed to support {@link ComponentContextWithSettingsStorage}. + * The {@link SettingsStorageManager} stores values of {@link GenericSerializableSettings} which do not match the default values, + * in other words the delta between current settings values and the default settings values. + * The implementation may use different kind of storages, e.g. LocalStorage, and/or {@link UserService}, and etc. + * + * @author Vladislav Chumak + * + * @param The {@link Settings} type of the settings of the root component/perspective containing all the settings for itself and its subcomponents + * @see ComponentContextWithSettingsStorage + * @see GenericSerializableSettings + */ +public interface SettingsStorageManager { + + /** + * Tells whether the underlying implementation offers persistence for settings, or not. + * + * @return {@code True} if settings storage is supported, otherwise {@code false} + */ + boolean supportsStore(); + + /** + * Retrieves the {@link Settings} of the root component using the provided callback. + * Depending on the storage media, the callback may be called whether synchronously + * or asynchronously. + * + * @param defaultSettings The system default settings for the root component + * @param asyncCallback The callback to be called when the settings have been retrieved + * from the settings storage + */ + void retrieveDefaultSettings(S defaultSettings, + OnSettingsLoadedCallback asyncCallback); + + /** + * Stores provided settings as User Settings (old term "Global Settings"). Only the delta between default settings and provided settings + * will be stored. + * + * @param globalSettingsJson The settings to store + * @param onSettingsStoredCallback The callback which is called when the settings storage process gets finished + */ + void storeGlobalSettingsJson(JSONObject globalSettingsJson, OnSettingsStoredCallback onSettingsStoredCallback); + + /** + * Stores provided settings as Document Settings (old term "Context specific Settings"). Only the delta between default settings and provided settings + * will be stored. + * + * @param contextSpecificSettingsJson The settings to store + * @param onSettingsStoredCallback The callback which is called when the settings storage process gets finished + */ + void storeContextSpecificSettingsJson(JSONObject contextSpecificSettingsJson, + OnSettingsStoredCallback onSettingsStoredCallback); + + /** + * Converts the provided settings object to JSON. + * + * @param newSettings The settings to convert + * @param pipelineLevel The pipeline level indicating whether the provided settings should be converted for Document Settings storage, or User Settings storage + * @param path The path of the corresponding settings in the settings tree according to the component which the provided settings belong to + * @return The converted settings as JSON + */ + JSONValue settingsToJSON(Settings newSettings, PipelineLevel pipelineLevel, List path); + + /** + * Retrieves the stored JSON representations of Document Settings and User Settings which get packed together in {@link SettingsJsons}. + * + * @param asyncCallback The callback which gets called when the persisted settings representation has been retrieved + */ + void retrieveSettingsJsons(AsyncCallback asyncCallback); + + /** + * Releases all resources and listener registrations acquired by this instance. + */ + void dispose(); + +} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/SettingsStringConverter.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/SettingsStringConverter.java similarity index 96% rename from java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/SettingsStringConverter.java rename to java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/SettingsStringConverter.java index 940416c08de..dac4e70460d 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/SettingsStringConverter.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/SettingsStringConverter.java @@ -1,66 +1,66 @@ -package com.sap.sse.gwt.client.shared.perspective; - -import com.google.gwt.json.client.JSONObject; -import com.google.gwt.json.client.JSONValue; -import com.sap.sse.common.settings.Settings; -import com.sap.sse.common.settings.generic.GenericSerializableSettings; -import com.sap.sse.common.settings.generic.SettingsMap; -import com.sap.sse.gwt.settings.SettingsToJsonSerializerGWT; -import com.sap.sse.gwt.settings.SettingsToUrlSerializer; - -public class SettingsStringConverter { - - private final SettingsToUrlSerializer urlSerializer = new SettingsToUrlSerializer(); - private final SettingsToJsonSerializerGWT jsonSerializer = new SettingsToJsonSerializerGWT(); - - @SuppressWarnings("unchecked") - public S deserializeFromCurrentUrl(S defaultSettings) { - if (defaultSettings instanceof GenericSerializableSettings) { - defaultSettings = (S) urlSerializer - .deserializeFromCurrentLocation((GenericSerializableSettings) defaultSettings); - } else if (defaultSettings instanceof SettingsMap) { - defaultSettings = (S) urlSerializer - .deserializeSettingsMapFromCurrentLocation((SettingsMap) defaultSettings); - } - return defaultSettings; - } - - @SuppressWarnings("unchecked") - public S deserializeFromJson(S defaultSettings, JSONObject jsonToDeserialize) { - if (defaultSettings instanceof GenericSerializableSettings) { - defaultSettings = (S) jsonSerializer.deserialize((GenericSerializableSettings) defaultSettings, - jsonToDeserialize); - } else if (defaultSettings instanceof SettingsMap) { - defaultSettings = (S) jsonSerializer.deserialize((SettingsMap) defaultSettings, jsonToDeserialize); - } - return defaultSettings; - } - - public SettingsJsons convertToSettingsJson(SettingsStrings settingsStrings) { - JSONObject globalSettingsJson = convertStringToJson(settingsStrings.getGlobalSettingsString()); - JSONObject contextSpecificSettingsJson = convertStringToJson(settingsStrings.getContextSpecificSettingsString()); - return new SettingsJsons(globalSettingsJson, contextSpecificSettingsJson); - } - - public JSONObject convertStringToJson(String str) { - return str == null ? null : jsonSerializer.parseStringToJsonObject(str); - } - - public String convertJsonToString(JSONObject json) { - return json == null ? null : jsonSerializer.jsonObjectToString(json); - } - - public SettingsStrings convertToSettingsStrings(SettingsJsons settingsJsons) { - String globalSettingsString = convertJsonToString(settingsJsons.getGlobalSettingsJson()); - String contextSpecificSettingsString = convertJsonToString(settingsJsons.getContextSpecificSettingsJson()); - return new SettingsStrings(globalSettingsString, contextSpecificSettingsString); - } - - public JSONValue serializeFromSettingsObject(Settings newSettings) { - if (newSettings instanceof GenericSerializableSettings) { - return jsonSerializer.serialize((GenericSerializableSettings) newSettings); - } - throw new IllegalStateException("Requested save of settings that is not Serializable!"); - } - -} +package com.sap.sse.gwt.client.shared.settings; + +import com.google.gwt.json.client.JSONObject; +import com.google.gwt.json.client.JSONValue; +import com.sap.sse.common.settings.Settings; +import com.sap.sse.common.settings.generic.GenericSerializableSettings; +import com.sap.sse.common.settings.generic.SettingsMap; +import com.sap.sse.gwt.settings.SettingsToJsonSerializerGWT; +import com.sap.sse.gwt.settings.SettingsToUrlSerializer; + +public class SettingsStringConverter { + + private final SettingsToUrlSerializer urlSerializer = new SettingsToUrlSerializer(); + private final SettingsToJsonSerializerGWT jsonSerializer = new SettingsToJsonSerializerGWT(); + + @SuppressWarnings("unchecked") + public S deserializeFromCurrentUrl(S defaultSettings) { + if (defaultSettings instanceof GenericSerializableSettings) { + defaultSettings = (S) urlSerializer + .deserializeFromCurrentLocation((GenericSerializableSettings) defaultSettings); + } else if (defaultSettings instanceof SettingsMap) { + defaultSettings = (S) urlSerializer + .deserializeSettingsMapFromCurrentLocation((SettingsMap) defaultSettings); + } + return defaultSettings; + } + + @SuppressWarnings("unchecked") + public S deserializeFromJson(S defaultSettings, JSONObject jsonToDeserialize) { + if (defaultSettings instanceof GenericSerializableSettings) { + defaultSettings = (S) jsonSerializer.deserialize((GenericSerializableSettings) defaultSettings, + jsonToDeserialize); + } else if (defaultSettings instanceof SettingsMap) { + defaultSettings = (S) jsonSerializer.deserialize((SettingsMap) defaultSettings, jsonToDeserialize); + } + return defaultSettings; + } + + public SettingsJsons convertToSettingsJson(SettingsStrings settingsStrings) { + JSONObject globalSettingsJson = convertStringToJson(settingsStrings.getGlobalSettingsString()); + JSONObject contextSpecificSettingsJson = convertStringToJson(settingsStrings.getContextSpecificSettingsString()); + return new SettingsJsons(globalSettingsJson, contextSpecificSettingsJson); + } + + public JSONObject convertStringToJson(String str) { + return str == null ? null : jsonSerializer.parseStringToJsonObject(str); + } + + public String convertJsonToString(JSONObject json) { + return json == null ? null : jsonSerializer.jsonObjectToString(json); + } + + public SettingsStrings convertToSettingsStrings(SettingsJsons settingsJsons) { + String globalSettingsString = convertJsonToString(settingsJsons.getGlobalSettingsJson()); + String contextSpecificSettingsString = convertJsonToString(settingsJsons.getContextSpecificSettingsJson()); + return new SettingsStrings(globalSettingsString, contextSpecificSettingsString); + } + + public JSONValue serializeFromSettingsObject(Settings newSettings) { + if (newSettings instanceof GenericSerializableSettings) { + return jsonSerializer.serialize((GenericSerializableSettings) newSettings); + } + throw new IllegalStateException("Requested save of settings that is not Serializable!"); + } + +} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/SettingsStrings.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/SettingsStrings.java similarity index 73% rename from java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/SettingsStrings.java rename to java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/SettingsStrings.java index a0c2e6fbfde..a6fbdc4c563 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/SettingsStrings.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/SettingsStrings.java @@ -1,21 +1,28 @@ -package com.sap.sse.gwt.client.shared.perspective; - -public class SettingsStrings { - - private final String globalSettingsString; - private final String contextSpecificSettingsString; - - public SettingsStrings(String globalSettingsString, String contextSpecificSettingsString) { - this.globalSettingsString = globalSettingsString; - this.contextSpecificSettingsString = contextSpecificSettingsString; - } - - public String getGlobalSettingsString() { - return globalSettingsString; - } - - public String getContextSpecificSettingsString() { - return contextSpecificSettingsString; - } - -} +package com.sap.sse.gwt.client.shared.settings; + +/** + * Simple class which wraps the JSON representations User Settings and Document Settings + * as two {@link String}s. + * + * @author Vladislav Chumak + * + */ +public class SettingsStrings { + + private final String globalSettingsString; + private final String contextSpecificSettingsString; + + public SettingsStrings(String globalSettingsString, String contextSpecificSettingsString) { + this.globalSettingsString = globalSettingsString; + this.contextSpecificSettingsString = contextSpecificSettingsString; + } + + public String getGlobalSettingsString() { + return globalSettingsString; + } + + public String getContextSpecificSettingsString() { + return contextSpecificSettingsString; + } + +} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/SimpleComponentContext.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/SimpleComponentContext.java similarity index 79% rename from java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/SimpleComponentContext.java rename to java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/SimpleComponentContext.java index 047a222bc5c..940a08ce23a 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/perspective/SimpleComponentContext.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/settings/SimpleComponentContext.java @@ -1,96 +1,114 @@ -package com.sap.sse.gwt.client.shared.perspective; - -import java.util.ArrayList; - -import com.sap.sse.common.settings.Settings; -import com.sap.sse.gwt.client.shared.components.Component; -import com.sap.sse.gwt.client.shared.components.ComponentLifecycle; - -/** - * Manages all default settings of perspectives and components. This simple implementation has no support for settings - * storage. It is only capable of creating new default settings by means of {@link ComponentLifecycle} of the root - * component managed by this {@link ComponentContext}. All in all, it is a dummy implementation for components which do - * not have support for settings storage. If you need settings to be stored, consider - * {@link ComponentContextWithSettingsStorage}. - * - * @author Vladislav Chumak - * - * @param - * The {@link Settings} type of the settings of the root component/perspective containing all the settings - * for itself and its subcomponents - */ -public class SimpleComponentContext implements ComponentContext { - - protected final ComponentLifecycle rootLifecycle; - - /** - * - * @param rootLifecycle - * The {@link ComponentLifecycle} of the root component/perspective - */ - public SimpleComponentContext(ComponentLifecycle rootLifecycle) { - this.rootLifecycle = rootLifecycle; - } - - /** - * This operation is unsupported for this simple implementation and will throw a - * {@link UnsupportedOperationException} when it is called. - */ - @Override - public void makeSettingsDefault(Component component, CS newDefaultSettings, final OnSettingsStoredCallback onSettingsStoredCallback) { - throw new UnsupportedOperationException("Make Default action is unsupported for this type of ComponentContext " - + this.getClass().getName() + " " + component.getPath() + " " + newDefaultSettings); - } - - @Override - public void storeSettingsForContext(Component component, CS newSettings, OnSettingsStoredCallback onSettingsStoredCallback) { - throw new UnsupportedOperationException("Settings storage is unsupported for this type of ComponentContext " - + this.getClass().getName() + " " + component.getPath() + " " + newSettings); - } - - /** - * {@inheritDoc} - */ - @Override - public ComponentLifecycle getRootLifecycle() { - return rootLifecycle; - } - - /** - * Gets the current default {@link Settings} of the root component managed - * by this {@link ComponentContext}. The returned {@link Settings} should - * contain all settings for the root component and its subcomponents. - * - * @return The {@link Settings} of the root component - */ - public S getDefaultSettings() { - return rootLifecycle.createDefaultSettings(); - } - - /** - * This method returns always {@code false}, because it does not offer functionality for settings storage. - */ - @Override - public boolean hasMakeCustomDefaultSettingsSupport(Component component) { - return false; - } - - /** - * {@inheritDoc} - */ - @Override - public void getInitialSettings(final OnSettingsLoadedCallback onInitialSettingsLoaded) { - onInitialSettingsLoaded.onSuccess(getDefaultSettings()); - } - - @Override - public void dispose() { - } - - @Override - public void getInitialSettingsForComponent(final Component component, final OnSettingsLoadedCallback callback) { - ComponentLifecycle componentLifecycle = ComponentUtils.determineLifecycle(new ArrayList<>(component.getPath()), rootLifecycle); - callback.onSuccess(componentLifecycle.createDefaultSettings()); - } - -} +package com.sap.sse.gwt.client.shared.settings; + +import java.util.ArrayList; + +import com.sap.sse.common.settings.Settings; +import com.sap.sse.gwt.client.shared.components.Component; +import com.sap.sse.gwt.client.shared.components.ComponentLifecycle; + +/** + * Manages all default settings of perspectives and components. This simple implementation has no support for settings + * storage. It is only capable of creating new default settings by means of {@link ComponentLifecycle#createDefaultSettings()} of the root + * component managed by this {@link ComponentContext}. All in all, it is a dummy implementation for components which does + * not have support for settings storage. If you need settings to be stored, consider + * {@link ComponentContextWithSettingsStorage}. + * + * @author Vladislav Chumak + * + * @param + * The {@link Settings} type of the settings of the root component/perspective containing all the settings + * for itself and its subcomponents + */ +public class SimpleComponentContext implements ComponentContext { + + /** + * The {@link ComponentLifecycle} of the root component/perspective + */ + protected final ComponentLifecycle rootLifecycle; + + /** + * + * @param rootLifecycle + * The {@link ComponentLifecycle} of the root component/perspective + */ + public SimpleComponentContext(ComponentLifecycle rootLifecycle) { + this.rootLifecycle = rootLifecycle; + } + + /** + * This operation is unsupported for this simple implementation and will throw a + * {@link UnsupportedOperationException} when it is called. + */ + @Override + public void makeSettingsDefault(Component component, CS newDefaultSettings, final OnSettingsStoredCallback onSettingsStoredCallback) { + throw new UnsupportedOperationException("Make Default action is unsupported for this type of ComponentContext " + + this.getClass().getName() + " " + component.getPath() + " " + newDefaultSettings); + } + + /** + * This operation is unsupported for this simple implementation and will throw a + * {@link UnsupportedOperationException} when it is called. + */ + @Override + public void storeSettingsForContext(Component component, CS newSettings, OnSettingsStoredCallback onSettingsStoredCallback) { + throw new UnsupportedOperationException("Settings storage is unsupported for this type of ComponentContext " + + this.getClass().getName() + " " + component.getPath() + " " + newSettings); + } + + /** + * {@inheritDoc} + */ + @Override + public ComponentLifecycle getRootLifecycle() { + return rootLifecycle; + } + + /** + * Gets the "System Default" {@link Settings} of the root component managed + * by this {@link ComponentContext}. The returned {@link Settings} should + * contain all settings for the root component and its subcomponents. + * + * @return The {@link Settings} of the root component + */ + public S getDefaultSettings() { + return rootLifecycle.createDefaultSettings(); + } + + /** + * This method returns always {@code false}, because it does not offer functionality for settings storage. + */ + @Override + public boolean hasMakeCustomDefaultSettingsSupport(Component component) { + return false; + } + + /** + * Retrieves the "System Default" {@link Settings} of the root component managed + * by this {@link ComponentContext} and passes them to the provided callback. + * + * @see #getDefaultSettings() + */ + @Override + public void getInitialSettings(final OnSettingsLoadedCallback onInitialSettingsLoaded) { + onInitialSettingsLoaded.onSuccess(getDefaultSettings()); + } + + /** + * {@inheritDoc} + */ + @Override + public void dispose() { + } + + /** + * Retrieves the "System Default" {@link Settings} of the provided component and passes them to the provided callback. + * + * @see #getDefaultSettings() + */ + @Override + public void getInitialSettingsForComponent(final Component component, final OnSettingsLoadedCallback callback) { + ComponentLifecycle componentLifecycle = ComponentUtils.determineLifecycle(new ArrayList<>(component.getPath()), rootLifecycle); + callback.onSuccess(componentLifecycle.createDefaultSettings()); + } + +} diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/settings/ComponentContextWithSettingsStorageAndPatching.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/settings/ComponentContextWithSettingsStorageAndPatching.java index bdd224c2f88..d697ab2ba2c 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/settings/ComponentContextWithSettingsStorageAndPatching.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/settings/ComponentContextWithSettingsStorageAndPatching.java @@ -1,26 +1,67 @@ -package com.sap.sse.security.ui.settings; - -import com.sap.sse.common.settings.Settings; -import com.sap.sse.gwt.client.shared.components.Component; -import com.sap.sse.gwt.client.shared.components.ComponentLifecycle; -import com.sap.sse.gwt.client.shared.perspective.ComponentContextWithSettingsStorage; -import com.sap.sse.gwt.client.shared.perspective.PipelineLevel; - -public class ComponentContextWithSettingsStorageAndPatching extends ComponentContextWithSettingsStorage { - - protected final UserSettingsStorageManagerWithPatching settingsStorageManager; - - public ComponentContextWithSettingsStorageAndPatching(ComponentLifecycle rootLifecycle, UserSettingsStorageManagerWithPatching settingsStorageManager) { - super(rootLifecycle, settingsStorageManager); - this.settingsStorageManager = (UserSettingsStorageManagerWithPatching) super.settingsStorageManager; - } - - public void addPatchForStoringSettings(Component component, PipelineLevel pipelineLevel, SettingsPatch settingsPatch) { - settingsStorageManager.addPatchForStoringSettings(component, pipelineLevel, settingsPatch); - } - - public void addPatchForLoadingSettings(Component component, PipelineLevel pipelineLevel, SettingsPatch settingsPatch) { - settingsStorageManager.addPatchForLoadingSettings(component, pipelineLevel, settingsPatch); - } - -} +package com.sap.sse.security.ui.settings; + +import com.sap.sse.common.settings.Settings; +import com.sap.sse.gwt.client.shared.components.Component; +import com.sap.sse.gwt.client.shared.components.ComponentLifecycle; +import com.sap.sse.gwt.client.shared.settings.ComponentContextWithSettingsStorage; +import com.sap.sse.gwt.client.shared.settings.PipelineLevel; + +/** + * Adds settings patching functionality to {@link ComponentContextWithSettingsStorage} implementation. + * This implementation provides additional methods for attaching of {@link SettingsPatch}. There are multiple hooks in + * the underlying {@link UserSettingsBuildingPipelineWithPatching} which can be used to influence the + * settings construction in its construction pipeline. After each {@link PipelineLevel} a custom {@link SettingsPatch} + * may be applied. The patch may partially or completely modify the resulting settings object in order to provide + * the desired behavior of default settings for a dynamic environment, e.g. RaceBoard which determines its default settings regarding + * to RaceModes, PlayModes and etc. + * + * + * @author Vladislav Chumak + * @see ComponentContextWithSettingsStorage + * + * @param + * The {@link Settings} type of the settings of the root component/perspective containing all the settings + * for itself and its subcomponents + * + */ +public class ComponentContextWithSettingsStorageAndPatching extends ComponentContextWithSettingsStorage { + + /** + * Manages the persistence layer of settings. + */ + protected final UserSettingsStorageManagerWithPatching settingsStorageManager; + + /** + * @param rootLifecycle + * The {@link ComponentLifecycle} of the root component/perspective + * @param settingsStorageManager + * The {@link UserSettingsStorageManagerWithPatching} to be used to access stored settings and to store new settings + */ + public ComponentContextWithSettingsStorageAndPatching(ComponentLifecycle rootLifecycle, UserSettingsStorageManagerWithPatching settingsStorageManager) { + super(rootLifecycle, settingsStorageManager); + this.settingsStorageManager = (UserSettingsStorageManagerWithPatching) super.settingsStorageManager; + } + + /** + * Adds a settings patch for transforming settings before storing them. + * + * @param component The component which the targeted settings for patching belong to + * @param pipelineLevel The pipeline level AFTER that the patch should be applied on settings + * @param settingsPatch The settings patch to apply on settings + */ + public void addPatchForStoringSettings(Component component, PipelineLevel pipelineLevel, SettingsPatch settingsPatch) { + settingsStorageManager.addPatchForStoringSettings(component, pipelineLevel, settingsPatch); + } + + /** + * Adds a settings patch for settings object construction. + * + * @param component The component which the targeted settings for patching belong to + * @param pipelineLevel The pipeline level AFTER that the patch should be applied on settings + * @param settingsPatch The settings patch to apply on settings + */ + public void addPatchForLoadingSettings(Component component, PipelineLevel pipelineLevel, SettingsPatch settingsPatch) { + settingsStorageManager.addPatchForLoadingSettings(component, pipelineLevel, settingsPatch); + } + +} diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/settings/PlaceBasedUserSettingsBuildingPipeline.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/settings/PlaceBasedUserSettingsBuildingPipeline.java index a4a76a3ee62..875eb7bbf89 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/settings/PlaceBasedUserSettingsBuildingPipeline.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/settings/PlaceBasedUserSettingsBuildingPipeline.java @@ -1,34 +1,34 @@ -package com.sap.sse.security.ui.settings; - -import com.sap.sse.common.settings.Settings; -import com.sap.sse.common.settings.generic.GenericSerializableSettings; -import com.sap.sse.common.settings.generic.SettingsMap; -import com.sap.sse.gwt.client.shared.perspective.SettingsStringConverter; -import com.sap.sse.gwt.settings.SettingsToStringSerializer; - -public class PlaceBasedUserSettingsBuildingPipeline extends UserSettingsBuildingPipeline { - - public PlaceBasedUserSettingsBuildingPipeline(final String serializedSettings) { - super(new SettingsStringConverter() { - - private final SettingsToStringSerializer settingsToStringSerializer = new SettingsToStringSerializer(); - - @SuppressWarnings("unchecked") - @Override - public S deserializeFromCurrentUrl(S defaultSettings) { - final S result; - if (defaultSettings instanceof SettingsMap) { - result = (S) settingsToStringSerializer.fromString(serializedSettings, (SettingsMap) defaultSettings); - } else if (defaultSettings instanceof GenericSerializableSettings) { - result = (S) settingsToStringSerializer.fromString(serializedSettings, - (GenericSerializableSettings) defaultSettings); - } else { - result = defaultSettings; - } - return result; - } - - }); - } - -} +package com.sap.sse.security.ui.settings; + +import com.sap.sse.common.settings.Settings; +import com.sap.sse.common.settings.generic.GenericSerializableSettings; +import com.sap.sse.common.settings.generic.SettingsMap; +import com.sap.sse.gwt.client.shared.settings.SettingsStringConverter; +import com.sap.sse.gwt.settings.SettingsToStringSerializer; + +public class PlaceBasedUserSettingsBuildingPipeline extends UserSettingsBuildingPipeline { + + public PlaceBasedUserSettingsBuildingPipeline(final String serializedSettings) { + super(new SettingsStringConverter() { + + private final SettingsToStringSerializer settingsToStringSerializer = new SettingsToStringSerializer(); + + @SuppressWarnings("unchecked") + @Override + public S deserializeFromCurrentUrl(S defaultSettings) { + final S result; + if (defaultSettings instanceof SettingsMap) { + result = (S) settingsToStringSerializer.fromString(serializedSettings, (SettingsMap) defaultSettings); + } else if (defaultSettings instanceof GenericSerializableSettings) { + result = (S) settingsToStringSerializer.fromString(serializedSettings, + (GenericSerializableSettings) defaultSettings); + } else { + result = defaultSettings; + } + return result; + } + + }); + } + +} diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/settings/PlaceBasedUserSettingsStorageManager.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/settings/PlaceBasedUserSettingsStorageManager.java index e9c923d0171..d0676fb2364 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/settings/PlaceBasedUserSettingsStorageManager.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/settings/PlaceBasedUserSettingsStorageManager.java @@ -1,12 +1,21 @@ -package com.sap.sse.security.ui.settings; - -import com.sap.sse.common.settings.Settings; -import com.sap.sse.security.ui.client.UserService; - -public class PlaceBasedUserSettingsStorageManager extends UserSettingsStorageManager { - - public PlaceBasedUserSettingsStorageManager(UserService userService, StorageDefinitionId storageDefinitionId, String serializedSettings) { - super(userService, storageDefinitionId, new PlaceBasedUserSettingsBuildingPipeline(serializedSettings)); - } - -} +package com.sap.sse.security.ui.settings; + +import com.sap.sse.common.settings.Settings; +import com.sap.sse.security.ui.client.UserService; + +/** + * Specialization of {@link UserSettingsStorageManager} which uses {@link PlaceBasedUserSettingsBuildingPipeline} for settings construction. + * + * @author Vladislav Chumak + * + * @param + * The {@link Settings} type of the settings of the root component/perspective containing all the settings + * for itself and its subcomponents + */ +public class PlaceBasedUserSettingsStorageManager extends UserSettingsStorageManager { + + public PlaceBasedUserSettingsStorageManager(UserService userService, StorageDefinitionId storageDefinitionId, String serializedSettings) { + super(userService, storageDefinitionId, new PlaceBasedUserSettingsBuildingPipeline(serializedSettings)); + } + +} diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/settings/SimpleSettingsStorageManager.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/settings/SimpleSettingsStorageManager.java index b334b7d293e..3ee1e16208c 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/settings/SimpleSettingsStorageManager.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/settings/SimpleSettingsStorageManager.java @@ -1,73 +1,92 @@ -package com.sap.sse.security.ui.settings; - -import java.util.List; - -import com.google.gwt.json.client.JSONObject; -import com.google.gwt.json.client.JSONValue; -import com.google.gwt.user.client.rpc.AsyncCallback; -import com.sap.sse.common.settings.Settings; -import com.sap.sse.gwt.client.shared.perspective.OnSettingsLoadedCallback; -import com.sap.sse.gwt.client.shared.perspective.OnSettingsStoredCallback; -import com.sap.sse.gwt.client.shared.perspective.PipelineLevel; -import com.sap.sse.gwt.client.shared.perspective.SettingsBuildingPipeline; -import com.sap.sse.gwt.client.shared.perspective.SettingsJsons; -import com.sap.sse.gwt.client.shared.perspective.SettingsStorageManager; - -/** - * This {@link SettingsStorageManager} implementation only reads settings from the URL. - * - * @param - * The {@link Settings} type of the settings of the root component/perspective containing all the settings - * for itself and its subcomponents - * @see SettingsStorageManager - */ -public class SimpleSettingsStorageManager implements SettingsStorageManager { - - protected final SettingsBuildingPipeline settingsBuildingPipeline; - - public SimpleSettingsStorageManager() { - this(new UrlSettingsBuildingPipeline()); - } - - public SimpleSettingsStorageManager(SettingsBuildingPipeline settingsBuildingPipeline) { - this.settingsBuildingPipeline = settingsBuildingPipeline; - } - - @Override - public boolean supportsStore() { - return false; - } - - /** - * {@inheritDoc} - */ - public void retrieveDefaultSettings(S defaultSettings, final OnSettingsLoadedCallback asyncCallback) { - asyncCallback.onSuccess(settingsBuildingPipeline.getSettingsObject(defaultSettings)); - } - - @Override - public void storeSettingsJsons(SettingsJsons settingsJsons, OnSettingsStoredCallback onSettingsStoredCallback) { - } - - @Override - public void storeGlobalSettingsJson(JSONObject globalSettingsJson, OnSettingsStoredCallback onSettingsStoredCallback) { - } - - @Override - public void storeContextSpecificSettingsJson(JSONObject contextSpecificSettingsJson, - OnSettingsStoredCallback onSettingsStoredCallback) { - } - - @Override - public JSONValue settingsToJSON(Settings newSettings, PipelineLevel pipelineLevel, List path) { - return null; - } - - @Override - public void retrieveSettingsJsons(AsyncCallback asyncCallback) { - } - - @Override - public void dispose() { - } -} +package com.sap.sse.security.ui.settings; + +import java.util.List; + +import com.google.gwt.json.client.JSONObject; +import com.google.gwt.json.client.JSONValue; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.sap.sse.common.settings.Settings; +import com.sap.sse.gwt.client.shared.settings.OnSettingsLoadedCallback; +import com.sap.sse.gwt.client.shared.settings.OnSettingsStoredCallback; +import com.sap.sse.gwt.client.shared.settings.PipelineLevel; +import com.sap.sse.gwt.client.shared.settings.SettingsJsons; +import com.sap.sse.gwt.client.shared.settings.SettingsStorageManager; + +/** + * This {@link SettingsStorageManager} implementation only reads settings from the URL and does not support settings storage. + * + * @param + * The {@link Settings} type of the settings of the root component/perspective containing all the settings + * for itself and its subcomponents + * @see SettingsStorageManager + */ +public class SimpleSettingsStorageManager implements SettingsStorageManager { + + /** + * The pipeline used for the settings construction. + */ + protected final UrlSettingsBuildingPipeline settingsBuildingPipeline; + + /** + * Constructs the instance with a {@link UrlSettingsBuildingPipeline}. + */ + public SimpleSettingsStorageManager() { + settingsBuildingPipeline = new UrlSettingsBuildingPipeline(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean supportsStore() { + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public void retrieveDefaultSettings(S defaultSettings, final OnSettingsLoadedCallback asyncCallback) { + asyncCallback.onSuccess(settingsBuildingPipeline.getSettingsObject(defaultSettings)); + } + + /** + * {@inheritDoc} + */ + @Override + public void storeGlobalSettingsJson(JSONObject globalSettingsJson, OnSettingsStoredCallback onSettingsStoredCallback) { + throw new UnsupportedOperationException("Settings storage is unsupported by SimpleSettingsStorageManager"); + } + + /** + * {@inheritDoc} + */ + @Override + public void storeContextSpecificSettingsJson(JSONObject contextSpecificSettingsJson, + OnSettingsStoredCallback onSettingsStoredCallback) { + throw new UnsupportedOperationException("Settings storage is unsupported by SimpleSettingsStorageManager"); + } + + /** + * {@inheritDoc} + */ + @Override + public JSONValue settingsToJSON(Settings newSettings, PipelineLevel pipelineLevel, List path) { + throw new UnsupportedOperationException("Settings storage is unsupported by SimpleSettingsStorageManager"); + } + + /** + * {@inheritDoc} + */ + @Override + public void retrieveSettingsJsons(AsyncCallback asyncCallback) { + throw new UnsupportedOperationException("Settings storage is unsupported by SimpleSettingsStorageManager"); + } + + /** + * {@inheritDoc} + */ + @Override + public void dispose() { + } +} diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/settings/UrlSettingsBuildingPipeline.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/settings/UrlSettingsBuildingPipeline.java index f334f4d201e..9c4f5c59d24 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/settings/UrlSettingsBuildingPipeline.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/settings/UrlSettingsBuildingPipeline.java @@ -1,23 +1,23 @@ -package com.sap.sse.security.ui.settings; - -import java.util.List; - -import com.google.gwt.json.client.JSONValue; -import com.sap.sse.common.settings.Settings; -import com.sap.sse.gwt.client.shared.perspective.AbstractSettingsBuildingPipeline; -import com.sap.sse.gwt.client.shared.perspective.PipelineLevel; -import com.sap.sse.gwt.client.shared.perspective.SettingsJsons; - -public class UrlSettingsBuildingPipeline extends AbstractSettingsBuildingPipeline { - - @Override - public S getSettingsObject(S defaultSettings, SettingsJsons settingsJsons) { - return settingsStringConverter.deserializeFromCurrentUrl(defaultSettings); - } - - @Override - public JSONValue getJsonObject(Settings settings, PipelineLevel pipelineLevel, List path) { - throw new UnsupportedOperationException("This pipeline does not support JSON conversion"); - } - -} +package com.sap.sse.security.ui.settings; + +import java.util.List; + +import com.google.gwt.json.client.JSONValue; +import com.sap.sse.common.settings.Settings; +import com.sap.sse.gwt.client.shared.settings.AbstractSettingsBuildingPipeline; +import com.sap.sse.gwt.client.shared.settings.PipelineLevel; +import com.sap.sse.gwt.client.shared.settings.SettingsJsons; + +public class UrlSettingsBuildingPipeline extends AbstractSettingsBuildingPipeline { + + @Override + public S getSettingsObject(S defaultSettings, SettingsJsons settingsJsons) { + return settingsStringConverter.deserializeFromCurrentUrl(defaultSettings); + } + + @Override + public JSONValue getJsonObject(Settings settings, PipelineLevel pipelineLevel, List path) { + throw new UnsupportedOperationException("This pipeline does not support JSON conversion"); + } + +} diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/settings/UserSettingsBuildingPipeline.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/settings/UserSettingsBuildingPipeline.java index 611799bd3e6..8e72eb8daa2 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/settings/UserSettingsBuildingPipeline.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/settings/UserSettingsBuildingPipeline.java @@ -1,37 +1,37 @@ -package com.sap.sse.security.ui.settings; - -import java.util.List; - -import com.google.gwt.json.client.JSONValue; -import com.sap.sse.common.settings.Settings; -import com.sap.sse.gwt.client.shared.perspective.AbstractSettingsBuildingPipeline; -import com.sap.sse.gwt.client.shared.perspective.PipelineLevel; -import com.sap.sse.gwt.client.shared.perspective.SettingsJsons; -import com.sap.sse.gwt.client.shared.perspective.SettingsStringConverter; - -public class UserSettingsBuildingPipeline extends AbstractSettingsBuildingPipeline { - - - public UserSettingsBuildingPipeline(SettingsStringConverter settingsStringConverter) { - super(settingsStringConverter); - } - - public UserSettingsBuildingPipeline() { - } - - public S getSettingsObject(S defaultSettings, SettingsJsons settingsJsons) { - if(settingsJsons.getContextSpecificSettingsJson() != null) { - defaultSettings = settingsStringConverter.deserializeFromJson(defaultSettings, settingsJsons.getContextSpecificSettingsJson()); - } else if(settingsJsons.getGlobalSettingsJson() != null) { - defaultSettings = settingsStringConverter.deserializeFromJson(defaultSettings, settingsJsons.getGlobalSettingsJson()); - } - defaultSettings = settingsStringConverter.deserializeFromCurrentUrl(defaultSettings); - return defaultSettings; - } - - @Override - public JSONValue getJsonObject(Settings newSettings, PipelineLevel pipelineLevel, List path) { - return settingsStringConverter.serializeFromSettingsObject(newSettings); - } - -} +package com.sap.sse.security.ui.settings; + +import java.util.List; + +import com.google.gwt.json.client.JSONValue; +import com.sap.sse.common.settings.Settings; +import com.sap.sse.gwt.client.shared.settings.AbstractSettingsBuildingPipeline; +import com.sap.sse.gwt.client.shared.settings.PipelineLevel; +import com.sap.sse.gwt.client.shared.settings.SettingsJsons; +import com.sap.sse.gwt.client.shared.settings.SettingsStringConverter; + +public class UserSettingsBuildingPipeline extends AbstractSettingsBuildingPipeline { + + + public UserSettingsBuildingPipeline(SettingsStringConverter settingsStringConverter) { + super(settingsStringConverter); + } + + public UserSettingsBuildingPipeline() { + } + + public S getSettingsObject(S defaultSettings, SettingsJsons settingsJsons) { + if(settingsJsons.getContextSpecificSettingsJson() != null) { + defaultSettings = settingsStringConverter.deserializeFromJson(defaultSettings, settingsJsons.getContextSpecificSettingsJson()); + } else if(settingsJsons.getGlobalSettingsJson() != null) { + defaultSettings = settingsStringConverter.deserializeFromJson(defaultSettings, settingsJsons.getGlobalSettingsJson()); + } + defaultSettings = settingsStringConverter.deserializeFromCurrentUrl(defaultSettings); + return defaultSettings; + } + + @Override + public JSONValue getJsonObject(Settings newSettings, PipelineLevel pipelineLevel, List path) { + return settingsStringConverter.serializeFromSettingsObject(newSettings); + } + +} diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/settings/UserSettingsBuildingPipelineWithPatching.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/settings/UserSettingsBuildingPipelineWithPatching.java index 4c900a9a4e4..941d0176da9 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/settings/UserSettingsBuildingPipelineWithPatching.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/settings/UserSettingsBuildingPipelineWithPatching.java @@ -1,127 +1,127 @@ -package com.sap.sse.security.ui.settings; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; - -import com.google.gwt.json.client.JSONValue; -import com.sap.sse.common.settings.Settings; -import com.sap.sse.gwt.client.shared.components.Component; -import com.sap.sse.gwt.client.shared.perspective.ComponentUtils; -import com.sap.sse.gwt.client.shared.perspective.PipelineLevel; -import com.sap.sse.gwt.client.shared.perspective.SettingsJsons; -import com.sap.sse.gwt.client.shared.perspective.SettingsStringConverter; - -class UserSettingsBuildingPipelineWithPatching extends UserSettingsBuildingPipeline { - - private SettingsPatches patchesForStoringSettings = new SettingsPatches(); - private SettingsPatches patchesForLoadingSettings = new SettingsPatches(); - - public UserSettingsBuildingPipelineWithPatching(SettingsStringConverter settingsStringConverter) { - super(settingsStringConverter); - } - - public UserSettingsBuildingPipelineWithPatching() { - } - - @Override - public CS getSettingsObject(CS defaultSettings, SettingsJsons settingsJsons) { - defaultSettings = applyPatchesForPipelineLevel(defaultSettings, PipelineLevel.SYSTEM_DEFAULTS); - if(settingsJsons.getContextSpecificSettingsJson() != null) { - defaultSettings = applyPatchesForPipelineLevel(defaultSettings, PipelineLevel.GLOBAL_DEFAULTS); - defaultSettings = settingsStringConverter.deserializeFromJson(defaultSettings, settingsJsons.getContextSpecificSettingsJson()); - defaultSettings = applyPatchesForPipelineLevel(defaultSettings, PipelineLevel.CONTEXT_SPECIFIC_DEFAULTS); - } else if (settingsJsons.getGlobalSettingsJson() != null) { - defaultSettings = settingsStringConverter.deserializeFromJson(defaultSettings, settingsJsons.getGlobalSettingsJson()); - defaultSettings = applyPatchesForPipelineLevel(defaultSettings, PipelineLevel.GLOBAL_DEFAULTS); - defaultSettings = applyPatchesForPipelineLevel(defaultSettings, PipelineLevel.CONTEXT_SPECIFIC_DEFAULTS); - } else { - defaultSettings = applyPatchesForPipelineLevel(defaultSettings, PipelineLevel.GLOBAL_DEFAULTS); - defaultSettings = applyPatchesForPipelineLevel(defaultSettings, PipelineLevel.CONTEXT_SPECIFIC_DEFAULTS); - } - defaultSettings = settingsStringConverter.deserializeFromCurrentUrl(defaultSettings); - return defaultSettings; - } - - private CS applyPatchesForPipelineLevel(CS defaultSettings, PipelineLevel pipelineLevel) { - for (Entry, List>> entry : patchesForLoadingSettings.getSettingsPatches(pipelineLevel).entrySet()) { - List path = entry.getKey(); - List> settingsPatches = entry.getValue(); - if(!settingsPatches.isEmpty()) { - Settings patchedComponentSettings = ComponentUtils.determineComponentSettingsFromPerspectiveSettings(new ArrayList<>(path), defaultSettings); - for (SettingsPatch settingsPatch : settingsPatches) { - patchedComponentSettings = patchSettings(patchedComponentSettings, settingsPatch); - } - defaultSettings = ComponentUtils.patchSettingsTree(new ArrayList<>(path), patchedComponentSettings, defaultSettings); - } - } - return defaultSettings; - } - - @Override - public JSONValue getJsonObject(Settings settings, PipelineLevel pipelineLevel, List path) { - for (PipelineLevel level : pipelineLevel.getSortedLevelsUntilCurrent()) { - List> settingsPatches = patchesForStoringSettings.getSettingsPatches(path, level); - for (SettingsPatch settingsPatch : settingsPatches) { - settings = patchSettings(settings, settingsPatch); - } - } - return super.getJsonObject(settings, pipelineLevel, path); - } - - @SuppressWarnings("unchecked") - private CS patchSettings(Settings settings, SettingsPatch settingsPatch) { - return settingsPatch.patchSettings((CS) settings); - } - - public void addPatchForStoringSettings(Component component, PipelineLevel pipelineLevel, SettingsPatch settingsPatch) { - patchesForStoringSettings.addSettingsPatch(component.getPath(), settingsPatch, pipelineLevel); - } - - public void addPatchForLoadingSettings(Component component, PipelineLevel pipelineLevel, SettingsPatch settingsPatch) { - patchesForLoadingSettings.addSettingsPatch(component.getPath(), settingsPatch, pipelineLevel); - } - - private static class SettingsPatches { - - private Map, List>>> patchesForSettings = new HashMap<>(); - - public void addSettingsPatch(List path, SettingsPatch settingsPatch, PipelineLevel pipelineLevel) { - Map, List>> pipelinePatches = patchesForSettings.get(pipelineLevel); - if(pipelinePatches == null) { - pipelinePatches = new HashMap<>(); - patchesForSettings.put(pipelineLevel, pipelinePatches); - } - List> componentPatches = pipelinePatches.get(path); - if(componentPatches == null) { - componentPatches = new ArrayList<>(); - pipelinePatches.put(path, componentPatches); - } - componentPatches.add(settingsPatch); - } - - public List> getSettingsPatches(List path, PipelineLevel pipelineLevel) { - Map, List>> pipelinePatches = patchesForSettings.get(pipelineLevel); - if(pipelinePatches == null) { - return Collections.emptyList(); - } - List> componentPatches = pipelinePatches.get(path); - if(componentPatches == null) { - return Collections.emptyList(); - } - return componentPatches; - } - - public Map, List>> getSettingsPatches(PipelineLevel pipelineLevel) { - Map, List>> pipelinePatches = patchesForSettings.get(pipelineLevel); - if(pipelinePatches == null) { - return Collections.emptyMap(); - } - return pipelinePatches; - } - } - +package com.sap.sse.security.ui.settings; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import com.google.gwt.json.client.JSONValue; +import com.sap.sse.common.settings.Settings; +import com.sap.sse.gwt.client.shared.components.Component; +import com.sap.sse.gwt.client.shared.settings.ComponentUtils; +import com.sap.sse.gwt.client.shared.settings.PipelineLevel; +import com.sap.sse.gwt.client.shared.settings.SettingsJsons; +import com.sap.sse.gwt.client.shared.settings.SettingsStringConverter; + +public class UserSettingsBuildingPipelineWithPatching extends UserSettingsBuildingPipeline { + + private SettingsPatches patchesForStoringSettings = new SettingsPatches(); + private SettingsPatches patchesForLoadingSettings = new SettingsPatches(); + + public UserSettingsBuildingPipelineWithPatching(SettingsStringConverter settingsStringConverter) { + super(settingsStringConverter); + } + + public UserSettingsBuildingPipelineWithPatching() { + } + + @Override + public CS getSettingsObject(CS defaultSettings, SettingsJsons settingsJsons) { + defaultSettings = applyPatchesForPipelineLevel(defaultSettings, PipelineLevel.SYSTEM_DEFAULTS); + if(settingsJsons.getContextSpecificSettingsJson() != null) { + defaultSettings = applyPatchesForPipelineLevel(defaultSettings, PipelineLevel.GLOBAL_DEFAULTS); + defaultSettings = settingsStringConverter.deserializeFromJson(defaultSettings, settingsJsons.getContextSpecificSettingsJson()); + defaultSettings = applyPatchesForPipelineLevel(defaultSettings, PipelineLevel.CONTEXT_SPECIFIC_DEFAULTS); + } else if (settingsJsons.getGlobalSettingsJson() != null) { + defaultSettings = settingsStringConverter.deserializeFromJson(defaultSettings, settingsJsons.getGlobalSettingsJson()); + defaultSettings = applyPatchesForPipelineLevel(defaultSettings, PipelineLevel.GLOBAL_DEFAULTS); + defaultSettings = applyPatchesForPipelineLevel(defaultSettings, PipelineLevel.CONTEXT_SPECIFIC_DEFAULTS); + } else { + defaultSettings = applyPatchesForPipelineLevel(defaultSettings, PipelineLevel.GLOBAL_DEFAULTS); + defaultSettings = applyPatchesForPipelineLevel(defaultSettings, PipelineLevel.CONTEXT_SPECIFIC_DEFAULTS); + } + defaultSettings = settingsStringConverter.deserializeFromCurrentUrl(defaultSettings); + return defaultSettings; + } + + private CS applyPatchesForPipelineLevel(CS defaultSettings, PipelineLevel pipelineLevel) { + for (Entry, List>> entry : patchesForLoadingSettings.getSettingsPatches(pipelineLevel).entrySet()) { + List path = entry.getKey(); + List> settingsPatches = entry.getValue(); + if(!settingsPatches.isEmpty()) { + Settings patchedComponentSettings = ComponentUtils.determineComponentSettingsFromPerspectiveSettings(new ArrayList<>(path), defaultSettings); + for (SettingsPatch settingsPatch : settingsPatches) { + patchedComponentSettings = patchSettings(patchedComponentSettings, settingsPatch); + } + defaultSettings = ComponentUtils.patchSettingsTree(new ArrayList<>(path), patchedComponentSettings, defaultSettings); + } + } + return defaultSettings; + } + + @Override + public JSONValue getJsonObject(Settings settings, PipelineLevel pipelineLevel, List path) { + for (PipelineLevel level : pipelineLevel.getSortedLevelsUntilCurrent()) { + List> settingsPatches = patchesForStoringSettings.getSettingsPatches(path, level); + for (SettingsPatch settingsPatch : settingsPatches) { + settings = patchSettings(settings, settingsPatch); + } + } + return super.getJsonObject(settings, pipelineLevel, path); + } + + @SuppressWarnings("unchecked") + private CS patchSettings(Settings settings, SettingsPatch settingsPatch) { + return settingsPatch.patchSettings((CS) settings); + } + + public void addPatchForStoringSettings(Component component, PipelineLevel pipelineLevel, SettingsPatch settingsPatch) { + patchesForStoringSettings.addSettingsPatch(component.getPath(), settingsPatch, pipelineLevel); + } + + public void addPatchForLoadingSettings(Component component, PipelineLevel pipelineLevel, SettingsPatch settingsPatch) { + patchesForLoadingSettings.addSettingsPatch(component.getPath(), settingsPatch, pipelineLevel); + } + + private static class SettingsPatches { + + private Map, List>>> patchesForSettings = new HashMap<>(); + + public void addSettingsPatch(List path, SettingsPatch settingsPatch, PipelineLevel pipelineLevel) { + Map, List>> pipelinePatches = patchesForSettings.get(pipelineLevel); + if(pipelinePatches == null) { + pipelinePatches = new HashMap<>(); + patchesForSettings.put(pipelineLevel, pipelinePatches); + } + List> componentPatches = pipelinePatches.get(path); + if(componentPatches == null) { + componentPatches = new ArrayList<>(); + pipelinePatches.put(path, componentPatches); + } + componentPatches.add(settingsPatch); + } + + public List> getSettingsPatches(List path, PipelineLevel pipelineLevel) { + Map, List>> pipelinePatches = patchesForSettings.get(pipelineLevel); + if(pipelinePatches == null) { + return Collections.emptyList(); + } + List> componentPatches = pipelinePatches.get(path); + if(componentPatches == null) { + return Collections.emptyList(); + } + return componentPatches; + } + + public Map, List>> getSettingsPatches(PipelineLevel pipelineLevel) { + Map, List>> pipelinePatches = patchesForSettings.get(pipelineLevel); + if(pipelinePatches == null) { + return Collections.emptyMap(); + } + return pipelinePatches; + } + } + } \ No newline at end of file diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/settings/UserSettingsStorageManager.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/settings/UserSettingsStorageManager.java index f61d772490d..83fbfd33954 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/settings/UserSettingsStorageManager.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/settings/UserSettingsStorageManager.java @@ -1,325 +1,342 @@ -package com.sap.sse.security.ui.settings; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Queue; - -import com.google.gwt.json.client.JSONObject; -import com.google.gwt.json.client.JSONValue; -import com.google.gwt.storage.client.Storage; -import com.google.gwt.user.client.rpc.AsyncCallback; -import com.sap.sse.common.settings.Settings; -import com.sap.sse.gwt.client.shared.perspective.IgnoreLocalSettings; -import com.sap.sse.gwt.client.shared.perspective.OnSettingsLoadedCallback; -import com.sap.sse.gwt.client.shared.perspective.OnSettingsStoredCallback; -import com.sap.sse.gwt.client.shared.perspective.PipelineLevel; -import com.sap.sse.gwt.client.shared.perspective.SettingsBuildingPipeline; -import com.sap.sse.gwt.client.shared.perspective.SettingsJsons; -import com.sap.sse.gwt.client.shared.perspective.SettingsStorageManager; -import com.sap.sse.gwt.client.shared.perspective.SettingsStrings; -import com.sap.sse.gwt.settings.SettingsToUrlSerializer; -import com.sap.sse.security.ui.client.UserService; -import com.sap.sse.security.ui.client.UserStatusEventHandler; -import com.sap.sse.security.ui.shared.UserDTO; - -/** - * The settings are stored in HTML5 LocalStorage, as well as on server in the user's account, in case the user is - * logged. - * - * @author Vladislav Chumak - * - * @param - * The {@link Settings} type of the settings of the root component/perspective containing all the settings - * for itself and its subcomponents - * @see SettingsStorageManager - */ -public class UserSettingsStorageManager extends SimpleSettingsStorageManager { - - /** - * The key which is associated with the global settings. Different keys will cause multiple/different settings - * instances stored in the storage. - */ - private final String storageGlobalKey; - - /** - * The key which is used to store the context specific settings. Each context with own context specific settings - * must have a unique key. - */ - private final String storageContextSpecificKey; - - private UserService userService; - - /** - * This is used, to ensure that only once the data is loaded remote, if a user logs in later, he must refresh, to - * avoid "complicated problems" - */ - private boolean initialUserSetting = false; - - private UserStatusEventHandler userStatusEventHandler = null; - - private Queue> retrieveSettingsCallbacksQueue = new LinkedList<>(); - - /** - * - * @param userService - * The {@link UserService} which is used for settings storage on server when the current user is logged - * in - * @param globalDefinitionId - * The key which is associated with the global settings. Different keys will cause multiple/different - * settings instances stored in the storage. - * @param contextDefinitionId - * The key which is used to store the context specific settings. Each context with own context specific - * settings must have a unique key. - */ - public UserSettingsStorageManager(UserService userService, StorageDefinitionId storageDefinitionId, SettingsBuildingPipeline settingsBuildingPipeline) { - super(settingsBuildingPipeline); - this.userService = userService; - this.storageGlobalKey = storageDefinitionId.generateStorageGlobalKey(); - this.storageContextSpecificKey = storageDefinitionId.generateStorageContextSpecificKey(); - } - - /** - * - * @param userService - * The {@link UserService} which is used for settings storage on server when the current user is logged - * in - * @param globalDefinitionId - * The key which is associated with the global settings. Different keys will cause multiple/different - * settings instances stored in the storage. - * @param contextDefinitionId - * The key which is used to store the context specific settings. Each context with own context specific - * settings must have a unique key. - */ - public UserSettingsStorageManager(UserService userService, StorageDefinitionId storageDefinitionId) { - this(userService, storageDefinitionId, new UserSettingsBuildingPipeline()); - } - - @Override - public boolean supportsStore() { - return true; - } - - private void storeSettingsStringsOnLocalStorage(SettingsStrings settingsStrings, boolean storeGlobalSettings, boolean storeContextSpecificSettings) { - Storage localStorage = Storage.getLocalStorageIfSupported(); - if (localStorage != null) { - if(storeGlobalSettings) { - localStorage.removeItem(storageGlobalKey); - if(settingsStrings.getGlobalSettingsString() != null) { - localStorage.setItem(storageGlobalKey, settingsStrings.getGlobalSettingsString()); - } - } - if(storeContextSpecificSettings) { - localStorage.removeItem(storageContextSpecificKey); - if(settingsStrings.getContextSpecificSettingsString() != null) { - localStorage.setItem(storageContextSpecificKey, settingsStrings.getContextSpecificSettingsString()); - } - } - } - } - - private void storeSettingsStringsOnServer(SettingsStrings settingsStrings, final OnSettingsStoredCallback onSettingsStoredCallback, boolean storeGlobalSettings, boolean storeContextSpecificSettings) { - Map keyValuePairs = new HashMap<>(); - if(storeGlobalSettings) { - keyValuePairs.put(storageGlobalKey, settingsStrings.getGlobalSettingsString()); - } - if(storeContextSpecificSettings) { - keyValuePairs.put(storageContextSpecificKey, settingsStrings.getContextSpecificSettingsString()); - } - userService.setPreferences(keyValuePairs, new AsyncCallback() { - - @Override - public void onFailure(Throwable caught) { - onSettingsStoredCallback.onError(caught); - } - - @Override - public void onSuccess(Void result) { - onSettingsStoredCallback.onSuccess(); - } - }); - } - - @Override - public void retrieveDefaultSettings(final S defaultSettings, final OnSettingsLoadedCallback callback) { - retrieveSettingsJsons(new AsyncCallback() { - - @Override - public void onFailure(Throwable caught) { - SettingsStrings localStorageSettings = retrieveSettingsStringsFromLocalStorage(); - S newDefaultSettings = settingsBuildingPipeline.getSettingsObject(defaultSettings, localStorageSettings); - callback.onError(caught, newDefaultSettings); - } - - @Override - public void onSuccess(SettingsJsons settingsJsons) { - S newDefaultSettings = settingsBuildingPipeline.getSettingsObject(defaultSettings, settingsJsons); - callback.onSuccess(newDefaultSettings); - } - }); - } - - @Override - public void dispose() { - super.dispose(); - if(userStatusEventHandler != null) { - userService.removeUserStatusEventHandler(userStatusEventHandler); - } - } - - private SettingsStrings retrieveSettingsStringsFromLocalStorage() { - Storage localStorage = Storage.getLocalStorageIfSupported(); - if (localStorage != null) { - String globalSettings = localStorage.getItem(storageGlobalKey); - String contextSpecificSettings = localStorage.getItem(storageContextSpecificKey); - return new SettingsStrings(globalSettings, contextSpecificSettings); - } - return new SettingsStrings(null, null); - } - - private void retrieveSettingsStringsFromServer(final AsyncCallback asyncCallback) { - List keys = new ArrayList<>(2); - keys.add(storageGlobalKey); - keys.add(storageContextSpecificKey); - userService.getPreferences(keys, new AsyncCallback>() { - - @Override - public void onSuccess(Map result) { - SettingsStrings settingsStrings = new SettingsStrings(result.get(storageGlobalKey), result.get(storageContextSpecificKey)); - asyncCallback.onSuccess(settingsStrings); - } - - @Override - public void onFailure(Throwable caught) { - asyncCallback.onFailure(caught); - } - }); - } - - @Override - public JSONValue settingsToJSON(Settings newSettings, PipelineLevel pipelineLevel, List path) { - return settingsBuildingPipeline.getJsonObject(newSettings, pipelineLevel, path); - } - - @Override - public void storeSettingsJsons(SettingsJsons settingsJsons, OnSettingsStoredCallback onSettingsStoredCallback) { - storeSettingsJsons(settingsJsons, onSettingsStoredCallback, true, true); - } - - private void storeSettingsJsons(SettingsJsons settingsJsons, OnSettingsStoredCallback onSettingsStoredCallback, boolean storeGlobalSettings, boolean storeContextSpecificSettings) { - SettingsStrings settingsStrings = settingsBuildingPipeline.getSettingsStringConverter().convertToSettingsStrings(settingsJsons); - storeSettingsStringsOnLocalStorage(settingsStrings, storeGlobalSettings, storeContextSpecificSettings); - if (userService.getCurrentUser() != null) { - storeSettingsStringsOnServer(settingsStrings, onSettingsStoredCallback, storeGlobalSettings, storeContextSpecificSettings); - } else { - onSettingsStoredCallback.onSuccess(); - } - } - - @Override - public void storeGlobalSettingsJson(JSONObject globalSettingsJson, - OnSettingsStoredCallback onSettingsStoredCallback) { - SettingsJsons settingsJsons = new SettingsJsons(globalSettingsJson, null); - storeSettingsJsons(settingsJsons, onSettingsStoredCallback, true, false); - } - - @Override - public void storeContextSpecificSettingsJson(JSONObject contextSpecificSettingsJson, - OnSettingsStoredCallback onSettingsStoredCallback) { - SettingsJsons settingsJsons = new SettingsJsons(null, contextSpecificSettingsJson); - storeSettingsJsons(settingsJsons, onSettingsStoredCallback, false, true); - } - - @Override - public void retrieveSettingsJsons(AsyncCallback asyncCallback) { - retrieveSettingsCallbacksQueue.add(asyncCallback); - if(userStatusEventHandler == null) { - this.userStatusEventHandler = new UserStatusEventHandler() { - - @Override - public void onUserStatusChange(UserDTO user) { - //call this method always on user status change event in order to cause sync between local storage and server - if(!initialUserSetting) { - initialUserSetting = true; - requestSettingsJsons(); - } - } - }; - userService.addUserStatusEventHandler(userStatusEventHandler, true); - } else if(initialUserSetting) { - requestSettingsJsons(); - } - } - - private void requestSettingsJsons() { - if (userService.getCurrentUser() == null) { - SettingsStrings settingsStrings = retrieveSettingsStringsFromLocalStorage(); - AsyncCallback asyncCallback; - while((asyncCallback = retrieveSettingsCallbacksQueue.poll()) != null) { - asyncCallback.onSuccess(settingsBuildingPipeline.getSettingsStringConverter().convertToSettingsJson(settingsStrings)); - } - } else { - retrieveSettingsStringsFromServer(new AsyncCallback() { - - @Override - public void onSuccess(SettingsStrings serverSettingsStrings) { - serverSettingsStrings = syncLocalStorageAndServer(serverSettingsStrings); - AsyncCallback asyncCallback; - while((asyncCallback = retrieveSettingsCallbacksQueue.poll()) != null) { - asyncCallback.onSuccess(settingsBuildingPipeline.getSettingsStringConverter().convertToSettingsJson(serverSettingsStrings)); - } - } - - @Override - public void onFailure(Throwable caught) { - AsyncCallback asyncCallback; - while((asyncCallback = retrieveSettingsCallbacksQueue.poll()) != null) { - asyncCallback.onFailure(caught); - } - } - }); - } - } - - private SettingsStrings syncLocalStorageAndServer(SettingsStrings serverSettingsStrings) { - if(serverSettingsStrings.getGlobalSettingsString() == null && serverSettingsStrings.getContextSpecificSettingsString() == null) { - SettingsStrings localStorageSettingsStrings = retrieveSettingsStringsFromLocalStorage(); - if(localStorageSettingsStrings.getGlobalSettingsString() != null || localStorageSettingsStrings.getContextSpecificSettingsString() != null) { - storeSettingsStringsOnServer(localStorageSettingsStrings, new OnSettingsStoredCallback() { - - @Override - public void onSuccess() { - //nothing to do - } - - @Override - public void onError(Throwable caught) { - //nothing to do - } - }, true, true); - } - serverSettingsStrings = localStorageSettingsStrings; - } else { - storeSettingsStringsOnLocalStorage(serverSettingsStrings, true, true); - } - return serverSettingsStrings; - } - - /** - * Creates a {@link SettingsStorageManager} instance based on the ignoreLocalSettings URL flag. if - * ignoreLocalSettings is set to true, a SimpleSettingsStorageManager is created. A - * UserSettingsStorageManager is created otherwise. - */ - public static SettingsStorageManager createSettingsStorageManager(UserService userService, StorageDefinitionId storageDefinitionId) { - if (getIgnoreLocalSettings().isIgnoreLocalSettings()) { - return new SimpleSettingsStorageManager<>(); - } - return new UserSettingsStorageManager<>(userService, storageDefinitionId); - } - - public static IgnoreLocalSettings getIgnoreLocalSettings() { - return new SettingsToUrlSerializer().deserializeFromCurrentLocation(new IgnoreLocalSettings()); - } -} +package com.sap.sse.security.ui.settings; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Queue; + +import com.google.gwt.json.client.JSONObject; +import com.google.gwt.json.client.JSONValue; +import com.google.gwt.storage.client.Storage; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.sap.sse.common.settings.Settings; +import com.sap.sse.gwt.client.shared.perspective.IgnoreLocalSettings; +import com.sap.sse.gwt.client.shared.settings.OnSettingsLoadedCallback; +import com.sap.sse.gwt.client.shared.settings.OnSettingsStoredCallback; +import com.sap.sse.gwt.client.shared.settings.PipelineLevel; +import com.sap.sse.gwt.client.shared.settings.SettingsBuildingPipeline; +import com.sap.sse.gwt.client.shared.settings.SettingsJsons; +import com.sap.sse.gwt.client.shared.settings.SettingsStorageManager; +import com.sap.sse.gwt.client.shared.settings.SettingsStrings; +import com.sap.sse.gwt.settings.SettingsToUrlSerializer; +import com.sap.sse.security.ui.client.UserService; +import com.sap.sse.security.ui.client.UserStatusEventHandler; +import com.sap.sse.security.ui.shared.UserDTO; + +/** + * The settings are stored in HTML5 LocalStorage, as well as on server in the user's account, in case the user is + * logged. + * + * @author Vladislav Chumak + * + * @param + * The {@link Settings} type of the settings of the root component/perspective containing all the settings + * for itself and its subcomponents + * @see SettingsStorageManager + */ +public class UserSettingsStorageManager implements SettingsStorageManager { + + /** + * The pipeline used for the settings construction. + */ + protected final SettingsBuildingPipeline settingsBuildingPipeline; + + /** + * The key which is associated with the User Settings. Different keys will cause multiple/different settings + * instances to be stored in the storage. + */ + private final String storageGlobalKey; + + /** + * The key which is associated with the Document Settings. Different keys will cause multiple/different settings + * instances to be stored in the storage. + */ + private final String storageContextSpecificKey; + + /** + * The userService instance which is used for server communication regarding settings loading and storing + */ + private UserService userService; + + /** + * Indicates whether the settings have been already loaded for the first time in order to maintain a single attachment of {@link UserStatusEventHandler}. + */ + private boolean initialUserSetting = false; + + /** + * Handler which is currently attached for listening the user's login state + */ + private UserStatusEventHandler userStatusEventHandler = null; + + /** + * Callbacks which are waiting to be called when persistent settings representation has loaded. + */ + private Queue> retrieveSettingsCallbacksQueue = new LinkedList<>(); + + /** + * Constructs the instance with a custom {@link SettingsBuildingPipeline}. + * + * @param userService The service which is used for server-side settings storage + * @param storageDefinitionId The definition for User Settings and Document Settings storage keys + * @param settingsBuildingPipeline The custom pipeline to be used for settings construction + */ + protected UserSettingsStorageManager(UserService userService, StorageDefinitionId storageDefinitionId, SettingsBuildingPipeline settingsBuildingPipeline) { + this.settingsBuildingPipeline = settingsBuildingPipeline; + this.userService = userService; + this.storageGlobalKey = storageDefinitionId.generateStorageGlobalKey(); + this.storageContextSpecificKey = storageDefinitionId.generateStorageContextSpecificKey(); + } + + /** + * Constructs the instance with {@link UserSettingsBuildingPipeline}. + * + * @param userService The service which is used for server-side settings storage + * @param storageDefinitionId The definition for User Settings and Document Settings storage keys + */ + public UserSettingsStorageManager(UserService userService, StorageDefinitionId storageDefinitionId) { + this(userService, storageDefinitionId, new UserSettingsBuildingPipeline()); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean supportsStore() { + return true; + } + + private void storeSettingsStringsOnLocalStorage(SettingsStrings settingsStrings, boolean storeGlobalSettings, boolean storeContextSpecificSettings) { + Storage localStorage = Storage.getLocalStorageIfSupported(); + if (localStorage != null) { + if(storeGlobalSettings) { + localStorage.removeItem(storageGlobalKey); + if(settingsStrings.getGlobalSettingsString() != null) { + localStorage.setItem(storageGlobalKey, settingsStrings.getGlobalSettingsString()); + } + } + if(storeContextSpecificSettings) { + localStorage.removeItem(storageContextSpecificKey); + if(settingsStrings.getContextSpecificSettingsString() != null) { + localStorage.setItem(storageContextSpecificKey, settingsStrings.getContextSpecificSettingsString()); + } + } + } + } + + private void storeSettingsStringsOnServer(SettingsStrings settingsStrings, final OnSettingsStoredCallback onSettingsStoredCallback, boolean storeGlobalSettings, boolean storeContextSpecificSettings) { + Map keyValuePairs = new HashMap<>(); + if(storeGlobalSettings) { + keyValuePairs.put(storageGlobalKey, settingsStrings.getGlobalSettingsString()); + } + if(storeContextSpecificSettings) { + keyValuePairs.put(storageContextSpecificKey, settingsStrings.getContextSpecificSettingsString()); + } + userService.setPreferences(keyValuePairs, new AsyncCallback() { + + @Override + public void onFailure(Throwable caught) { + onSettingsStoredCallback.onError(caught); + } + + @Override + public void onSuccess(Void result) { + onSettingsStoredCallback.onSuccess(); + } + }); + } + + /** + * {@inheritDoc} + */ + @Override + public void retrieveDefaultSettings(final S defaultSettings, final OnSettingsLoadedCallback callback) { + retrieveSettingsJsons(new AsyncCallback() { + + @Override + public void onFailure(Throwable caught) { + SettingsStrings localStorageSettings = retrieveSettingsStringsFromLocalStorage(); + S newDefaultSettings = settingsBuildingPipeline.getSettingsObject(defaultSettings, localStorageSettings); + callback.onError(caught, newDefaultSettings); + } + + @Override + public void onSuccess(SettingsJsons settingsJsons) { + S newDefaultSettings = settingsBuildingPipeline.getSettingsObject(defaultSettings, settingsJsons); + callback.onSuccess(newDefaultSettings); + } + }); + } + + /** + * {@inheritDoc} + */ + @Override + public void dispose() { + if(userStatusEventHandler != null) { + userService.removeUserStatusEventHandler(userStatusEventHandler); + } + } + + private SettingsStrings retrieveSettingsStringsFromLocalStorage() { + Storage localStorage = Storage.getLocalStorageIfSupported(); + if (localStorage != null) { + String globalSettings = localStorage.getItem(storageGlobalKey); + String contextSpecificSettings = localStorage.getItem(storageContextSpecificKey); + return new SettingsStrings(globalSettings, contextSpecificSettings); + } + return new SettingsStrings(null, null); + } + + private void retrieveSettingsStringsFromServer(final AsyncCallback asyncCallback) { + List keys = new ArrayList<>(2); + keys.add(storageGlobalKey); + keys.add(storageContextSpecificKey); + userService.getPreferences(keys, new AsyncCallback>() { + + @Override + public void onSuccess(Map result) { + SettingsStrings settingsStrings = new SettingsStrings(result.get(storageGlobalKey), result.get(storageContextSpecificKey)); + asyncCallback.onSuccess(settingsStrings); + } + + @Override + public void onFailure(Throwable caught) { + asyncCallback.onFailure(caught); + } + }); + } + + /** + * {@inheritDoc} + */ + @Override + public JSONValue settingsToJSON(Settings newSettings, PipelineLevel pipelineLevel, List path) { + return settingsBuildingPipeline.getJsonObject(newSettings, pipelineLevel, path); + } + + private void storeSettingsJsons(SettingsJsons settingsJsons, OnSettingsStoredCallback onSettingsStoredCallback, boolean storeGlobalSettings, boolean storeContextSpecificSettings) { + SettingsStrings settingsStrings = settingsBuildingPipeline.getSettingsStringConverter().convertToSettingsStrings(settingsJsons); + storeSettingsStringsOnLocalStorage(settingsStrings, storeGlobalSettings, storeContextSpecificSettings); + if (userService.getCurrentUser() != null) { + storeSettingsStringsOnServer(settingsStrings, onSettingsStoredCallback, storeGlobalSettings, storeContextSpecificSettings); + } else { + onSettingsStoredCallback.onSuccess(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void storeGlobalSettingsJson(JSONObject globalSettingsJson, + OnSettingsStoredCallback onSettingsStoredCallback) { + SettingsJsons settingsJsons = new SettingsJsons(globalSettingsJson, null); + storeSettingsJsons(settingsJsons, onSettingsStoredCallback, true, false); + } + + /** + * {@inheritDoc} + */ + @Override + public void storeContextSpecificSettingsJson(JSONObject contextSpecificSettingsJson, + OnSettingsStoredCallback onSettingsStoredCallback) { + SettingsJsons settingsJsons = new SettingsJsons(null, contextSpecificSettingsJson); + storeSettingsJsons(settingsJsons, onSettingsStoredCallback, false, true); + } + + /** + * {@inheritDoc} + */ + @Override + public void retrieveSettingsJsons(AsyncCallback asyncCallback) { + retrieveSettingsCallbacksQueue.add(asyncCallback); + if(userStatusEventHandler == null) { + this.userStatusEventHandler = new UserStatusEventHandler() { + + @Override + public void onUserStatusChange(UserDTO user) { + //call this method always on user status change event in order to cause sync between local storage and server + if(!initialUserSetting) { + initialUserSetting = true; + requestSettingsJsons(); + } + } + }; + userService.addUserStatusEventHandler(userStatusEventHandler, true); + } else if(initialUserSetting) { + requestSettingsJsons(); + } + } + + private void requestSettingsJsons() { + if (userService.getCurrentUser() == null) { + SettingsStrings settingsStrings = retrieveSettingsStringsFromLocalStorage(); + AsyncCallback asyncCallback; + while((asyncCallback = retrieveSettingsCallbacksQueue.poll()) != null) { + asyncCallback.onSuccess(settingsBuildingPipeline.getSettingsStringConverter().convertToSettingsJson(settingsStrings)); + } + } else { + retrieveSettingsStringsFromServer(new AsyncCallback() { + + @Override + public void onSuccess(SettingsStrings serverSettingsStrings) { + serverSettingsStrings = syncLocalStorageAndServer(serverSettingsStrings); + AsyncCallback asyncCallback; + while((asyncCallback = retrieveSettingsCallbacksQueue.poll()) != null) { + asyncCallback.onSuccess(settingsBuildingPipeline.getSettingsStringConverter().convertToSettingsJson(serverSettingsStrings)); + } + } + + @Override + public void onFailure(Throwable caught) { + AsyncCallback asyncCallback; + while((asyncCallback = retrieveSettingsCallbacksQueue.poll()) != null) { + asyncCallback.onFailure(caught); + } + } + }); + } + } + + private SettingsStrings syncLocalStorageAndServer(SettingsStrings serverSettingsStrings) { + if(serverSettingsStrings.getGlobalSettingsString() == null && serverSettingsStrings.getContextSpecificSettingsString() == null) { + SettingsStrings localStorageSettingsStrings = retrieveSettingsStringsFromLocalStorage(); + if(localStorageSettingsStrings.getGlobalSettingsString() != null || localStorageSettingsStrings.getContextSpecificSettingsString() != null) { + storeSettingsStringsOnServer(localStorageSettingsStrings, new OnSettingsStoredCallback() { + + @Override + public void onSuccess() { + //nothing to do + } + + @Override + public void onError(Throwable caught) { + //nothing to do + } + }, true, true); + } + serverSettingsStrings = localStorageSettingsStrings; + } else { + storeSettingsStringsOnLocalStorage(serverSettingsStrings, true, true); + } + return serverSettingsStrings; + } + + /** + * Creates a {@link SettingsStorageManager} instance based on the ignoreLocalSettings URL flag. if + * ignoreLocalSettings is set to true, a SimpleSettingsStorageManager is created. A + * UserSettingsStorageManager is created otherwise. + */ + public static SettingsStorageManager createSettingsStorageManager(UserService userService, StorageDefinitionId storageDefinitionId) { + if (getIgnoreLocalSettings().isIgnoreLocalSettings()) { + return new SimpleSettingsStorageManager<>(); + } + return new UserSettingsStorageManager<>(userService, storageDefinitionId); + } + + public static IgnoreLocalSettings getIgnoreLocalSettings() { + return new SettingsToUrlSerializer().deserializeFromCurrentLocation(new IgnoreLocalSettings()); + } +} diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/settings/UserSettingsStorageManagerWithPatching.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/settings/UserSettingsStorageManagerWithPatching.java index 3b6391d9c5b..b26b44bb2a5 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/settings/UserSettingsStorageManagerWithPatching.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/settings/UserSettingsStorageManagerWithPatching.java @@ -1,25 +1,63 @@ -package com.sap.sse.security.ui.settings; - -import com.sap.sse.common.settings.Settings; -import com.sap.sse.gwt.client.shared.components.Component; -import com.sap.sse.gwt.client.shared.perspective.PipelineLevel; -import com.sap.sse.security.ui.client.UserService; - -public class UserSettingsStorageManagerWithPatching extends UserSettingsStorageManager { - - private final UserSettingsBuildingPipelineWithPatching settingsBuildingPipeline; - - public UserSettingsStorageManagerWithPatching(UserService userService, StorageDefinitionId storageDefinitionId) { - super(userService, storageDefinitionId, new UserSettingsBuildingPipelineWithPatching()); - settingsBuildingPipeline = (UserSettingsBuildingPipelineWithPatching) super.settingsBuildingPipeline; - } - - public void addPatchForStoringSettings(Component component, PipelineLevel pipelineLevel, SettingsPatch settingsPatch) { - settingsBuildingPipeline.addPatchForStoringSettings(component, pipelineLevel, settingsPatch); - } - - public void addPatchForLoadingSettings(Component component, PipelineLevel pipelineLevel, SettingsPatch settingsPatch) { - settingsBuildingPipeline.addPatchForLoadingSettings(component, pipelineLevel, settingsPatch); - } - -} +package com.sap.sse.security.ui.settings; + +import com.sap.sse.common.settings.Settings; +import com.sap.sse.gwt.client.shared.components.Component; +import com.sap.sse.gwt.client.shared.settings.PipelineLevel; +import com.sap.sse.security.ui.client.UserService; + +/** + * Specialization of {@link UserSettingsStorageManager} which uses {@link UserSettingsBuildingPipelineWithPatching} for settings construction. + * This implementation provides additional methods for attaching of {@link SettingsPatch}. There are multiple hooks in + * the underlying {@link UserSettingsBuildingPipelineWithPatching} which can be used to influence the + * settings construction in its construction pipeline. After each {@link PipelineLevel} a custom {@link SettingsPatch} + * may be applied. The patch may partially or completely modify the resulting settings object in order to provide + * the desired behavior of default settings for a dynamic environment, e.g. RaceBoard which determines its default settings regarding + * to RaceModes, PlayModes and etc. + * + * @author Vladislav Chumak + * + * @param + * The {@link Settings} type of the settings of the root component/perspective containing all the settings + * for itself and its subcomponents + */ +public class UserSettingsStorageManagerWithPatching extends UserSettingsStorageManager { + + /** + * The typed pipeline used for the settings construction. + */ + protected final UserSettingsBuildingPipelineWithPatching settingsBuildingPipeline; + + /** + * Constructs the instance with {@link UserSettingsBuildingPipelineWithPatching}. + * + * @param userService The service which is used for server-side settings storage + * @param storageDefinitionId The definition for User Settings and Document Settings storage keys + */ + public UserSettingsStorageManagerWithPatching(UserService userService, StorageDefinitionId storageDefinitionId) { + super(userService, storageDefinitionId, new UserSettingsBuildingPipelineWithPatching()); + settingsBuildingPipeline = (UserSettingsBuildingPipelineWithPatching) super.settingsBuildingPipeline; + } + + /** + * Adds a settings patch for transforming settings before storing them. + * + * @param component The component which the targeted settings for patching belong to + * @param pipelineLevel The pipeline level AFTER that the patch should be applied on settings + * @param settingsPatch The settings patch to apply on settings + */ + public void addPatchForStoringSettings(Component component, PipelineLevel pipelineLevel, SettingsPatch settingsPatch) { + settingsBuildingPipeline.addPatchForStoringSettings(component, pipelineLevel, settingsPatch); + } + + /** + * Adds a settings patch for settings object construction. + * + * @param component The component which the targeted settings for patching belong to + * @param pipelineLevel The pipeline level AFTER that the patch should be applied on settings + * @param settingsPatch The settings patch to apply on settings + */ + public void addPatchForLoadingSettings(Component component, PipelineLevel pipelineLevel, SettingsPatch settingsPatch) { + settingsBuildingPipeline.addPatchForLoadingSettings(component, pipelineLevel, settingsPatch); + } + +}